Compare commits
	
		
			11 Commits
		
	
	
		
			2.1.1+38
			...
			619c90cdd9
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 619c90cdd9 | |||
| 168d51c9fe | |||
| d4b831f98e | |||
| 4d96a15c31 | |||
| 06dd3e092a | |||
| 82fe9e287a | |||
| dc1c285de1 | |||
| 5a3313e94f | |||
| 61032c84f1 | |||
| 36a5b8fb39 | |||
| 3eda464e03 | 
							
								
								
									
										30
									
								
								api/Passport/Developer Notify All Users.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								api/Passport/Developer Notify All Users.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| meta { | ||||
|   name: Developer Notify All Users | ||||
|   type: http | ||||
|   seq: 1 | ||||
| } | ||||
|  | ||||
| post { | ||||
|   url: {{endpoint}}/cgi/id/dev/notify/all | ||||
|   body: json | ||||
|   auth: bearer | ||||
| } | ||||
|  | ||||
| auth:bearer { | ||||
|   token: {{atk}} | ||||
| } | ||||
|  | ||||
| body:json { | ||||
|   { | ||||
|     "client_id": "{{third_client_id}}", | ||||
|     "client_secret":"{{third_client_tk}}", | ||||
|     "type": "general", | ||||
|     "subject": "Merry Christmas!", | ||||
|     "subtitle": "一条来自 Solar Network 团队的信息", | ||||
|     "content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄", | ||||
|     "metadata": { | ||||
|       "image": "6EqsYQwmFRCkbmhR" | ||||
|     }, | ||||
|     "priority": 10 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										9
									
								
								api/bruno.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								api/bruno.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| { | ||||
|   "version": "1", | ||||
|   "name": "Solar Network", | ||||
|   "type": "collection", | ||||
|   "ignore": [ | ||||
|     "node_modules", | ||||
|     ".git" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										8
									
								
								api/environments/Prod.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								api/environments/Prod.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| vars { | ||||
|   endpoint: https://api.sn.solsynth.dev | ||||
|   third_client_id: alphabot | ||||
| } | ||||
| vars:secret [ | ||||
|   atk, | ||||
|   third_client_tk | ||||
| ] | ||||
| @@ -281,16 +281,22 @@ | ||||
|     "one": "{} attachment", | ||||
|     "other": "{} attachments" | ||||
|   }, | ||||
|   "fieldAttachmentRandomId": "Random ID", | ||||
|   "addAttachmentFromAlbum": "Add from album", | ||||
|   "addAttachmentFromClipboard": "Paste file", | ||||
|   "addAttachmentFromCameraPhoto": "Take photo", | ||||
|   "addAttachmentFromCameraVideo": "Take video", | ||||
|   "addAttachmentFromRandomId": "Link via RID", | ||||
|   "attachmentPastedImage": "Pasted Image", | ||||
|   "attachmentInsertLink": "Insert Link", | ||||
|   "attachmentSetAsPostThumbnail": "Set as post thumbnail", | ||||
|   "attachmentUnsetAsPostThumbnail": "Unset as post thumbnail", | ||||
|   "attachmentSetThumbnail": "Set thumbnail", | ||||
|   "attachmentCopyRandomId": "Copy RID", | ||||
|   "attachmentUpload": "Upload", | ||||
|   "attachmentInputDialog": "Upload attachments", | ||||
|   "attachmentInputUseRandomId": "Use Random ID", | ||||
|   "attachmentInputNew": "New Upload", | ||||
|   "notification": "Notification", | ||||
|   "notificationUnreadCount": { | ||||
|     "zero": "All notifications read", | ||||
| @@ -378,9 +384,26 @@ | ||||
|   "dailyCheckNegativeHint5Description": "Lost connection at a crucial moment", | ||||
|   "dailyCheckNegativeHint6": "Going out", | ||||
|   "dailyCheckNegativeHint6Description": "Forgot your umbrella and got caught in the rain", | ||||
|   "happyBirthday": "Happy birthday, {}!", | ||||
|   "celebrateBirthday": "Happy birthday, {}!", | ||||
|   "celebrateMerryXmas": "Merry christmas, {}!", | ||||
|   "celebrateNewYear": "Happy new year, {}!", | ||||
|   "celebrateValentineDay": "Today is valentine's day, {}!", | ||||
|   "celebrateLaborDay": "Today is labor day, {}.", | ||||
|   "celebrateMotherDay": "Today is mother's day, {}.", | ||||
|   "celebrateChildrenDay": "Today is children's day, {}!", | ||||
|   "celebrateFatherDay": "Today is father's day, {}.", | ||||
|   "celebrateHalloween": "Happy halloween, {}!", | ||||
|   "celebrateThanksgiving": "Today is thanksgiving day, {}!", | ||||
|   "pendingBirthday": "Birthday in {}", | ||||
|   "pendingMerryXmas": "Christmas in {}", | ||||
|   "pendingNewYear": "New year in {}", | ||||
|   "pendingValentineDay": "Valentine's day in {}", | ||||
|   "pendingLaborDay": "Labor day in {}", | ||||
|   "pendingMotherDay": "Mother's day in {}", | ||||
|   "pendingChildrenDay": "Children's day in {}", | ||||
|   "pendingFatherDay": "Father's day in {}", | ||||
|   "pendingHalloween": "Halloween in {}", | ||||
|   "pendingThanksgiving": "Thanksgiving day in {}", | ||||
|   "friendNew": "Add Friend", | ||||
|   "friendRequests": "Friend Requests", | ||||
|   "friendRequestsDescription": { | ||||
| @@ -488,5 +511,7 @@ | ||||
|   "postCategoryNews": "News", | ||||
|   "postCategoryKnowledge": "Knowledge", | ||||
|   "postCategoryLiterature": "Literature", | ||||
|   "postCategoryUncategorized": "Uncategorized" | ||||
|   "postCategoryFunny": "Funny", | ||||
|   "postCategoryUncategorized": "Uncategorized", | ||||
|   "waitingForUpload": "Waiting for upload" | ||||
| } | ||||
|   | ||||
| @@ -279,16 +279,22 @@ | ||||
|     "one": "{} 个附件", | ||||
|     "other": "{} 个附件" | ||||
|   }, | ||||
|   "fieldAttachmentRandomId": "访问 ID", | ||||
|   "addAttachmentFromAlbum": "从相册中添加附件", | ||||
|   "addAttachmentFromClipboard": "粘贴附件", | ||||
|   "addAttachmentFromCameraPhoto": "拍摄照片", | ||||
|   "addAttachmentFromCameraVideo": "拍摄视频", | ||||
|   "addAttachmentFromRandomId": "通过访问 ID 链接", | ||||
|   "attachmentPastedImage": "粘贴的图片", | ||||
|   "attachmentInsertLink": "插入连接", | ||||
|   "attachmentSetAsPostThumbnail": "设置为帖子缩略图", | ||||
|   "attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图", | ||||
|   "attachmentSetThumbnail": "设置缩略图", | ||||
|   "attachmentCopyRandomId": "复制访问 ID", | ||||
|   "attachmentUpload": "上传", | ||||
|   "attachmentInputDialog": "上传附件", | ||||
|   "attachmentInputUseRandomId": "使用访问 ID", | ||||
|   "attachmentInputNew": "新上传附件", | ||||
|   "notification": "通知", | ||||
|   "notificationUnreadCount": { | ||||
|     "zero": "无未读通知", | ||||
| @@ -376,9 +382,26 @@ | ||||
|   "dailyCheckNegativeHint5Description": "关键时刻断网", | ||||
|   "dailyCheckNegativeHint6": "出门", | ||||
|   "dailyCheckNegativeHint6Description": "忘带伞遇上大雨", | ||||
|   "happyBirthday": "生日快乐,{}!", | ||||
|   "celebrateBirthday": "生日快乐,{}!", | ||||
|   "celebrateMerryXmas": "圣诞快乐,{}!", | ||||
|   "celebrateNewYear": "新年快乐,{}!", | ||||
|   "celebrateValentineDay": "今天是情人节,{}!", | ||||
|   "celebrateLaborDay": "今天是劳动节,{}。", | ||||
|   "celebrateMotherDay": "今天是母亲节,{}。", | ||||
|   "celebrateChildrenDay": "今天是儿童节,{}!", | ||||
|   "celebrateFatherDay": "今天是父亲节,{}。", | ||||
|   "celebrateHalloween": "快乐在圣诞节,{}!", | ||||
|   "celebrateThanksgiving": "今天是感恩节,{}!", | ||||
|   "pendingBirthday": "{} 过生日", | ||||
|   "pendingMerryXmas": "{} 过圣诞节", | ||||
|   "pendingNewYear": "{} 跨年", | ||||
|   "pendingValentineDay": "{} 过情人节", | ||||
|   "pendingLaborDay": "{} 过劳动节", | ||||
|   "pendingMotherDay": "{} 过母亲节", | ||||
|   "pendingChildrenDay": "{} 过儿童节", | ||||
|   "pendingFatherDay": "{} 过父亲节", | ||||
|   "pendingHalloween": "{} 过圣诞节", | ||||
|   "pendingThanksgiving": "{} 过感恩节", | ||||
|   "friendNew": "添加好友", | ||||
|   "friendRequests": "好友请求", | ||||
|   "friendRequestsDescription": { | ||||
| @@ -486,5 +509,7 @@ | ||||
|   "postCategoryNews": "新闻", | ||||
|   "postCategoryKnowledge": "知识", | ||||
|   "postCategoryLiterature": "文学", | ||||
|   "postCategoryUncategorized": "未分类" | ||||
|   "postCategoryFunny": "搞笑", | ||||
|   "postCategoryUncategorized": "未分类", | ||||
|   "waitingForUpload": "等待上传" | ||||
| } | ||||
|   | ||||
| @@ -376,9 +376,26 @@ | ||||
|   "dailyCheckNegativeHint5Description": "關鍵時刻斷網", | ||||
|   "dailyCheckNegativeHint6": "出門", | ||||
|   "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", | ||||
|   "happyBirthday": "生日快樂,{}!", | ||||
|   "celebrateBirthday": "生日快樂,{}!", | ||||
|   "celebrateMerryXmas": "聖誕快樂,{}!", | ||||
|   "celebrateNewYear": "新年快樂,{}!", | ||||
|   "celebrateValentineDay": "今天是情人節,{}!", | ||||
|   "celebrateLaborDay": "今天是勞動節,{}。", | ||||
|   "celebrateMotherDay": "今天是母親節,{}。", | ||||
|   "celebrateChildrenDay": "今天是兒童節,{}!", | ||||
|   "celebrateFatherDay": "今天是父親節,{}。", | ||||
|   "celebrateHalloween": "快樂在聖誕節,{}!", | ||||
|   "celebrateThanksgiving": "今天是感恩節,{}!", | ||||
|   "pendingBirthday": "{} 過生日", | ||||
|   "pendingMerryXmas": "{} 過聖誕節", | ||||
|   "pendingNewYear": "{} 跨年", | ||||
|   "pendingValentineDay": "{} 過情人節", | ||||
|   "pendingLaborDay": "{} 過勞動節", | ||||
|   "pendingMotherDay": "{} 過母親節", | ||||
|   "pendingChildrenDay": "{} 過兒童節", | ||||
|   "pendingFatherDay": "{} 過父親節", | ||||
|   "pendingHalloween": "{} 過聖誕節", | ||||
|   "pendingThanksgiving": "{} 過感恩節", | ||||
|   "friendNew": "添加好友", | ||||
|   "friendRequests": "好友請求", | ||||
|   "friendRequestsDescription": { | ||||
| @@ -486,5 +503,6 @@ | ||||
|   "postCategoryNews": "新聞", | ||||
|   "postCategoryKnowledge": "知識", | ||||
|   "postCategoryLiterature": "文學", | ||||
|   "postCategoryFunny": "搞笑", | ||||
|   "postCategoryUncategorized": "未分類" | ||||
| } | ||||
|   | ||||
| @@ -376,9 +376,26 @@ | ||||
|   "dailyCheckNegativeHint5Description": "關鍵時刻斷網", | ||||
|   "dailyCheckNegativeHint6": "出門", | ||||
|   "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", | ||||
|   "happyBirthday": "生日快樂,{}!", | ||||
|   "celebrateBirthday": "生日快樂,{}!", | ||||
|   "celebrateMerryXmas": "聖誕快樂,{}!", | ||||
|   "celebrateNewYear": "新年快樂,{}!", | ||||
|   "celebrateValentineDay": "今天是情人節,{}!", | ||||
|   "celebrateLaborDay": "今天是勞動節,{}。", | ||||
|   "celebrateMotherDay": "今天是母親節,{}。", | ||||
|   "celebrateChildrenDay": "今天是兒童節,{}!", | ||||
|   "celebrateFatherDay": "今天是父親節,{}。", | ||||
|   "celebrateHalloween": "快樂在聖誕節,{}!", | ||||
|   "celebrateThanksgiving": "今天是感恩節,{}!", | ||||
|   "pendingBirthday": "{} 過生日", | ||||
|   "pendingMerryXmas": "{} 過聖誕節", | ||||
|   "pendingNewYear": "{} 跨年", | ||||
|   "pendingValentineDay": "{} 過情人節", | ||||
|   "pendingLaborDay": "{} 過勞動節", | ||||
|   "pendingMotherDay": "{} 過母親節", | ||||
|   "pendingChildrenDay": "{} 過兒童節", | ||||
|   "pendingFatherDay": "{} 過父親節", | ||||
|   "pendingHalloween": "{} 過聖誕節", | ||||
|   "pendingThanksgiving": "{} 過感恩節", | ||||
|   "friendNew": "新增好友", | ||||
|   "friendRequests": "好友請求", | ||||
|   "friendRequestsDescription": { | ||||
| @@ -486,5 +503,6 @@ | ||||
|   "postCategoryNews": "新聞", | ||||
|   "postCategoryKnowledge": "知識", | ||||
|   "postCategoryLiterature": "文學", | ||||
|   "postCategoryFunny": "搞笑", | ||||
|   "postCategoryUncategorized": "未分類" | ||||
| } | ||||
|   | ||||
| @@ -173,7 +173,7 @@ PODS: | ||||
|   - in_app_review (2.0.0): | ||||
|     - Flutter | ||||
|   - Kingfisher (8.1.3) | ||||
|   - livekit_client (2.3.2): | ||||
|   - livekit_client (2.3.3): | ||||
|     - Flutter | ||||
|     - flutter_webrtc | ||||
|     - WebRTC-SDK (= 125.6422.06) | ||||
| @@ -386,7 +386,7 @@ SPEC CHECKSUMS: | ||||
|   image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 | ||||
|   in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 | ||||
|   Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef | ||||
|   livekit_client: 6108dad8b77db3142bafd4c630f471d0a54335cd | ||||
|   livekit_client: 02cf2cc4357a655af12ccee70ff5596ae4e6feef | ||||
|   media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 | ||||
|   media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a | ||||
|   media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e | ||||
|   | ||||
| @@ -80,7 +80,11 @@ class NotificationService: UNNotificationServiceExtension { | ||||
|          | ||||
|         let metadataCopy = metadata as? [String: String] ?? [:] | ||||
|         let avatarUrl = getAttachmentUrl(for: avatarIdentifier) | ||||
|         KingfisherManager.shared.retrieveImage(with: URL(string: avatarUrl)!, completionHandler: { result in | ||||
|          | ||||
|         let targetSize = 640 | ||||
|         let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit) | ||||
|          | ||||
|         KingfisherManager.shared.retrieveImage(with: URL(string: avatarUrl)!, options: [.processor(scaleProcessor)], completionHandler: { result in | ||||
|             var image: Data? | ||||
|             switch result { | ||||
|             case .success(let value): | ||||
|   | ||||
| @@ -30,6 +30,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/special_day.dart'; | ||||
| import 'package:surface/providers/theme.dart'; | ||||
| import 'package:surface/providers/user_directory.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| @@ -148,6 +149,9 @@ class SolianApp extends StatelessWidget { | ||||
|             ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)), | ||||
|             ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)), | ||||
|             ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)), | ||||
|  | ||||
|             // Additional helper layer | ||||
|             Provider(create: (ctx) => SpecialDayProvider(ctx)), | ||||
|           ], | ||||
|           child: _AppDelegate(), | ||||
|         ), | ||||
| @@ -265,6 +269,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||
|       // The Network initialization will also save initialize the Config, so it not need to be initialized again | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.initializeUserAgent(); | ||||
|       await sn.setConfigWithNative(); | ||||
|       if (!mounted) return; | ||||
|       final ua = context.read<UserProvider>(); | ||||
|       await ua.initialize(); | ||||
|   | ||||
| @@ -215,4 +215,18 @@ class SnAttachmentProvider { | ||||
|  | ||||
|     return place; | ||||
|   } | ||||
|  | ||||
|   Future<SnAttachment> updateOne( | ||||
|     int id, | ||||
|     String alt, { | ||||
|     required Map<String, dynamic> metadata, | ||||
|     bool isMature = false, | ||||
|   }) async { | ||||
|     final resp = await _sn.client.put('/cgi/uc/attachments/$id', data: { | ||||
|       'alt': alt, | ||||
|       'metadata': metadata, | ||||
|       'is_mature': isMature, | ||||
|     }); | ||||
|     return SnAttachment.fromJson(resp.data); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -68,9 +68,8 @@ class SnNetworkProvider { | ||||
|     _config.initialize().then((_) { | ||||
|       _prefs = _config.prefs; | ||||
|       client.options.baseUrl = _config.serverUrl; | ||||
|       if (!context.mounted) return; | ||||
|       _home.saveWidgetData("nex_server_url", client.options.baseUrl); | ||||
|     }); | ||||
|  | ||||
|   } | ||||
|  | ||||
|   static Future<Dio> createOffContextClient() async { | ||||
| @@ -109,6 +108,10 @@ class SnNetworkProvider { | ||||
|     return client; | ||||
|   } | ||||
|  | ||||
|   Future<void> setConfigWithNative() async { | ||||
|     _home.saveWidgetData("nex_server_url", client.options.baseUrl); | ||||
|   } | ||||
|  | ||||
|   static Future<String> _getUserAgent() async { | ||||
|     final String platformInfo; | ||||
|     if (kIsWeb) { | ||||
|   | ||||
							
								
								
									
										136
									
								
								lib/providers/special_day.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								lib/providers/special_day.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| import 'package:flutter/widgets.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
|  | ||||
| // Stored as key: month, day | ||||
| const Map<String, (int, int)> kSpecialDays = { | ||||
|   // Birthday is dynamically generated according to the user's profile | ||||
|   'NewYear': (1, 1), | ||||
|   'ValentineDay': (2, 14), | ||||
|   'LaborDay': (5, 1), | ||||
|   'MotherDay': (5, 11), | ||||
|   'ChildrenDay': (6, 1), | ||||
|   'FatherDay': (8, 8), | ||||
|   'Halloween': (10, 31), | ||||
|   'Thanksgiving': (11, 28), | ||||
|   'MerryXmas': (12, 25), | ||||
| }; | ||||
|  | ||||
| const Map<String, String> kSpecialDaysSymbol = { | ||||
|   'Birthday': '🎂', | ||||
|   'NewYear': '🎉', | ||||
|   'MerryXmas': '🎄', | ||||
|   'ValentineDay': '💑', | ||||
|   'LaborDay': '🏋️', | ||||
|   'MotherDay': '👩', | ||||
|   'ChildrenDay': '👶', | ||||
|   'FatherDay': '👨', | ||||
|   'Halloween': '🎃', | ||||
|   'Thanksgiving': '🎅', | ||||
| }; | ||||
|  | ||||
| class SpecialDayProvider { | ||||
|   late final UserProvider _user; | ||||
|  | ||||
|   SpecialDayProvider(BuildContext context) { | ||||
|     _user = context.read<UserProvider>(); | ||||
|   } | ||||
|  | ||||
|   List<String> getSpecialDays() { | ||||
|     final now = DateTime.now().toLocal(); | ||||
|     final birthday = _user.user?.profile?.birthday?.toLocal(); | ||||
|     final isBirthday = birthday != null && birthday.day == now.day && birthday.month == now.month; | ||||
|  | ||||
|     return [ | ||||
|       if (isBirthday) 'Birthday', | ||||
|       ...kSpecialDays.keys.where( | ||||
|         (key) => kSpecialDays[key]!.$1 == now.month && kSpecialDays[key]!.$2 == now.day, | ||||
|       ), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   (String, DateTime)? getLastSpecialDay() { | ||||
|     final now = DateTime.now().toLocal(); | ||||
|     final birthday = _user.user?.profile?.birthday?.toLocal(); | ||||
|  | ||||
|     final Map<String, (int, int)> specialDays = { | ||||
|       if (birthday != null) 'Birthday': (birthday.month, birthday.day), | ||||
|       ...kSpecialDays, | ||||
|     }; | ||||
|  | ||||
|     DateTime? lastDate; | ||||
|     String? lastEvent; | ||||
|  | ||||
|     for (final entry in specialDays.entries) { | ||||
|       final eventName = entry.key; | ||||
|       final (month, day) = entry.value; | ||||
|  | ||||
|       var specialDayThisYear = DateTime(now.year, month, day); | ||||
|       var specialDayLastYear = DateTime(now.year - 1, month, day); | ||||
|  | ||||
|       if (specialDayThisYear.isBefore(now)) { | ||||
|         if (lastDate == null || specialDayThisYear.isAfter(lastDate)) { | ||||
|           lastDate = specialDayThisYear; | ||||
|           lastEvent = eventName; | ||||
|         } | ||||
|       } else if (specialDayLastYear.isBefore(now)) { | ||||
|         if (lastDate == null || specialDayLastYear.isAfter(lastDate)) { | ||||
|           lastDate = specialDayLastYear; | ||||
|           lastEvent = eventName; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (lastEvent != null && lastDate != null) { | ||||
|       return (lastEvent, lastDate); | ||||
|     } | ||||
|  | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   (String, DateTime)? getNextSpecialDay() { | ||||
|     final now = DateTime.now().toLocal(); | ||||
|     final birthday = _user.user?.profile?.birthday?.toLocal(); | ||||
|  | ||||
|     // Stored as key: month, day | ||||
|     final Map<String, (int, int)> specialDays = { | ||||
|       if (birthday != null) 'Birthday': (birthday.month, birthday.day), | ||||
|       ...kSpecialDays, | ||||
|     }; | ||||
|  | ||||
|     DateTime? closestDate; | ||||
|     String? closestEvent; | ||||
|  | ||||
|     for (final entry in specialDays.entries) { | ||||
|       final eventName = entry.key; | ||||
|       final (month, day) = entry.value; | ||||
|  | ||||
|       // Calculate the special day's DateTime in the current year | ||||
|       var specialDay = DateTime(now.year, month, day); | ||||
|  | ||||
|       // If the special day has already passed this year, consider it for the next year | ||||
|       if (specialDay.isBefore(now)) { | ||||
|         specialDay = DateTime(now.year + 1, month, day); | ||||
|       } | ||||
|  | ||||
|       // Check if this special day is closer than the previously found one | ||||
|       if (closestDate == null || specialDay.isBefore(closestDate)) { | ||||
|         closestDate = specialDay; | ||||
|         closestEvent = eventName; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (closestEvent != null && closestDate != null) { | ||||
|       return (closestEvent, closestDate); | ||||
|     } | ||||
|  | ||||
|     // No special day found | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   double getSpecialDayProgress(DateTime last, DateTime next) { | ||||
|     final totalDuration = next.difference(last).inSeconds.toDouble(); | ||||
|     final elapsedDuration = DateTime.now().difference(last).inSeconds.toDouble(); | ||||
|     return (elapsedDuration / totalDuration).clamp(0.0, 1.0); | ||||
|   } | ||||
| } | ||||
| @@ -31,10 +31,10 @@ class UserProvider extends ChangeNotifier { | ||||
|     final value = _config.prefs.getString(kAtkStoreKey); | ||||
|     isAuthorized = value != null; | ||||
|     notifyListeners(); | ||||
|     refreshUser().then((value) { | ||||
|     refreshUser().then((value) async { | ||||
|       if (value != null) { | ||||
|         log('Logged in as @${value.name}'); | ||||
|         _home.saveWidgetData('user', value.toJson()); | ||||
|         log('Atk: ${await atk}'); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -13,7 +13,7 @@ class HomeWidgetProvider { | ||||
|  | ||||
|   Future<void> initialize() async { | ||||
|     if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return; | ||||
|     if (!kIsWeb && Platform.isIOS) { | ||||
|     if (Platform.isIOS) { | ||||
|       await HomeWidget.setAppGroupId("group.solsynth.solian"); | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -22,8 +22,9 @@ const Map<String, IconData> kCategoryIcons = { | ||||
|   'sports': Symbols.sports_soccer, | ||||
|   'music': Symbols.music_note, | ||||
|   'news': Symbols.newspaper, | ||||
|   'knowledge': Symbols.book, | ||||
|   'knowledge': Symbols.library_books, | ||||
|   'literature': Symbols.book, | ||||
|   'funny': Symbols.attractions, | ||||
| }; | ||||
|  | ||||
| class ExploreScreen extends StatefulWidget { | ||||
| @@ -184,26 +185,27 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|                 preferredSize: const Size.fromHeight(50), | ||||
|                 child: SizedBox( | ||||
|                   height: 50, | ||||
|                   child: ListView.builder( | ||||
|                     padding: const EdgeInsets.only(left: 8, right: 8, bottom: 12), | ||||
|                   child: SingleChildScrollView( | ||||
|                     scrollDirection: Axis.horizontal, | ||||
|                     itemCount: _categories.length, | ||||
|                     itemBuilder: (context, idx) { | ||||
|                       final ele = _categories[idx]; | ||||
|                       return StyledWidget(ChoiceChip( | ||||
|                         avatar: Icon(kCategoryIcons[ele.alias] ?? Symbols.question_mark), | ||||
|                         label: Text( | ||||
|                           'postCategory${ele.alias.capitalize()}'.trExists() | ||||
|                               ? 'postCategory${ele.alias.capitalize()}'.tr() | ||||
|                               : ele.name, | ||||
|                         ), | ||||
|                         selected: _selectedCategory == ele.alias, | ||||
|                         onSelected: (value) { | ||||
|                           _selectedCategory = value ? ele.alias : null; | ||||
|                           _refreshPosts(); | ||||
|                         }, | ||||
|                       )).padding(horizontal: 4); | ||||
|                     }, | ||||
|                     padding: const EdgeInsets.only(left: 8, right: 8, bottom: 12), | ||||
|                     child: Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.center, | ||||
|                       children: _categories.map((ele) { | ||||
|                         return StyledWidget(ChoiceChip( | ||||
|                           avatar: Icon(kCategoryIcons[ele.alias] ?? Symbols.question_mark), | ||||
|                           label: Text( | ||||
|                             'postCategory${ele.alias.capitalize()}'.trExists() | ||||
|                                 ? 'postCategory${ele.alias.capitalize()}'.tr() | ||||
|                                 : ele.name, | ||||
|                           ), | ||||
|                           selected: _selectedCategory == ele.alias, | ||||
|                           onSelected: (value) { | ||||
|                             _selectedCategory = value ? ele.alias : null; | ||||
|                             _refreshPosts(); | ||||
|                           }, | ||||
|                         )).padding(horizontal: 4); | ||||
|                       }).toList(), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|   | ||||
| @@ -11,11 +11,13 @@ import 'package:go_router/go_router.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/post.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/special_day.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/providers/widget.dart'; | ||||
| import 'package:surface/types/check_in.dart'; | ||||
| @@ -79,8 +81,8 @@ class _HomeScreenState extends State<HomeScreen> { | ||||
|                 child: Column( | ||||
|                   mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     _HomeDashSpecialDayWidget().padding(bottom: 8, horizontal: 8), | ||||
|                     _HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)), | ||||
|                     _HomeDashSpecialDayWidget().padding(horizontal: 8), | ||||
|                     StaggeredGrid.extent( | ||||
|                       maxCrossAxisExtent: 280, | ||||
|                       mainAxisSpacing: 8, | ||||
| @@ -156,36 +158,59 @@ class _HomeDashSpecialDayWidget extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final ua = context.watch<UserProvider>(); | ||||
|     final today = DateTime.now(); | ||||
|     final birthday = ua.user?.profile?.birthday?.toLocal(); | ||||
|     final isBirthday = birthday != null && birthday.day == today.day && birthday.month == today.month; | ||||
|     final dayz = context.watch<SpecialDayProvider>(); | ||||
|  | ||||
|     return Column( | ||||
|       spacing: 8, | ||||
|       children: [ | ||||
|         if (isBirthday) | ||||
|           Card( | ||||
|             child: ListTile( | ||||
|               leading: Text('🎂').fontSize(24), | ||||
|               title: Text('happyBirthday').tr(args: [ua.user?.nick ?? 'user']), | ||||
|             ), | ||||
|           ).padding(bottom: 8), | ||||
|         if (today.month == 12 && today.day == 25) | ||||
|           Card( | ||||
|             child: ListTile( | ||||
|               leading: Text('🎄').fontSize(24), | ||||
|               title: Text('celebrateMerryXmas').tr(args: [ua.user?.nick ?? 'user']), | ||||
|             ), | ||||
|     final days = dayz.getSpecialDays(); | ||||
|  | ||||
|     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()); | ||||
|     } | ||||
|  | ||||
|     final nextOne = dayz.getNextSpecialDay(); | ||||
|     final lastOne = dayz.getLastSpecialDay(); | ||||
|  | ||||
|     if (nextOne != null && lastOne != null) { | ||||
|       var (name, date) = nextOne; | ||||
|       date = date.add(Duration(days: 1)); | ||||
|       final progress = dayz.getSpecialDayProgress(lastOne.$2, date); | ||||
|       final diff = nextOne.$2.add(-const Duration(days: 1)).difference(lastOne.$2); | ||||
|       return Card( | ||||
|         child: ListTile( | ||||
|           leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24), | ||||
|           title: Text('pending$name').tr(args: [RelativeTime(context).format(date)]), | ||||
|           subtitle: Row( | ||||
|             crossAxisAlignment: CrossAxisAlignment.center, | ||||
|             children: [ | ||||
|               Text('${diff.inDays}d · ${(progress * 100).toStringAsFixed(2)}%'), | ||||
|               const Gap(8), | ||||
|               Expanded( | ||||
|                 child: LinearProgressIndicator( | ||||
|                   value: progress, | ||||
|                   borderRadius: BorderRadius.circular(8), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         if (today.month == 1 && today.day == 1) | ||||
|           Card( | ||||
|             child: ListTile( | ||||
|               leading: Text('🎉').fontSize(24), | ||||
|               title: Text('celebrateNewYear').tr(args: [ua.user?.nick ?? 'user']), | ||||
|             ), | ||||
|           ), | ||||
|       ], | ||||
|     ); | ||||
|         ), | ||||
|       ).padding(bottom: 8); | ||||
|     } | ||||
|  | ||||
|     return const SizedBox.shrink(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -493,9 +518,7 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati | ||||
|     setState(() => _isBusy = true); | ||||
|     try { | ||||
|       final pt = context.read<SnPostContentProvider>(); | ||||
|       final home = context.read<HomeWidgetProvider>(); | ||||
|       _posts = await pt.listRecommendations(); | ||||
|       home.saveWidgetData('post_featured', _posts!.first.toJson()); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|   | ||||
| @@ -96,38 +96,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   final _imagePicker = ImagePicker(); | ||||
|  | ||||
|   void _takeMedia(bool isVideo) async { | ||||
|     final result = isVideo | ||||
|         ? await _imagePicker.pickVideo(source: ImageSource.camera) | ||||
|         : await _imagePicker.pickImage(source: ImageSource.camera); | ||||
|     if (result == null) return; | ||||
|     _writeController.addAttachments([ | ||||
|       PostWriteMedia.fromFile(result), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   void _selectMedia() async { | ||||
|     final result = await _imagePicker.pickMultipleMedia(); | ||||
|     if (result.isEmpty) return; | ||||
|     _writeController.addAttachments( | ||||
|       result.map((e) => PostWriteMedia.fromFile(e)), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _pasteMedia() async { | ||||
|     final imageBytes = await Pasteboard.image; | ||||
|     if (imageBytes == null) return; | ||||
|     _writeController.addAttachments([ | ||||
|       PostWriteMedia.fromBytes( | ||||
|         imageBytes, | ||||
|         'attachmentPastedImage'.tr(), | ||||
|         PostWriteMediaType.image, | ||||
|       ), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _writeController.dispose(); | ||||
| @@ -435,63 +403,12 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                               scrollDirection: Axis.vertical, | ||||
|                               child: Row( | ||||
|                                 children: [ | ||||
|                                   PopupMenuButton( | ||||
|                                     icon: Icon( | ||||
|                                       Symbols.add_photo_alternate, | ||||
|                                       color: Theme.of(context).colorScheme.primary, | ||||
|                                     ), | ||||
|                                     itemBuilder: (context) => [ | ||||
|                                       if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|                                         PopupMenuItem( | ||||
|                                           child: Row( | ||||
|                                             children: [ | ||||
|                                               const Icon(Symbols.photo_camera), | ||||
|                                               const Gap(16), | ||||
|                                               Text('addAttachmentFromCameraPhoto').tr(), | ||||
|                                             ], | ||||
|                                           ), | ||||
|                                           onTap: () { | ||||
|                                             _takeMedia(false); | ||||
|                                           }, | ||||
|                                         ), | ||||
|                                       if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|                                         PopupMenuItem( | ||||
|                                           child: Row( | ||||
|                                             children: [ | ||||
|                                               const Icon(Symbols.videocam), | ||||
|                                               const Gap(16), | ||||
|                                               Text('addAttachmentFromCameraVideo').tr(), | ||||
|                                             ], | ||||
|                                           ), | ||||
|                                           onTap: () { | ||||
|                                             _takeMedia(true); | ||||
|                                           }, | ||||
|                                         ), | ||||
|                                       PopupMenuItem( | ||||
|                                         child: Row( | ||||
|                                           children: [ | ||||
|                                             const Icon(Symbols.photo_library), | ||||
|                                             const Gap(16), | ||||
|                                             Text('addAttachmentFromAlbum').tr(), | ||||
|                                           ], | ||||
|                                         ), | ||||
|                                         onTap: () { | ||||
|                                           _selectMedia(); | ||||
|                                         }, | ||||
|                                       ), | ||||
|                                       PopupMenuItem( | ||||
|                                         child: Row( | ||||
|                                           children: [ | ||||
|                                             const Icon(Symbols.content_paste), | ||||
|                                             const Gap(16), | ||||
|                                             Text('addAttachmentFromClipboard').tr(), | ||||
|                                           ], | ||||
|                                         ), | ||||
|                                         onTap: () { | ||||
|                                           _pasteMedia(); | ||||
|                                         }, | ||||
|                                       ), | ||||
|                                     ], | ||||
|                                   AddPostMediaButton( | ||||
|                                     onAdd: (items) { | ||||
|                                       setState(() { | ||||
|                                         _writeController.addAttachments(items); | ||||
|                                       }); | ||||
|                                     }, | ||||
|                                   ), | ||||
|                                 ], | ||||
|                               ), | ||||
|   | ||||
| @@ -99,11 +99,16 @@ class _PostSearchScreenState extends State<PostSearchScreen> { | ||||
|         ], | ||||
|       ).padding(horizontal: 24, vertical: 16), | ||||
|     ).then((_) { | ||||
|       _posts.clear(); | ||||
|       _fetchPosts(); | ||||
|       _refreshPosts(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<void> _refreshPosts() { | ||||
|     _postCount = null; | ||||
|     _posts.clear(); | ||||
|     return _fetchPosts(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     const labelShadows = <Shadow>[ | ||||
| @@ -144,8 +149,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> { | ||||
|                     setState(() => _posts[idx] = data); | ||||
|                   }, | ||||
|                   onDeleted: () { | ||||
|                     _posts.clear(); | ||||
|                     _fetchPosts(); | ||||
|                     _refreshPosts(); | ||||
|                   }, | ||||
|                 ), | ||||
|                 onTap: () { | ||||
| @@ -176,10 +180,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> { | ||||
|                     _searchTerm = value; | ||||
|                   }, | ||||
|                   onSubmitted: (value) { | ||||
|                     setState(() => _posts.clear()); | ||||
|  | ||||
|                     _searchTerm = value; | ||||
|                     _fetchPosts(); | ||||
|                     _refreshPosts(); | ||||
|                   }, | ||||
|                 ), | ||||
|                 if (_lastTook != null) | ||||
|   | ||||
							
								
								
									
										114
									
								
								lib/widgets/attachment/attachment_input.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								lib/widgets/attachment/attachment_input.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_attachment.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
|  | ||||
| class AttachmentInputDialog extends StatefulWidget { | ||||
|   final String? title; | ||||
|  | ||||
|   const AttachmentInputDialog({super.key, required this.title}); | ||||
|  | ||||
|   @override | ||||
|   State<AttachmentInputDialog> createState() => _AttachmentInputDialogState(); | ||||
| } | ||||
|  | ||||
| class _AttachmentInputDialogState extends State<AttachmentInputDialog> { | ||||
|   final _randomIdController = TextEditingController(); | ||||
|  | ||||
|   XFile? _thumbnailFile; | ||||
|  | ||||
|   void _pickImage() async { | ||||
|     final picker = ImagePicker(); | ||||
|     final result = await picker.pickImage(source: ImageSource.gallery); | ||||
|     if (result == null) return; | ||||
|     setState(() => _thumbnailFile = result); | ||||
|   } | ||||
|  | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   void _finishUp() async { | ||||
|     if (_isBusy) return; | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|  | ||||
|     if (_randomIdController.text.isNotEmpty) { | ||||
|       try { | ||||
|         final attachment = await attach.getOne(_randomIdController.text); | ||||
|         if (!mounted) return; | ||||
|         Navigator.pop(context, attachment); | ||||
|       } catch (err) { | ||||
|         if (!mounted) return; | ||||
|         context.showErrorDialog(err); | ||||
|       } | ||||
|     } else if (_thumbnailFile != null) { | ||||
|       try { | ||||
|         final attachment = await attach.directUploadOne( | ||||
|           (await _thumbnailFile!.readAsBytes()).buffer.asUint8List(), | ||||
|           _thumbnailFile!.path, | ||||
|           'interactive', | ||||
|           null, | ||||
|         ); | ||||
|         if (!mounted) return; | ||||
|         Navigator.pop(context, attachment); | ||||
|       } catch (err) { | ||||
|         if (!mounted) return; | ||||
|         context.showErrorDialog(err); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AlertDialog( | ||||
|       title: Text(widget.title ?? 'attachmentInputDialog').tr(), | ||||
|       content: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: [ | ||||
|           Text('attachmentInputUseRandomId').tr().fontSize(14), | ||||
|           const Gap(8), | ||||
|           TextField( | ||||
|             controller: _randomIdController, | ||||
|             decoration: InputDecoration( | ||||
|               labelText: 'fieldAttachmentRandomId'.tr(), | ||||
|               border: const OutlineInputBorder(), | ||||
|             ), | ||||
|             onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           ), | ||||
|           const Gap(24), | ||||
|           Text('attachmentInputNew').tr().fontSize(14), | ||||
|           Card( | ||||
|             child: ListTile( | ||||
|               contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|               leading: const Icon(Symbols.add_photo_alternate), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               title: Text('addAttachmentFromAlbum').tr(), | ||||
|               subtitle: _thumbnailFile == null ? Text('unset').tr() : Text('waitingForUpload').tr(), | ||||
|               onTap: () { | ||||
|                 _pickImage(); | ||||
|               }, | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       actions: [ | ||||
|         TextButton( | ||||
|           child: Text('dialogDismiss').tr(), | ||||
|           onPressed: _isBusy ? null : () { | ||||
|             Navigator.pop(context); | ||||
|           }, | ||||
|         ), | ||||
|         TextButton( | ||||
|           onPressed: _isBusy ? null : () => _finishUp(), | ||||
|           child: Text('dialogConfirm').tr(), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/chat.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_list.dart'; | ||||
| import 'package:surface/widgets/context_menu.dart'; | ||||
| import 'package:surface/widgets/link_preview.dart'; | ||||
| import 'package:surface/widgets/markdown_content.dart'; | ||||
| import 'package:swipe_to/swipe_to.dart'; | ||||
| @@ -53,7 +54,7 @@ class ChatMessage extends StatelessWidget { | ||||
|       swipeSensitivity: 20, | ||||
|       onLeftSwipe: onReply != null ? (_) => onReply!(data) : null, | ||||
|       onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null, | ||||
|       child: ContextMenuRegion( | ||||
|       child: ContextMenuArea( | ||||
|         contextMenu: ContextMenu( | ||||
|           entries: [ | ||||
|             MenuHeader(text: "eventResourceTag".tr(args: ['#${data.id}'])), | ||||
|   | ||||
| @@ -123,40 +123,6 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|   } | ||||
|  | ||||
|   final List<PostWriteMedia> _attachments = List.empty(growable: true); | ||||
|   final _imagePicker = ImagePicker(); | ||||
|  | ||||
|   void _takeMedia(bool isVideo) async { | ||||
|     final result = isVideo | ||||
|         ? await _imagePicker.pickVideo(source: ImageSource.camera) | ||||
|         : await _imagePicker.pickImage(source: ImageSource.camera); | ||||
|     if (result == null) return; | ||||
|     _attachments.add( | ||||
|       PostWriteMedia.fromFile(result), | ||||
|     ); | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   void _selectMedia() async { | ||||
|     final result = await _imagePicker.pickMultipleMedia(); | ||||
|     if (result.isEmpty) return; | ||||
|     _attachments.addAll( | ||||
|       result.map((e) => PostWriteMedia.fromFile(e)), | ||||
|     ); | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   void _pasteMedia() async { | ||||
|     final imageBytes = await Pasteboard.image; | ||||
|     if (imageBytes == null) return; | ||||
|     _attachments.add( | ||||
|       PostWriteMedia.fromBytes( | ||||
|         imageBytes, | ||||
|         'attachmentPastedImage'.tr(), | ||||
|         PostWriteMediaType.image, | ||||
|       ), | ||||
|     ); | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
| @@ -294,63 +260,12 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|                 ), | ||||
|               ), | ||||
|               const Gap(8), | ||||
|               PopupMenuButton( | ||||
|                 icon: Icon( | ||||
|                   Symbols.add_photo_alternate, | ||||
|                   color: Theme.of(context).colorScheme.primary, | ||||
|                 ), | ||||
|                 itemBuilder: (context) => [ | ||||
|                   if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|                     PopupMenuItem( | ||||
|                       child: Row( | ||||
|                         children: [ | ||||
|                           const Icon(Symbols.photo_camera), | ||||
|                           const Gap(16), | ||||
|                           Text('addAttachmentFromCameraPhoto').tr(), | ||||
|                         ], | ||||
|                       ), | ||||
|                       onTap: () { | ||||
|                         _takeMedia(false); | ||||
|                       }, | ||||
|                     ), | ||||
|                   if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|                     PopupMenuItem( | ||||
|                       child: Row( | ||||
|                         children: [ | ||||
|                           const Icon(Symbols.videocam), | ||||
|                           const Gap(16), | ||||
|                           Text('addAttachmentFromCameraVideo').tr(), | ||||
|                         ], | ||||
|                       ), | ||||
|                       onTap: () { | ||||
|                         _takeMedia(true); | ||||
|                       }, | ||||
|                     ), | ||||
|                   PopupMenuItem( | ||||
|                     child: Row( | ||||
|                       children: [ | ||||
|                         const Icon(Symbols.photo_library), | ||||
|                         const Gap(16), | ||||
|                         Text('addAttachmentFromAlbum').tr(), | ||||
|                       ], | ||||
|                     ), | ||||
|                     onTap: () { | ||||
|                       _selectMedia(); | ||||
|                     }, | ||||
|                   ), | ||||
|                   PopupMenuItem( | ||||
|                     child: Row( | ||||
|                       children: [ | ||||
|                         const Icon(Symbols.content_paste), | ||||
|                         const Gap(16), | ||||
|                         Text('addAttachmentFromClipboard').tr(), | ||||
|                       ], | ||||
|                     ), | ||||
|                     onTap: () { | ||||
|                       _pasteMedia(); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               AddPostMediaButton( | ||||
|                 onAdd: (items) { | ||||
|                   setState(() { | ||||
|                     _attachments.addAll(items); | ||||
|                   }); | ||||
|                 }, | ||||
|               ), | ||||
|               IconButton( | ||||
|                 onPressed: _isBusy ? null : _sendMessage, | ||||
|   | ||||
							
								
								
									
										47
									
								
								lib/widgets/context_menu.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								lib/widgets/context_menu.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_animate/flutter_animate.dart'; | ||||
| import 'package:flutter_context_menu/flutter_context_menu.dart'; | ||||
| import 'package:responsive_framework/responsive_framework.dart'; | ||||
|  | ||||
| class ContextMenuArea extends StatelessWidget { | ||||
|   final ContextMenu contextMenu; | ||||
|   final Widget child; | ||||
|   final ValueChanged<dynamic>? onItemSelected; | ||||
|  | ||||
|   const ContextMenuArea({ | ||||
|     super.key, | ||||
|     required this.contextMenu, | ||||
|     required this.child, | ||||
|     this.onItemSelected, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     Offset mousePosition = Offset.zero; | ||||
|  | ||||
|     return Listener( | ||||
|       onPointerDown: (event) { | ||||
|         mousePosition = event.position; | ||||
|         final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE); | ||||
|         if (!isCollapseDrawer) { | ||||
|           final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET); | ||||
|           // Leave padding for side navigation | ||||
|           mousePosition = isExpandDrawer | ||||
|               ? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2) | ||||
|               : mousePosition.copyWith(dx: mousePosition.dx - 72 * 2); | ||||
|         } | ||||
|       }, | ||||
|       child: GestureDetector( | ||||
|         onLongPress: () => _showMenu(context, mousePosition), | ||||
|         onSecondaryTap: () => _showMenu(context, mousePosition), | ||||
|         child: child, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _showMenu(BuildContext context, Offset mousePosition) async { | ||||
|     final menu = contextMenu.copyWith(position: contextMenu.position ?? mousePosition); | ||||
|     final value = await showContextMenu(context, contextMenu: menu); | ||||
|     onItemSelected?.call(value); | ||||
|   } | ||||
| } | ||||
| @@ -6,15 +6,23 @@ import 'package:dismissible_page/dismissible_page.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_context_menu/flutter_context_menu.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:pasteboard/pasteboard.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/controllers/post_write_controller.dart'; | ||||
| import 'package:surface/providers/sn_attachment.dart'; | ||||
| 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/context_menu.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| class PostMediaPendingList extends StatelessWidget { | ||||
|   final PostWriteMedia? thumbnail; | ||||
| @@ -70,6 +78,32 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _setThumbnail(BuildContext context, int idx) async { | ||||
|     if (idx == -1) { | ||||
|       // Thumbnail only can set on video or audio. And thumbnail of the post must be an image, so it's not possible to set thumbnail on the post thumbnail. | ||||
|       return; | ||||
|     } else if (attachments[idx].attachment == null) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     final thumbnail = await showDialog<SnAttachment?>( | ||||
|       context: context, | ||||
|       builder: (context) => AttachmentInputDialog( | ||||
|         title: 'attachmentSetThumbnail'.tr(), | ||||
|       ), | ||||
|     ); | ||||
|     if (thumbnail == null) return; | ||||
|     if (!context.mounted) return; | ||||
|  | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|     final newAttach = await attach.updateOne(attachments[idx].attachment!.id, thumbnail.alt, metadata: { | ||||
|       ...attachments[idx].attachment!.metadata, | ||||
|       'thumbnail': thumbnail.rid, | ||||
|     }); | ||||
|  | ||||
|     onUpdate!(idx, PostWriteMedia(newAttach)); | ||||
|   } | ||||
|  | ||||
|   Future<void> _deleteAttachment(BuildContext context, int idx) async { | ||||
|     final media = idx == -1 ? thumbnail! : attachments[idx]; | ||||
|     if (media.attachment == null) return; | ||||
| @@ -87,9 +121,17 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ContextMenu _buildContextMenu(BuildContext context, int idx, PostWriteMedia media) { | ||||
|   ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) { | ||||
|     return ContextMenu( | ||||
|       entries: [ | ||||
|         if (media.attachment != null && media.type == PostWriteMediaType.video) | ||||
|           MenuItem( | ||||
|             label: 'attachmentSetThumbnail'.tr(), | ||||
|             icon: Symbols.image, | ||||
|             onSelected: () { | ||||
|               _setThumbnail(context, idx); | ||||
|             }, | ||||
|           ), | ||||
|         if (media.attachment == null && onUpload != null) | ||||
|           MenuItem( | ||||
|               label: 'attachmentUpload'.tr(), | ||||
| @@ -97,7 +139,10 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|               onSelected: () { | ||||
|                 onUpload!(idx); | ||||
|               }), | ||||
|         if (media.attachment != null && onPostSetThumbnail != null && idx != -1) | ||||
|         if (media.attachment != null && | ||||
|             media.type == PostWriteMediaType.image && | ||||
|             onPostSetThumbnail != null && | ||||
|             idx != -1) | ||||
|           MenuItem( | ||||
|             label: 'attachmentSetAsPostThumbnail'.tr(), | ||||
|             icon: Symbols.gallery_thumbnail, | ||||
| @@ -105,7 +150,7 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|               onPostSetThumbnail!(idx); | ||||
|             }, | ||||
|           ) | ||||
|         else if (media.attachment != null && onPostSetThumbnail != null) | ||||
|         else if (media.attachment != null && media.type == PostWriteMediaType.image && onPostSetThumbnail != null) | ||||
|           MenuItem( | ||||
|             label: 'attachmentUnsetAsPostThumbnail'.tr(), | ||||
|             icon: Symbols.cancel, | ||||
| @@ -138,6 +183,14 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|             icon: Symbols.crop, | ||||
|             onSelected: () => _cropImage(context, idx), | ||||
|           ), | ||||
|         if (media.attachment != null) | ||||
|           MenuItem( | ||||
|             label: 'attachmentCopyRandomId'.tr(), | ||||
|             icon: Symbols.content_copy, | ||||
|             onSelected: () { | ||||
|               Clipboard.setData(ClipboardData(text: media.attachment!.rid)); | ||||
|             }, | ||||
|           ), | ||||
|         if (media.attachment != null && onRemove != null) | ||||
|           MenuItem( | ||||
|             label: 'delete'.tr(), | ||||
| @@ -168,48 +221,17 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|     final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||||
|  | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Container( | ||||
|       constraints: const BoxConstraints(maxHeight: 120), | ||||
|       child: Row( | ||||
|         children: [ | ||||
|           const Gap(8), | ||||
|           if (thumbnail != null) | ||||
|             ContextMenuRegion( | ||||
|               contextMenu: _buildContextMenu(context, -1, thumbnail!), | ||||
|               child: Container( | ||||
|                 decoration: BoxDecoration( | ||||
|                   border: Border.all( | ||||
|                     color: Theme.of(context).dividerColor, | ||||
|                     width: 1, | ||||
|                   ), | ||||
|                   borderRadius: BorderRadius.circular(8), | ||||
|                 ), | ||||
|                 child: ClipRRect( | ||||
|                   borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                   child: AspectRatio( | ||||
|                     aspectRatio: 1, | ||||
|                     child: switch (thumbnail!.type) { | ||||
|                       PostWriteMediaType.image => Container( | ||||
|                         color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                         child: LayoutBuilder(builder: (context, constraints) { | ||||
|                             return Image( | ||||
|                               image: thumbnail!.getImageProvider( | ||||
|                                 context, | ||||
|                                 width: (constraints.maxWidth * devicePixelRatio).round(), | ||||
|                                 height: (constraints.maxHeight * devicePixelRatio).round(), | ||||
|                               )!, | ||||
|                               fit: BoxFit.contain, | ||||
|                             ); | ||||
|                           }), | ||||
|                       ), | ||||
|                       _ => Container( | ||||
|                           color: Theme.of(context).colorScheme.surface, | ||||
|                           child: const Icon(Symbols.docs).center(), | ||||
|                         ), | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ContextMenuArea( | ||||
|               contextMenu: _createContextMenu(context, -1, thumbnail!), | ||||
|               child: _PostMediaPendingItem(media: thumbnail!), | ||||
|             ), | ||||
|           if (thumbnail != null) | ||||
|             const VerticalDivider(width: 1, thickness: 1).padding( | ||||
| @@ -224,42 +246,9 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|               itemCount: attachments.length, | ||||
|               itemBuilder: (context, idx) { | ||||
|                 final media = attachments[idx]; | ||||
|                 return ContextMenuRegion( | ||||
|                   contextMenu: _buildContextMenu(context, idx, media), | ||||
|                   child: Container( | ||||
|                     decoration: BoxDecoration( | ||||
|                       border: Border.all( | ||||
|                         color: Theme.of(context).dividerColor, | ||||
|                         width: 1, | ||||
|                       ), | ||||
|                       borderRadius: BorderRadius.circular(8), | ||||
|                     ), | ||||
|                     child: ClipRRect( | ||||
|                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                       child: AspectRatio( | ||||
|                         aspectRatio: 1, | ||||
|                         child: switch (media.type) { | ||||
|                           PostWriteMediaType.image => Container( | ||||
|                             color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                             child: LayoutBuilder(builder: (context, constraints) { | ||||
|                                 return Image( | ||||
|                                   image: media.getImageProvider( | ||||
|                                     context, | ||||
|                                     width: (constraints.maxWidth * devicePixelRatio).round(), | ||||
|                                     height: (constraints.maxHeight * devicePixelRatio).round(), | ||||
|                                   )!, | ||||
|                                   fit: BoxFit.contain, | ||||
|                                 ); | ||||
|                               }), | ||||
|                           ), | ||||
|                           _ => Container( | ||||
|                               color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                               child: const Icon(Symbols.docs).center(), | ||||
|                             ), | ||||
|                         }, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 return ContextMenuArea( | ||||
|                   contextMenu: _createContextMenu(context, idx, media), | ||||
|                   child: _PostMediaPendingItem(media: media), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
| @@ -269,3 +258,218 @@ class PostMediaPendingList extends StatelessWidget { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PostMediaPendingItem extends StatelessWidget { | ||||
|   final PostWriteMedia media; | ||||
|  | ||||
|   const _PostMediaPendingItem({ | ||||
|     required this.media, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||||
|  | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Container( | ||||
|       decoration: BoxDecoration( | ||||
|         border: Border.all( | ||||
|           color: Theme.of(context).dividerColor, | ||||
|           width: 1, | ||||
|         ), | ||||
|         borderRadius: BorderRadius.circular(8), | ||||
|       ), | ||||
|       child: ClipRRect( | ||||
|         borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|         child: AspectRatio( | ||||
|           aspectRatio: 1, | ||||
|           child: switch (media.type) { | ||||
|             PostWriteMediaType.image => Container( | ||||
|                 color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                 child: LayoutBuilder(builder: (context, constraints) { | ||||
|                   return Image( | ||||
|                     image: media.getImageProvider( | ||||
|                       context, | ||||
|                       width: (constraints.maxWidth * devicePixelRatio).round(), | ||||
|                       height: (constraints.maxHeight * devicePixelRatio).round(), | ||||
|                     )!, | ||||
|                     fit: BoxFit.contain, | ||||
|                   ); | ||||
|                 }), | ||||
|               ), | ||||
|             PostWriteMediaType.video => Container( | ||||
|                 color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                 child: media.attachment?.metadata['thumbnail'] != null | ||||
|                     ? AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment?.metadata['thumbnail'])) | ||||
|                     : const Icon(Symbols.videocam).center(), | ||||
|               ), | ||||
|             _ => Container( | ||||
|                 color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                 child: const Icon(Symbols.docs).center(), | ||||
|               ), | ||||
|           }, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class AddPostMediaButton extends StatelessWidget { | ||||
|   final Function(Iterable<PostWriteMedia>) onAdd; | ||||
|  | ||||
|   const AddPostMediaButton({super.key, required this.onAdd}); | ||||
|  | ||||
|   void _takeMedia(bool isVideo) async { | ||||
|     final picker = ImagePicker(); | ||||
|     final result = isVideo | ||||
|         ? await picker.pickVideo(source: ImageSource.camera) | ||||
|         : await picker.pickImage(source: ImageSource.camera); | ||||
|     if (result == null) return; | ||||
|     onAdd([PostWriteMedia.fromFile(result)]); | ||||
|   } | ||||
|  | ||||
|   void _selectMedia() async { | ||||
|     final picker = ImagePicker(); | ||||
|     final result = await picker.pickMultipleMedia(); | ||||
|     if (result.isEmpty) return; | ||||
|     onAdd( | ||||
|       result.map((e) => PostWriteMedia.fromFile(e)), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _pasteMedia() async { | ||||
|     final imageBytes = await Pasteboard.image; | ||||
|     if (imageBytes == null) return; | ||||
|     onAdd([ | ||||
|       PostWriteMedia.fromBytes( | ||||
|         imageBytes, | ||||
|         'attachmentPastedImage'.tr(), | ||||
|         PostWriteMediaType.image, | ||||
|       ), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   void _linkRandomId(BuildContext context) async { | ||||
|     final randomIdController = TextEditingController(); | ||||
|     final randomId = await showDialog<String?>( | ||||
|       context: context, | ||||
|       builder: (context) => AlertDialog( | ||||
|         title: Text('addAttachmentFromRandomId').tr(), | ||||
|         content: Column( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: [ | ||||
|             TextField( | ||||
|               controller: randomIdController, | ||||
|               decoration: InputDecoration( | ||||
|                 labelText: 'fieldAttachmentRandomId'.tr(), | ||||
|                 border: const UnderlineInputBorder(), | ||||
|               ), | ||||
|             ), | ||||
|             const Gap(8), | ||||
|           ], | ||||
|         ), | ||||
|         actions: [ | ||||
|           TextButton( | ||||
|             child: Text('dialogDismiss').tr(), | ||||
|             onPressed: () { | ||||
|               Navigator.pop(context); | ||||
|             }, | ||||
|           ), | ||||
|           TextButton( | ||||
|             child: Text('dialogConfirm').tr(), | ||||
|             onPressed: () { | ||||
|               Navigator.pop(context, randomIdController.text); | ||||
|             }, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|     WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|       randomIdController.dispose(); | ||||
|     }); | ||||
|     if (randomId == null || randomId.isEmpty) return; | ||||
|     if (!context.mounted) return; | ||||
|  | ||||
|     final attach = context.read<SnAttachmentProvider>(); | ||||
|     final attachment = await attach.getOne(randomId); | ||||
|  | ||||
|     onAdd([ | ||||
|       PostWriteMedia(attachment), | ||||
|     ]); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return PopupMenuButton( | ||||
|       icon: Icon( | ||||
|         Symbols.add_photo_alternate, | ||||
|         color: Theme.of(context).colorScheme.primary, | ||||
|       ), | ||||
|       itemBuilder: (context) => [ | ||||
|         if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|           PopupMenuItem( | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 const Icon(Symbols.photo_camera), | ||||
|                 const Gap(16), | ||||
|                 Text('addAttachmentFromCameraPhoto').tr(), | ||||
|               ], | ||||
|             ), | ||||
|             onTap: () { | ||||
|               _takeMedia(false); | ||||
|             }, | ||||
|           ), | ||||
|         if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) | ||||
|           PopupMenuItem( | ||||
|             child: Row( | ||||
|               children: [ | ||||
|                 const Icon(Symbols.videocam), | ||||
|                 const Gap(16), | ||||
|                 Text('addAttachmentFromCameraVideo').tr(), | ||||
|               ], | ||||
|             ), | ||||
|             onTap: () { | ||||
|               _takeMedia(true); | ||||
|             }, | ||||
|           ), | ||||
|         PopupMenuItem( | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               const Icon(Symbols.photo_library), | ||||
|               const Gap(16), | ||||
|               Text('addAttachmentFromAlbum').tr(), | ||||
|             ], | ||||
|           ), | ||||
|           onTap: () { | ||||
|             _selectMedia(); | ||||
|           }, | ||||
|         ), | ||||
|         PopupMenuItem( | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               const Icon(Symbols.link), | ||||
|               const Gap(16), | ||||
|               Text('addAttachmentFromRandomId').tr(), | ||||
|             ], | ||||
|           ), | ||||
|           onTap: () { | ||||
|             _linkRandomId(context); | ||||
|           }, | ||||
|         ), | ||||
|         PopupMenuItem( | ||||
|           child: Row( | ||||
|             children: [ | ||||
|               const Icon(Symbols.content_paste), | ||||
|               const Gap(16), | ||||
|               Text('addAttachmentFromClipboard').tr(), | ||||
|             ], | ||||
|           ), | ||||
|           onTap: () { | ||||
|             _pasteMedia(); | ||||
|           }, | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -282,20 +282,6 @@ class _PostCategoriesFieldState extends State<PostCategoriesField> { | ||||
|                 : null, | ||||
|           ), | ||||
|           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           onChanged: (value) { | ||||
|             for (final divider in kTagsDividers) { | ||||
|               if (value.endsWith(divider)) { | ||||
|                 final tagValue = value.substring(0, value.length - 1); | ||||
|                 if (tagValue.isEmpty) return; | ||||
|                 if (!_currentCategories.contains(tagValue)) { | ||||
|                   setState(() => _currentCategories.add(tagValue)); | ||||
|                 } | ||||
|                 controller.clear(); | ||||
|                 widget.onUpdate(_currentCategories); | ||||
|                 break; | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
|           onSubmitted: (_) { | ||||
|             onSubmitted(); | ||||
|           }, | ||||
|   | ||||
							
								
								
									
										16
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -753,10 +753,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_udid | ||||
|       sha256: be464dc5b1fb7ee894f6a32d65c086ca5e177fdcf9375ac08d77495b98150f84 | ||||
|       sha256: "166bee5989a58c66b8b62000ea65edccc7c8167bbafdbb08022638db330dd030" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.1" | ||||
|     version: "4.0.0" | ||||
|   flutter_web_plugins: | ||||
|     dependency: "direct main" | ||||
|     description: flutter | ||||
| @@ -766,10 +766,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_webrtc | ||||
|       sha256: "430859fb5b763d7556d06ef287cfca582e17d9a2dc36da26017f25a5c0b2523e" | ||||
|       sha256: "0e138a0a3bf6830c29c8439b17be0e222d0de27fa72f24e6aee4d34de72f22ef" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.12.4" | ||||
|     version: "0.12.5" | ||||
|   freezed: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -934,10 +934,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: image_picker_android | ||||
|       sha256: fa8141602fde3f7e2f81dbf043613eb44dfa325fa0bcf93c0f142c9f7a2c193e | ||||
|       sha256: aa6f1280b670861ac45220cc95adc59bb6ae130259d36f980ccb62220dc5e59f | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.8.12+18" | ||||
|     version: "0.8.12+19" | ||||
|   image_picker_for_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1086,10 +1086,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: livekit_client | ||||
|       sha256: "7802b5de1cae2ee3439db730d24d31c6dcbce173c5e6db2fc5774039a290bc2d" | ||||
|       sha256: a3ff529fe6745ee40cdedcd021d81c4a6ad946dd495e782596f2856eeeabc739 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.2" | ||||
|     version: "2.3.3" | ||||
|   logging: | ||||
|     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.1.1+38 | ||||
| version: 2.1.1+39 | ||||
|  | ||||
| environment: | ||||
|   sdk: ^3.5.4 | ||||
| @@ -80,7 +80,7 @@ dependencies: | ||||
|   firebase_core: ^3.8.0 | ||||
|   firebase_messaging: ^15.1.5 | ||||
|   firebase_analytics: ^11.3.5 | ||||
|   flutter_udid: ^3.0.0 | ||||
|   flutter_udid: ^4.0.0 | ||||
|   media_kit: ^1.1.11 | ||||
|   media_kit_video: ^1.2.5 | ||||
|   media_kit_libs_video: ^1.0.5 | ||||
|   | ||||
							
								
								
									
										221
									
								
								web/index.html
									
									
									
									
									
								
							
							
						
						
									
										221
									
								
								web/index.html
									
									
									
									
									
								
							| @@ -1,130 +1,133 @@ | ||||
| <!DOCTYPE html><html><head> | ||||
|   <!-- | ||||
|     If you are serving your web app in a path other than the root, change the | ||||
|     href value below to reflect the base path you are serving from. | ||||
| <!DOCTYPE html> | ||||
| <html lang="en" oncontextmenu="event.preventDefault();"> | ||||
| <head> | ||||
|     <!-- | ||||
|       If you are serving your web app in a path other than the root, change the | ||||
|       href value below to reflect the base path you are serving from. | ||||
|  | ||||
|     The path provided below has to start and end with a slash "/" in order for | ||||
|     it to work correctly. | ||||
|       The path provided below has to start and end with a slash "/" in order for | ||||
|       it to work correctly. | ||||
|  | ||||
|     For more details: | ||||
|     * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base | ||||
|       For more details: | ||||
|       * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base | ||||
|  | ||||
|     This is a placeholder for base href that will be replaced by the value of | ||||
|     the `--base-href` argument provided to `flutter build`. | ||||
|   --> | ||||
|   <base href="$FLUTTER_BASE_HREF"> | ||||
|       This is a placeholder for base href that will be replaced by the value of | ||||
|       the `--base-href` argument provided to `flutter build`. | ||||
|     --> | ||||
|     <base href="$FLUTTER_BASE_HREF"> | ||||
|  | ||||
|   <meta charset="UTF-8"> | ||||
|   <meta content="IE=Edge" http-equiv="X-UA-Compatible"> | ||||
|   <meta name="description" content="A new Flutter project."> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta content="IE=Edge" http-equiv="X-UA-Compatible"> | ||||
|     <meta name="description" content="A new Flutter project."> | ||||
|  | ||||
|   <!-- iOS meta tags & icons --> | ||||
|   <meta name="apple-mobile-web-app-capable" content="yes"> | ||||
|   <meta name="apple-mobile-web-app-status-bar-style" content="black"> | ||||
|   <meta name="apple-mobile-web-app-title" content="surface"> | ||||
|   <link rel="apple-touch-icon" href="icons/Icon-192.png"> | ||||
|     <!-- iOS meta tags & icons --> | ||||
|     <meta name="apple-mobile-web-app-capable" content="yes"> | ||||
|     <meta name="apple-mobile-web-app-status-bar-style" content="black"> | ||||
|     <meta name="apple-mobile-web-app-title" content="surface"> | ||||
|     <link rel="apple-touch-icon" href="icons/Icon-192.png"> | ||||
|  | ||||
|   <!-- Favicon --> | ||||
|   <link rel="icon" type="image/png" href="favicon.png"> | ||||
|     <!-- Favicon --> | ||||
|     <link rel="icon" type="image/png" href="favicon.png"> | ||||
|  | ||||
|   <title>Solian</title> | ||||
|   <link rel="manifest" href="manifest.json"> | ||||
|    | ||||
|    | ||||
|   <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"> | ||||
|    | ||||
|    | ||||
|    | ||||
|    | ||||
|   <style id="splash-screen-style"> | ||||
|     html { | ||||
|       height: 100% | ||||
|     } | ||||
|     <title>Solian</title> | ||||
|     <link rel="manifest" href="manifest.json"> | ||||
|  | ||||
|     body { | ||||
|       margin: 0; | ||||
|       min-height: 100%; | ||||
|       background-color: #ffffff; | ||||
|           background-size: 100% 100%; | ||||
|     } | ||||
|  | ||||
|     .center { | ||||
|       margin: 0; | ||||
|       position: absolute; | ||||
|       top: 50%; | ||||
|       left: 50%; | ||||
|       -ms-transform: translate(-50%, -50%); | ||||
|       transform: translate(-50%, -50%); | ||||
|     } | ||||
|     <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" | ||||
|           name="viewport"> | ||||
|  | ||||
|     .contain { | ||||
|       display:block; | ||||
|       width:100%; height:100%; | ||||
|       object-fit: contain; | ||||
|     } | ||||
|  | ||||
|     .stretch { | ||||
|       display:block; | ||||
|       width:100%; height:100%; | ||||
|     } | ||||
|     <style id="splash-screen-style"> | ||||
|         html { | ||||
|           height: 100% | ||||
|         } | ||||
|  | ||||
|     .cover { | ||||
|       display:block; | ||||
|       width:100%; height:100%; | ||||
|       object-fit: cover; | ||||
|     } | ||||
|         body { | ||||
|           margin: 0; | ||||
|           min-height: 100%; | ||||
|           background-color: #ffffff; | ||||
|               background-size: 100% 100%; | ||||
|         } | ||||
|  | ||||
|     .bottom { | ||||
|       position: absolute; | ||||
|       bottom: 0; | ||||
|       left: 50%; | ||||
|       -ms-transform: translate(-50%, 0); | ||||
|       transform: translate(-50%, 0); | ||||
|     } | ||||
|         .center { | ||||
|           margin: 0; | ||||
|           position: absolute; | ||||
|           top: 50%; | ||||
|           left: 50%; | ||||
|           -ms-transform: translate(-50%, -50%); | ||||
|           transform: translate(-50%, -50%); | ||||
|         } | ||||
|  | ||||
|     .bottomLeft { | ||||
|       position: absolute; | ||||
|       bottom: 0; | ||||
|       left: 0; | ||||
|     } | ||||
|         .contain { | ||||
|           display:block; | ||||
|           width:100%; height:100%; | ||||
|           object-fit: contain; | ||||
|         } | ||||
|  | ||||
|     .bottomRight { | ||||
|       position: absolute; | ||||
|       bottom: 0; | ||||
|       right: 0; | ||||
|     } | ||||
|         .stretch { | ||||
|           display:block; | ||||
|           width:100%; height:100%; | ||||
|         } | ||||
|  | ||||
|     @media (prefers-color-scheme: dark) { | ||||
|       body { | ||||
|         background-color: #000000; | ||||
|           } | ||||
|     } | ||||
|   </style> | ||||
|   <script id="splash-screen-script"> | ||||
|     function removeSplashFromWeb() { | ||||
|       document.getElementById("splash")?.remove(); | ||||
|       document.getElementById("splash-branding")?.remove(); | ||||
|       document.body.style.background = "transparent"; | ||||
|     } | ||||
|   </script> | ||||
|         .cover { | ||||
|           display:block; | ||||
|           width:100%; height:100%; | ||||
|           object-fit: cover; | ||||
|         } | ||||
|  | ||||
|         .bottom { | ||||
|           position: absolute; | ||||
|           bottom: 0; | ||||
|           left: 50%; | ||||
|           -ms-transform: translate(-50%, 0); | ||||
|           transform: translate(-50%, 0); | ||||
|         } | ||||
|  | ||||
|         .bottomLeft { | ||||
|           position: absolute; | ||||
|           bottom: 0; | ||||
|           left: 0; | ||||
|         } | ||||
|  | ||||
|         .bottomRight { | ||||
|           position: absolute; | ||||
|           bottom: 0; | ||||
|           right: 0; | ||||
|         } | ||||
|  | ||||
|         @media (prefers-color-scheme: dark) { | ||||
|           body { | ||||
|             background-color: #000000; | ||||
|               } | ||||
|         } | ||||
|     </style> | ||||
|     <script id="splash-screen-script"> | ||||
|         function removeSplashFromWeb() { | ||||
|           document.getElementById("splash")?.remove(); | ||||
|           document.getElementById("splash-branding")?.remove(); | ||||
|           document.body.style.background = "transparent"; | ||||
|         } | ||||
|     </script> | ||||
| </head> | ||||
| <body> | ||||
|   <picture id="splash-branding"> | ||||
|     <source srcset="splash/img/branding-1x.png 1x, splash/img/branding-2x.png 2x, splash/img/branding-3x.png 3x, splash/img/branding-4x.png 4x" media="(prefers-color-scheme: light)"> | ||||
|     <source srcset="splash/img/branding-dark-1x.png 1x, splash/img/branding-dark-2x.png 2x, splash/img/branding-dark-3x.png 3x, splash/img/branding-dark-4x.png 4x" media="(prefers-color-scheme: dark)"> | ||||
| <picture id="splash-branding"> | ||||
|     <source srcset="splash/img/branding-1x.png 1x, splash/img/branding-2x.png 2x, splash/img/branding-3x.png 3x, splash/img/branding-4x.png 4x" | ||||
|             media="(prefers-color-scheme: light)"> | ||||
|     <source srcset="splash/img/branding-dark-1x.png 1x, splash/img/branding-dark-2x.png 2x, splash/img/branding-dark-3x.png 3x, splash/img/branding-dark-4x.png 4x" | ||||
|             media="(prefers-color-scheme: dark)"> | ||||
|     <img class="bottom" aria-hidden="true" src="splash/img/branding-1x.png" alt=""> | ||||
|   </picture> | ||||
|   <picture id="splash"> | ||||
|       <source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" media="(prefers-color-scheme: light)"> | ||||
|       <source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" media="(prefers-color-scheme: dark)"> | ||||
|       <img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt=""> | ||||
|   </picture> | ||||
|    | ||||
|    | ||||
|    | ||||
|    | ||||
|    | ||||
|   <script src="flutter_bootstrap.js" async=""></script> | ||||
| </picture> | ||||
| <picture id="splash"> | ||||
|     <source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" | ||||
|             media="(prefers-color-scheme: light)"> | ||||
|     <source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" | ||||
|             media="(prefers-color-scheme: dark)"> | ||||
|     <img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt=""> | ||||
| </picture> | ||||
|  | ||||
|  | ||||
| </body></html> | ||||
| <script src="flutter_bootstrap.js" async=""></script> | ||||
|  | ||||
|  | ||||
| </body> | ||||
| </html> | ||||
		Reference in New Issue
	
	Block a user