Compare commits
	
		
			18 Commits
		
	
	
		
			2.2.1+42
			...
			ecf362cffc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ecf362cffc | |||
| f4ab7671d8 | |||
| a2a3018917 | |||
| 0bdb664000 | |||
| 9c3b61ce57 | |||
| d06df3d278 | |||
| 547ba19e61 | |||
| cb05ff2e9e | |||
| f614da7918 | |||
| a3c8dafff9 | |||
| fa978a7cd1 | |||
| aaa0a562b4 | |||
| 590a4ce2a6 | |||
| f26edce071 | |||
| 603799ea32 | |||
| a32baf7798 | |||
| 498c9af663 | |||
| 202dbff6d3 | 
| @@ -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 { | ||||
|   | ||||
							
								
								
									
										19
									
								
								api/Paperclip/Stickers/Create Sticker Pack.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								api/Paperclip/Stickers/Create Sticker Pack.bru
									
									
									
									
									
										Normal 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!" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										20
									
								
								api/Paperclip/Stickers/Create Sticker.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/Paperclip/Stickers/Create Sticker.bru
									
									
									
									
									
										Normal 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 | ||||
|   } | ||||
| } | ||||
| @@ -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
									
								
							
							
						
						
									
										7
									
								
								api/collection.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| auth { | ||||
|   mode: bearer | ||||
| } | ||||
|  | ||||
| auth:bearer { | ||||
|   token: {{atk}} | ||||
| } | ||||
| @@ -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 {}", | ||||
|   | ||||
| @@ -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": "{} 跨年", | ||||
|   | ||||
| @@ -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": "{} 跨年", | ||||
|   | ||||
| @@ -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": "{} 跨年", | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										38
									
								
								lib/providers/sn_sticker.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								lib/providers/sn_sticker.dart
									
									
									
									
									
										Normal 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; | ||||
|   } | ||||
| } | ||||
| @@ -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)); | ||||
| } | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|             ], | ||||
|           ); | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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)), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|   | ||||
| @@ -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); | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|     }; | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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, | ||||
|             ), | ||||
|           ), | ||||
|         ); | ||||
|   | ||||
| @@ -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']}', | ||||
|   | ||||
							
								
								
									
										86
									
								
								lib/widgets/attachment/pending_attachment_alt.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								lib/widgets/attachment/pending_attachment_alt.dart
									
									
									
									
									
										Normal 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()), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -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), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
							
								
								
									
										53
									
								
								lib/widgets/chat/chat_typing_indicator.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								lib/widgets/chat/chat_typing_indicator.dart
									
									
									
									
									
										Normal 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, | ||||
|         ); | ||||
|   } | ||||
| } | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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(), | ||||
|   | ||||
| @@ -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, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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")) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										144
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										144
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -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: | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user