Compare commits

...

18 Commits

Author SHA1 Message Date
ecf362cffc 🚀 Launch 2.2.1+47 2025-01-05 12:13:43 +08:00
f4ab7671d8 🐛 Bug fixes on resetting post write controller 2025-01-05 12:06:49 +08:00
a2a3018917 🚀 Launch 2.2.1+46 2025-01-04 22:05:39 +08:00
0bdb664000 ⚗️ Remove initialization screen 2025-01-04 21:51:22 +08:00
9c3b61ce57 Lunar calendar festivals 2025-01-04 21:49:48 +08:00
d06df3d278 Stickers 2025-01-04 21:26:28 +08:00
547ba19e61 🐛 Fix attachment zoom meta throw error 2025-01-04 19:14:40 +08:00
cb05ff2e9e 🚀 Launch 2.2.1+44 2025-01-01 19:38:57 +08:00
f614da7918 💄 Optimize attachment list 2025-01-01 17:57:41 +08:00
a3c8dafff9 User typing status
🐛 Bug fixes
2025-01-01 16:45:37 +08:00
fa978a7cd1 🐛 Fix notification mark all as read issue 2025-01-01 11:50:06 +08:00
aaa0a562b4 💄 Fix transparent icon color issue 2025-01-01 01:48:35 +08:00
590a4ce2a6 🐛 Bug fixes on chat message rendering 2025-01-01 01:11:35 +08:00
f26edce071 🚀 Launch 2.2.1+43 2024-12-29 23:58:31 +08:00
603799ea32 🐛 Fix high quality icon issue 2024-12-29 23:52:48 +08:00
a32baf7798 Able to set attachment alt text 2024-12-29 23:50:56 +08:00
498c9af663 💄 Optimize chatting input
 Rollback universal image
2024-12-29 23:30:29 +08:00
202dbff6d3 🐛 Bug fixes 2024-12-29 23:11:50 +08:00
39 changed files with 1811 additions and 492 deletions

View File

@ -7,11 +7,7 @@ meta {
post {
url: {{endpoint}}/cgi/uc/boosts/1/activate
body: none
auth: bearer
}
auth:bearer {
token: {{atk}}
auth: inherit
}
body:json {

View File

@ -0,0 +1,19 @@
meta {
name: Create Sticker Pack
type: http
seq: 1
}
post {
url: {{endpoint}}/cgi/uc/stickers/packs
body: json
auth: inherit
}
body:json {
{
"prefix": "cat",
"name": "Solar Network full of Cats!",
"description": "The sticker packs is full of stickers which related with cats!"
}
}

View File

@ -0,0 +1,20 @@
meta {
name: Create Sticker
type: http
seq: 2
}
post {
url: {{endpoint}}/cgi/uc/stickers
body: json
auth: inherit
}
body:json {
{
"alias": "AteChip",
"name": "Cat ate chips",
"attachment_id": "d0b692cc64054463",
"pack_id": 2
}
}

View File

@ -7,11 +7,7 @@ meta {
post {
url: {{endpoint}}/cgi/id/dev/notify/all
body: json
auth: bearer
}
auth:bearer {
token: {{atk}}
auth: inherit
}
body:json {

7
api/collection.bru Normal file
View File

@ -0,0 +1,7 @@
auth {
mode: bearer
}
auth:bearer {
token: {{atk}}
}

View File

@ -281,7 +281,12 @@
"one": "{} attachment",
"other": "{} attachments"
},
"messageTyping": {
"one": "{} is typing...",
"other": "{} are typing..."
},
"fieldAttachmentRandomId": "Random ID",
"fieldAttachmentAlt": "Alternative text",
"addAttachmentFromAlbum": "Add from album",
"addAttachmentFromClipboard": "Paste file",
"addAttachmentFromCameraPhoto": "Take photo",
@ -293,6 +298,7 @@
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
"attachmentCompressVideo": "Re-encode video",
"attachmentSetThumbnail": "Set thumbnail",
"attachmentSetAlt": "Set alternative text",
"attachmentCopyRandomId": "Copy RID",
"attachmentUpload": "Upload",
"attachmentInputDialog": "Upload attachments",
@ -408,6 +414,9 @@
"celebrateBirthday": "Happy birthday, {}!",
"celebrateMerryXmas": "Merry christmas, {}",
"celebrateNewYear": "Happy new year, {}",
"celebrateLunarNewYear": "Happy lunar new year, {}",
"celebrateMidAutumn": "Happy mid-autumn festival, {}",
"celebrateDragonBoat": "Happy dragon boat festival, {}",
"celebrateValentineDay": "Today is valentine's day, {}!",
"celebrateLaborDay": "Today is labor day, {}.",
"celebrateMotherDay": "Today is mother's day, {}.",
@ -417,6 +426,9 @@
"celebrateThanksgiving": "Today is thanksgiving day, {}!",
"pendingBirthday": "Birthday in {}",
"pendingMerryXmas": "Christmas in {}",
"pendingLunarNewYear": "Lunar new year in {}",
"pendingMidAutumn": "Mid-autumn festival in {}",
"pendingDragonBoat": "Dragon boat festival in {}",
"pendingNewYear": "New year in {}",
"pendingValentineDay": "Valentine's day in {}",
"pendingLaborDay": "Labor day in {}",

View File

@ -279,7 +279,12 @@
"one": "{} 个附件",
"other": "{} 个附件"
},
"messageTyping": {
"one": "{} 正在输入",
"other": "{} 正在输入"
},
"fieldAttachmentRandomId": "访问 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "从相册中添加附件",
"addAttachmentFromClipboard": "粘贴附件",
"addAttachmentFromCameraPhoto": "拍摄照片",
@ -291,6 +296,7 @@
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
"attachmentCompressVideo": "重新编码视频",
"attachmentSetThumbnail": "设置缩略图",
"attachmentSetAlt": "设置概述文字",
"attachmentCopyRandomId": "复制访问 ID",
"attachmentUpload": "上传",
"attachmentInputDialog": "上传附件",
@ -404,6 +410,9 @@
"dailyCheckNegativeHint6": "出门",
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
"celebrateBirthday": "生日快乐,{}",
"celebrateLunarNewYear": "春节快乐,{}",
"celebrateMidAutumn": "中秋节快乐,{}",
"celebrateDragonBoat": "端午节快乐,{}",
"celebrateMerryXmas": "圣诞快乐,{}",
"celebrateNewYear": "新年快乐,{}",
"celebrateValentineDay": "今天是情人节,{}",
@ -413,6 +422,9 @@
"celebrateFatherDay": "今天是父亲节,{}。",
"celebrateHalloween": "快乐在圣诞节,{}",
"celebrateThanksgiving": "今天是感恩节,{}",
"pendingLunarNewYear": "{} 过春节",
"pendingMidAutumn": "{} 过中秋节",
"pendingDragonBoat": "{} 过端午节",
"pendingBirthday": "{} 过生日",
"pendingMerryXmas": "{} 过圣诞节",
"pendingNewYear": "{} 跨年",

View File

@ -279,7 +279,12 @@
"one": "{} 個附件",
"other": "{} 個附件"
},
"messageTyping": {
"one": "{} 正在輸入",
"other": "{} 正在輸入"
},
"fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件",
"addAttachmentFromClipboard": "粘貼附件",
"addAttachmentFromCameraPhoto": "拍攝照片",
@ -291,6 +296,7 @@
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
"attachmentCompressVideo": "重新編碼視頻",
"attachmentSetThumbnail": "設置縮略圖",
"attachmentSetAlt": "設置概述文字",
"attachmentCopyRandomId": "複製訪問 ID",
"attachmentUpload": "上傳",
"attachmentInputDialog": "上傳附件",
@ -404,6 +410,9 @@
"dailyCheckNegativeHint6": "出門",
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
"celebrateBirthday": "生日快樂,{}",
"celebrateLunarNewYear": "春節快樂,{}",
"celebrateMidAutumn": "中秋節快樂,{}",
"celebrateDragonBoat": "端午節快樂,{}",
"celebrateMerryXmas": "聖誕快樂,{}",
"celebrateNewYear": "新年快樂,{}",
"celebrateValentineDay": "今天是情人節,{}",
@ -413,6 +422,9 @@
"celebrateFatherDay": "今天是父親節,{}。",
"celebrateHalloween": "快樂在聖誕節,{}",
"celebrateThanksgiving": "今天是感恩節,{}",
"pendingLunarNewYear": "{} 過春節",
"pendingMidAutumn": "{} 過中秋節",
"pendingDragonBoat": "{} 過端午節",
"pendingBirthday": "{} 過生日",
"pendingMerryXmas": "{} 過聖誕節",
"pendingNewYear": "{} 跨年",

View File

@ -279,7 +279,12 @@
"one": "{} 個附件",
"other": "{} 個附件"
},
"messageTyping": {
"one": "{} 正在輸入",
"other": "{} 正在輸入"
},
"fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件",
"addAttachmentFromClipboard": "粘貼附件",
"addAttachmentFromCameraPhoto": "拍攝照片",
@ -291,6 +296,7 @@
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
"attachmentCompressVideo": "重新編碼視頻",
"attachmentSetThumbnail": "設置縮略圖",
"attachmentSetAlt": "設置概述文字",
"attachmentCopyRandomId": "複製訪問 ID",
"attachmentUpload": "上傳",
"attachmentInputDialog": "上傳附件",
@ -404,6 +410,9 @@
"dailyCheckNegativeHint6": "出門",
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
"celebrateBirthday": "生日快樂,{}",
"celebrateLunarNewYear": "春節快樂,{}",
"celebrateMidAutumn": "中秋節快樂,{}",
"celebrateDragonBoat": "端午節快樂,{}",
"celebrateMerryXmas": "聖誕快樂,{}",
"celebrateNewYear": "新年快樂,{}",
"celebrateValentineDay": "今天是情人節,{}",
@ -413,6 +422,9 @@
"celebrateFatherDay": "今天是父親節,{}。",
"celebrateHalloween": "快樂在聖誕節,{}",
"celebrateThanksgiving": "今天是感恩節,{}",
"pendingLunarNewYear": "{} 過春節",
"pendingMidAutumn": "{} 過中秋節",
"pendingDragonBoat": "{} 過端午節",
"pendingBirthday": "{} 過生日",
"pendingMerryXmas": "{} 過聖誕節",
"pendingNewYear": "{} 跨年",

View File

@ -211,6 +211,9 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
- url_launcher_ios (0.0.1):
- Flutter
@ -256,6 +259,7 @@ DEPENDENCIES:
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_compress (from `.symlinks/plugins/video_compress/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
@ -343,6 +347,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_compress:
@ -374,7 +380,7 @@ SPEC CHECKSUMS:
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
@ -401,6 +407,7 @@ SPEC CHECKSUMS:
SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import 'package:collection/collection.dart';
@ -11,6 +12,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/websocket.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/types/websocket.dart';
import 'package:uuid/uuid.dart';
class ChatMessageController extends ChangeNotifier {
@ -36,8 +38,7 @@ class ChatMessageController extends ChangeNotifier {
int? messageTotal;
bool get isAllLoaded =>
messageTotal != null && messages.length >= messageTotal!;
bool get isAllLoaded => messageTotal != null && messages.length >= messageTotal!;
String? _boxKey;
SnChannel? channel;
@ -50,8 +51,10 @@ class ChatMessageController extends ChangeNotifier {
/// Stored as a list of nonce to provide the loading state
final List<String> unconfirmedMessages = List.empty(growable: true);
Box<SnChatMessage>? get _box =>
(_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
Box<SnChatMessage>? get _box => (_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
final List<SnChannelMember> typingMembers = List.empty(growable: true);
final Map<int, Timer> typingInactiveTimer = {};
Future<void> initialize(SnChannel chan) async {
channel = chan;
@ -78,22 +81,17 @@ class ChatMessageController extends ChangeNotifier {
if (event.payload?['channel_id'] != channel?.id) break;
final member = SnChannelMember.fromJson(event.payload!['member']);
if (member.id == profile?.id) break;
// TODO impl typing users
// if (!_typingUsers.any((x) => x.id == member.id)) {
// setState(() {
// _typingUsers.add(member);
// });
// }
// _typingInactiveTimer[member.id]?.cancel();
// _typingInactiveTimer[member.id] = Timer(
// const Duration(seconds: 3),
// () {
// setState(() {
// _typingUsers.removeWhere((x) => x.id == member.id);
// _typingInactiveTimer.remove(member.id);
// });
// },
// );
if (!typingMembers.any((x) => x.id == member.id)) {
typingMembers.add(member);
print('Typing member: ${typingMembers.map((ele) => member.id)}');
notifyListeners();
}
typingInactiveTimer[member.id]?.cancel();
typingInactiveTimer[member.id] = Timer(const Duration(seconds: 3), () {
typingMembers.removeWhere((x) => x.id == member.id);
typingInactiveTimer.remove(member.id);
notifyListeners();
});
}
});
@ -101,6 +99,35 @@ class ChatMessageController extends ChangeNotifier {
notifyListeners();
}
Timer? _typingNotifyTimer;
bool _typingStatus = false;
Future<void> _sendTypingStatusPackage() async {
_ws.conn?.sink.add(jsonEncode(
WebSocketPackage(
method: 'status.typing',
endpoint: 'im',
payload: {
'channel_id': channel!.id,
},
).toJson(),
));
}
void pingTypingStatus() {
if (!_typingStatus) {
_sendTypingStatusPackage();
_typingStatus = true;
}
if (_typingNotifyTimer == null || !_typingNotifyTimer!.isActive) {
_typingNotifyTimer?.cancel();
_typingNotifyTimer = Timer(const Duration(milliseconds: 1850), () {
_typingStatus = false;
});
}
}
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
if (_box == null) return;
await _box!.putAll({
@ -167,8 +194,7 @@ class ChatMessageController extends ChangeNotifier {
switch (message.type) {
case 'messages.edit':
if (message.relatedEventId != null) {
final idx =
messages.indexWhere((x) => x.id == message.relatedEventId);
final idx = messages.indexWhere((x) => x.id == message.relatedEventId);
if (idx != -1) {
final newBody = message.body;
newBody.remove('related_event');
@ -207,8 +233,7 @@ class ChatMessageController extends ChangeNotifier {
'algorithm': 'plain',
if (quoteId != null) 'quote_event': quoteId,
if (relatedId != null) 'related_event': relatedId,
if (attachments != null && attachments.isNotEmpty)
'attachments': attachments,
if (attachments != null && attachments.isNotEmpty) 'attachments': attachments,
};
// Mock the message locally
@ -305,8 +330,7 @@ class ChatMessageController extends ChangeNotifier {
if (out == null) {
try {
final resp = await _sn.client
.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
final resp = await _sn.client.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
out = SnChatMessage.fromJson(resp.data);
_saveMessageToLocal([out]);
} catch (_) {
@ -341,9 +365,7 @@ class ChatMessageController extends ChangeNotifier {
bool forceRemote = false,
}) async {
late List<SnChatMessage> out;
if (_box != null &&
(_box!.length >= take + offset || forceLocal) &&
!forceRemote) {
if (_box != null && (_box!.length >= take + offset || forceLocal) && !forceRemote) {
out = _box!.keys
.toList()
.cast<int>()
@ -386,8 +408,7 @@ class ChatMessageController extends ChangeNotifier {
quoteEvent: quoteEvent,
attachments: attachments
.where(
(ele) =>
out[i].body['attachments']?.contains(ele?.rid) ?? false,
(ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false,
)
.toList(),
),
@ -395,10 +416,7 @@ class ChatMessageController extends ChangeNotifier {
}
// Preload sender accounts
final accountId = out
.where((ele) => ele.sender.accountId >= 0)
.map((ele) => ele.sender.accountId)
.toSet();
final accountId = out.where((ele) => ele.sender.accountId >= 0).map((ele) => ele.sender.accountId).toSet();
await _ud.listAccount(accountId);
return out;

View File

@ -104,7 +104,7 @@ class PostWriteMedia {
if (attachment != null) {
final sn = context.read<SnNetworkProvider>();
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
if (width != null && height != null) {
if (width != null && height != null && !kIsWeb) {
return ResizeImage(
provider,
width: width,
@ -213,11 +213,11 @@ class PostWriteController extends ChangeNotifier {
aliasController.text = post.alias ?? '';
publishedAt = post.publishedAt;
publishedUntil = post.publishedUntil;
visibleUsers = List.from(post.visibleUsersList ?? []);
invisibleUsers = List.from(post.invisibleUsersList ?? []);
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
visibility = post.visibility;
tags = List.from(post.tags.map((ele) => ele.alias));
categories = List.from(post.categories.map((ele) => ele.alias));
tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
@ -344,9 +344,10 @@ class PostWriteController extends ChangeNotifier {
if (titleController.text.isNotEmpty) 'title': titleController.text,
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(),
'tags': tags.map((ele) => {'alias': ele}).toList(),
'categories': categories.map((ele) => {'alias': ele}).toList(),
'attachments':
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
'tags': tags.map((ele) => {'alias': ele}).toList(growable: true),
'categories': categories.map((ele) => {'alias': ele}).toList(growable: true),
'visibility': visibility,
'visible_users_list': visibleUsers,
'invisible_users_list': invisibleUsers,
@ -622,13 +623,15 @@ class PostWriteController extends ChangeNotifier {
void reset() {
publishedAt = null;
publishedUntil = null;
thumbnail = null;
visibility = 0;
titleController.clear();
descriptionController.clear();
contentController.clear();
aliasController.clear();
tags.clear();
categories.clear();
attachments.clear();
tags = List.empty(growable: true);
categories = List.empty(growable: true);
attachments = List.empty(growable: true);
editingPost = null;
replyingPost = null;
repostingPost = null;

View File

@ -10,7 +10,6 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:package_info_plus/package_info_plus.dart';
@ -18,7 +17,6 @@ import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/firebase_options.dart';
import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart';
@ -30,6 +28,7 @@ import 'package:surface/providers/post.dart';
import 'package:surface/providers/relationship.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/providers/special_day.dart';
import 'package:surface/providers/theme.dart';
import 'package:surface/providers/user_directory.dart';
@ -41,7 +40,6 @@ import 'package:surface/types/chat.dart';
import 'package:surface/types/realm.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/version_label.dart';
import 'package:version/version.dart';
import 'package:workmanager/workmanager.dart';
import 'package:in_app_review/in_app_review.dart';
@ -144,6 +142,7 @@ class SolianApp extends StatelessWidget {
Provider(create: (ctx) => SnPostContentProvider(ctx)),
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
Provider(create: (ctx) => SnStickerProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
@ -208,8 +207,6 @@ class _AppSplashScreen extends StatefulWidget {
}
class _AppSplashScreenState extends State<_AppSplashScreen> {
bool _isReady = false;
void _tryRequestRating() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('first_boot_time')) {
@ -282,8 +279,6 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
} catch (err) {
if (!mounted) return;
await context.showErrorDialog(err);
} finally {
setState(() => _isReady = true);
}
}
@ -303,32 +298,6 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
@override
Widget build(BuildContext context) {
if (!_isReady) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: Container(
constraints: const BoxConstraints(maxWidth: 180),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (MediaQuery.of(context).platformBrightness == Brightness.dark)
Image.asset("assets/icon/icon-dark.png", width: 64, height: 64)
else
Image.asset("assets/icon/icon.png", width: 64, height: 64),
const Gap(6),
LinearProgressIndicator(
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
),
const Gap(20),
Text('appInitializing'.tr(), textAlign: TextAlign.center),
AppVersionLabel(),
],
),
).center(),
);
}
return widget.child;
}
}

View File

@ -0,0 +1,38 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
class SnStickerProvider {
late final SnNetworkProvider _sn;
final Map<String, SnSticker?> _cache = {};
SnStickerProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
}
bool hasNotSticker(String alias) {
return _cache.containsKey(alias) && _cache[alias] == null;
}
Future<SnSticker?> lookupSticker(String alias) async {
if (_cache.containsKey(alias)) {
return _cache[alias];
}
try {
final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
final sticker = SnSticker.fromJson(resp.data);
_cache[alias] = sticker;
return sticker;
} catch (err) {
_cache[alias] = null;
log('[Sticker] Failed to lookup sticker $alias: $err');
}
return null;
}
}

View File

@ -3,9 +3,12 @@ import 'package:provider/provider.dart';
import 'package:surface/providers/userinfo.dart';
// Stored as key: month, day
const Map<String, (int, int)> kSpecialDays = {
final Map<String, (int, int)> kSpecialDays = {
// Birthday is dynamically generated according to the user's profile
'NewYear': (1, 1),
'LunarNewYear': (lunarToGregorian(null, 1, 1).month, lunarToGregorian(null, 1, 1).day),
'MidAutumn': (lunarToGregorian(null, 8, 15).month, lunarToGregorian(null, 8, 15).day),
'DragonBoat': (lunarToGregorian(null, 5, 5).month, lunarToGregorian(null, 5, 5).day),
'ValentineDay': (2, 14),
'LaborDay': (5, 1),
'MotherDay': (5, 11),
@ -19,6 +22,9 @@ const Map<String, (int, int)> kSpecialDays = {
const Map<String, String> kSpecialDaysSymbol = {
'Birthday': '🎂',
'NewYear': '🎉',
'LunarNewYear': '🎉',
'MidAutumn': '🥮',
'DragonBoat': '🐲',
'MerryXmas': '🎄',
'ValentineDay': '💑',
'LaborDay': '🏋️',
@ -134,3 +140,45 @@ class SpecialDayProvider {
return (elapsedDuration / totalDuration).clamp(0.0, 1.0);
}
}
final Map<int, LunarYear> lunarYearData = {
2025: LunarYear(
startDate: DateTime(2025, 1, 29),
months: [29, 30, 30, 29, 30, 29, 29, 30, 30, 29, 30, 29],
leapMonth: 0,
),
};
class LunarYear {
final DateTime startDate;
final List<int> months;
final int leapMonth;
LunarYear({required this.startDate, required this.months, required this.leapMonth});
}
DateTime lunarToGregorian(int? year, int month, int day, {bool isLeapMonth = false}) {
year = year ?? DateTime.now().year;
final lunarYear = lunarYearData[year];
if (lunarYear == null) {
throw Exception('Lunar data for year $year not found');
}
int leapMonth = lunarYear.leapMonth;
if (isLeapMonth && (leapMonth == 0 || leapMonth != month)) {
throw Exception('Invalid leap month for year $year');
}
int daysFromStart = 0;
for (int i = 0; i < month - 1; i++) {
daysFromStart += lunarYear.months[i];
}
if (isLeapMonth) {
daysFromStart += lunarYear.months[month - 1];
}
daysFromStart += day - 1;
return lunarYear.startDate.add(Duration(days: daysFromStart));
}

View File

@ -87,7 +87,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
try {
final resp = await sn.client.request(
widget.editingChannelAlias != null
? '/cgi/im/channels/$scope/${widget.editingChannelAlias}'
? '/cgi/im/channels/$scope/${_editingChannel!.id}'
: '/cgi/im/channels/$scope',
data: payload,
options: Options(

View File

@ -17,6 +17,7 @@ import 'package:surface/types/chat.dart';
import 'package:surface/widgets/chat/call/call_prejoin.dart';
import 'package:surface/widgets/chat/chat_message.dart';
import 'package:surface/widgets/chat/chat_message_input.dart';
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -280,11 +281,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
Expanded(
child: InfiniteList(
reverse: true,
padding: const EdgeInsets.only(
left: 12,
right: 12,
top: 12,
),
padding: const EdgeInsets.only(top: 12),
hasReachedMax: _messageController.isAllLoaded,
itemCount: _messageController.messages.length,
isLoading: _messageController.isLoading,
@ -310,23 +307,20 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
return Align(
alignment: Alignment.centerLeft,
child: Container(
constraints: BoxConstraints(maxWidth: 480),
child: ChatMessage(
data: message,
isMerged: canMerge,
hasMerged: canMergePrevious,
isPending: _messageController.unconfirmedMessages.contains(message.uuid),
onReply: (value) {
_inputGlobalKey.currentState?.setReply(value);
},
onEdit: (value) {
_inputGlobalKey.currentState?.setEdit(value);
},
onDelete: (value) {
_inputGlobalKey.currentState?.deleteMessage(value);
},
),
child: ChatMessage(
data: message,
isMerged: canMerge,
hasMerged: canMergePrevious,
isPending: _messageController.unconfirmedMessages.contains(message.uuid),
onReply: (value) {
_inputGlobalKey.currentState?.setReply(value);
},
onEdit: (value) {
_inputGlobalKey.currentState?.setEdit(value);
},
onDelete: (value) {
_inputGlobalKey.currentState?.deleteMessage(value);
},
),
);
},
@ -335,11 +329,17 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
if (!_messageController.isPending)
Material(
elevation: 2,
child: ChatMessageInput(
key: _inputGlobalKey,
otherMember: _otherMember,
controller: _messageController,
).padding(bottom: MediaQuery.of(context).padding.bottom),
child: Column(
children: [
ChatTypingIndicator(controller: _messageController),
ChatMessageInput(
key: _inputGlobalKey,
otherMember: _otherMember,
controller: _messageController,
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
),
],
);

View File

@ -153,9 +153,14 @@ class _HomeDashUpdateWidget extends StatelessWidget {
}
}
class _HomeDashSpecialDayWidget extends StatelessWidget {
class _HomeDashSpecialDayWidget extends StatefulWidget {
const _HomeDashSpecialDayWidget();
@override
State<_HomeDashSpecialDayWidget> createState() => _HomeDashSpecialDayWidgetState();
}
class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
@override
Widget build(BuildContext context) {
final ua = context.watch<UserProvider>();
@ -165,21 +170,20 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
if (days.isNotEmpty) {
return Column(
spacing: 8,
children: days.map((ele) {
return Card(
child: ListTile(
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
subtitle: Text(
DateFormat('y/M/d').format(DateTime.now().copyWith(
month: kSpecialDays[ele]!.$1,
day: kSpecialDays[ele]!.$2,
)),
),
),
).padding(bottom: 8);
}).toList());
return Card(
child: ListTile(
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
subtitle: Text(
DateFormat('y/M/d').format(DateTime.now().copyWith(
month: kSpecialDays[ele]?.$1,
day: kSpecialDays[ele]?.$2,
)),
),
),
).padding(bottom: 8);
}).toList());
}
final nextOne = dayz.getNextSpecialDay();
@ -193,7 +197,7 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
return Card(
child: ListTile(
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
title: Text('pending$name').tr(args: [RelativeTime(context).format(date)]),
title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]),
subtitle: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -204,6 +208,9 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
separatorType: SeparatorType.symbol,
decoration: BoxDecoration(),
padding: EdgeInsets.zero,
onDone: () {
setState(() {});
},
),
const Gap(12),
Expanded(

View File

@ -82,24 +82,15 @@ class _NotificationScreenState extends State<NotificationScreen> {
if (!mounted) return;
setState(() => _isSubmitting = true);
List<int> markList = List.empty(growable: true);
for (final element in _notifications) {
if (element.id <= 0) continue;
if (element.readAt != null) continue;
markList.add(element.id);
}
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.put('/cgi/id/notifications/read', data: {
'messages': markList,
});
final resp = await sn.client.put('/cgi/id/notifications/read/all');
_notifications.clear();
_fetchNotifications();
if (!mounted) return;
context.showSnackbar(
'notificationMarkAllReadPrompt'.plural(markList.length),
'notificationMarkAllReadPrompt'.plural(resp.data['count']),
);
} catch (err) {
if (!mounted) return;

View File

@ -365,30 +365,31 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: _writeController.temporaryRestored
? Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.restore, size: 20),
const Gap(8),
Expanded(child: Text('postLocalDraftRestored').tr()),
InkWell(
child: Text('dialogDismiss').tr(),
onTap: () {
_writeController.reset();
},
? Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
],
)
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.restore, size: 20),
const Gap(8),
Expanded(child: Text('postLocalDraftRestored').tr()),
InkWell(
child: Text('dialogDismiss').tr(),
onTap: () {
_writeController.reset();
},
),
],
))
: const SizedBox.shrink(),
)
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)

View File

@ -88,9 +88,9 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
title: Text(_realm?.name ?? 'loading'.tr()),
bottom: TabBar(
tabs: [
Tab(icon: const Icon(Symbols.home)),
Tab(icon: const Icon(Symbols.group)),
Tab(icon: const Icon(Symbols.settings)),
Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
],
),
),

View File

@ -141,3 +141,39 @@ class SnAttachmentBoost with _$SnAttachmentBoost {
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) => _$SnAttachmentBoostFromJson(json);
}
@freezed
class SnSticker with _$SnSticker {
const factory SnSticker({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String alias,
required String name,
required int attachmentId,
required SnAttachment attachment,
required int packId,
required SnStickerPack pack,
required int accountId,
}) = _SnSticker;
factory SnSticker.fromJson(Map<String, Object?> json) => _$SnStickerFromJson(json);
}
@freezed
class SnStickerPack with _$SnStickerPack {
const factory SnStickerPack({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String prefix,
required String name,
required String description,
required List<SnSticker>? stickers,
required int accountId,
}) = _SnStickerPack;
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json);
}

View File

@ -2272,3 +2272,738 @@ abstract class _SnAttachmentBoost implements SnAttachmentBoost {
_$$SnAttachmentBoostImplCopyWith<_$SnAttachmentBoostImpl> get copyWith =>
throw _privateConstructorUsedError;
}
SnSticker _$SnStickerFromJson(Map<String, dynamic> json) {
return _SnSticker.fromJson(json);
}
/// @nodoc
mixin _$SnSticker {
int get id => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
DateTime get updatedAt => throw _privateConstructorUsedError;
DateTime? get deletedAt => throw _privateConstructorUsedError;
String get alias => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
int get attachmentId => throw _privateConstructorUsedError;
SnAttachment get attachment => throw _privateConstructorUsedError;
int get packId => throw _privateConstructorUsedError;
SnStickerPack get pack => throw _privateConstructorUsedError;
int get accountId => throw _privateConstructorUsedError;
/// Serializes this SnSticker to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnStickerCopyWith<SnSticker> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnStickerCopyWith<$Res> {
factory $SnStickerCopyWith(SnSticker value, $Res Function(SnSticker) then) =
_$SnStickerCopyWithImpl<$Res, SnSticker>;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String alias,
String name,
int attachmentId,
SnAttachment attachment,
int packId,
SnStickerPack pack,
int accountId});
$SnAttachmentCopyWith<$Res> get attachment;
$SnStickerPackCopyWith<$Res> get pack;
}
/// @nodoc
class _$SnStickerCopyWithImpl<$Res, $Val extends SnSticker>
implements $SnStickerCopyWith<$Res> {
_$SnStickerCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? alias = null,
Object? name = null,
Object? attachmentId = null,
Object? attachment = null,
Object? packId = null,
Object? pack = null,
Object? accountId = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
alias: null == alias
? _value.alias
: alias // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
attachmentId: null == attachmentId
? _value.attachmentId
: attachmentId // ignore: cast_nullable_to_non_nullable
as int,
attachment: null == attachment
? _value.attachment
: attachment // ignore: cast_nullable_to_non_nullable
as SnAttachment,
packId: null == packId
? _value.packId
: packId // ignore: cast_nullable_to_non_nullable
as int,
pack: null == pack
? _value.pack
: pack // ignore: cast_nullable_to_non_nullable
as SnStickerPack,
accountId: null == accountId
? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAttachmentCopyWith<$Res> get attachment {
return $SnAttachmentCopyWith<$Res>(_value.attachment, (value) {
return _then(_value.copyWith(attachment: value) as $Val);
});
}
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnStickerPackCopyWith<$Res> get pack {
return $SnStickerPackCopyWith<$Res>(_value.pack, (value) {
return _then(_value.copyWith(pack: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$SnStickerImplCopyWith<$Res>
implements $SnStickerCopyWith<$Res> {
factory _$$SnStickerImplCopyWith(
_$SnStickerImpl value, $Res Function(_$SnStickerImpl) then) =
__$$SnStickerImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String alias,
String name,
int attachmentId,
SnAttachment attachment,
int packId,
SnStickerPack pack,
int accountId});
@override
$SnAttachmentCopyWith<$Res> get attachment;
@override
$SnStickerPackCopyWith<$Res> get pack;
}
/// @nodoc
class __$$SnStickerImplCopyWithImpl<$Res>
extends _$SnStickerCopyWithImpl<$Res, _$SnStickerImpl>
implements _$$SnStickerImplCopyWith<$Res> {
__$$SnStickerImplCopyWithImpl(
_$SnStickerImpl _value, $Res Function(_$SnStickerImpl) _then)
: super(_value, _then);
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? alias = null,
Object? name = null,
Object? attachmentId = null,
Object? attachment = null,
Object? packId = null,
Object? pack = null,
Object? accountId = null,
}) {
return _then(_$SnStickerImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
alias: null == alias
? _value.alias
: alias // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
attachmentId: null == attachmentId
? _value.attachmentId
: attachmentId // ignore: cast_nullable_to_non_nullable
as int,
attachment: null == attachment
? _value.attachment
: attachment // ignore: cast_nullable_to_non_nullable
as SnAttachment,
packId: null == packId
? _value.packId
: packId // ignore: cast_nullable_to_non_nullable
as int,
pack: null == pack
? _value.pack
: pack // ignore: cast_nullable_to_non_nullable
as SnStickerPack,
accountId: null == accountId
? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnStickerImpl implements _SnSticker {
const _$SnStickerImpl(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.alias,
required this.name,
required this.attachmentId,
required this.attachment,
required this.packId,
required this.pack,
required this.accountId});
factory _$SnStickerImpl.fromJson(Map<String, dynamic> json) =>
_$$SnStickerImplFromJson(json);
@override
final int id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
final DateTime? deletedAt;
@override
final String alias;
@override
final String name;
@override
final int attachmentId;
@override
final SnAttachment attachment;
@override
final int packId;
@override
final SnStickerPack pack;
@override
final int accountId;
@override
String toString() {
return 'SnSticker(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, alias: $alias, name: $name, attachmentId: $attachmentId, attachment: $attachment, packId: $packId, pack: $pack, accountId: $accountId)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnStickerImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.alias, alias) || other.alias == alias) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.attachmentId, attachmentId) ||
other.attachmentId == attachmentId) &&
(identical(other.attachment, attachment) ||
other.attachment == attachment) &&
(identical(other.packId, packId) || other.packId == packId) &&
(identical(other.pack, pack) || other.pack == pack) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
createdAt,
updatedAt,
deletedAt,
alias,
name,
attachmentId,
attachment,
packId,
pack,
accountId);
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnStickerImplCopyWith<_$SnStickerImpl> get copyWith =>
__$$SnStickerImplCopyWithImpl<_$SnStickerImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnStickerImplToJson(
this,
);
}
}
abstract class _SnSticker implements SnSticker {
const factory _SnSticker(
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final DateTime? deletedAt,
required final String alias,
required final String name,
required final int attachmentId,
required final SnAttachment attachment,
required final int packId,
required final SnStickerPack pack,
required final int accountId}) = _$SnStickerImpl;
factory _SnSticker.fromJson(Map<String, dynamic> json) =
_$SnStickerImpl.fromJson;
@override
int get id;
@override
DateTime get createdAt;
@override
DateTime get updatedAt;
@override
DateTime? get deletedAt;
@override
String get alias;
@override
String get name;
@override
int get attachmentId;
@override
SnAttachment get attachment;
@override
int get packId;
@override
SnStickerPack get pack;
@override
int get accountId;
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnStickerImplCopyWith<_$SnStickerImpl> get copyWith =>
throw _privateConstructorUsedError;
}
SnStickerPack _$SnStickerPackFromJson(Map<String, dynamic> json) {
return _SnStickerPack.fromJson(json);
}
/// @nodoc
mixin _$SnStickerPack {
int get id => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
DateTime get updatedAt => throw _privateConstructorUsedError;
DateTime? get deletedAt => throw _privateConstructorUsedError;
String get prefix => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
String get description => throw _privateConstructorUsedError;
List<SnSticker>? get stickers => throw _privateConstructorUsedError;
int get accountId => throw _privateConstructorUsedError;
/// Serializes this SnStickerPack to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnStickerPackCopyWith<SnStickerPack> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnStickerPackCopyWith<$Res> {
factory $SnStickerPackCopyWith(
SnStickerPack value, $Res Function(SnStickerPack) then) =
_$SnStickerPackCopyWithImpl<$Res, SnStickerPack>;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String prefix,
String name,
String description,
List<SnSticker>? stickers,
int accountId});
}
/// @nodoc
class _$SnStickerPackCopyWithImpl<$Res, $Val extends SnStickerPack>
implements $SnStickerPackCopyWith<$Res> {
_$SnStickerPackCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? prefix = null,
Object? name = null,
Object? description = null,
Object? stickers = freezed,
Object? accountId = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
prefix: null == prefix
? _value.prefix
: prefix // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
stickers: freezed == stickers
? _value.stickers
: stickers // ignore: cast_nullable_to_non_nullable
as List<SnSticker>?,
accountId: null == accountId
? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$SnStickerPackImplCopyWith<$Res>
implements $SnStickerPackCopyWith<$Res> {
factory _$$SnStickerPackImplCopyWith(
_$SnStickerPackImpl value, $Res Function(_$SnStickerPackImpl) then) =
__$$SnStickerPackImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String prefix,
String name,
String description,
List<SnSticker>? stickers,
int accountId});
}
/// @nodoc
class __$$SnStickerPackImplCopyWithImpl<$Res>
extends _$SnStickerPackCopyWithImpl<$Res, _$SnStickerPackImpl>
implements _$$SnStickerPackImplCopyWith<$Res> {
__$$SnStickerPackImplCopyWithImpl(
_$SnStickerPackImpl _value, $Res Function(_$SnStickerPackImpl) _then)
: super(_value, _then);
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? prefix = null,
Object? name = null,
Object? description = null,
Object? stickers = freezed,
Object? accountId = null,
}) {
return _then(_$SnStickerPackImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
prefix: null == prefix
? _value.prefix
: prefix // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
stickers: freezed == stickers
? _value._stickers
: stickers // ignore: cast_nullable_to_non_nullable
as List<SnSticker>?,
accountId: null == accountId
? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnStickerPackImpl implements _SnStickerPack {
const _$SnStickerPackImpl(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.prefix,
required this.name,
required this.description,
required final List<SnSticker>? stickers,
required this.accountId})
: _stickers = stickers;
factory _$SnStickerPackImpl.fromJson(Map<String, dynamic> json) =>
_$$SnStickerPackImplFromJson(json);
@override
final int id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
final DateTime? deletedAt;
@override
final String prefix;
@override
final String name;
@override
final String description;
final List<SnSticker>? _stickers;
@override
List<SnSticker>? get stickers {
final value = _stickers;
if (value == null) return null;
if (_stickers is EqualUnmodifiableListView) return _stickers;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
final int accountId;
@override
String toString() {
return 'SnStickerPack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, prefix: $prefix, name: $name, description: $description, stickers: $stickers, accountId: $accountId)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnStickerPackImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.prefix, prefix) || other.prefix == prefix) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.description, description) ||
other.description == description) &&
const DeepCollectionEquality().equals(other._stickers, _stickers) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
createdAt,
updatedAt,
deletedAt,
prefix,
name,
description,
const DeepCollectionEquality().hash(_stickers),
accountId);
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnStickerPackImplCopyWith<_$SnStickerPackImpl> get copyWith =>
__$$SnStickerPackImplCopyWithImpl<_$SnStickerPackImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnStickerPackImplToJson(
this,
);
}
}
abstract class _SnStickerPack implements SnStickerPack {
const factory _SnStickerPack(
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final DateTime? deletedAt,
required final String prefix,
required final String name,
required final String description,
required final List<SnSticker>? stickers,
required final int accountId}) = _$SnStickerPackImpl;
factory _SnStickerPack.fromJson(Map<String, dynamic> json) =
_$SnStickerPackImpl.fromJson;
@override
int get id;
@override
DateTime get createdAt;
@override
DateTime get updatedAt;
@override
DateTime? get deletedAt;
@override
String get prefix;
@override
String get name;
@override
String get description;
@override
List<SnSticker>? get stickers;
@override
int get accountId;
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnStickerPackImplCopyWith<_$SnStickerPackImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -218,3 +218,66 @@ Map<String, dynamic> _$$SnAttachmentBoostImplToJson(
'attachment': instance.attachment.toJson(),
'account': instance.account,
};
_$SnStickerImpl _$$SnStickerImplFromJson(Map<String, dynamic> json) =>
_$SnStickerImpl(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
alias: json['alias'] as String,
name: json['name'] as String,
attachmentId: (json['attachment_id'] as num).toInt(),
attachment:
SnAttachment.fromJson(json['attachment'] as Map<String, dynamic>),
packId: (json['pack_id'] as num).toInt(),
pack: SnStickerPack.fromJson(json['pack'] as Map<String, dynamic>),
accountId: (json['account_id'] as num).toInt(),
);
Map<String, dynamic> _$$SnStickerImplToJson(_$SnStickerImpl instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'alias': instance.alias,
'name': instance.name,
'attachment_id': instance.attachmentId,
'attachment': instance.attachment.toJson(),
'pack_id': instance.packId,
'pack': instance.pack.toJson(),
'account_id': instance.accountId,
};
_$SnStickerPackImpl _$$SnStickerPackImplFromJson(Map<String, dynamic> json) =>
_$SnStickerPackImpl(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
prefix: json['prefix'] as String,
name: json['name'] as String,
description: json['description'] as String,
stickers: (json['stickers'] as List<dynamic>?)
?.map((e) => SnSticker.fromJson(e as Map<String, dynamic>))
.toList(),
accountId: (json['account_id'] as num).toInt(),
);
Map<String, dynamic> _$$SnStickerPackImplToJson(_$SnStickerPackImpl instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'prefix': instance.prefix,
'name': instance.name,
'description': instance.description,
'stickers': instance.stickers?.map((e) => e.toJson()).toList(),
'account_id': instance.accountId,
};

View File

@ -315,6 +315,7 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
}
return MaterialDesktopVideoControlsTheme(
key: Key('material-desktop-video-controls-theme-$_showOriginal'),
normal: MaterialDesktopVideoControlsThemeData(
buttonBarButtonSize: 24,
buttonBarButtonColor: Colors.white,
@ -324,14 +325,16 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
MaterialDesktopCustomButton(
iconSize: 24,
onPressed: _toggleOriginal,
icon: Builder(builder: (context) {
return _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24);
}),
icon: Icon(
_showOriginal ? Symbols.high_quality : Symbols.sd,
size: 24,
),
),
],
),
fullscreen: const MaterialDesktopVideoControlsThemeData(),
child: MaterialVideoControlsTheme(
key: Key('material-video-controls-theme-$_showOriginal'),
normal: MaterialVideoControlsThemeData(
buttonBarButtonSize: 24,
buttonBarButtonColor: Colors.white,

View File

@ -15,10 +15,10 @@ class AttachmentList extends StatefulWidget {
final List<SnAttachment?> data;
final bool bordered;
final bool gridded;
final bool noGrow;
final BoxFit fit;
final double? maxHeight;
final double? minWidth;
final double? maxWidth;
final EdgeInsets? padding;
const AttachmentList({
@ -26,10 +26,10 @@ class AttachmentList extends StatefulWidget {
required this.data,
this.bordered = false,
this.gridded = false,
this.noGrow = false,
this.fit = BoxFit.cover,
this.maxHeight,
this.minWidth,
this.maxWidth,
this.padding,
});
@ -106,76 +106,38 @@ class _AttachmentListState extends State<AttachmentList> {
}
if (widget.gridded) {
return Padding(
padding: widget.padding ?? EdgeInsets.zero,
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: StaggeredGrid.count(
crossAxisCount: math.min(widget.data.length, 2),
crossAxisSpacing: 4,
mainAxisSpacing: 4,
children: widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: Container(
constraints: constraints,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: BoxFit.cover,
),
),
onTap: () {
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null).cast(),
initialIndex: idx,
heroTags: heroTags,
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
),
)
.toList(),
),
return Container(
margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
);
}
return AspectRatio(
aspectRatio: (widget.data.firstOrNull?.data['ratio'] ?? 1).toDouble(),
child: Container(
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
child: ScrollConfiguration(
behavior: _AttachmentListScrollBehavior(),
child: ListView.separated(
shrinkWrap: true,
itemCount: widget.data.length,
itemBuilder: (context, idx) {
return Container(
constraints: constraints,
child: AspectRatio(
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
child: GestureDetector(
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: StaggeredGrid.count(
crossAxisCount: math.min(widget.data.length, 2),
crossAxisSpacing: 4,
mainAxisSpacing: 4,
children: widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: Container(
constraints: constraints,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: BoxFit.cover,
),
),
onTap: () {
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data:
widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
data: widget.data.where((ele) => ele != null).cast(),
initialIndex: idx,
heroTags: heroTags,
),
@ -183,44 +145,76 @@ class _AttachmentListState extends State<AttachmentList> {
rootNavigator: true,
);
},
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
)
.toList(),
),
),
);
}
return Container(
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
child: ScrollConfiguration(
behavior: _AttachmentListScrollBehavior(),
child: ListView.separated(
padding: widget.padding,
shrinkWrap: true,
itemCount: widget.data.length,
itemBuilder: (context, idx) {
return Container(
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
child: AspectRatio(
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
child: GestureDetector(
onTap: () {
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
initialIndex: idx,
heroTags: heroTags,
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(
data: widget.data[idx],
heroTag: heroTags[idx],
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: AttachmentItem(
data: widget.data[idx],
heroTag: heroTags[idx],
),
),
Positioned(
right: 8,
bottom: 8,
child: Chip(
label: Text('${idx + 1}/${widget.data.length}'),
),
),
Positioned(
right: 8,
bottom: 8,
child: Chip(
label: Text('${idx + 1}/${widget.data.length}'),
),
],
),
),
],
),
),
);
},
separatorBuilder: (context, index) => const Gap(8),
padding: widget.padding,
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
),
),
);
},
separatorBuilder: (context, index) => const Gap(8),
physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
),
),
);

View File

@ -231,7 +231,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
children: [
IgnorePointer(
child: AccountImage(
content: account!.avatar,
content: account?.avatar,
radius: 19,
),
),
@ -246,7 +246,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
style: Theme.of(context).textTheme.bodySmall,
),
Text(
account.nick,
account?.nick ?? 'unknown'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
@ -312,11 +312,6 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
]),
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['ShutterSpeed'] != null)
Text(
item.metadata['exif']?['ShutterSpeed'],
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['ISO'] != null)
Text(
'ISO${item.metadata['exif']?['ISO']}',

View File

@ -0,0 +1,86 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/widgets/dialog.dart';
class PendingAttachmentAltDialog extends StatefulWidget {
final PostWriteMedia media;
const PendingAttachmentAltDialog({super.key, required this.media});
@override
State<PendingAttachmentAltDialog> createState() => _PendingAttachmentAltDialogState();
}
class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog> {
final _contentController = TextEditingController();
@override
void initState() {
super.initState();
_contentController.text = widget.media.attachment!.alt;
}
bool _isBusy = false;
Future<void> _performAction() async {
if (_isBusy) return;
setState(() => _isBusy = true);
try {
final attach = context.read<SnAttachmentProvider>();
final result = await attach.updateOne(
widget.media.attachment!,
alt: _contentController.text,
);
if (!mounted) return;
attach.putCache([result]);
Navigator.pop(context, result);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
setState(() => _isBusy = false);
}
}
@override
void dispose() {
_contentController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('attachmentSetAlt').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _contentController,
decoration: InputDecoration(
labelText: 'fieldAttachmentAlt'.tr(),
border: const UnderlineInputBorder(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: [
TextButton(
onPressed: _isBusy ? null : () {
Navigator.pop(context);
},
child: Text('dialogDismiss'.tr()),
),
TextButton(
onPressed: _isBusy ? null : () => _performAction(),
child: Text('dialogConfirm'.tr()),
),
],
);
}
}

View File

@ -24,6 +24,7 @@ class ChatMessage extends StatelessWidget {
final Function(SnChatMessage)? onReply;
final Function(SnChatMessage)? onEdit;
final Function(SnChatMessage)? onDelete;
final EdgeInsets padding;
const ChatMessage({
super.key,
@ -35,6 +36,7 @@ class ChatMessage extends StatelessWidget {
this.onReply,
this.onEdit,
this.onDelete,
this.padding = const EdgeInsets.only(left: 12, right: 12),
});
@override
@ -53,7 +55,7 @@ class ChatMessage extends StatelessWidget {
iconOnRightSwipe: Symbols.edit,
swipeSensitivity: 20,
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null,
onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
child: ContextMenuArea(
contextMenu: ContextMenu(
entries: [
@ -87,84 +89,89 @@ class ChatMessage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMerged && !isCompact)
AccountImage(
content: user?.avatar,
)
else if (isMerged)
const Gap(40),
const Gap(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMerged)
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
if (isCompact)
AccountImage(
content: user?.avatar,
radius: 12,
).padding(right: 6),
Text(
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
).bold(),
const Gap(6),
Text(
dateFormatter.format(data.createdAt.toLocal()),
).fontSize(13),
],
),
if (isCompact) const Gap(4),
if (data.preload?.quoteEvent != null)
StyledWidget(Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
Padding(
padding: isCompact ? EdgeInsets.zero : padding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMerged && !isCompact)
AccountImage(
content: user?.avatar,
)
else if (isMerged)
const Gap(40),
const Gap(8),
Expanded(
child: Container(
constraints: BoxConstraints(maxWidth: 480),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMerged)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (isCompact)
AccountImage(
content: user?.avatar,
radius: 12,
).padding(right: 8),
Text(
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
).bold(),
const Gap(8),
Text(
dateFormatter.format(data.createdAt.toLocal()),
).fontSize(13),
],
),
),
padding: const EdgeInsets.only(
left: 4,
right: 4,
top: 8,
bottom: 6,
),
child: ChatMessage(
data: data.preload!.quoteEvent!,
isCompact: true,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
)).padding(bottom: 4, top: 4),
switch (data.type) {
'messages.new' => _ChatMessageText(data: data),
_ => _ChatMessageSystemNotify(data: data),
},
],
),
)
],
).opacity(isPending ? 0.5 : 1),
if (data.body['text'] != null && (data.body['text']?.isNotEmpty ?? false))
if (isCompact) const Gap(8),
if (data.preload?.quoteEvent != null)
StyledWidget(Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
padding: const EdgeInsets.only(
left: 4,
right: 4,
top: 8,
bottom: 6,
),
child: ChatMessage(
data: data.preload!.quoteEvent!,
isCompact: true,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
)).padding(bottom: 4, top: 4),
switch (data.type) {
'messages.new' => _ChatMessageText(data: data),
_ => _ChatMessageSystemNotify(data: data),
},
],
),
),
)
],
).opacity(isPending ? 0.5 : 1),
),
if (data.body['text'] != null && data.type == 'messages.new' && (data.body['text']?.isNotEmpty ?? false))
LinkPreviewWidget(text: data.body['text']!),
if (data.preload?.attachments?.isNotEmpty ?? false)
AttachmentList(
data: data.preload!.attachments!,
bordered: true,
gridded: true,
noGrow: true,
maxHeight: 520,
padding: const EdgeInsets.only(top: 8),
maxHeight: 560,
maxWidth: 480,
minWidth: 480,
padding: padding.copyWith(top: 8),
),
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(6),
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(8),
],
),
),

View File

@ -11,7 +11,6 @@ import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/markdown_content.dart';
import 'package:surface/widgets/post/post_media_pending_list.dart';
class ChatMessageInput extends StatefulWidget {
@ -33,6 +32,16 @@ class ChatMessageInputState extends State<ChatMessageInput> {
final TextEditingController _contentController = TextEditingController();
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
_contentController.addListener(() {
if (_contentController.text.isNotEmpty) {
widget.controller.pingTypingStatus();
}
});
}
void setReply(SnChatMessage? value) {
setState(() => _replyingMessage = value);
}
@ -161,75 +170,82 @@ class ChatMessageInputState extends State<ChatMessageInput> {
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Padding(
padding: _replyingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
child: _replyingMessage != null
? MaterialBanner(
padding: const EdgeInsets.only(left: 16.0),
leading: const Icon(Symbols.reply),
backgroundColor: Colors.transparent,
content: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_replyingMessage?.body['text'] != null)
MarkdownTextContent(
content: _replyingMessage?.body['text'],
),
],
child: _replyingMessage != null
? Container(
padding: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
actions: [
TextButton(
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.reply, size: 20),
const Gap(8),
Expanded(
child: Text(
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Gap(16),
InkWell(
child: Text('cancel'.tr()),
onPressed: () {
onTap: () {
setState(() => _replyingMessage = null);
},
),
],
)
: const SizedBox.shrink(),
),
).padding(vertical: 8),
)
: const SizedBox.shrink(),
)
.height(_replyingMessage != null ? 54 + 8 : 0, animate: true)
.height(_replyingMessage != null ? 38 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Padding(
padding: _editingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
child: _editingMessage != null
? MaterialBanner(
padding: const EdgeInsets.only(left: 16.0),
leading: const Icon(Symbols.edit),
backgroundColor: Colors.transparent,
content: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_editingMessage?.body['text'] != null)
MarkdownTextContent(
content: _editingMessage?.body['text'],
),
],
child: _editingMessage != null
? Container(
padding: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
actions: [
TextButton(
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.edit, size: 20),
const Gap(8),
Expanded(
child: Text(
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Gap(16),
InkWell(
child: Text('cancel'.tr()),
onPressed: () {
onTap: () {
_contentController.clear();
setState(() => _editingMessage = null);
},
),
],
)
: const SizedBox.shrink(),
),
).padding(vertical: 8),
)
: const SizedBox.shrink(),
)
.height(_editingMessage != null ? 54 + 8 : 0, animate: true)
.height(_editingMessage != null ? 38 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SizedBox(
height: 56,

View File

@ -0,0 +1,53 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/providers/user_directory.dart';
class ChatTypingIndicator extends StatelessWidget {
final ChatMessageController controller;
const ChatTypingIndicator({super.key, required this.controller});
@override
Widget build(BuildContext context) {
final ud = context.read<UserDirectoryProvider>();
return StyledWidget(controller.typingMembers.isEmpty
? const SizedBox.shrink()
: Container(
padding: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: Row(
children: [
const Icon(Symbols.more_horiz, weight: 600, size: 20),
const Gap(8),
Text(
'messageTyping'.plural(controller.typingMembers.length, args: [
controller.typingMembers
.map((ele) => (ele.nick?.isNotEmpty ?? false)
? ele.nick!
: ud.getAccountFromCache(ele.accountId)?.name ?? 'unknown')
.join(', '),
]),
),
],
),
))
.height(controller.typingMembers.isNotEmpty ? 38 : 0, animate: true)
.animate(
const Duration(milliseconds: 300),
Curves.fastLinearToSlowEaseIn,
);
}
}

View File

@ -6,7 +6,10 @@ import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:markdown/markdown.dart' as markdown;
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/universal_image.dart';
@ -21,6 +24,7 @@ class MarkdownTextContent extends StatelessWidget {
final String content;
final bool isSelectable;
final bool isAutoWarp;
final bool isEnlargeSticker;
final TextScaler? textScaler;
final List<SnAttachment?>? attachments;
@ -29,6 +33,7 @@ class MarkdownTextContent extends StatelessWidget {
required this.content,
this.isSelectable = false,
this.isAutoWarp = false,
this.isEnlargeSticker = false,
this.textScaler,
this.attachments,
});
@ -78,6 +83,7 @@ class MarkdownTextContent extends StatelessWidget {
<markdown.InlineSyntax>[
if (isAutoWarp) markdown.LineBreakSyntax(),
_UserNameCardInlineSyntax(),
_CustomEmoteInlineSyntax(context),
markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(),
markdown.CodeSyntax(),
@ -108,6 +114,38 @@ class MarkdownTextContent extends StatelessWidget {
if (url.startsWith('solink://')) {
final segments = url.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'stickers':
final alias = segments[1];
final st = context.read<SnStickerProvider>();
final sn = context.read<SnNetworkProvider>();
final double size = isEnlargeSticker ? 128 : 32;
return Container(
width: size,
height: size,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: FutureBuilder<SnSticker?>(
future: st.lookupSticker(alias),
builder: (context, snapshot) {
if (snapshot.hasData) {
return UniversalImage(
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
fit: BoxFit.cover,
width: size,
height: size,
cacheHeight: size,
cacheWidth: size,
);
}
return const SizedBox.shrink();
},
),
),
);
case 'attachments':
final attachment = attachments?.firstWhere(
(ele) => ele?.rid == segments[1],
@ -194,6 +232,28 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
}
}
class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
final BuildContext context;
_CustomEmoteInlineSyntax(this.context) : super(r':([-\w]+):');
@override
bool onMatch(markdown.InlineParser parser, Match match) {
final SnStickerProvider st = context.read<SnStickerProvider>();
final alias = match[1]!.toUpperCase();
if (st.hasNotSticker(alias)) {
parser.advanceBy(1);
return false;
}
final element = markdown.Element.empty('img');
element.attributes['src'] = 'solink://stickers/$alias';
parser.addNode(element);
return true;
}
}
class _MarkdownTextCodeElement extends MarkdownElementBuilder {
@override
Widget? visitElementAfter(

View File

@ -876,6 +876,7 @@ class _PostContentBody extends StatelessWidget {
if (data.body['content'] == null) return const SizedBox.shrink();
return MarkdownTextContent(
isSelectable: isSelectable,
isEnlargeSticker: true,
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: data.body['content'],
attachments: data.preload?.attachments,

View File

@ -21,6 +21,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_input.dart';
import 'package:surface/widgets/attachment/attachment_zoom.dart';
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
import 'package:surface/widgets/context_menu.dart';
import 'package:surface/widgets/dialog.dart';
@ -157,6 +158,16 @@ class PostMediaPendingList extends StatelessWidget {
onUpdate!(idx, result);
}
Future<void> _setAlt(BuildContext context, int idx) async {
final result = await showDialog<SnAttachment?>(
context: context,
builder: (context) => PendingAttachmentAltDialog(media: attachments[idx]),
);
if (result == null) return;
onUpdate!(idx, PostWriteMedia(result));
}
ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
return ContextMenu(
@ -169,6 +180,14 @@ class PostMediaPendingList extends StatelessWidget {
_compressVideo(context, idx);
},
),
if (media.attachment != null)
MenuItem(
label: 'attachmentSetAlt'.tr(),
icon: Symbols.description,
onSelected: () {
_setAlt(context, idx);
},
),
if (media.attachment != null)
MenuItem(
label: 'attachmentBoost'.tr(),

View File

@ -1,4 +1,4 @@
import 'package:extended_image/extended_image.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
@ -7,6 +7,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:flutter_animate/flutter_animate.dart';
// Keep this import to make the web image render work
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:surface/providers/config.dart';
class UniversalImage extends StatelessWidget {
@ -33,54 +34,64 @@ class UniversalImage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final quality = filterQuality ?? context.read<ConfigProvider>().imageQuality;
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final double? resizeHeight = cacheHeight != null ? (cacheHeight! * devicePixelRatio) : null;
final double? resizeWidth = cacheWidth != null ? (cacheWidth! * devicePixelRatio) : null;
return ExtendedImage.network(
url,
return Image(
filterQuality: filterQuality ?? context.read<ConfigProvider>().imageQuality,
image: kIsWeb
? UniversalImage.provider(url)
: ResizeImage(
UniversalImage.provider(url),
width: resizeWidth?.round(),
height: resizeHeight?.round(),
policy: ResizeImagePolicy.fit,
),
width: width,
height: height,
fit: fit,
cache: true,
compressionRatio: kIsWeb ? 1 : switch(quality) {
FilterQuality.high => 1,
FilterQuality.medium => 0.75,
FilterQuality.low => 0.5,
FilterQuality.none => 0.25,
},
filterQuality: quality,
enableLoadState: true,
retries: 3,
loadStateChanged: (ExtendedImageState state) {
if (state.extendedImageLoadState == LoadState.completed) {
return state.completedWidget;
} else if (state.extendedImageLoadState == LoadState.failed) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Container(
constraints: const BoxConstraints(maxWidth: 280),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimateWidgetExtensions(Icon(Symbols.close, size: 24))
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
Text(
state.lastException.toString(),
textAlign: TextAlign.center,
loadingBuilder: noProgressIndicator
? null
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: TweenAnimationBuilder(
tween: Tween(
begin: 0,
end: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: 0,
),
],
).center(),
),
);
}
return Center(
child: CircularProgressIndicator(
value: state.loadingProgress != null
? state.loadingProgress!.cumulativeBytesLoaded / state.loadingProgress!.expectedTotalBytes!
: null,
),
);
},
duration: const Duration(milliseconds: 300),
builder: (context, value, _) => CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
),
),
);
},
errorBuilder: noErrorWidget
? null
: (context, error, stackTrace) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Container(
constraints: const BoxConstraints(maxWidth: 280),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimateWidgetExtensions(Icon(Symbols.close, size: 24))
.animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms),
Text(
error.toString(),
textAlign: TextAlign.center,
),
],
).center(),
),
);
},
);
}
@ -88,10 +99,9 @@ class UniversalImage extends StatelessWidget {
// This place used to use network image or cached network image depending on the platform.
// But now the cached network image is working on every platform.
// So we just use it now.
return ExtendedNetworkImageProvider(
return CachedNetworkImageProvider(
url,
cache: true,
retries: 3,
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
);
}
}

View File

@ -26,6 +26,7 @@ import path_provider_foundation
import screen_brightness_macos
import share_plus
import shared_preferences_foundation
import sqflite_darwin
import url_launcher_macos
import video_compress
import wakelock_plus
@ -52,6 +53,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))

View File

@ -165,6 +165,9 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- url_launcher_macos (0.0.1):
- FlutterMacOS
- video_compress (0.3.0):
@ -198,6 +201,7 @@ DEPENDENCIES:
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
@ -267,6 +271,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
video_compress:
@ -311,6 +317,7 @@ SPEC CHECKSUMS:
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269

View File

@ -182,6 +182,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.9.3"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
cassowary:
dependency: transitive
description:
@ -430,22 +454,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.7"
extended_image:
dependency: "direct main"
description:
name: extended_image
sha256: "93890a88d89ce017789f6c031c32ad8d2c685f1a5c25c169550746d973ca5e44"
url: "https://pub.dev"
source: hosted
version: "9.0.9"
extended_image_library:
dependency: transitive
description:
name: extended_image_library
sha256: "9a94ec9314aa206cfa35f16145c3cd6e2c924badcc670eaaca8a3a8063a68cd7"
url: "https://pub.dev"
source: hosted
version: "4.0.5"
fading_edge_scrollview:
dependency: transitive
description:
@ -635,6 +643,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.2.2"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
url: "https://pub.dev"
source: hosted
version: "3.4.1"
flutter_colorpicker:
dependency: "direct main"
description:
@ -692,10 +708,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "1152ab0067ca5a2ebeb862fe0a762057202cceb22b7e62692dcbabf6483891bb"
sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
version: "2.4.4"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -750,10 +766,10 @@ packages:
dependency: "direct main"
description:
name: flutter_webrtc
sha256: "3efe9828f19a07d29a51a726759ad0c70a840d231548a1c7d0332075a94db1df"
sha256: e82ffd0d0b79621c5554eed73509d7f5bd286d57fef29a573846785c65237fb1
url: "https://pub.dev"
source: hosted
version: "0.12.5+hotfix.1"
version: "0.12.5+hotfix.2"
freezed:
dependency: "direct dev"
description:
@ -874,14 +890,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.2"
http_client_helper:
dependency: transitive
description:
name: http_client_helper
sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
http_multi_server:
dependency: transitive
description:
@ -894,10 +902,10 @@ packages:
dependency: transitive
description:
name: http_parser
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
version: "4.1.2"
icons_launcher:
dependency: "direct dev"
description:
@ -942,10 +950,10 @@ packages:
dependency: transitive
description:
name: image_picker_ios
sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
url: "https://pub.dev"
source: hosted
version: "0.8.12+1"
version: "0.8.12+2"
image_picker_linux:
dependency: transitive
description:
@ -1078,10 +1086,10 @@ packages:
dependency: "direct main"
description:
name: livekit_client
sha256: "7cdeb3eaeec7fb70a4cf88d9caabccbef9e3bd5f0b23c086320bc5c9acb2770b"
sha256: a19bcf8640b45e0730b1e3e3e78be7882dad680c6ebe8ae75294fd8d4612450d
url: "https://pub.dev"
source: hosted
version: "2.3.4"
version: "2.3.4+hotfix.2"
logging:
dependency: transitive
description:
@ -1134,10 +1142,10 @@ packages:
dependency: "direct main"
description:
name: material_symbols_icons
sha256: "64404f47f8e0a9d20478468e5decef867a688660bad7173adcd20418d7f892c9"
sha256: "89aac72d25dd49303f71b3b1e70f8374791846729365b25bebc2a2531e5b86cd"
url: "https://pub.dev"
source: hosted
version: "4.2801.0"
version: "4.2801.1"
media_kit:
dependency: "direct main"
description:
@ -1242,6 +1250,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.0"
octo_image:
dependency: transitive
description:
name: octo_image
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
package_config:
dependency: transitive
description:
@ -1530,6 +1546,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.1"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
safe_local_storage:
dependency: transitive
description:
@ -1622,10 +1646,10 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: "3c7e73920c694a436afaf65ab60ce3453d91f84208d761fbd83fc21182134d93"
sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a
url: "https://pub.dev"
source: hosted
version: "2.3.4"
version: "2.3.5"
shared_preferences_android:
dependency: transitive
description:
@ -1743,6 +1767,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqflite:
dependency: transitive
description:
name: sqflite
sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
url: "https://pub.dev"
source: hosted
version: "2.5.4+6"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
url: "https://pub.dev"
source: hosted
version: "2.4.1+1"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
stack_trace:
dependency: transitive
description:
@ -2067,10 +2131,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69"
sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29"
url: "https://pub.dev"
source: hosted
version: "5.9.0"
version: "5.10.0"
win32_registry:
dependency: transitive
description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 2.2.1+42
version: 2.2.1+47
environment:
sdk: ^3.5.4
@ -115,7 +115,7 @@ dependencies:
flutter_webrtc: ^0.12.5+hotfix.1
slide_countdown: ^2.0.2
video_compress: ^3.1.3
extended_image: ^9.0.9
cached_network_image: ^3.4.1
dev_dependencies:
flutter_test: