Compare commits
	
		
			21 Commits
		
	
	
		
			2.2.2+60
			...
			3818328afe
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3818328afe | |||
| 11627e2455 | |||
| 3f82c06ff8 | |||
| 2350f59131 | |||
| 9fe7c9530a | |||
| 52f1826e91 | |||
| 28a4c86dbf | |||
| 85e48ce03b | |||
| efef61a8ea | |||
| 10ead95af9 | |||
| 838ee4d55d | |||
| 13e42429a9 | |||
| c6ce3fe2b7 | |||
| ae9a7eb0fd | |||
| 5d6fb2442f | |||
| 5a85985534 | |||
| c80499db03 | |||
| b8dcdb2315 | |||
| b7b921f1f4 | |||
| 319d5c7d7f | |||
| 4b5b001739 | 
| @@ -26,7 +26,7 @@ | ||||
|         <activity | ||||
|             android:name=".MainActivity" | ||||
|             android:exported="true" | ||||
|             android:launchMode="singleTask" | ||||
|             android:launchMode="singleInstance" | ||||
|             android:taskAffinity="" | ||||
|             android:theme="@style/LaunchTheme" | ||||
|             android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" | ||||
|   | ||||
| @@ -12,9 +12,9 @@ post { | ||||
|  | ||||
| body:json { | ||||
|   { | ||||
|     "alias": "AteChip", | ||||
|     "name": "Cat ate chips", | ||||
|     "attachment_id": "d0b692cc64054463", | ||||
|     "pack_id": 2 | ||||
|     "alias": "Meltdown", | ||||
|     "name": "Meltdown", | ||||
|     "attachment_id": "IpDPHEbWDDCbBofX", | ||||
|     "pack_id": 4 | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										11
									
								
								api/Paperclip/Stickers/Get Sticker Packs.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/Paperclip/Stickers/Get Sticker Packs.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| meta { | ||||
|   name: Get Sticker Packs | ||||
|   type: http | ||||
|   seq: 3 | ||||
| } | ||||
|  | ||||
| get { | ||||
|   url: {{endpoint}}/cgi/uc/stickers/packs | ||||
|   body: none | ||||
|   auth: none | ||||
| } | ||||
							
								
								
									
										15
									
								
								api/Paperclip/Stickers/Get Stickers.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								api/Paperclip/Stickers/Get Stickers.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| meta { | ||||
|   name: Get Stickers | ||||
|   type: http | ||||
|   seq: 4 | ||||
| } | ||||
|  | ||||
| get { | ||||
|   url: {{endpoint}}/cgi/uc/stickers?take=10 | ||||
|   body: none | ||||
|   auth: none | ||||
| } | ||||
|  | ||||
| params:query { | ||||
|   take: 10 | ||||
| } | ||||
							
								
								
									
										26
									
								
								api/Passport/Developer Notify One User.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								api/Passport/Developer Notify One User.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| meta { | ||||
|   name: Developer Notify One User | ||||
|   type: http | ||||
|   seq: 2 | ||||
| } | ||||
|  | ||||
| post { | ||||
|   url: {{endpoint}}/cgi/id/dev/notify/1 | ||||
|   body: json | ||||
|   auth: inherit | ||||
| } | ||||
|  | ||||
| body:json { | ||||
|   { | ||||
|     "client_id": "{{third_client_id}}", | ||||
|     "client_secret":"{{third_client_tk}}", | ||||
|     "type": "general", | ||||
|     "subject": "测试", | ||||
|     "subtitle": "Alphabot です", | ||||
|     "content": "全新通知动画", | ||||
|     "metadata": { | ||||
|       "image": "D2EDbcrsTugs3xk5" | ||||
|     }, | ||||
|     "priority": 10 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										20
									
								
								api/Wallet/Create Order.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/Wallet/Create Order.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| meta { | ||||
|   name: Create Order | ||||
|   type: http | ||||
|   seq: 1 | ||||
| } | ||||
|  | ||||
| post { | ||||
|   url: {{endpoint}}/cgi/wa/orders | ||||
|   body: json | ||||
|   auth: none | ||||
| } | ||||
|  | ||||
| body:json { | ||||
|   { | ||||
|     "client_id": "highland-mc", | ||||
|     "client_secret": "(3^DLAvo3v", | ||||
|     "remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。", | ||||
|     "amount": 500 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										21
									
								
								api/Wallet/Create Transaction.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								api/Wallet/Create Transaction.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| meta { | ||||
|   name: Create Transaction | ||||
|   type: http | ||||
|   seq: 3 | ||||
| } | ||||
|  | ||||
| post { | ||||
|   url: {{endpoint}}/cgi/wa/transactions | ||||
|   body: json | ||||
|   auth: none | ||||
| } | ||||
|  | ||||
| body:json { | ||||
|   { | ||||
|     "client_id": "alphabot", | ||||
|     "client_secret": "_uR0sVnHTh", | ||||
|     "remark": "新年红包", | ||||
|     "amount": 9705, | ||||
|     "payee_id": 2 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										20
									
								
								api/Wallet/Get Order.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/Wallet/Get Order.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| meta { | ||||
|   name: Get Order | ||||
|   type: http | ||||
|   seq: 2 | ||||
| } | ||||
|  | ||||
| get { | ||||
|   url: {{endpoint}}/cgi/wa/orders/4 | ||||
|   body: none | ||||
|   auth: none | ||||
| } | ||||
|  | ||||
| body:json { | ||||
|   { | ||||
|     "client_id": "highland-mc", | ||||
|     "client_secret": "(3^DLAvo3v", | ||||
|     "remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。", | ||||
|     "amount": 500 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										20
									
								
								api/Wallet/Get Transaction.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/Wallet/Get Transaction.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| meta { | ||||
|   name: Get Transaction | ||||
|   type: http | ||||
|   seq: 4 | ||||
| } | ||||
|  | ||||
| get { | ||||
|   url: {{endpoint}}/cgi/wa/transactions/67 | ||||
|   body: none | ||||
|   auth: inherit | ||||
| } | ||||
|  | ||||
| body:json { | ||||
|   { | ||||
|     "client_id": "highland-mc", | ||||
|     "client_secret": "(3^DLAvo3v", | ||||
|     "remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。", | ||||
|     "amount": 500 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								assets/icon/tray-icon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/icon/tray-icon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 16 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icon/tray-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/icon/tray-icon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 228 KiB | 
| @@ -241,6 +241,8 @@ | ||||
|   "settingsMisc": "Misc", | ||||
|   "settingsMiscAbout": "About", | ||||
|   "settingsMiscAboutDescription": "View the version information of Solian.", | ||||
|   "settingsAccountLanguage": "Account Language", | ||||
|   "settingsAccountLanguageDescription": "Set the language for email, notification, and other account-related content.", | ||||
|   "sensitiveContent": "Sensitive Content", | ||||
|   "sensitiveContentCollapsed": "Sensitive content has been collapsed.", | ||||
|   "sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.", | ||||
| @@ -562,6 +564,7 @@ | ||||
|   "shareIntent": "Share", | ||||
|   "shareIntentDescription": "What do you want to do with the content you are sharing?", | ||||
|   "shareIntentPostStory": "Post a Story", | ||||
|   "shareIntentSendChannel": "Share to Channel", | ||||
|   "updateAvailable": "Update Available", | ||||
|   "updateOngoing": "Updating, please wait...", | ||||
|   "custom": "Custom", | ||||
| @@ -574,6 +577,7 @@ | ||||
|   "colorSchemeWhite": "White", | ||||
|   "colorSchemeBlack": "Black", | ||||
|   "colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.", | ||||
|   "postFeaturedComment": "Featured Comment", | ||||
|   "postCategoryTechnology": "Technology", | ||||
|   "postCategoryGaming": "Gaming", | ||||
|   "postCategoryLife": "Life", | ||||
| @@ -604,5 +608,7 @@ | ||||
|     "one": "{} Source Point", | ||||
|     "other": "{} Source Points" | ||||
|   }, | ||||
|   "aiThinkingProcess": "AI Thinking Process" | ||||
|   "aiThinkingProcess": "AI Thinking Process", | ||||
|   "accountSettingsApplied": "Account settings have been applied.", | ||||
|   "trayMenuExit": "Exit" | ||||
| } | ||||
|   | ||||
| @@ -239,6 +239,8 @@ | ||||
|   "settingsMisc": "杂项", | ||||
|   "settingsMiscAbout": "关于", | ||||
|   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", | ||||
|   "settingsAccountLanguage": "帐号偏好语言", | ||||
|   "settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。", | ||||
|   "sensitiveContent": "敏感内容", | ||||
|   "sensitiveContentCollapsed": "敏感内容已折叠。", | ||||
|   "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。", | ||||
| @@ -560,6 +562,7 @@ | ||||
|   "shareIntent": "分享", | ||||
|   "shareIntentDescription": "您想对您分享的内容做些什么?", | ||||
|   "shareIntentPostStory": "发布动态", | ||||
|   "shareIntentSendChannel": "分享到聊天频道", | ||||
|   "updateAvailable": "检测到更新可用", | ||||
|   "updateOngoing": "正在更新,请稍后……", | ||||
|   "custom": "自定义", | ||||
| @@ -572,6 +575,7 @@ | ||||
|   "colorSchemeWhite": "白色", | ||||
|   "colorSchemeBlack": "黑色", | ||||
|   "colorSchemeApplied": "主题色已应用,可能需要重启来生效。", | ||||
|   "postFeaturedComment": "精选评论", | ||||
|   "postCategoryTechnology": "技术", | ||||
|   "postCategoryGaming": "游戏", | ||||
|   "postCategoryLife": "生活", | ||||
| @@ -602,5 +606,7 @@ | ||||
|     "one": "{} 源点", | ||||
|     "other": "{} 源点" | ||||
|   }, | ||||
|   "aiThinkingProcess": "AI 思考过程" | ||||
|   "aiThinkingProcess": "AI 思考过程", | ||||
|   "accountSettingsApplied": "帐号设置已应用。", | ||||
|   "trayMenuExit": "退出" | ||||
| } | ||||
|   | ||||
| @@ -239,6 +239,8 @@ | ||||
|   "settingsMisc": "雜項", | ||||
|   "settingsMiscAbout": "關於", | ||||
|   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", | ||||
|   "settingsAccountLanguage": "帳號偏好語言", | ||||
|   "settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。", | ||||
|   "sensitiveContent": "敏感內容", | ||||
|   "sensitiveContentCollapsed": "敏感內容已摺疊。", | ||||
|   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", | ||||
| @@ -560,6 +562,7 @@ | ||||
|   "shareIntent": "分享", | ||||
|   "shareIntentDescription": "您想對您分享的內容做些什麼?", | ||||
|   "shareIntentPostStory": "發佈動態", | ||||
|   "shareIntentSendChannel": "分享到聊天頻道", | ||||
|   "updateAvailable": "檢測到更新可用", | ||||
|   "updateOngoing": "正在更新,請稍後……", | ||||
|   "custom": "自定義", | ||||
| @@ -572,6 +575,7 @@ | ||||
|   "colorSchemeWhite": "白色", | ||||
|   "colorSchemeBlack": "黑色", | ||||
|   "colorSchemeApplied": "主題色已應用,可能需要重啓來生效。", | ||||
|   "postFeaturedComment": "精選評論", | ||||
|   "postCategoryTechnology": "技術", | ||||
|   "postCategoryGaming": "遊戲", | ||||
|   "postCategoryLife": "生活", | ||||
| @@ -602,5 +606,6 @@ | ||||
|     "one": "{} 源點", | ||||
|     "other": "{} 源點" | ||||
|   }, | ||||
|   "aiThinkingProcess": "AI 思考過程" | ||||
|   "aiThinkingProcess": "AI 思考過程", | ||||
|   "accountSettingsApplied": "帳號設置已應用。" | ||||
| } | ||||
|   | ||||
| @@ -239,6 +239,8 @@ | ||||
|   "settingsMisc": "雜項", | ||||
|   "settingsMiscAbout": "關於", | ||||
|   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", | ||||
|   "settingsAccountLanguage": "帳號偏好語言", | ||||
|   "settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。", | ||||
|   "sensitiveContent": "敏感內容", | ||||
|   "sensitiveContentCollapsed": "敏感內容已摺疊。", | ||||
|   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", | ||||
| @@ -560,6 +562,7 @@ | ||||
|   "shareIntent": "分享", | ||||
|   "shareIntentDescription": "您想對您分享的內容做些什麼?", | ||||
|   "shareIntentPostStory": "發佈動態", | ||||
|   "shareIntentSendChannel": "分享到聊天頻道", | ||||
|   "updateAvailable": "檢測到更新可用", | ||||
|   "updateOngoing": "正在更新,請稍後……", | ||||
|   "custom": "自定義", | ||||
| @@ -572,6 +575,7 @@ | ||||
|   "colorSchemeWhite": "白色", | ||||
|   "colorSchemeBlack": "黑色", | ||||
|   "colorSchemeApplied": "主題色已應用,可能需要重啟來生效。", | ||||
|   "postFeaturedComment": "精選評論", | ||||
|   "postCategoryTechnology": "技術", | ||||
|   "postCategoryGaming": "遊戲", | ||||
|   "postCategoryLife": "生活", | ||||
| @@ -602,5 +606,6 @@ | ||||
|     "one": "{} 源點", | ||||
|     "other": "{} 源點" | ||||
|   }, | ||||
|   "aiThinkingProcess": "AI 思考過程" | ||||
|   "aiThinkingProcess": "AI 思考過程", | ||||
|   "accountSettingsApplied": "帳號設置已應用。" | ||||
| } | ||||
|   | ||||
| @@ -43,58 +43,58 @@ PODS: | ||||
|     - Flutter | ||||
|   - file_saver (0.0.1): | ||||
|     - Flutter | ||||
|   - Firebase/Analytics (11.6.0): | ||||
|   - Firebase/Analytics (11.7.0): | ||||
|     - Firebase/Core | ||||
|   - Firebase/Core (11.6.0): | ||||
|   - Firebase/Core (11.7.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseAnalytics (~> 11.6.0) | ||||
|   - Firebase/CoreOnly (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|   - Firebase/Messaging (11.6.0): | ||||
|     - FirebaseAnalytics (~> 11.7.0) | ||||
|   - Firebase/CoreOnly (11.7.0): | ||||
|     - FirebaseCore (~> 11.7.0) | ||||
|   - Firebase/Messaging (11.7.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseMessaging (~> 11.6.0) | ||||
|   - firebase_analytics (11.4.1): | ||||
|     - Firebase/Analytics (= 11.6.0) | ||||
|     - FirebaseMessaging (~> 11.7.0) | ||||
|   - firebase_analytics (11.4.2): | ||||
|     - Firebase/Analytics (= 11.7.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
|   - firebase_core (3.10.1): | ||||
|     - Firebase/CoreOnly (= 11.6.0) | ||||
|   - firebase_core (3.11.0): | ||||
|     - Firebase/CoreOnly (= 11.7.0) | ||||
|     - Flutter | ||||
|   - firebase_messaging (15.2.1): | ||||
|     - Firebase/Messaging (= 11.6.0) | ||||
|   - firebase_messaging (15.2.2): | ||||
|     - Firebase/Messaging (= 11.7.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
|   - FirebaseAnalytics (11.6.0): | ||||
|     - FirebaseAnalytics/AdIdSupport (= 11.6.0) | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|   - FirebaseAnalytics (11.7.0): | ||||
|     - FirebaseAnalytics/AdIdSupport (= 11.7.0) | ||||
|     - FirebaseCore (~> 11.7.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseAnalytics/AdIdSupport (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|   - FirebaseAnalytics/AdIdSupport (11.7.0): | ||||
|     - FirebaseCore (~> 11.7.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleAppMeasurement (= 11.6.0) | ||||
|     - GoogleAppMeasurement (= 11.7.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseCore (11.6.0): | ||||
|     - FirebaseCoreInternal (~> 11.6.0) | ||||
|   - FirebaseCore (11.7.0): | ||||
|     - FirebaseCoreInternal (~> 11.7.0) | ||||
|     - GoogleUtilities/Environment (~> 8.0) | ||||
|     - GoogleUtilities/Logger (~> 8.0) | ||||
|   - FirebaseCoreInternal (11.6.0): | ||||
|   - FirebaseCoreInternal (11.7.0): | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|   - FirebaseInstallations (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|   - FirebaseInstallations (11.7.0): | ||||
|     - FirebaseCore (~> 11.7.0) | ||||
|     - GoogleUtilities/Environment (~> 8.0) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.0) | ||||
|     - PromisesObjC (~> 2.4) | ||||
|   - FirebaseMessaging (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|   - FirebaseMessaging (11.7.0): | ||||
|     - FirebaseCore (~> 11.7.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleDataTransport (~> 10.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
| @@ -123,21 +123,21 @@ PODS: | ||||
|   - gal (1.0.0): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - GoogleAppMeasurement (11.6.0): | ||||
|     - GoogleAppMeasurement/AdIdSupport (= 11.6.0) | ||||
|   - GoogleAppMeasurement (11.7.0): | ||||
|     - GoogleAppMeasurement/AdIdSupport (= 11.7.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/AdIdSupport (11.6.0): | ||||
|     - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0) | ||||
|   - GoogleAppMeasurement/AdIdSupport (11.7.0): | ||||
|     - GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/WithoutAdIdSupport (11.6.0): | ||||
|   - GoogleAppMeasurement/WithoutAdIdSupport (11.7.0): | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
| @@ -179,7 +179,7 @@ PODS: | ||||
|     - Flutter | ||||
|   - in_app_review (2.0.0): | ||||
|     - Flutter | ||||
|   - Kingfisher (8.1.3) | ||||
|   - Kingfisher (8.2.0) | ||||
|   - livekit_client (2.3.5): | ||||
|     - Flutter | ||||
|     - flutter_webrtc | ||||
| @@ -381,15 +381,15 @@ SPEC CHECKSUMS: | ||||
|   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 | ||||
|   file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 | ||||
|   file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 | ||||
|   Firebase: 374a441a91ead896215703a674d58cdb3e9d772b | ||||
|   firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e | ||||
|   firebase_core: e2aa06dbd854d961f8ce46c2e20933bee1bf2d2b | ||||
|   firebase_messaging: 96cf6d67121b3f39746b2a4f29a26c0eee4af70e | ||||
|   FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 | ||||
|   FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa | ||||
|   FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 | ||||
|   FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c | ||||
|   FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021 | ||||
|   Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4 | ||||
|   firebase_analytics: 7236e6115c1b4e62c2270faa29c052a317e31107 | ||||
|   firebase_core: aa979ae726f00b3ef4ccf59dfb96170af84efbd4 | ||||
|   firebase_messaging: 3af84b6a90aeac4d7a67fbf4c43a91e7083bea1f | ||||
|   FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec | ||||
|   FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4 | ||||
|   FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881 | ||||
|   FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9 | ||||
|   FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c | ||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||
|   flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc | ||||
|   flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 | ||||
| @@ -397,13 +397,13 @@ SPEC CHECKSUMS: | ||||
|   flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab | ||||
|   flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1 | ||||
|   gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 | ||||
|   GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3 | ||||
|   GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967 | ||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||
|   GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d | ||||
|   home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 | ||||
|   image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 | ||||
|   in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 | ||||
|   Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef | ||||
|   Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d | ||||
|   livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976 | ||||
|   media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 | ||||
|   media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a | ||||
|   | ||||
							
								
								
									
										106
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										106
									
								
								lib/main.dart
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:developer'; | ||||
| import 'dart:io'; | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||
| import 'package:croppy/croppy.dart'; | ||||
| @@ -10,8 +11,10 @@ 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:flutter/services.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:hotkey_manager/hotkey_manager.dart'; | ||||
| import 'package:package_info_plus/package_info_plus.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:relative_time/relative_time.dart'; | ||||
| @@ -40,6 +43,7 @@ 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:tray_manager/tray_manager.dart'; | ||||
| import 'package:version/version.dart'; | ||||
| import 'package:workmanager/workmanager.dart'; | ||||
| import 'package:in_app_review/in_app_review.dart'; | ||||
| @@ -206,7 +210,7 @@ class _AppSplashScreen extends StatefulWidget { | ||||
|   State<_AppSplashScreen> createState() => _AppSplashScreenState(); | ||||
| } | ||||
|  | ||||
| class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||
| class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { | ||||
|   void _tryRequestRating() async { | ||||
|     final prefs = await SharedPreferences.getInstance(); | ||||
|     if (prefs.containsKey('first_boot_time')) { | ||||
| @@ -281,6 +285,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||
|       final notify = context.read<NotificationProvider>(); | ||||
|       notify.listen(); | ||||
|       await notify.registerPushNotifications(); | ||||
|       if (!mounted) return; | ||||
|       final sticker = context.read<SnStickerProvider>(); | ||||
|       await sticker.listStickerEagerly(); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       await context.showErrorDialog(err); | ||||
| @@ -291,9 +298,62 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||
|     await widgetUpdateRandomPost(); | ||||
|   } | ||||
|  | ||||
|   Future<void> _hotkeyInitialization() async { | ||||
|     if (kIsWeb) return; | ||||
|  | ||||
|     if (Platform.isMacOS) { | ||||
|       HotKey quitHotKey = HotKey( | ||||
|         key: PhysicalKeyboardKey.keyQ, | ||||
|         modifiers: [HotKeyModifier.meta], | ||||
|         scope: HotKeyScope.inapp, | ||||
|       ); | ||||
|       await hotKeyManager.register(quitHotKey, keyUpHandler: (_) { | ||||
|         _appLifecycleListener?.dispose(); | ||||
|         SystemChannels.platform.invokeMethod('SystemNavigator.pop'); | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _trayInitialization() async { | ||||
|     if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; | ||||
|  | ||||
|     final icon = Platform.isWindows ? 'assets/icon/tray-icon.ico' : 'assets/icon/tray-icon.png'; | ||||
|     final appVersion = await PackageInfo.fromPlatform(); | ||||
|  | ||||
|     trayManager.addListener(this); | ||||
|     await trayManager.setIcon(icon); | ||||
|  | ||||
|     Menu menu = Menu( | ||||
|       items: [ | ||||
|         MenuItem( | ||||
|           key: 'version_label', | ||||
|           label: 'Solian ${appVersion.version}+${appVersion.buildNumber}', | ||||
|           disabled: true, | ||||
|         ), | ||||
|         MenuItem.separator(), | ||||
|         MenuItem( | ||||
|           key: 'exit', | ||||
|           label: 'trayMenuExit'.tr(), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|     await trayManager.setContextMenu(menu); | ||||
|   } | ||||
|  | ||||
|   AppLifecycleListener? _appLifecycleListener; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|  | ||||
|     if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) { | ||||
|       _appLifecycleListener = AppLifecycleListener( | ||||
|         onExitRequested: _onExitRequested, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     _trayInitialization(); | ||||
|     _hotkeyInitialization(); | ||||
|     _initialize().then((_) { | ||||
|       _postInitialization(); | ||||
|       _tryRequestRating(); | ||||
| @@ -301,6 +361,50 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<AppExitResponse> _onExitRequested() async { | ||||
|     appWindow.hide(); | ||||
|     return AppExitResponse.cancel; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void onTrayIconMouseDown() { | ||||
|     if (Platform.isWindows) { | ||||
|       context.read<NotificationProvider>().clearTray(); | ||||
|       appWindow.show(); | ||||
|     } else { | ||||
|       trayManager.popUpContextMenu(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void onTrayIconRightMouseDown() { | ||||
|     if (Platform.isWindows) { | ||||
|       trayManager.popUpContextMenu(); | ||||
|     } else { | ||||
|       context.read<NotificationProvider>().clearTray(); | ||||
|       appWindow.show(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void onTrayMenuItemClick(MenuItem menuItem) { | ||||
|     switch (menuItem.key) { | ||||
|       case 'exit': | ||||
|         _appLifecycleListener?.dispose(); | ||||
|         SystemChannels.platform.invokeMethod('SystemNavigator.pop'); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) { | ||||
|       trayManager.removeListener(this); | ||||
|       hotKeyManager.unregisterAll(); | ||||
|     } | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final cfg = context.read<ConfigProvider>(); | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/providers/websocket.dart'; | ||||
| import 'package:surface/types/notification.dart'; | ||||
| import 'package:tray_manager/tray_manager.dart'; | ||||
|  | ||||
| class NotificationProvider extends ChangeNotifier { | ||||
|   late final SnNetworkProvider _sn; | ||||
| @@ -71,22 +72,48 @@ class NotificationProvider extends ChangeNotifier { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   int showingCount = 0; | ||||
|   int showingTrayCount = 0; | ||||
|   List<SnNotification> notifications = List.empty(growable: true); | ||||
|  | ||||
|   void listen() { | ||||
|     _ws.stream.stream.listen((event) { | ||||
|       if (event.method == 'notifications.new') { | ||||
|         final notification = SnNotification.fromJson(event.payload!); | ||||
|         if (showingCount < 0) showingCount = 0; | ||||
|         showingCount++; | ||||
|         showingTrayCount++; | ||||
|         notifications.add(notification); | ||||
|         Future.delayed(const Duration(seconds: 3), () { | ||||
|           if (showingCount >= 0) showingCount--; | ||||
|           notifyListeners(); | ||||
|         }); | ||||
|         notifyListeners(); | ||||
|         updateTray(); | ||||
|         final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true; | ||||
|         if (doHaptic) HapticFeedback.lightImpact(); | ||||
|         if (doHaptic) HapticFeedback.mediumImpact(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void clearTray() { | ||||
|     showingTrayCount = 0; | ||||
|     updateTray(); | ||||
|   } | ||||
|  | ||||
|   void updateTray() { | ||||
|     if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; | ||||
|     if (notifications.isEmpty) { | ||||
|       trayManager.setTitle(''); | ||||
|     } else { | ||||
|       trayManager.setTitle(' ${notifications.length.toString()}'); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void clear() { | ||||
|     showingCount = 0; | ||||
|     notifications.clear(); | ||||
|     updateTray(); | ||||
|     notifyListeners(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -9,6 +9,10 @@ class SnStickerProvider { | ||||
|   late final SnNetworkProvider _sn; | ||||
|   final Map<String, SnSticker?> _cache = {}; | ||||
|  | ||||
|   final Map<int, List<SnSticker>> stickersByPack = {}; | ||||
|  | ||||
|   List<SnSticker> get stickers => _cache.values.where((ele) => ele != null).cast<SnSticker>().toList(); | ||||
|  | ||||
|   SnStickerProvider(BuildContext context) { | ||||
|     _sn = context.read<SnNetworkProvider>(); | ||||
|   } | ||||
| @@ -17,6 +21,12 @@ class SnStickerProvider { | ||||
|     return _cache.containsKey(alias) && _cache[alias] == null; | ||||
|   } | ||||
|  | ||||
|   void _cacheSticker(SnSticker sticker) { | ||||
|     _cache['${sticker.pack.prefix}:${sticker.alias}'] = sticker; | ||||
|     if (stickersByPack[sticker.pack.id] == null) stickersByPack[sticker.pack.id] = List.empty(growable: true); | ||||
|     if (!stickersByPack[sticker.pack.id]!.contains(sticker)) stickersByPack[sticker.pack.id]!.add(sticker); | ||||
|   } | ||||
|  | ||||
|   Future<SnSticker?> lookupSticker(String alias) async { | ||||
|     if (_cache.containsKey(alias)) { | ||||
|       return _cache[alias]; | ||||
| @@ -25,7 +35,7 @@ class SnStickerProvider { | ||||
|     try { | ||||
|       final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias'); | ||||
|       final sticker = SnSticker.fromJson(resp.data); | ||||
|       _cache[alias] = sticker; | ||||
|       _cacheSticker(sticker); | ||||
|  | ||||
|       return sticker; | ||||
|     } catch (err) { | ||||
| @@ -35,4 +45,30 @@ class SnStickerProvider { | ||||
|  | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   Future<void> listStickerEagerly() async { | ||||
|     var count = await listSticker(); | ||||
|     for (var page = 1; count > 0; count -= 10) { | ||||
|       await listSticker(page: page); | ||||
|       page++; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<int> listSticker({int page = 0}) async { | ||||
|     try { | ||||
|       final resp = await _sn.client.get('/cgi/uc/stickers', queryParameters: { | ||||
|         'take': 10, | ||||
|         'offset': page * 10, | ||||
|       }); | ||||
|       final data = resp.data; | ||||
|       final stickers = List.from(data['data']).map((ele) => SnSticker.fromJson(ele)); | ||||
|       for (final sticker in stickers) { | ||||
|         _cacheSticker(sticker); | ||||
|       } | ||||
|       return data['count'] as int; | ||||
|     } catch (err) { | ||||
|       log('[Sticker] Failed to list stickers: $err'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -53,4 +53,11 @@ class UserProvider extends ChangeNotifier { | ||||
|     user = null; | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   void setLanguage(String? value) { | ||||
|     if (value == null) return; | ||||
|     if (user == null) return; | ||||
|     user = user!.copyWith(language: value); | ||||
|     notifyListeners(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -33,7 +33,16 @@ class WebSocketProvider extends ChangeNotifier { | ||||
|     await connect(); | ||||
|   } | ||||
|  | ||||
|   Completer<void>? _connectCompleter; | ||||
|  | ||||
|   Future<void> connect({noRetry = false}) async { | ||||
|     if(_connectCompleter != null) { | ||||
|       await _connectCompleter!.future; | ||||
|       _connectCompleter = null; | ||||
|     } | ||||
|  | ||||
|     _connectCompleter = Completer<void>(); | ||||
|  | ||||
|     if (!_ua.isAuthorized) return; | ||||
|     if (isConnected || conn != null) { | ||||
|       disconnect(); | ||||
| @@ -64,12 +73,13 @@ class WebSocketProvider extends ChangeNotifier { | ||||
|         log('Retry connecting to websocket in 3 seconds...'); | ||||
|         return Future.delayed( | ||||
|           const Duration(seconds: 3), | ||||
|           () => connect(noRetry: true), | ||||
|               () => connect(noRetry: true), | ||||
|         ); | ||||
|       } | ||||
|     } finally { | ||||
|       isBusy = false; | ||||
|       notifyListeners(); | ||||
|       _connectCompleter!.complete(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -47,6 +47,7 @@ class HomeWidgetProvider { | ||||
| } | ||||
|  | ||||
| Future<void> widgetUpdateRandomPost() async { | ||||
|   if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) return; | ||||
|   final snc = await SnNetworkProvider.createOffContextClient(); | ||||
|   final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1'); | ||||
|   final post = SnPost.fromJson(resp.data['data'][0]); | ||||
|   | ||||
| @@ -73,7 +73,7 @@ final _appRoutes = [ | ||||
|           postRepostId: int.tryParse( | ||||
|             state.uri.queryParameters['reposting'] ?? '', | ||||
|           ), | ||||
|           extraProps: state.extra as PostEditorExtraProps?, | ||||
|           extraProps: state.extra as PostEditorExtra?, | ||||
|         ), | ||||
|       ), | ||||
|       GoRoute( | ||||
| @@ -156,6 +156,7 @@ final _appRoutes = [ | ||||
|         builder: (context, state) => ChatRoomScreen( | ||||
|           scope: state.pathParameters['scope']!, | ||||
|           alias: state.pathParameters['alias']!, | ||||
|           extra: state.extra as ChatRoomScreenExtra?, | ||||
|         ), | ||||
|       ), | ||||
|       GoRoute( | ||||
|   | ||||
| @@ -28,7 +28,19 @@ class AccountScreen extends StatelessWidget { | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: AutoAppBarLeading(), | ||||
|         title: Text("screenAccount").tr(), | ||||
|         title: Text( | ||||
|           "screenAccount", | ||||
|           style: TextStyle( | ||||
|             color: Colors.white, | ||||
|             shadows: [ | ||||
|               Shadow( | ||||
|                 offset: Offset(1, 1), | ||||
|                 blurRadius: 5.0, | ||||
|                 color: Color.fromARGB(255, 0, 0, 0), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ).tr(), | ||||
|         flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty | ||||
|             ? Stack( | ||||
|                 fit: StackFit.expand, | ||||
|   | ||||
| @@ -1,17 +1,41 @@ | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.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_network.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:intl/locale.dart'; | ||||
|  | ||||
| class AccountSettingsScreen extends StatelessWidget { | ||||
|   const AccountSettingsScreen({super.key}); | ||||
|  | ||||
|   Future<void> _setAccountLanguage(BuildContext context, Locale? value) async { | ||||
|     if (value == null) return; | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final ua = context.read<UserProvider>(); | ||||
|       await sn.client.put('/cgi/id/users/me/language', data: { | ||||
|         'language': value.toString(), | ||||
|       }); | ||||
|       if (!context.mounted) return; | ||||
|       context.showSnackbar('accountSettingsApplied'.tr()); | ||||
|       await ua.refreshUser(); | ||||
|     } catch (err) { | ||||
|       if (!context.mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final ua = context.watch<UserProvider>(); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: PageBackButton(), | ||||
| @@ -21,6 +45,42 @@ class AccountSettingsScreen extends StatelessWidget { | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             ListTile( | ||||
|               title: Text('settingsAccountLanguage').tr(), | ||||
|               subtitle: Text('settingsAccountLanguageDescription').tr(), | ||||
|               contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|               leading: const Icon(Symbols.translate), | ||||
|               trailing: DropdownButtonHideUnderline( | ||||
|                 child: DropdownButton2<Locale?>( | ||||
|                   isExpanded: true, | ||||
|                   items: [ | ||||
|                     ...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) { | ||||
|                       return DropdownMenuItem<Locale?>( | ||||
|                         value: Locale.parse(ele.toString()), | ||||
|                         child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14), | ||||
|                       ); | ||||
|                     }), | ||||
|                   ], | ||||
|                   value: ua.user?.language != null ? Locale.parse(ua.user!.language) : Locale.parse('en-US'), | ||||
|                   onChanged: (Locale? value) { | ||||
|                     if (value == null) return; | ||||
|                     _setAccountLanguage(context, value); | ||||
|                     ua.setLanguage(value.toString()); | ||||
|                   }, | ||||
|                   buttonStyleData: const ButtonStyleData( | ||||
|                     padding: EdgeInsets.symmetric( | ||||
|                       horizontal: 16, | ||||
|                       vertical: 5, | ||||
|                     ), | ||||
|                     height: 40, | ||||
|                     width: 160, | ||||
|                   ), | ||||
|                   menuItemStyleData: const MenuItemStyleData( | ||||
|                     height: 40, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             ListTile( | ||||
|               title: Text('accountProfileEdit').tr(), | ||||
|               subtitle: Text('accountProfileEditSubtitle').tr(), | ||||
|   | ||||
| @@ -44,6 +44,7 @@ class _RegisterScreenState extends State<RegisterScreen> { | ||||
|         'nick': nickname, | ||||
|         'email': email, | ||||
|         'password': password, | ||||
|         'language': EasyLocalization.of(context)!.currentLocale.toString(), | ||||
|       }); | ||||
|  | ||||
|       if (!context.mounted) return; | ||||
|   | ||||
| @@ -10,8 +10,10 @@ import 'package:surface/providers/channel.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/user_directory.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/account.dart'; | ||||
| import 'package:surface/types/chat.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/account/account_select.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| @@ -20,6 +22,7 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
| class ChannelDetailScreen extends StatefulWidget { | ||||
|   final String scope; | ||||
|   final String alias; | ||||
|  | ||||
|   const ChannelDetailScreen({ | ||||
|     super.key, | ||||
|     required this.scope, | ||||
| @@ -55,8 +58,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client | ||||
|           .get('/cgi/im/channels/${_channel!.keyPath}/members/me'); | ||||
|       final resp = await sn.client.get('/cgi/im/channels/${_channel!.keyPath}/members/me'); | ||||
|       _profile = SnChannelMember.fromJson(resp.data); | ||||
|       _notifyLevel = _profile!.notify; | ||||
|       if (!mounted) return; | ||||
| @@ -143,6 +145,25 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _addMember(SnAccount related) async { | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.post( | ||||
|         '/cgi/im/channels/${_channel!.keyPath}/members', | ||||
|         data: {'related': related.name}, | ||||
|       ); | ||||
|       if (!mounted) return; | ||||
|       context.showSnackbar('channelMemberAdded'.tr()); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _showChannelProfileDetail() { | ||||
|     showDialog( | ||||
|       context: context, | ||||
| @@ -166,13 +187,16 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _showMemberAdd() { | ||||
|     showModalBottomSheet( | ||||
|   void _showMemberAdd() async { | ||||
|     final user = await showModalBottomSheet<SnAccount?>( | ||||
|       context: context, | ||||
|       builder: (context) => _NewChannelMemberWidget( | ||||
|         channel: _channel!, | ||||
|       builder: (context) => AccountSelect( | ||||
|         title: 'channelMemberAdd'.tr(), | ||||
|       ), | ||||
|     ); | ||||
|     if (!mounted) return; | ||||
|     if (user == null) return; | ||||
|     _addMember(user); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -221,11 +245,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|               Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   Text('channelDetailPersonalRegion') | ||||
|                       .bold() | ||||
|                       .fontSize(17) | ||||
|                       .tr() | ||||
|                       .padding(horizontal: 20, bottom: 4), | ||||
|                   Text('channelDetailPersonalRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), | ||||
|                   ListTile( | ||||
|                     leading: const Icon(Symbols.notifications), | ||||
|                     trailing: DropdownButtonHideUnderline( | ||||
| @@ -264,8 +284,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|                   ), | ||||
|                   ListTile( | ||||
|                     leading: AccountImage( | ||||
|                       content: | ||||
|                           ud.getAccountFromCache(_profile!.accountId)?.avatar, | ||||
|                       content: ud.getAccountFromCache(_profile!.accountId)?.avatar, | ||||
|                       radius: 18, | ||||
|                     ), | ||||
|                     trailing: const Icon(Symbols.chevron_right), | ||||
| @@ -284,8 +303,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|                       trailing: const Icon(Symbols.chevron_right), | ||||
|                       title: Text('channelActionLeave').tr(), | ||||
|                       subtitle: Text('channelActionLeaveDescription').tr(), | ||||
|                       contentPadding: | ||||
|                           const EdgeInsets.symmetric(horizontal: 24), | ||||
|                       contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                       onTap: _leaveChannel, | ||||
|                     ), | ||||
|                 ], | ||||
| @@ -293,11 +311,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text('channelDetailMemberRegion') | ||||
|                     .bold() | ||||
|                     .fontSize(17) | ||||
|                     .tr() | ||||
|                     .padding(horizontal: 20, bottom: 4), | ||||
|                 Text('channelDetailMemberRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), | ||||
|                 ListTile( | ||||
|                   leading: const Icon(Symbols.group), | ||||
|                   trailing: const Icon(Symbols.chevron_right), | ||||
| @@ -319,11 +333,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text('channelDetailAdminRegion') | ||||
|                     .bold() | ||||
|                     .fontSize(17) | ||||
|                     .tr() | ||||
|                     .padding(horizontal: 20, bottom: 4), | ||||
|                 Text('channelDetailAdminRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), | ||||
|                 ListTile( | ||||
|                   leading: const Icon(Symbols.edit), | ||||
|                   trailing: const Icon(Symbols.chevron_right), | ||||
| @@ -362,18 +372,17 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
| class _ChannelProfileDetailDialog extends StatefulWidget { | ||||
|   final SnChannel channel; | ||||
|   final SnChannelMember current; | ||||
|  | ||||
|   const _ChannelProfileDetailDialog({ | ||||
|     required this.channel, | ||||
|     required this.current, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<_ChannelProfileDetailDialog> createState() => | ||||
|       _ChannelProfileDetailDialogState(); | ||||
|   State<_ChannelProfileDetailDialog> createState() => _ChannelProfileDetailDialogState(); | ||||
| } | ||||
|  | ||||
| class _ChannelProfileDetailDialogState | ||||
|     extends State<_ChannelProfileDetailDialog> { | ||||
| class _ChannelProfileDetailDialogState extends State<_ChannelProfileDetailDialog> { | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   final TextEditingController _nickController = TextEditingController(); | ||||
| @@ -444,11 +453,11 @@ class _ChannelProfileDetailDialogState | ||||
|  | ||||
| class _ChannelMemberListWidget extends StatefulWidget { | ||||
|   final SnChannel channel; | ||||
|  | ||||
|   const _ChannelMemberListWidget({required this.channel}); | ||||
|  | ||||
|   @override | ||||
|   State<_ChannelMemberListWidget> createState() => | ||||
|       _ChannelMemberListWidgetState(); | ||||
|   State<_ChannelMemberListWidget> createState() => _ChannelMemberListWidgetState(); | ||||
| } | ||||
|  | ||||
| class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | ||||
| @@ -463,12 +472,10 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | ||||
|     try { | ||||
|       final ud = context.read<UserDirectoryProvider>(); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get( | ||||
|           '/cgi/im/channels/${widget.channel.keyPath}/members', | ||||
|           queryParameters: { | ||||
|             'take': 10, | ||||
|             'offset': 0, | ||||
|           }); | ||||
|       final resp = await sn.client.get('/cgi/im/channels/${widget.channel.keyPath}/members', queryParameters: { | ||||
|         'take': 10, | ||||
|         'offset': 0, | ||||
|       }); | ||||
|       final out = List<SnChannelMember>.from( | ||||
|         resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [], | ||||
|       ); | ||||
| @@ -526,9 +533,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | ||||
|           children: [ | ||||
|             const Icon(Symbols.group, size: 24), | ||||
|             const Gap(16), | ||||
|             Text('channelMemberManage') | ||||
|                 .tr() | ||||
|                 .textStyle(Theme.of(context).textTheme.titleLarge!), | ||||
|             Text('channelMemberManage').tr().textStyle(Theme.of(context).textTheme.titleLarge!), | ||||
|           ], | ||||
|         ).padding(horizontal: 20, top: 16, bottom: 12), | ||||
|         Expanded( | ||||
| @@ -539,8 +544,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | ||||
|             }, | ||||
|             child: InfiniteList( | ||||
|               itemCount: _members.length, | ||||
|               hasReachedMax: | ||||
|                   _totalCount != null && _members.length >= _totalCount!, | ||||
|               hasReachedMax: _totalCount != null && _members.length >= _totalCount!, | ||||
|               isLoading: _isBusy, | ||||
|               onFetchData: _fetchMembers, | ||||
|               itemBuilder: (context, index) { | ||||
| @@ -551,8 +555,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | ||||
|                     content: ud.getAccountFromCache(member.accountId)?.avatar, | ||||
|                   ), | ||||
|                   title: Text( | ||||
|                     ud.getAccountFromCache(member.accountId)?.name ?? | ||||
|                         'unknown'.tr(), | ||||
|                     ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(), | ||||
|                   ), | ||||
|                   subtitle: Text(member.nick ?? 'unknown'.tr()), | ||||
|                   trailing: SizedBox( | ||||
| @@ -562,8 +565,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | ||||
|                       mainAxisAlignment: MainAxisAlignment.end, | ||||
|                       children: [ | ||||
|                         IconButton( | ||||
|                           onPressed: | ||||
|                               _isUpdating ? null : () => _deleteMember(member), | ||||
|                           onPressed: _isUpdating ? null : () => _deleteMember(member), | ||||
|                           icon: const Icon(Symbols.person_remove), | ||||
|                         ), | ||||
|                       ], | ||||
| @@ -578,83 +580,3 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _NewChannelMemberWidget extends StatefulWidget { | ||||
|   final SnChannel channel; | ||||
|   const _NewChannelMemberWidget({required this.channel}); | ||||
|  | ||||
|   @override | ||||
|   State<_NewChannelMemberWidget> createState() => | ||||
|       _NewChannelMemberWidgetState(); | ||||
| } | ||||
|  | ||||
| class _NewChannelMemberWidgetState extends State<_NewChannelMemberWidget> { | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   final TextEditingController _relatedController = TextEditingController(); | ||||
|  | ||||
|   Future<void> _performAction() async { | ||||
|     if (_relatedController.text.isEmpty) return; | ||||
|  | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.post( | ||||
|         '/cgi/im/channels/${widget.channel.keyPath}/members', | ||||
|         data: { | ||||
|           'related': _relatedController.text, | ||||
|         }, | ||||
|       ); | ||||
|       if (!mounted) return; | ||||
|       Navigator.pop(context, true); | ||||
|       context.showSnackbar('channelMemberAdded'.tr()); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     super.dispose(); | ||||
|     _relatedController.dispose(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return StyledWidget(Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         Text( | ||||
|           'channelMemberAdd', | ||||
|           style: Theme.of(context).textTheme.titleLarge, | ||||
|         ).tr(), | ||||
|         const Gap(12), | ||||
|         TextField( | ||||
|           controller: _relatedController, | ||||
|           readOnly: _isBusy, | ||||
|           autocorrect: false, | ||||
|           autofocus: true, | ||||
|           textCapitalization: TextCapitalization.none, | ||||
|           decoration: InputDecoration( | ||||
|             labelText: 'fieldMemberRelatedName'.tr(), | ||||
|             suffix: SizedBox( | ||||
|               height: 24, | ||||
|               child: IconButton( | ||||
|                 onPressed: _isBusy ? null : () => _performAction(), | ||||
|                 icon: Icon(Symbols.send), | ||||
|                 visualDensity: | ||||
|                     const VisualDensity(horizontal: -4, vertical: -4), | ||||
|                 padding: EdgeInsets.zero, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|         ) | ||||
|       ], | ||||
|     )).padding(all: 24); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| @@ -9,9 +10,12 @@ import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/controllers/chat_message_controller.dart'; | ||||
| import 'package:surface/controllers/post_write_controller.dart'; | ||||
| import 'package:surface/providers/channel.dart'; | ||||
| import 'package:surface/providers/chat_call.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/user_directory.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/providers/websocket.dart'; | ||||
| import 'package:surface/types/chat.dart'; | ||||
| import 'package:surface/widgets/chat/call/call_prejoin.dart'; | ||||
| @@ -23,14 +27,19 @@ import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
|  | ||||
| import '../../providers/user_directory.dart'; | ||||
| import '../../providers/userinfo.dart'; | ||||
| class ChatRoomScreenExtra { | ||||
|   final String? initialText; | ||||
|   final List<PostWriteMedia>? initialAttachments; | ||||
|  | ||||
|   ChatRoomScreenExtra({this.initialText, this.initialAttachments}); | ||||
| } | ||||
|  | ||||
| class ChatRoomScreen extends StatefulWidget { | ||||
|   final String scope; | ||||
|   final String alias; | ||||
|   final ChatRoomScreenExtra? extra; | ||||
|  | ||||
|   const ChatRoomScreen({super.key, required this.scope, required this.alias}); | ||||
|   const ChatRoomScreen({super.key, required this.scope, required this.alias, this.extra}); | ||||
|  | ||||
|   @override | ||||
|   State<ChatRoomScreen> createState() => _ChatRoomScreenState(); | ||||
| @@ -177,8 +186,23 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|     _messageController = ChatMessageController(context); | ||||
|     _fetchChannel().then((_) async { | ||||
|       await _messageController.initialize(_channel!); | ||||
|       await _messageController.checkUpdate(); | ||||
|       await _fetchOngoingCall(); | ||||
|  | ||||
|       if (widget.extra != null) { | ||||
|         WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|           log('[ChatInput] Setting initial text and attachments...'); | ||||
|           if (widget.extra!.initialText != null) { | ||||
|             _inputGlobalKey.currentState?.setInitialText(widget.extra!.initialText!); | ||||
|           } | ||||
|           if (widget.extra!.initialAttachments != null) { | ||||
|             _inputGlobalKey.currentState?.setInitialAttachments(widget.extra!.initialAttachments!); | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       await Future.wait([ | ||||
|         _messageController.checkUpdate(), | ||||
|         _fetchOngoingCall(), | ||||
|       ]); | ||||
|     }); | ||||
|  | ||||
|     final ws = context.read<WebSocketProvider>(); | ||||
|   | ||||
| @@ -6,15 +6,15 @@ import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/relationship.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/account.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/account/account_select.dart'; | ||||
| import 'package:surface/widgets/app_bar_leading.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| import '../providers/userinfo.dart'; | ||||
| import '../widgets/unauthorized_hint.dart'; | ||||
| import 'package:surface/widgets/unauthorized_hint.dart'; | ||||
|  | ||||
| const kFriendStatus = { | ||||
|   0: 'friendStatusPending', | ||||
| @@ -168,6 +168,24 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<void> _sendRequest(SnAccount user) async { | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.post('/cgi/id/users/me/relations', data: { | ||||
|         'related': user.name, | ||||
|       }); | ||||
|       if (!mounted) return; | ||||
|       context.showSnackbar('friendRequestSent'.tr()); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
| @@ -199,11 +217,16 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|       ), | ||||
|       floatingActionButton: FloatingActionButton( | ||||
|         child: const Icon(Symbols.add), | ||||
|         onPressed: () { | ||||
|           showModalBottomSheet( | ||||
|         onPressed: () async { | ||||
|           final user = await showModalBottomSheet<SnAccount?>( | ||||
|             context: context, | ||||
|             builder: (context) => _NewFriendWidget(), | ||||
|             builder: (context) => AccountSelect( | ||||
|               title: 'friendNew'.tr(), | ||||
|             ), | ||||
|           ); | ||||
|           if (!mounted) return; | ||||
|           if (user == null) return; | ||||
|           _sendRequest(user); | ||||
|         }, | ||||
|       ), | ||||
|       body: Column( | ||||
| @@ -231,8 +254,7 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               onTap: _showBlocks, | ||||
|             ), | ||||
|           if (_requests.isNotEmpty || _blocks.isNotEmpty) | ||||
|             const Divider(height: 1), | ||||
|           if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1), | ||||
|           Expanded( | ||||
|             child: MediaQuery.removePadding( | ||||
|               context: context, | ||||
| @@ -264,16 +286,12 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|                               mainAxisAlignment: MainAxisAlignment.end, | ||||
|                               children: [ | ||||
|                                 InkWell( | ||||
|                                   onTap: _isUpdating | ||||
|                                       ? null | ||||
|                                       : () => _changeRelation(relation, 2), | ||||
|                                   onTap: _isUpdating ? null : () => _changeRelation(relation, 2), | ||||
|                                   child: Text('friendBlock').tr(), | ||||
|                                 ), | ||||
|                                 const Gap(8), | ||||
|                                 InkWell( | ||||
|                                   onTap: _isUpdating | ||||
|                                       ? null | ||||
|                                       : () => _deleteRelation(relation), | ||||
|                                   onTap: _isUpdating ? null : () => _deleteRelation(relation), | ||||
|                                   child: Text('friendDeleteAction').tr(), | ||||
|                                 ), | ||||
|                               ], | ||||
| @@ -293,83 +311,9 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _NewFriendWidget extends StatefulWidget { | ||||
|   const _NewFriendWidget(); | ||||
|  | ||||
|   @override | ||||
|   State<_NewFriendWidget> createState() => _NewFriendWidgetState(); | ||||
| } | ||||
|  | ||||
| class _NewFriendWidgetState extends State<_NewFriendWidget> { | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   final TextEditingController _relatedController = TextEditingController(); | ||||
|  | ||||
|   Future<void> _sendRequest() async { | ||||
|     if (_relatedController.text.isEmpty) return; | ||||
|  | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.post('/cgi/id/users/me/relations', data: { | ||||
|         'related': _relatedController.text, | ||||
|       }); | ||||
|       if (!mounted) return; | ||||
|       Navigator.pop(context, true); | ||||
|       context.showSnackbar('friendRequestSent'.tr()); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     super.dispose(); | ||||
|     _relatedController.dispose(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return StyledWidget(Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         Text( | ||||
|           'friendNew', | ||||
|           style: Theme.of(context).textTheme.titleLarge, | ||||
|         ).tr(), | ||||
|         const Gap(12), | ||||
|         TextField( | ||||
|           controller: _relatedController, | ||||
|           readOnly: _isBusy, | ||||
|           autocorrect: false, | ||||
|           autofocus: true, | ||||
|           textCapitalization: TextCapitalization.none, | ||||
|           decoration: InputDecoration( | ||||
|             labelText: 'fieldFriendRelatedName'.tr(), | ||||
|             suffix: SizedBox( | ||||
|               height: 24, | ||||
|               child: IconButton( | ||||
|                 onPressed: _isBusy ? null : () => _sendRequest(), | ||||
|                 icon: Icon(Symbols.send), | ||||
|                 visualDensity: | ||||
|                     const VisualDensity(horizontal: -4, vertical: -4), | ||||
|                 padding: EdgeInsets.zero, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|         ) | ||||
|       ], | ||||
|     )).padding(all: 24); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _FriendshipListWidget extends StatefulWidget { | ||||
|   final List<SnRelationship> relations; | ||||
|  | ||||
|   const _FriendshipListWidget({required this.relations}); | ||||
|  | ||||
|   @override | ||||
| @@ -476,9 +420,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> { | ||||
|               mainAxisAlignment: MainAxisAlignment.center, | ||||
|               crossAxisAlignment: CrossAxisAlignment.end, | ||||
|               children: [ | ||||
|                 Text(kFriendStatus[relation.status] ?? 'unknown') | ||||
|                     .tr() | ||||
|                     .opacity(0.75), | ||||
|                 Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75), | ||||
|                 if (relation.status == 0) | ||||
|                   Row( | ||||
|                     mainAxisAlignment: MainAxisAlignment.end, | ||||
| @@ -499,8 +441,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> { | ||||
|                     mainAxisAlignment: MainAxisAlignment.end, | ||||
|                     children: [ | ||||
|                       InkWell( | ||||
|                         onTap: | ||||
|                             _isBusy ? null : () => _changeRelation(relation, 1), | ||||
|                         onTap: _isBusy ? null : () => _changeRelation(relation, 1), | ||||
|                         child: Text('friendUnblock').tr(), | ||||
|                       ), | ||||
|                       const Gap(8), | ||||
|   | ||||
| @@ -288,6 +288,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> { | ||||
|               child: InkWell( | ||||
|                 borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   spacing: 4, | ||||
|                   children: [ | ||||
|                     Text( | ||||
|   | ||||
| @@ -7,6 +7,7 @@ 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:surface/providers/notification.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/notification.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| @@ -21,6 +22,16 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
| import '../providers/userinfo.dart'; | ||||
| import '../widgets/unauthorized_hint.dart'; | ||||
|  | ||||
| const Map<String, IconData> kNotificationTopicIcons = { | ||||
|   'general': Symbols.notifications, | ||||
|   'passport.security.alert': Symbols.gpp_maybe, | ||||
|   'passport.security.otp': Symbols.password, | ||||
|   'interactive.subscription': Symbols.subscriptions, | ||||
|   'interactive.feedback': Symbols.add_reaction, | ||||
|   'messaging.callStart': Symbols.call_received, | ||||
|   'wallet.transaction.new': Symbols.receipt, | ||||
| }; | ||||
|  | ||||
| class NotificationScreen extends StatefulWidget { | ||||
|   const NotificationScreen({super.key}); | ||||
|  | ||||
| @@ -36,15 +47,6 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|   final List<SnNotification> _notifications = List.empty(growable: true); | ||||
|   int? _totalCount; | ||||
|  | ||||
|   static const Map<String, IconData> kNotificationTopicIcons = { | ||||
|     'passport.security.alert': Symbols.gpp_maybe, | ||||
|     'passport.security.otp': Symbols.password, | ||||
|     'interactive.subscription': Symbols.subscriptions, | ||||
|     'interactive.feedback': Symbols.add_reaction, | ||||
|     'messaging.callStart': Symbols.call_received, | ||||
|     'wallet.transaction.new': Symbols.receipt, | ||||
|   }; | ||||
|  | ||||
|   Future<void> _fetchNotifications() async { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|     if (!ua.isAuthorized) return; | ||||
| @@ -53,6 +55,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final nty = context.read<NotificationProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/id/notifications?take=10'); | ||||
|       _totalCount = resp.data['count']; | ||||
|       _notifications.addAll( | ||||
| @@ -61,6 +64,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|                 .cast<SnNotification>() ?? | ||||
|             [], | ||||
|       ); | ||||
|       nty.updateTray(); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
| @@ -87,9 +91,11 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final nty = context.read<NotificationProvider>(); | ||||
|       final resp = await sn.client.put('/cgi/id/notifications/read/all'); | ||||
|       _notifications.clear(); | ||||
|       _fetchNotifications(); | ||||
|       nty.clear(); | ||||
|  | ||||
|       if (!mounted) return; | ||||
|       context.showSnackbar( | ||||
|   | ||||
| @@ -1,11 +1,17 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/gestures.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hotkey_manager/hotkey_manager.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:pasteboard/pasteboard.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/controllers/post_write_controller.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| @@ -20,13 +26,15 @@ import 'package:surface/widgets/post/post_meta_editor.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
|  | ||||
| class PostEditorExtraProps { | ||||
| import '../../types/attachment.dart'; | ||||
|  | ||||
| class PostEditorExtra { | ||||
|   final String? text; | ||||
|   final String? title; | ||||
|   final String? description; | ||||
|   final List<PostWriteMedia>? attachments; | ||||
|  | ||||
|   const PostEditorExtraProps({ | ||||
|   const PostEditorExtra({ | ||||
|     this.text, | ||||
|     this.title, | ||||
|     this.description, | ||||
| @@ -39,7 +47,7 @@ class PostEditorScreen extends StatefulWidget { | ||||
|   final int? postEditId; | ||||
|   final int? postReplyId; | ||||
|   final int? postRepostId; | ||||
|   final PostEditorExtraProps? extraProps; | ||||
|   final PostEditorExtra? extraProps; | ||||
|  | ||||
|   const PostEditorScreen({ | ||||
|     super.key, | ||||
| @@ -94,15 +102,39 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   final HotKey _pasteHotKey = HotKey( | ||||
|     key: PhysicalKeyboardKey.keyV, | ||||
|     modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control], | ||||
|     scope: HotKeyScope.inapp, | ||||
|   ); | ||||
|  | ||||
|   void _registerHotKey() { | ||||
|     if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; | ||||
|     hotKeyManager.register(_pasteHotKey, keyDownHandler: (_) async { | ||||
|       final imageBytes = await Pasteboard.image; | ||||
|       if (imageBytes == null) return; | ||||
|       _writeController.addAttachments([ | ||||
|         PostWriteMedia.fromBytes( | ||||
|           imageBytes, | ||||
|           'attachmentPastedImage'.tr(), | ||||
|           SnMediaType.image, | ||||
|         ), | ||||
|       ]); | ||||
|       setState(() {}); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _writeController.dispose(); | ||||
|     if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey); | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _registerHotKey(); | ||||
|     if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) { | ||||
|       context.showErrorDialog('Unknown post type'); | ||||
|       Navigator.pop(context); | ||||
| @@ -153,6 +185,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                       ), | ||||
|                 ), | ||||
|               ]), | ||||
|               maxLines: 2, | ||||
|             ), | ||||
|             actions: [ | ||||
|               IconButton( | ||||
| @@ -250,121 +283,130 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|               ), | ||||
|               const Divider(height: 1), | ||||
|               Expanded( | ||||
|                 child: SingleChildScrollView( | ||||
|                   padding: EdgeInsets.only(bottom: 8), | ||||
|                   child: Column( | ||||
|                     children: [ | ||||
|                       // Replying Notice | ||||
|                       if (_writeController.replyingPost != null) | ||||
|                         Column( | ||||
|                           children: [ | ||||
|                             ExpansionTile( | ||||
|                               minTileHeight: 48, | ||||
|                               leading: const Icon(Symbols.reply).padding(left: 4), | ||||
|                               title: Text('postReplyingNotice') | ||||
|                                   .fontSize(15) | ||||
|                                   .tr(args: ['@${_writeController.replyingPost!.publisher.name}']), | ||||
|                               children: <Widget>[PostItem(data: _writeController.replyingPost!)], | ||||
|                             ), | ||||
|                             const Divider(height: 1), | ||||
|                           ], | ||||
|                         ), | ||||
|                       // Reposting Notice | ||||
|                       if (_writeController.repostingPost != null) | ||||
|                         Column( | ||||
|                           children: [ | ||||
|                             ExpansionTile( | ||||
|                               minTileHeight: 48, | ||||
|                               leading: const Icon(Symbols.forward).padding(left: 4), | ||||
|                               title: Text('postRepostingNotice') | ||||
|                                   .fontSize(15) | ||||
|                                   .tr(args: ['@${_writeController.repostingPost!.publisher.name}']), | ||||
|                               children: <Widget>[ | ||||
|                                 PostItem( | ||||
|                                   data: _writeController.repostingPost!, | ||||
|                                 ) | ||||
|                 child: Stack( | ||||
|                   children: [ | ||||
|                     SingleChildScrollView( | ||||
|                       padding: EdgeInsets.only(bottom: 160), | ||||
|                       child: Column( | ||||
|                         children: [ | ||||
|                           // Replying Notice | ||||
|                           if (_writeController.replyingPost != null) | ||||
|                             Column( | ||||
|                               children: [ | ||||
|                                 ExpansionTile( | ||||
|                                   minTileHeight: 48, | ||||
|                                   leading: const Icon(Symbols.reply).padding(left: 4), | ||||
|                                   title: Text('postReplyingNotice') | ||||
|                                       .fontSize(15) | ||||
|                                       .tr(args: ['@${_writeController.replyingPost!.publisher.name}']), | ||||
|                                   children: <Widget>[PostItem(data: _writeController.replyingPost!)], | ||||
|                                 ), | ||||
|                                 const Divider(height: 1), | ||||
|                               ], | ||||
|                             ), | ||||
|                             const Divider(height: 1), | ||||
|                           ], | ||||
|                         ), | ||||
|                       // Editing Notice | ||||
|                       if (_writeController.editingPost != null) | ||||
|                         Column( | ||||
|                           children: [ | ||||
|                             ExpansionTile( | ||||
|                               minTileHeight: 48, | ||||
|                               leading: const Icon(Symbols.edit_note).padding(left: 4), | ||||
|                               title: Text('postEditingNotice') | ||||
|                                   .fontSize(15) | ||||
|                                   .tr(args: ['@${_writeController.editingPost!.publisher.name}']), | ||||
|                               children: <Widget>[PostItem(data: _writeController.editingPost!)], | ||||
|                           // Reposting Notice | ||||
|                           if (_writeController.repostingPost != null) | ||||
|                             Column( | ||||
|                               children: [ | ||||
|                                 ExpansionTile( | ||||
|                                   minTileHeight: 48, | ||||
|                                   leading: const Icon(Symbols.forward).padding(left: 4), | ||||
|                                   title: Text('postRepostingNotice') | ||||
|                                       .fontSize(15) | ||||
|                                       .tr(args: ['@${_writeController.repostingPost!.publisher.name}']), | ||||
|                                   children: <Widget>[ | ||||
|                                     PostItem( | ||||
|                                       data: _writeController.repostingPost!, | ||||
|                                     ) | ||||
|                                   ], | ||||
|                                 ), | ||||
|                                 const Divider(height: 1), | ||||
|                               ], | ||||
|                             ), | ||||
|                             const Divider(height: 1), | ||||
|                           ], | ||||
|                         ), | ||||
|                       // Content Input Area | ||||
|                       Container( | ||||
|                         constraints: const BoxConstraints(maxWidth: 640), | ||||
|                         child: TextField( | ||||
|                           controller: _writeController.contentController, | ||||
|                           maxLines: null, | ||||
|                           decoration: InputDecoration( | ||||
|                             hintText: 'fieldPostContent'.tr(), | ||||
|                             hintStyle: TextStyle(fontSize: 14), | ||||
|                             isCollapsed: true, | ||||
|                             contentPadding: const EdgeInsets.symmetric( | ||||
|                               horizontal: 16, | ||||
|                           // Editing Notice | ||||
|                           if (_writeController.editingPost != null) | ||||
|                             Column( | ||||
|                               children: [ | ||||
|                                 ExpansionTile( | ||||
|                                   minTileHeight: 48, | ||||
|                                   leading: const Icon(Symbols.edit_note).padding(left: 4), | ||||
|                                   title: Text('postEditingNotice') | ||||
|                                       .fontSize(15) | ||||
|                                       .tr(args: ['@${_writeController.editingPost!.publisher.name}']), | ||||
|                                   children: <Widget>[PostItem(data: _writeController.editingPost!)], | ||||
|                                 ), | ||||
|                                 const Divider(height: 1), | ||||
|                               ], | ||||
|                             ), | ||||
|                           // Content Input Area | ||||
|                           Container( | ||||
|                             constraints: const BoxConstraints(maxWidth: 640), | ||||
|                             child: TextField( | ||||
|                               controller: _writeController.contentController, | ||||
|                               maxLines: null, | ||||
|                               decoration: InputDecoration( | ||||
|                                 hintText: 'fieldPostContent'.tr(), | ||||
|                                 hintStyle: TextStyle(fontSize: 14), | ||||
|                                 isCollapsed: true, | ||||
|                                 contentPadding: const EdgeInsets.symmetric( | ||||
|                                   horizontal: 16, | ||||
|                                 ), | ||||
|                                 border: InputBorder.none, | ||||
|                               ), | ||||
|                               onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                             ), | ||||
|                             border: InputBorder.none, | ||||
|                           ), | ||||
|                           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                         ), | ||||
|                         ] | ||||
|                             .expandIndexed( | ||||
|                               (idx, ele) => [ | ||||
|                                 if (idx != 0 || _writeController.isRelatedNull) const Gap(8), | ||||
|                                 ele, | ||||
|                               ], | ||||
|                             ) | ||||
|                             .toList(), | ||||
|                       ), | ||||
|                     ] | ||||
|                         .expandIndexed( | ||||
|                           (idx, ele) => [ | ||||
|                             if (idx != 0 || _writeController.isRelatedNull) const Gap(8), | ||||
|                             ele, | ||||
|                           ], | ||||
|                         ) | ||||
|                         .toList(), | ||||
|                   ), | ||||
|                     ), | ||||
|                     if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null) | ||||
|                       Positioned( | ||||
|                         bottom: 0, | ||||
|                         left: 0, | ||||
|                         right: 0, | ||||
|                         child: PostMediaPendingList( | ||||
|                           thumbnail: _writeController.thumbnail, | ||||
|                           attachments: _writeController.attachments, | ||||
|                           isBusy: _writeController.isBusy, | ||||
|                           onUpload: (int idx) async { | ||||
|                             await _writeController.uploadSingleAttachment(context, idx); | ||||
|                           }, | ||||
|                           onPostSetThumbnail: (int? idx) { | ||||
|                             _writeController.setThumbnail(idx); | ||||
|                           }, | ||||
|                           onInsertLink: (int idx) async { | ||||
|                             _writeController.contentController.text += | ||||
|                                 '\n'; | ||||
|                           }, | ||||
|                           onUpdate: (int idx, PostWriteMedia updatedMedia) async { | ||||
|                             _writeController.setIsBusy(true); | ||||
|                             try { | ||||
|                               _writeController.setAttachmentAt(idx, updatedMedia); | ||||
|                             } finally { | ||||
|                               _writeController.setIsBusy(false); | ||||
|                             } | ||||
|                           }, | ||||
|                           onRemove: (int idx) async { | ||||
|                             _writeController.setIsBusy(true); | ||||
|                             try { | ||||
|                               _writeController.removeAttachmentAt(idx); | ||||
|                             } finally { | ||||
|                               _writeController.setIsBusy(false); | ||||
|                             } | ||||
|                           }, | ||||
|                           onUpdateBusy: (state) => _writeController.setIsBusy(state), | ||||
|                         ).padding(bottom: 8), | ||||
|                       ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|               if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null) | ||||
|                 PostMediaPendingList( | ||||
|                   thumbnail: _writeController.thumbnail, | ||||
|                   attachments: _writeController.attachments, | ||||
|                   isBusy: _writeController.isBusy, | ||||
|                   onUpload: (int idx) async { | ||||
|                     await _writeController.uploadSingleAttachment(context, idx); | ||||
|                   }, | ||||
|                   onPostSetThumbnail: (int? idx) { | ||||
|                     _writeController.setThumbnail(idx); | ||||
|                   }, | ||||
|                   onInsertLink: (int idx) async { | ||||
|                     _writeController.contentController.text += | ||||
|                         '\n'; | ||||
|                   }, | ||||
|                   onUpdate: (int idx, PostWriteMedia updatedMedia) async { | ||||
|                     _writeController.setIsBusy(true); | ||||
|                     try { | ||||
|                       _writeController.setAttachmentAt(idx, updatedMedia); | ||||
|                     } finally { | ||||
|                       _writeController.setIsBusy(false); | ||||
|                     } | ||||
|                   }, | ||||
|                   onRemove: (int idx) async { | ||||
|                     _writeController.setIsBusy(true); | ||||
|                     try { | ||||
|                       _writeController.removeAttachmentAt(idx); | ||||
|                     } finally { | ||||
|                       _writeController.setIsBusy(false); | ||||
|                     } | ||||
|                   }, | ||||
|                   onUpdateBusy: (state) => _writeController.setIsBusy(state), | ||||
|                 ).padding(bottom: 8), | ||||
|               Material( | ||||
|                 elevation: 2, | ||||
|                 child: Column( | ||||
|   | ||||
| @@ -8,9 +8,11 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/user_directory.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/account.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/types/realm.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/account/account_select.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
| @@ -229,13 +231,35 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _showMemberAdd() { | ||||
|     showModalBottomSheet( | ||||
|   Future<void> _addMember(SnAccount related) async { | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.post( | ||||
|         '/cgi/id/realms/${widget.realm!.alias}/members', | ||||
|         data: {'related': related.name}, | ||||
|       ); | ||||
|       if (!mounted) return; | ||||
|       context.showSnackbar('realmMemberAdded'.tr()); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void _showMemberAdd() async { | ||||
|     final user = await showModalBottomSheet<SnAccount?>( | ||||
|       context: context, | ||||
|       builder: (context) => _NewRealmMemberWidget( | ||||
|         realm: widget.realm!, | ||||
|       builder: (context) => AccountSelect( | ||||
|         title: 'realmMemberAdd'.tr(), | ||||
|       ), | ||||
|     ); | ||||
|     if (!mounted) return; | ||||
|     if (user == null) return; | ||||
|     _addMember(user); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -293,85 +317,6 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _NewRealmMemberWidget extends StatefulWidget { | ||||
|   final SnRealm realm; | ||||
|  | ||||
|   const _NewRealmMemberWidget({required this.realm}); | ||||
|  | ||||
|   @override | ||||
|   State<_NewRealmMemberWidget> createState() => _NewRealmMemberWidgetState(); | ||||
| } | ||||
|  | ||||
| class _NewRealmMemberWidgetState extends State<_NewRealmMemberWidget> { | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   final TextEditingController _relatedController = TextEditingController(); | ||||
|  | ||||
|   Future<void> _performAction() async { | ||||
|     if (_relatedController.text.isEmpty) return; | ||||
|  | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.post( | ||||
|         '/cgi/id/realms/${widget.realm.alias}/members', | ||||
|         data: { | ||||
|           'related': _relatedController.text, | ||||
|         }, | ||||
|       ); | ||||
|       if (!mounted) return; | ||||
|       Navigator.pop(context, true); | ||||
|       context.showSnackbar('channelMemberAdded'.tr()); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     super.dispose(); | ||||
|     _relatedController.dispose(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return StyledWidget(Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         Text( | ||||
|           'realmMemberAdd', | ||||
|           style: Theme.of(context).textTheme.titleLarge, | ||||
|         ).tr(), | ||||
|         const Gap(12), | ||||
|         TextField( | ||||
|           controller: _relatedController, | ||||
|           readOnly: _isBusy, | ||||
|           autocorrect: false, | ||||
|           autofocus: true, | ||||
|           textCapitalization: TextCapitalization.none, | ||||
|           decoration: InputDecoration( | ||||
|             labelText: 'fieldMemberRelatedName'.tr(), | ||||
|             suffix: SizedBox( | ||||
|               height: 24, | ||||
|               child: IconButton( | ||||
|                 onPressed: _isBusy ? null : () => _performAction(), | ||||
|                 icon: Icon(Symbols.send), | ||||
|                 visualDensity: const VisualDensity(horizontal: -4, vertical: -4), | ||||
|                 padding: EdgeInsets.zero, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|         ) | ||||
|       ], | ||||
|     )).padding(all: 24); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _RealmSettingsWidget extends StatefulWidget { | ||||
|   final SnRealm? realm; | ||||
|   final Function() onUpdate; | ||||
|   | ||||
| @@ -8,9 +8,20 @@ import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:receive_sharing_intent/receive_sharing_intent.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/controllers/post_write_controller.dart'; | ||||
| import 'package:surface/providers/channel.dart'; | ||||
| import 'package:surface/providers/user_directory.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/screens/chat/room.dart'; | ||||
| import 'package:surface/screens/post/post_editor.dart'; | ||||
| import 'package:surface/types/chat.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
|  | ||||
| class AppSharingListener extends StatefulWidget { | ||||
|   final Widget child; | ||||
| @@ -51,20 +62,39 @@ class _AppSharingListenerState extends State<AppSharingListener> { | ||||
|                           pathParameters: { | ||||
|                             'mode': 'stories', | ||||
|                           }, | ||||
|                           extra: PostEditorExtraProps( | ||||
|                           extra: PostEditorExtra( | ||||
|                             text: value | ||||
|                                 .where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type)) | ||||
|                                 .map((e) => e.path).join('\n'), | ||||
|                                 .map((e) => e.path) | ||||
|                                 .join('\n'), | ||||
|                             attachments: value | ||||
|                                 .where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type)) | ||||
|                                 .map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(), | ||||
|                                 .where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image] | ||||
|                                     .contains(e.type)) | ||||
|                                 .map((e) => PostWriteMedia.fromFile(XFile(e.path))) | ||||
|                                 .toList(), | ||||
|                           ), | ||||
|                         ); | ||||
|                         Navigator.pop(context); | ||||
|                       }, | ||||
|                     ), | ||||
|                     ListTile( | ||||
|                       contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|                       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), | ||||
|                       leading: Icon(Icons.chat_outlined), | ||||
|                       trailing: const Icon(Icons.chevron_right), | ||||
|                       title: Text('shareIntentSendChannel').tr(), | ||||
|                       onTap: () { | ||||
|                         showModalBottomSheet( | ||||
|                           context: context, | ||||
|                           builder: (context) => _ShareIntentChannelSelect(value: value), | ||||
|                         ).then((val) { | ||||
|                           if (!context.mounted) return; | ||||
|                           if (val == true) Navigator.pop(context); | ||||
|                         }); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 ).width(280), | ||||
|               ) | ||||
|             ], | ||||
|           ), | ||||
| @@ -103,7 +133,7 @@ class _AppSharingListenerState extends State<AppSharingListener> { | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { | ||||
|     if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { | ||||
|       _initialize(); | ||||
|       _initialHandle(); | ||||
|     } | ||||
| @@ -120,3 +150,193 @@ class _AppSharingListenerState extends State<AppSharingListener> { | ||||
|     return widget.child; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _ShareIntentChannelSelect extends StatefulWidget { | ||||
|   final Iterable<SharedMediaFile> value; | ||||
|  | ||||
|   const _ShareIntentChannelSelect({required this.value}); | ||||
|  | ||||
|   @override | ||||
|   State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState(); | ||||
| } | ||||
|  | ||||
| class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { | ||||
|   bool _isBusy = true; | ||||
|  | ||||
|   List<SnChannel>? _channels; | ||||
|   Map<int, SnChatMessage>? _lastMessages; | ||||
|  | ||||
|   void _refreshChannels() { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|     if (!ua.isAuthorized) { | ||||
|       setState(() => _isBusy = false); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     final chan = context.read<ChatChannelProvider>(); | ||||
|     chan.fetchChannels().listen((channels) async { | ||||
|       final lastMessages = await chan.getLastMessages(channels); | ||||
|       _lastMessages = {for (final val in lastMessages) val.channelId: val}; | ||||
|       channels.sort((a, b) { | ||||
|         if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) { | ||||
|           return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt); | ||||
|         } | ||||
|         if (_lastMessages!.containsKey(a.id)) return -1; | ||||
|         if (_lastMessages!.containsKey(b.id)) return 1; | ||||
|         return 0; | ||||
|       }); | ||||
|  | ||||
|       if (!mounted) return; | ||||
|       final ud = context.read<UserDirectoryProvider>(); | ||||
|       for (final channel in channels) { | ||||
|         if (channel.type == 1) { | ||||
|           await ud.listAccount( | ||||
|             channel.members | ||||
|                     ?.cast<SnChannelMember?>() | ||||
|                     .map((ele) => ele?.accountId) | ||||
|                     .where((ele) => ele != null) | ||||
|                     .toSet() ?? | ||||
|                 {}, | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (mounted) setState(() => _channels = channels); | ||||
|     }) | ||||
|       ..onError((err) { | ||||
|         if (!mounted) return; | ||||
|         context.showErrorDialog(err); | ||||
|         setState(() => _isBusy = false); | ||||
|       }) | ||||
|       ..onDone(() { | ||||
|         if (!mounted) return; | ||||
|         setState(() => _isBusy = false); | ||||
|       }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _refreshChannels(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|     final ud = context.read<UserDirectoryProvider>(); | ||||
|  | ||||
|     return Column( | ||||
|       children: [ | ||||
|         Row( | ||||
|           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|           children: [ | ||||
|             const Icon(Symbols.chat, size: 24), | ||||
|             const Gap(16), | ||||
|             Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(), | ||||
|           ], | ||||
|         ).padding(horizontal: 20, top: 16, bottom: 12), | ||||
|         LoadingIndicator(isActive: _isBusy), | ||||
|         Expanded( | ||||
|           child: MediaQuery.removePadding( | ||||
|             context: context, | ||||
|             removeTop: true, | ||||
|             child: RefreshIndicator( | ||||
|               onRefresh: () => Future.sync(() => _refreshChannels()), | ||||
|               child: ListView.builder( | ||||
|                 itemCount: _channels?.length ?? 0, | ||||
|                 itemBuilder: (context, idx) { | ||||
|                   final channel = _channels![idx]; | ||||
|                   final lastMessage = _lastMessages?[channel.id]; | ||||
|  | ||||
|                   if (channel.type == 1) { | ||||
|                     final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere( | ||||
|                           (ele) => ele?.accountId != ua.user?.id, | ||||
|                           orElse: () => null, | ||||
|                         ); | ||||
|  | ||||
|                     return ListTile( | ||||
|                       title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name), | ||||
|                       subtitle: lastMessage != null | ||||
|                           ? Text( | ||||
|                               '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||
|                               maxLines: 1, | ||||
|                               overflow: TextOverflow.ellipsis, | ||||
|                             ) | ||||
|                           : Text( | ||||
|                               'channelDirectMessageDescription'.tr(args: [ | ||||
|                                 '@${ud.getAccountFromCache(otherMember?.accountId)?.name}', | ||||
|                               ]), | ||||
|                               maxLines: 1, | ||||
|                               overflow: TextOverflow.ellipsis, | ||||
|                             ), | ||||
|                       contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                       leading: AccountImage( | ||||
|                         content: ud.getAccountFromCache(otherMember?.accountId)?.avatar, | ||||
|                       ), | ||||
|                       onTap: () { | ||||
|                         GoRouter.of(context).pushNamed( | ||||
|                           'chatRoom', | ||||
|                           pathParameters: { | ||||
|                             'scope': channel.realm?.alias ?? 'global', | ||||
|                             'alias': channel.alias, | ||||
|                           }, | ||||
|                         ).then((value) { | ||||
|                           if (mounted) _refreshChannels(); | ||||
|                         }); | ||||
|                       }, | ||||
|                     ); | ||||
|                   } | ||||
|  | ||||
|                   return ListTile( | ||||
|                     title: Text(channel.name), | ||||
|                     subtitle: lastMessage != null | ||||
|                         ? Text( | ||||
|                             '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||
|                             maxLines: 1, | ||||
|                             overflow: TextOverflow.ellipsis, | ||||
|                           ) | ||||
|                         : Text( | ||||
|                             channel.description, | ||||
|                             maxLines: 1, | ||||
|                             overflow: TextOverflow.ellipsis, | ||||
|                           ), | ||||
|                     contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                     leading: AccountImage( | ||||
|                       content: null, | ||||
|                       fallbackWidget: const Icon(Symbols.chat, size: 20), | ||||
|                     ), | ||||
|                     onTap: () { | ||||
|                       Navigator.pop(context, true); | ||||
|                       GoRouter.of(context) | ||||
|                           .pushNamed( | ||||
|                         'chatRoom', | ||||
|                         pathParameters: { | ||||
|                           'scope': channel.realm?.alias ?? 'global', | ||||
|                           'alias': channel.alias, | ||||
|                         }, | ||||
|                         extra: ChatRoomScreenExtra( | ||||
|                           initialText: widget.value | ||||
|                               .where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type)) | ||||
|                               .map((e) => e.path) | ||||
|                               .join('\n'), | ||||
|                           initialAttachments: widget.value | ||||
|                               .where((e) => | ||||
|                                   [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type)) | ||||
|                               .map((e) => PostWriteMedia.fromFile(XFile(e.path))) | ||||
|                               .toList(), | ||||
|                         ), | ||||
|                       ) | ||||
|                           .then((value) { | ||||
|                         if (value == true) _refreshChannels(); | ||||
|                       }); | ||||
|                     }, | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -33,7 +33,7 @@ Future<ThemeData> createAppTheme( | ||||
|     brightness: brightness, | ||||
|   ); | ||||
|  | ||||
|   final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false; | ||||
|   final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false; | ||||
|   final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true); | ||||
|  | ||||
|   return ThemeData( | ||||
| @@ -51,9 +51,9 @@ Future<ThemeData> createAppTheme( | ||||
|     ), | ||||
|     appBarTheme: AppBarTheme( | ||||
|       centerTitle: true, | ||||
|       elevation: hasAppBarBlurry ? 0 : null, | ||||
|       backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary, | ||||
|       foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary, | ||||
|       elevation: hasAppBarTransparent ? 0 : null, | ||||
|       backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary, | ||||
|       foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary, | ||||
|     ), | ||||
|     pageTransitionsTheme: PageTransitionsTheme( | ||||
|       builders: { | ||||
|   | ||||
| @@ -21,6 +21,7 @@ class SnAccount with _$SnAccount { | ||||
|     required String name, | ||||
|     required String nick, | ||||
|     required Map<String, dynamic> permNodes, | ||||
|     required String language, | ||||
|     required SnAccountProfile? profile, | ||||
|     @Default([]) List<SnAccountBadge> badges, | ||||
|     required DateTime? suspendedAt, | ||||
|   | ||||
| @@ -33,6 +33,7 @@ mixin _$SnAccount { | ||||
|   String get name => throw _privateConstructorUsedError; | ||||
|   String get nick => throw _privateConstructorUsedError; | ||||
|   Map<String, dynamic> get permNodes => throw _privateConstructorUsedError; | ||||
|   String get language => throw _privateConstructorUsedError; | ||||
|   SnAccountProfile? get profile => throw _privateConstructorUsedError; | ||||
|   List<SnAccountBadge> get badges => throw _privateConstructorUsedError; | ||||
|   DateTime? get suspendedAt => throw _privateConstructorUsedError; | ||||
| @@ -69,6 +70,7 @@ abstract class $SnAccountCopyWith<$Res> { | ||||
|       String name, | ||||
|       String nick, | ||||
|       Map<String, dynamic> permNodes, | ||||
|       String language, | ||||
|       SnAccountProfile? profile, | ||||
|       List<SnAccountBadge> badges, | ||||
|       DateTime? suspendedAt, | ||||
| @@ -107,6 +109,7 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount> | ||||
|     Object? name = null, | ||||
|     Object? nick = null, | ||||
|     Object? permNodes = null, | ||||
|     Object? language = null, | ||||
|     Object? profile = freezed, | ||||
|     Object? badges = null, | ||||
|     Object? suspendedAt = freezed, | ||||
| @@ -164,6 +167,10 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount> | ||||
|           ? _value.permNodes | ||||
|           : permNodes // ignore: cast_nullable_to_non_nullable | ||||
|               as Map<String, dynamic>, | ||||
|       language: null == language | ||||
|           ? _value.language | ||||
|           : language // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       profile: freezed == profile | ||||
|           ? _value.profile | ||||
|           : profile // ignore: cast_nullable_to_non_nullable | ||||
| @@ -231,6 +238,7 @@ abstract class _$$SnAccountImplCopyWith<$Res> | ||||
|       String name, | ||||
|       String nick, | ||||
|       Map<String, dynamic> permNodes, | ||||
|       String language, | ||||
|       SnAccountProfile? profile, | ||||
|       List<SnAccountBadge> badges, | ||||
|       DateTime? suspendedAt, | ||||
| @@ -268,6 +276,7 @@ class __$$SnAccountImplCopyWithImpl<$Res> | ||||
|     Object? name = null, | ||||
|     Object? nick = null, | ||||
|     Object? permNodes = null, | ||||
|     Object? language = null, | ||||
|     Object? profile = freezed, | ||||
|     Object? badges = null, | ||||
|     Object? suspendedAt = freezed, | ||||
| @@ -325,6 +334,10 @@ class __$$SnAccountImplCopyWithImpl<$Res> | ||||
|           ? _value._permNodes | ||||
|           : permNodes // ignore: cast_nullable_to_non_nullable | ||||
|               as Map<String, dynamic>, | ||||
|       language: null == language | ||||
|           ? _value.language | ||||
|           : language // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       profile: freezed == profile | ||||
|           ? _value.profile | ||||
|           : profile // ignore: cast_nullable_to_non_nullable | ||||
| @@ -373,6 +386,7 @@ class _$SnAccountImpl extends _SnAccount { | ||||
|       required this.name, | ||||
|       required this.nick, | ||||
|       required final Map<String, dynamic> permNodes, | ||||
|       required this.language, | ||||
|       required this.profile, | ||||
|       final List<SnAccountBadge> badges = const [], | ||||
|       required this.suspendedAt, | ||||
| @@ -429,6 +443,8 @@ class _$SnAccountImpl extends _SnAccount { | ||||
|     return EqualUnmodifiableMapView(_permNodes); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   final String language; | ||||
|   @override | ||||
|   final SnAccountProfile? profile; | ||||
|   final List<SnAccountBadge> _badges; | ||||
| @@ -453,7 +469,7 @@ class _$SnAccountImpl extends _SnAccount { | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)'; | ||||
|     return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -479,6 +495,8 @@ class _$SnAccountImpl extends _SnAccount { | ||||
|             (identical(other.nick, nick) || other.nick == nick) && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other._permNodes, _permNodes) && | ||||
|             (identical(other.language, language) || | ||||
|                 other.language == language) && | ||||
|             (identical(other.profile, profile) || other.profile == profile) && | ||||
|             const DeepCollectionEquality().equals(other._badges, _badges) && | ||||
|             (identical(other.suspendedAt, suspendedAt) || | ||||
| @@ -509,6 +527,7 @@ class _$SnAccountImpl extends _SnAccount { | ||||
|         name, | ||||
|         nick, | ||||
|         const DeepCollectionEquality().hash(_permNodes), | ||||
|         language, | ||||
|         profile, | ||||
|         const DeepCollectionEquality().hash(_badges), | ||||
|         suspendedAt, | ||||
| @@ -548,6 +567,7 @@ abstract class _SnAccount extends SnAccount { | ||||
|       required final String name, | ||||
|       required final String nick, | ||||
|       required final Map<String, dynamic> permNodes, | ||||
|       required final String language, | ||||
|       required final SnAccountProfile? profile, | ||||
|       final List<SnAccountBadge> badges, | ||||
|       required final DateTime? suspendedAt, | ||||
| @@ -586,6 +606,8 @@ abstract class _SnAccount extends SnAccount { | ||||
|   @override | ||||
|   Map<String, dynamic> get permNodes; | ||||
|   @override | ||||
|   String get language; | ||||
|   @override | ||||
|   SnAccountProfile? get profile; | ||||
|   @override | ||||
|   List<SnAccountBadge> get badges; | ||||
|   | ||||
| @@ -26,6 +26,7 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) => | ||||
|       name: json['name'] as String, | ||||
|       nick: json['nick'] as String, | ||||
|       permNodes: json['perm_nodes'] as Map<String, dynamic>, | ||||
|       language: json['language'] as String, | ||||
|       profile: json['profile'] == null | ||||
|           ? null | ||||
|           : SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>), | ||||
| @@ -56,6 +57,7 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) => | ||||
|       'name': instance.name, | ||||
|       'nick': instance.nick, | ||||
|       'perm_nodes': instance.permNodes, | ||||
|       'language': instance.language, | ||||
|       'profile': instance.profile?.toJson(), | ||||
|       'badges': instance.badges.map((e) => e.toJson()).toList(), | ||||
|       'suspended_at': instance.suspendedAt?.toIso8601String(), | ||||
|   | ||||
| @@ -152,6 +152,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> { | ||||
|       child: GestureDetector( | ||||
|         behavior: HitTestBehavior.translucent, | ||||
|         child: Scaffold( | ||||
|           backgroundColor: Colors.transparent, | ||||
|           body: Stack( | ||||
|             children: [ | ||||
|               Builder(builder: (context) { | ||||
|   | ||||
| @@ -1,17 +1,28 @@ | ||||
| import 'dart:io'; | ||||
| import 'dart:math' show min; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:hotkey_manager/hotkey_manager.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/chat_message_controller.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/providers/sn_sticker.dart'; | ||||
| 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/post/post_media_pending_list.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| class ChatMessageInput extends StatefulWidget { | ||||
|   final ChatMessageController controller; | ||||
| @@ -32,9 +43,30 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|   final TextEditingController _contentController = TextEditingController(); | ||||
|   final FocusNode _focusNode = FocusNode(); | ||||
|  | ||||
|   final HotKey _pasteHotKey = HotKey( | ||||
|     key: PhysicalKeyboardKey.keyV, | ||||
|     modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control], | ||||
|     scope: HotKeyScope.inapp, | ||||
|   ); | ||||
|  | ||||
|   void _registerHotKey() { | ||||
|     if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; | ||||
|     hotKeyManager.register(_pasteHotKey, keyDownHandler: (_) async { | ||||
|       final imageBytes = await Pasteboard.image; | ||||
|       if (imageBytes == null) return; | ||||
|       _attachments.add(PostWriteMedia.fromBytes( | ||||
|         imageBytes, | ||||
|         'attachmentPastedImage'.tr(), | ||||
|         SnMediaType.image, | ||||
|       )); | ||||
|       setState(() {}); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _registerHotKey(); | ||||
|     _contentController.addListener(() { | ||||
|       if (_contentController.text.isNotEmpty) { | ||||
|         widget.controller.pingTypingStatus(); | ||||
| @@ -46,6 +78,16 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|     setState(() => _replyingMessage = value); | ||||
|   } | ||||
|  | ||||
|   void setInitialText(String? value) { | ||||
|     _contentController.text = value ?? ''; | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   void setInitialAttachments(List<PostWriteMedia>? value) { | ||||
|     _attachments.addAll(value ?? []); | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   void setEdit(SnChatMessage? value) { | ||||
|     _contentController.text = value?.body['text'] ?? ''; | ||||
|     _attachments.clear(); | ||||
| @@ -134,10 +176,34 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|  | ||||
|   final List<PostWriteMedia> _attachments = List.empty(growable: true); | ||||
|  | ||||
|   OverlayEntry? _overlayEntry; | ||||
|  | ||||
|   void _showEmojiPicker(BuildContext context) { | ||||
|     final overlay = Overlay.of(context); | ||||
|     _overlayEntry = OverlayEntry( | ||||
|       builder: (context) => Positioned( | ||||
|         bottom: 16 + MediaQuery.of(context).padding.bottom, | ||||
|         right: 16, | ||||
|         child: _StickerPicker( | ||||
|           originalText: _contentController.text, | ||||
|           onDismiss: () => _dismissEmojiPicker(), | ||||
|           onInsert: (str) => _contentController.text = str, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     overlay.insert(_overlayEntry!); | ||||
|   } | ||||
|  | ||||
|   void _dismissEmojiPicker() { | ||||
|     _overlayEntry?.remove(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _contentController.dispose(); | ||||
|     _focusNode.dispose(); | ||||
|     if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey); | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
| @@ -279,6 +345,19 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|                 ), | ||||
|               ), | ||||
|               const Gap(8), | ||||
|               IconButton( | ||||
|                 icon: Icon( | ||||
|                   Symbols.mood, | ||||
|                   color: Theme.of(context).colorScheme.primary, | ||||
|                 ), | ||||
|                 visualDensity: const VisualDensity( | ||||
|                   horizontal: -4, | ||||
|                   vertical: -4, | ||||
|                 ), | ||||
|                 onPressed: () { | ||||
|                   _showEmojiPicker(context); | ||||
|                 }, | ||||
|               ), | ||||
|               AddPostMediaButton( | ||||
|                 onAdd: (items) { | ||||
|                   setState(() { | ||||
| @@ -304,3 +383,107 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _StickerPicker extends StatelessWidget { | ||||
|   final String originalText; | ||||
|   final Function? onDismiss; | ||||
|   final Function(String)? onInsert; | ||||
|  | ||||
|   const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final sticker = context.read<SnStickerProvider>(); | ||||
|     return GestureDetector( | ||||
|       onTap: () { | ||||
|         onDismiss?.call(); | ||||
|       }, | ||||
|       child: Container( | ||||
|         constraints: BoxConstraints(maxWidth: min(360, MediaQuery.of(context).size.width), maxHeight: 240), | ||||
|         child: Material( | ||||
|           elevation: 8, | ||||
|           borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|           child: ClipRRect( | ||||
|             borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|             child: ListView( | ||||
|               padding: EdgeInsets.zero, | ||||
|               children: sticker.stickersByPack.entries | ||||
|                   .map((e) { | ||||
|                     return <Widget>[ | ||||
|                       Container( | ||||
|                         margin: EdgeInsets.only(bottom: 8), | ||||
|                         padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|                         color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                         child: Column( | ||||
|                           mainAxisSize: MainAxisSize.min, | ||||
|                           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                           children: [ | ||||
|                             Text(e.value.first.pack.name).bold(), | ||||
|                             Text(e.value.first.pack.description), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                       GridView.builder( | ||||
|                         physics: const NeverScrollableScrollPhysics(), | ||||
|                         padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), | ||||
|                         shrinkWrap: true, | ||||
|                         gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( | ||||
|                           maxCrossAxisExtent: 48, | ||||
|                           childAspectRatio: 1.0, | ||||
|                           mainAxisSpacing: 8, | ||||
|                           crossAxisSpacing: 8, | ||||
|                         ), | ||||
|                         itemCount: e.value.length, | ||||
|                         itemBuilder: (context, index) { | ||||
|                           final sn = context.read<SnNetworkProvider>(); | ||||
|                           final element = e.value[index]; | ||||
|                           return GestureDetector( | ||||
|                             onTap: () { | ||||
|                               final withSpace = originalText.isNotEmpty; | ||||
|                               onInsert?.call( | ||||
|                                   '$originalText${withSpace ? ' ' : ''}:${element.pack.prefix}${element.alias}:'); | ||||
|                               onDismiss?.call(); | ||||
|                             }, | ||||
|                             child: Tooltip( | ||||
|                               richMessage: TextSpan( | ||||
|                                 children: [ | ||||
|                                   TextSpan( | ||||
|                                       text: ':${element.pack.prefix}${element.alias}:\n', | ||||
|                                       style: GoogleFonts.robotoMono()), | ||||
|                                   TextSpan(text: element.name).bold(), | ||||
|                                 ], | ||||
|                               ), | ||||
|                               child: Container( | ||||
|                                 width: 48, | ||||
|                                 height: 48, | ||||
|                                 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: UniversalImage( | ||||
|                                     sn.getAttachmentUrl(element.attachment.rid), | ||||
|                                     width: 48, | ||||
|                                     height: 48, | ||||
|                                     cacheHeight: 48, | ||||
|                                     cacheWidth: 48, | ||||
|                                     fit: BoxFit.contain, | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ), | ||||
|                           ); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ]; | ||||
|                   }) | ||||
|                   .expand((ele) => ele) | ||||
|                   .toList(), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'package:gap/gap.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/providers/websocket.dart'; | ||||
|  | ||||
| @@ -13,6 +14,9 @@ class ConnectionIndicator extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final ws = context.watch<WebSocketProvider>(); | ||||
|     final cfg = context.watch<ConfigProvider>(); | ||||
|  | ||||
|     final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0; | ||||
|  | ||||
|     return ListenableBuilder( | ||||
|       listenable: ws, | ||||
| @@ -22,45 +26,50 @@ class ConnectionIndicator extends StatelessWidget { | ||||
|  | ||||
|         return IgnorePointer( | ||||
|           ignoring: !show, | ||||
|           child: GestureDetector( | ||||
|             child: Material( | ||||
|               elevation: 2, | ||||
|               shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), | ||||
|               color: Theme.of(context).colorScheme.secondaryContainer, | ||||
|               child: ua.isAuthorized | ||||
|                   ? Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.center, | ||||
|                       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                       children: [ | ||||
|                         if (ws.isBusy) | ||||
|                           Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) | ||||
|                         else if (!ws.isConnected) | ||||
|                           Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) | ||||
|                         else | ||||
|                           Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer), | ||||
|                         const Gap(8), | ||||
|                         if (ws.isBusy) | ||||
|                           const CircularProgressIndicator(strokeWidth: 2.5) | ||||
|                               .width(12) | ||||
|                               .height(12) | ||||
|                               .padding(horizontal: 4, right: 4) | ||||
|                         else if (!ws.isConnected) | ||||
|                           const Icon(Symbols.power_off, size: 18) | ||||
|                         else | ||||
|                           const Icon(Symbols.power, size: 18), | ||||
|                       ], | ||||
|                     ).padding(horizontal: 8, vertical: 4) | ||||
|                   : const SizedBox.shrink(), | ||||
|             ).opacity(show ? 1 : 0, animate: true).animate( | ||||
|                   const Duration(milliseconds: 300), | ||||
|                   Curves.easeInOut, | ||||
|                 ), | ||||
|             onTap: () { | ||||
|               if (!ws.isConnected && !ws.isBusy) { | ||||
|                 ws.connect(); | ||||
|               } | ||||
|             }, | ||||
|           ), | ||||
|           child: Center( | ||||
|             child: GestureDetector( | ||||
|               child: Material( | ||||
|                 elevation: 2, | ||||
|                 shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), | ||||
|                 color: Theme.of(context).colorScheme.secondaryContainer, | ||||
|                 child: ua.isAuthorized | ||||
|                     ? Row( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         mainAxisAlignment: MainAxisAlignment.center, | ||||
|                         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                         children: [ | ||||
|                           if (ws.isBusy) | ||||
|                             Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) | ||||
|                           else if (!ws.isConnected) | ||||
|                             Text('serverDisconnected') | ||||
|                                 .tr() | ||||
|                                 .textColor(Theme.of(context).colorScheme.onSecondaryContainer) | ||||
|                           else | ||||
|                             Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer), | ||||
|                           const Gap(8), | ||||
|                           if (ws.isBusy) | ||||
|                             const CircularProgressIndicator(strokeWidth: 2.5) | ||||
|                                 .width(12) | ||||
|                                 .height(12) | ||||
|                                 .padding(horizontal: 4, right: 4) | ||||
|                           else if (!ws.isConnected) | ||||
|                             const Icon(Symbols.power_off, size: 18) | ||||
|                           else | ||||
|                             const Icon(Symbols.power, size: 18), | ||||
|                         ], | ||||
|                       ).padding(horizontal: 8, vertical: 4) | ||||
|                     : const SizedBox.shrink(), | ||||
|               ).opacity(show ? 1 : 0, animate: true).animate( | ||||
|                     const Duration(milliseconds: 300), | ||||
|                     Curves.easeInOut, | ||||
|                   ), | ||||
|               onTap: () { | ||||
|                 if (!ws.isConnected && !ws.isBusy) { | ||||
|                   ws.connect(); | ||||
|                 } | ||||
|               }, | ||||
|             ), | ||||
|           ).padding(left: marginLeft), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   | ||||
| @@ -28,7 +28,7 @@ class ContextMenuArea extends StatelessWidget { | ||||
|           // Leave padding for side navigation | ||||
|           mousePosition = cfg.drawerIsExpanded | ||||
|               ? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2) | ||||
|               : mousePosition.copyWith(dx: mousePosition.dx - 72 * 2); | ||||
|               : mousePosition.copyWith(dx: mousePosition.dx - 80 * 2); | ||||
|         } | ||||
|       }, | ||||
|       child: GestureDetector( | ||||
|   | ||||
| @@ -20,6 +20,7 @@ class MarkdownTextContent extends StatelessWidget { | ||||
|   final bool isAutoWarp; | ||||
|   final bool isEnlargeSticker; | ||||
|   final TextScaler? textScaler; | ||||
|   final Color? textColor; | ||||
|   final List<SnAttachment?>? attachments; | ||||
|  | ||||
|   const MarkdownTextContent({ | ||||
| @@ -28,6 +29,7 @@ class MarkdownTextContent extends StatelessWidget { | ||||
|     this.isAutoWarp = false, | ||||
|     this.isEnlargeSticker = false, | ||||
|     this.textScaler, | ||||
|     this.textColor, | ||||
|     this.attachments, | ||||
|   }); | ||||
|  | ||||
| @@ -42,6 +44,7 @@ class MarkdownTextContent extends StatelessWidget { | ||||
|         Theme.of(context), | ||||
|       ).copyWith( | ||||
|         textScaler: textScaler, | ||||
|         p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null, | ||||
|         blockquote: TextStyle( | ||||
|           color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|         ), | ||||
| @@ -126,14 +129,27 @@ class MarkdownTextContent extends StatelessWidget { | ||||
|                     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 GestureDetector( | ||||
|                             child: UniversalImage( | ||||
|                               sn.getAttachmentUrl(snapshot.data!.attachment.rid), | ||||
|                               fit: BoxFit.contain, | ||||
|                               width: size, | ||||
|                               height: size, | ||||
|                               cacheHeight: size, | ||||
|                               cacheWidth: size, | ||||
|                             ), | ||||
|                             onTap: () { | ||||
|                               if (snapshot.data == null) return; | ||||
|                               context.pushTransparentRoute( | ||||
|                                 AttachmentZoomView( | ||||
|                                   data: [snapshot.data!.attachment], | ||||
|                                   initialIndex: 0, | ||||
|                                   heroTags: [const Uuid().v4()], | ||||
|                                 ), | ||||
|                                 backgroundColor: Colors.black.withOpacity(0.7), | ||||
|                                 rootNavigator: true, | ||||
|                               ); | ||||
|                             }); | ||||
|                       } | ||||
|                       return const SizedBox.shrink(); | ||||
|                     }, | ||||
| @@ -142,7 +158,7 @@ class MarkdownTextContent extends StatelessWidget { | ||||
|               ); | ||||
|             case 'attachments': | ||||
|               final attachment = attachments?.firstWhere( | ||||
|                     (ele) => ele?.rid == segments[1], | ||||
|                 (ele) => ele?.rid == segments[1], | ||||
|                 orElse: () => null, | ||||
|               ); | ||||
|               if (attachment != null) { | ||||
|   | ||||
| @@ -31,34 +31,37 @@ class _AppRailNavigationState extends State<AppRailNavigation> { | ||||
|       builder: (context, _) { | ||||
|         final destinations = nav.destinations.where((ele) => ele.isPinned).toList(); | ||||
|  | ||||
|         return NavigationRail( | ||||
|           selectedIndex: | ||||
|               nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null, | ||||
|           destinations: [ | ||||
|             ...destinations.where((ele) => ele.isPinned).map((ele) { | ||||
|               return NavigationRailDestination( | ||||
|                 icon: ele.icon, | ||||
|                 label: Text(ele.label).tr(), | ||||
|               ); | ||||
|             }), | ||||
|           ], | ||||
|           trailing: Expanded( | ||||
|             child: Align( | ||||
|               alignment: Alignment.bottomCenter, | ||||
|               child: StyledWidget( | ||||
|                 IconButton( | ||||
|                   icon: const Icon(Symbols.menu), | ||||
|                   onPressed: () { | ||||
|                     Scaffold.of(context).openDrawer(); | ||||
|                   }, | ||||
|                 ), | ||||
|               ).padding(bottom: 16), | ||||
|         return SizedBox( | ||||
|           width: 80, | ||||
|           child: NavigationRail( | ||||
|             selectedIndex: | ||||
|                 nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null, | ||||
|             destinations: [ | ||||
|               ...destinations.where((ele) => ele.isPinned).map((ele) { | ||||
|                 return NavigationRailDestination( | ||||
|                   icon: ele.icon, | ||||
|                   label: Text(ele.label).tr(), | ||||
|                 ); | ||||
|               }), | ||||
|             ], | ||||
|             trailing: Expanded( | ||||
|               child: Align( | ||||
|                 alignment: Alignment.bottomCenter, | ||||
|                 child: StyledWidget( | ||||
|                   IconButton( | ||||
|                     icon: const Icon(Symbols.menu), | ||||
|                     onPressed: () { | ||||
|                       Scaffold.of(context).openDrawer(); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ).padding(bottom: 16), | ||||
|               ), | ||||
|             ), | ||||
|             onDestinationSelected: (idx) { | ||||
|               nav.setIndex(idx); | ||||
|               GoRouter.of(context).goNamed(destinations[idx].screen); | ||||
|             }, | ||||
|           ), | ||||
|           onDestinationSelected: (idx) { | ||||
|             nav.setIndex(idx); | ||||
|             GoRouter.of(context).goNamed(destinations[idx].screen); | ||||
|           }, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   | ||||
| @@ -140,6 +140,7 @@ class AppRootScaffold extends StatelessWidget { | ||||
|     ); | ||||
|  | ||||
|     final safeTop = MediaQuery.of(context).padding.top; | ||||
|     final safeBottom = MediaQuery.of(context).padding.bottom; | ||||
|  | ||||
|     return Scaffold( | ||||
|       key: globalRootScaffoldKey, | ||||
| @@ -191,7 +192,10 @@ class AppRootScaffold extends StatelessWidget { | ||||
|             ], | ||||
|           ), | ||||
|           Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()), | ||||
|           Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()), | ||||
|           if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)) | ||||
|             Positioned(bottom: safeBottom > 0 ? safeBottom : 16, left: 0, right: 0, child: ConnectionIndicator()) | ||||
|           else | ||||
|             Positioned(top: safeTop > 0 ? safeTop : 16, left: 0, right: 0, child: ConnectionIndicator()), | ||||
|         ], | ||||
|       ), | ||||
|       drawer: !isExpandedDrawer ? AppNavigationDrawer() : null, | ||||
|   | ||||
| @@ -1,60 +1,181 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_animate/flutter_animate.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:responsive_framework/responsive_framework.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/notification.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/screens/notification.dart'; | ||||
| import 'package:surface/types/notification.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| class NotifyIndicator extends StatelessWidget { | ||||
| import 'markdown_content.dart'; | ||||
|  | ||||
| class NotifyIndicator extends StatefulWidget { | ||||
|   const NotifyIndicator({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<NotifyIndicator> createState() => _NotifyIndicatorState(); | ||||
| } | ||||
|  | ||||
| class _NotifyIndicatorState extends State<NotifyIndicator> with SingleTickerProviderStateMixin { | ||||
|   late final AnimationController _animationController = AnimationController( | ||||
|     vsync: this, | ||||
|     duration: const Duration(milliseconds: 300), | ||||
|   ); | ||||
|  | ||||
|   void _markOneAsRead(SnNotification notification) async { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|     if (!ua.isAuthorized) return; | ||||
|  | ||||
|     if (notification.id == 0) return; | ||||
|     if (notification.readAt != null) return; | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.put('/cgi/id/notifications/read/${notification.id}'); | ||||
|  | ||||
|       if (!mounted) return; | ||||
|       context.showSnackbar( | ||||
|         'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']), | ||||
|       ); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _animationController.dispose(); | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|     final ua = context.read<UserProvider>(); | ||||
|     final nty = context.watch<NotificationProvider>(); | ||||
|  | ||||
|     final show = nty.notifications.isNotEmpty && ua.isAuthorized; | ||||
|     final isMobile = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE); | ||||
|  | ||||
|     final show = nty.showingCount > 0 && ua.isAuthorized; | ||||
|  | ||||
|     if (show) { | ||||
|       _animationController.animateTo(1); | ||||
|     } else { | ||||
|       _animationController.animateTo(0); | ||||
|     } | ||||
|  | ||||
|     return ListenableBuilder( | ||||
|         listenable: nty, | ||||
|         builder: (context, _) { | ||||
|           final current = nty.notifications.lastOrNull; | ||||
|  | ||||
|           return IgnorePointer( | ||||
|             ignoring: !show, | ||||
|             child: GestureDetector( | ||||
|               child: Material( | ||||
|                 elevation: 2, | ||||
|                 shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), | ||||
|                 color: Theme.of(context).colorScheme.secondaryContainer, | ||||
|                 child: ua.isAuthorized | ||||
|                     ? Row( | ||||
|                         mainAxisAlignment: MainAxisAlignment.center, | ||||
|                         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                         children: [ | ||||
|                           Text( | ||||
|                             nty.notifications.lastOrNull?.title ?? | ||||
|                                 'notificationUnreadCount'.plural(nty.notifications.length), | ||||
|                             maxLines: 1, | ||||
|                             overflow: TextOverflow.ellipsis, | ||||
|                           ), | ||||
|                           if (nty.notifications.lastOrNull?.body != null) | ||||
|                             Text( | ||||
|                               nty.notifications.lastOrNull!.body, | ||||
|                               maxLines: 1, | ||||
|                               overflow: TextOverflow.ellipsis, | ||||
|                             ).padding(left: 4), | ||||
|                           const Gap(8), | ||||
|                           const Icon(Symbols.notifications_unread, size: 18), | ||||
|                         ], | ||||
|                       ).padding(horizontal: 8, vertical: 4) | ||||
|                     : const SizedBox.shrink(), | ||||
|               ).opacity(show ? 1 : 0, animate: true).animate( | ||||
|                     const Duration(milliseconds: 300), | ||||
|                     Curves.easeInOut, | ||||
|               child: Animate( | ||||
|                 autoPlay: false, | ||||
|                 controller: _animationController, | ||||
|                 effects: [ | ||||
|                   SlideEffect( | ||||
|                     begin: isMobile ? Offset(0, -1) : Offset(1, 0), | ||||
|                     end: Offset(0, 0), | ||||
|                     duration: Duration(milliseconds: 300), | ||||
|                     curve: Curves.fastEaseInToSlowEaseOut, | ||||
|                   ), | ||||
|                   FadeEffect( | ||||
|                     begin: 0.0, | ||||
|                     end: 1.0, | ||||
|                     duration: Duration(milliseconds: 300), | ||||
|                     curve: Curves.easeInOut, | ||||
|                   ), | ||||
|                 ], | ||||
|                 child: Container( | ||||
|                   padding: const EdgeInsets.symmetric(vertical: 16), | ||||
|                   width: double.infinity, | ||||
|                   constraints: BoxConstraints( | ||||
|                     maxWidth: isMobile ? MediaQuery.of(context).size.width - 16 : 360, | ||||
|                   ), | ||||
|                   child: Material( | ||||
|                     elevation: 2, | ||||
|                     borderRadius: BorderRadius.circular(8), | ||||
|                     color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                     child: Row( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       children: [ | ||||
|                         if (current?.metadata['avatar'] != null) | ||||
|                           CircleAvatar( | ||||
|                             radius: 14, | ||||
|                             backgroundImage: UniversalImage.provider( | ||||
|                               sn.getAttachmentUrl(current!.metadata['avatar']), | ||||
|                             ), | ||||
|                           ) | ||||
|                         else | ||||
|                           Icon(kNotificationTopicIcons[current?.topic] ?? Symbols.notifications), | ||||
|                         const Gap(16), | ||||
|                         Expanded( | ||||
|                           child: Column( | ||||
|                             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                             children: [ | ||||
|                               Text( | ||||
|                                 current?.title ?? 'Notification', | ||||
|                                 style: Theme.of(context).textTheme.bodyMedium!.copyWith( | ||||
|                                       fontWeight: FontWeight.bold, | ||||
|                                     ), | ||||
|                               ), | ||||
|                               if (current?.subtitle?.isNotEmpty ?? false) | ||||
|                                 Text( | ||||
|                                   current!.subtitle!, | ||||
|                                   style: Theme.of(context).textTheme.bodyMedium!.copyWith( | ||||
|                                         fontWeight: FontWeight.bold, | ||||
|                                       ), | ||||
|                                 ), | ||||
|                               MarkdownTextContent( | ||||
|                                 content: current?.body ?? '', | ||||
|                                 isAutoWarp: true, | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                         const Gap(16), | ||||
|                         Column( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.end, | ||||
|                           children: [ | ||||
|                             Text(DateFormat('HH:mm').format(current?.createdAt.toLocal() ?? DateTime.now())) | ||||
|                                 .fontSize(12) | ||||
|                                 .padding(right: 2), | ||||
|                             const Gap(6), | ||||
|                             if (current?.metadata['image'] != null) | ||||
|                               SizedBox( | ||||
|                                 width: 40, | ||||
|                                 height: 40, | ||||
|                                 child: ClipRRect( | ||||
|                                   borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                                   child: AutoResizeUniversalImage( | ||||
|                                     sn.getAttachmentUrl(current?.metadata['image']), | ||||
|                                     fit: BoxFit.cover, | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ], | ||||
|                     ).padding(horizontal: 16, vertical: 12), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               onTap: () { | ||||
|                 nty.clear(); | ||||
|                 if (current != null) { | ||||
|                   _markOneAsRead(current); | ||||
|                 } | ||||
|               }, | ||||
|             ), | ||||
|           ); | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import 'dart:developer'; | ||||
| import 'dart:io'; | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| @@ -7,6 +6,7 @@ import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:file_saver/file_saver.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_animate/flutter_animate.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| @@ -189,6 +189,7 @@ class PostItem extends StatelessWidget { | ||||
|               ), | ||||
|             ), | ||||
|             Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8), | ||||
|             _PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12), | ||||
|             _PostBottomAction( | ||||
|               data: data, | ||||
|               showComments: showComments, | ||||
| @@ -270,6 +271,7 @@ class PostItem extends StatelessWidget { | ||||
|           LinkPreviewWidget( | ||||
|             text: data.body['content'], | ||||
|           ).padding(horizontal: 4), | ||||
|         _PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12), | ||||
|         Container( | ||||
|           constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), | ||||
|           child: Column( | ||||
| @@ -1125,6 +1127,95 @@ class _PostTruncatedHint extends StatelessWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PostFeaturedComment extends StatefulWidget { | ||||
|   final SnPost data; | ||||
|   final double? maxWidth; | ||||
|  | ||||
|   const _PostFeaturedComment({required this.data, this.maxWidth}); | ||||
|  | ||||
|   @override | ||||
|   State<_PostFeaturedComment> createState() => _PostFeaturedCommentState(); | ||||
| } | ||||
|  | ||||
| class _PostFeaturedCommentState extends State<_PostFeaturedComment> { | ||||
|   SnPost? _featuredComment; | ||||
|  | ||||
|   Future<void> _fetchComments() async { | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: { | ||||
|         'take': 1, | ||||
|       }); | ||||
|       setState(() => _featuredComment = SnPost.fromJson(resp.data[0])); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     if (widget.data.metric.replyCount > 0) { | ||||
|       _fetchComments(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (widget.data.metric.replyCount == 0) return const SizedBox.shrink(); | ||||
|     if (_featuredComment == null) return const SizedBox.shrink(); | ||||
|  | ||||
|     return AnimateWidgetExtensions(Container( | ||||
|       constraints: BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity), | ||||
|       margin: const EdgeInsets.only(top: 8), | ||||
|       width: double.infinity, | ||||
|       child: Material( | ||||
|         borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|         color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|         child: InkWell( | ||||
|           borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|           onTap: () { | ||||
|             showModalBottomSheet( | ||||
|               context: context, | ||||
|               useRootNavigator: true, | ||||
|               builder: (context) => PostCommentListPopup( | ||||
|                 postId: widget.data.id, | ||||
|                 commentCount: widget.data.metric.replyCount, | ||||
|               ), | ||||
|             ); | ||||
|           }, | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               Text('postFeaturedComment', style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 16)).tr(), | ||||
|               const Gap(4), | ||||
|               Row( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                 children: [ | ||||
|                   CircleAvatar( | ||||
|                     radius: 12, | ||||
|                     backgroundImage: UniversalImage.provider( | ||||
|                       _featuredComment!.publisher.avatar, | ||||
|                     ), | ||||
|                   ), | ||||
|                   const Gap(8), | ||||
|                   Expanded( | ||||
|                     child: MarkdownTextContent( | ||||
|                       content: _featuredComment!.body['content'], | ||||
|                       isAutoWarp: true, | ||||
|                     ), | ||||
|                   ) | ||||
|                 ], | ||||
|               ), | ||||
|             ], | ||||
|           ).padding(horizontal: 16, vertical: 8), | ||||
|         ), | ||||
|       ), | ||||
|     )).animate().fadeIn(duration: 300.ms, curve: Curves.easeInOut); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PostAbuseReportDialog extends StatefulWidget { | ||||
|   final SnPost data; | ||||
|  | ||||
|   | ||||
| @@ -61,7 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> { | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|       HapticFeedback.mediumImpact(); | ||||
|       HapticFeedback.heavyImpact(); | ||||
|     } catch (err) { | ||||
|       // ignore: use_build_context_synchronously | ||||
|       if (context.mounted) context.showErrorDialog(err); | ||||
|   | ||||
| @@ -11,9 +11,11 @@ | ||||
| #include <file_selector_linux/file_selector_plugin.h> | ||||
| #include <flutter_udid/flutter_udid_plugin.h> | ||||
| #include <flutter_webrtc/flutter_web_r_t_c_plugin.h> | ||||
| #include <hotkey_manager_linux/hotkey_manager_linux_plugin.h> | ||||
| #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> | ||||
| #include <media_kit_video/media_kit_video_plugin.h> | ||||
| #include <pasteboard/pasteboard_plugin.h> | ||||
| #include <tray_manager/tray_manager_plugin.h> | ||||
| #include <url_launcher_linux/url_launcher_plugin.h> | ||||
|  | ||||
| void fl_register_plugins(FlPluginRegistry* registry) { | ||||
| @@ -32,6 +34,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { | ||||
|   g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = | ||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); | ||||
|   flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); | ||||
|   g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar = | ||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin"); | ||||
|   hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar); | ||||
|   g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = | ||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); | ||||
|   media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); | ||||
| @@ -41,6 +46,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { | ||||
|   g_autoptr(FlPluginRegistrar) pasteboard_registrar = | ||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin"); | ||||
|   pasteboard_plugin_register_with_registrar(pasteboard_registrar); | ||||
|   g_autoptr(FlPluginRegistrar) tray_manager_registrar = | ||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); | ||||
|   tray_manager_plugin_register_with_registrar(tray_manager_registrar); | ||||
|   g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = | ||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); | ||||
|   url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); | ||||
|   | ||||
| @@ -8,9 +8,11 @@ list(APPEND FLUTTER_PLUGIN_LIST | ||||
|   file_selector_linux | ||||
|   flutter_udid | ||||
|   flutter_webrtc | ||||
|   hotkey_manager_linux | ||||
|   media_kit_libs_linux | ||||
|   media_kit_video | ||||
|   pasteboard | ||||
|   tray_manager | ||||
|   url_launcher_linux | ||||
| ) | ||||
|  | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import flutter_inappwebview_macos | ||||
| import flutter_udid | ||||
| import flutter_webrtc | ||||
| import gal | ||||
| import hotkey_manager_macos | ||||
| import in_app_review | ||||
| import livekit_client | ||||
| import media_kit_libs_macos_video | ||||
| @@ -29,6 +30,7 @@ import screen_brightness_macos | ||||
| import share_plus | ||||
| import shared_preferences_foundation | ||||
| import sqflite_darwin | ||||
| import tray_manager | ||||
| import url_launcher_macos | ||||
| import video_compress | ||||
| import wakelock_plus | ||||
| @@ -47,6 +49,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | ||||
|   FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) | ||||
|   FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) | ||||
|   GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) | ||||
|   HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin")) | ||||
|   InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) | ||||
|   LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) | ||||
|   MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) | ||||
| @@ -58,6 +61,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | ||||
|   SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) | ||||
|   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) | ||||
|   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) | ||||
|   TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) | ||||
|   UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) | ||||
|   VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) | ||||
|   WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) | ||||
|   | ||||
| @@ -14,59 +14,59 @@ PODS: | ||||
|     - FlutterMacOS | ||||
|   - file_selector_macos (0.0.1): | ||||
|     - FlutterMacOS | ||||
|   - Firebase/Analytics (11.6.0): | ||||
|   - Firebase/Analytics (11.7.0): | ||||
|     - Firebase/Core | ||||
|   - Firebase/Core (11.6.0): | ||||
|   - Firebase/Core (11.7.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseAnalytics (~> 11.6.0) | ||||
|   - Firebase/CoreOnly (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|   - Firebase/Messaging (11.6.0): | ||||
|     - FirebaseAnalytics (~> 11.7.0) | ||||
|   - Firebase/CoreOnly (11.7.0): | ||||
|     - FirebaseCore (~> 11.7.0) | ||||
|   - Firebase/Messaging (11.7.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseMessaging (~> 11.6.0) | ||||
|   - firebase_analytics (11.4.1): | ||||
|     - Firebase/Analytics (= 11.6.0) | ||||
|     - FirebaseMessaging (~> 11.7.0) | ||||
|   - firebase_analytics (11.4.2): | ||||
|     - Firebase/Analytics (= 11.7.0) | ||||
|     - firebase_core | ||||
|     - FlutterMacOS | ||||
|   - firebase_core (3.10.1): | ||||
|     - Firebase/CoreOnly (~> 11.6.0) | ||||
|   - firebase_core (3.11.0): | ||||
|     - Firebase/CoreOnly (~> 11.7.0) | ||||
|     - FlutterMacOS | ||||
|   - firebase_messaging (15.2.1): | ||||
|     - Firebase/CoreOnly (~> 11.6.0) | ||||
|     - Firebase/Messaging (~> 11.6.0) | ||||
|   - firebase_messaging (15.2.2): | ||||
|     - Firebase/CoreOnly (~> 11.7.0) | ||||
|     - Firebase/Messaging (~> 11.7.0) | ||||
|     - firebase_core | ||||
|     - FlutterMacOS | ||||
|   - FirebaseAnalytics (11.6.0): | ||||
|     - FirebaseAnalytics/AdIdSupport (= 11.6.0) | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|   - FirebaseAnalytics (11.7.0): | ||||
|     - FirebaseAnalytics/AdIdSupport (= 11.7.0) | ||||
|     - FirebaseCore (~> 11.7.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseAnalytics/AdIdSupport (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|   - FirebaseAnalytics/AdIdSupport (11.7.0): | ||||
|     - FirebaseCore (~> 11.7.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleAppMeasurement (= 11.6.0) | ||||
|     - GoogleAppMeasurement (= 11.7.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseCore (11.6.0): | ||||
|     - FirebaseCoreInternal (~> 11.6.0) | ||||
|   - FirebaseCore (11.7.0): | ||||
|     - FirebaseCoreInternal (~> 11.7.0) | ||||
|     - GoogleUtilities/Environment (~> 8.0) | ||||
|     - GoogleUtilities/Logger (~> 8.0) | ||||
|   - FirebaseCoreInternal (11.6.0): | ||||
|   - FirebaseCoreInternal (11.7.0): | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|   - FirebaseInstallations (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|   - FirebaseInstallations (11.7.0): | ||||
|     - FirebaseCore (~> 11.7.0) | ||||
|     - GoogleUtilities/Environment (~> 8.0) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.0) | ||||
|     - PromisesObjC (~> 2.4) | ||||
|   - FirebaseMessaging (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|   - FirebaseMessaging (11.7.0): | ||||
|     - FirebaseCore (~> 11.7.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleDataTransport (~> 10.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
| @@ -87,21 +87,21 @@ PODS: | ||||
|   - gal (1.0.0): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - GoogleAppMeasurement (11.6.0): | ||||
|     - GoogleAppMeasurement/AdIdSupport (= 11.6.0) | ||||
|   - GoogleAppMeasurement (11.7.0): | ||||
|     - GoogleAppMeasurement/AdIdSupport (= 11.7.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/AdIdSupport (11.6.0): | ||||
|     - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0) | ||||
|   - GoogleAppMeasurement/AdIdSupport (11.7.0): | ||||
|     - GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/WithoutAdIdSupport (11.6.0): | ||||
|   - GoogleAppMeasurement/WithoutAdIdSupport (11.7.0): | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
| @@ -137,6 +137,10 @@ PODS: | ||||
|   - GoogleUtilities/UserDefaults (8.0.2): | ||||
|     - GoogleUtilities/Logger | ||||
|     - GoogleUtilities/Privacy | ||||
|   - HotKey (0.2.1) | ||||
|   - hotkey_manager_macos (0.0.1): | ||||
|     - FlutterMacOS | ||||
|     - HotKey | ||||
|   - in_app_review (2.0.0): | ||||
|     - FlutterMacOS | ||||
|   - livekit_client (2.3.5): | ||||
| @@ -174,6 +178,8 @@ PODS: | ||||
|   - sqflite_darwin (0.0.4): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - tray_manager (0.0.1): | ||||
|     - FlutterMacOS | ||||
|   - url_launcher_macos (0.0.1): | ||||
|     - FlutterMacOS | ||||
|   - video_compress (0.3.0): | ||||
| @@ -198,6 +204,7 @@ DEPENDENCIES: | ||||
|   - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) | ||||
|   - FlutterMacOS (from `Flutter/ephemeral`) | ||||
|   - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) | ||||
|   - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) | ||||
|   - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) | ||||
|   - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) | ||||
|   - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) | ||||
| @@ -210,6 +217,7 @@ DEPENDENCIES: | ||||
|   - 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`) | ||||
|   - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) | ||||
|   - 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`) | ||||
| @@ -225,6 +233,7 @@ SPEC REPOS: | ||||
|     - GoogleAppMeasurement | ||||
|     - GoogleDataTransport | ||||
|     - GoogleUtilities | ||||
|     - HotKey | ||||
|     - nanopb | ||||
|     - OrderedSet | ||||
|     - PromisesObjC | ||||
| @@ -262,6 +271,8 @@ EXTERNAL SOURCES: | ||||
|     :path: Flutter/ephemeral | ||||
|   gal: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin | ||||
|   hotkey_manager_macos: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos | ||||
|   in_app_review: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos | ||||
|   livekit_client: | ||||
| @@ -286,6 +297,8 @@ EXTERNAL SOURCES: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin | ||||
|   sqflite_darwin: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin | ||||
|   tray_manager: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos | ||||
|   url_launcher_macos: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos | ||||
|   video_compress: | ||||
| @@ -301,23 +314,25 @@ SPEC CHECKSUMS: | ||||
|   file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af | ||||
|   file_saver: 44e6fbf666677faf097302460e214e977fdd977b | ||||
|   file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d | ||||
|   Firebase: 374a441a91ead896215703a674d58cdb3e9d772b | ||||
|   firebase_analytics: 91efc58e8e37964469fdd59ad11ba36bc97e75d6 | ||||
|   firebase_core: 75e003524565fb5bd80c9960bc5892e8475821cd | ||||
|   firebase_messaging: 082a385eb98b5bb843a566cb30404859c4bd6e25 | ||||
|   FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 | ||||
|   FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa | ||||
|   FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 | ||||
|   FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c | ||||
|   FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021 | ||||
|   Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4 | ||||
|   firebase_analytics: 41d88c024a7756462a803e36236ba74f24cdc2c5 | ||||
|   firebase_core: 751d3d919b95d4ae46ab049d0d64d42d4eec086b | ||||
|   firebase_messaging: cc174f19945e9541e140e3cb0118448e59b38c6c | ||||
|   FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec | ||||
|   FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4 | ||||
|   FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881 | ||||
|   FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9 | ||||
|   FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c | ||||
|   flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b | ||||
|   flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52 | ||||
|   flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8 | ||||
|   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 | ||||
|   gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 | ||||
|   GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3 | ||||
|   GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967 | ||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||
|   GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d | ||||
|   HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 | ||||
|   hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160 | ||||
|   in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 | ||||
|   livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1 | ||||
|   media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 | ||||
| @@ -334,6 +349,7 @@ SPEC CHECKSUMS: | ||||
|   share_plus: 1fa619de8392a4398bfaf176d441853922614e89 | ||||
|   shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 | ||||
|   sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d | ||||
|   tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 | ||||
|   url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 | ||||
|   video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f | ||||
|   wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 | ||||
|   | ||||
							
								
								
									
										124
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										124
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -13,10 +13,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: _flutterfire_internals | ||||
|       sha256: e4f2a7ef31b0ab2c89d2bde35ef3e6e6aff1dce5e66069c6540b0e9cfe33ee6b | ||||
|       sha256: e051259913915ea5bc8fe18664596bea08592fd123930605d562969cd7315fcd | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.50" | ||||
|     version: "1.3.51" | ||||
|   _macros: | ||||
|     dependency: transitive | ||||
|     description: dart | ||||
| @@ -538,34 +538,34 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: firebase_analytics | ||||
|       sha256: eac382bbcd5ae78c1d1ce5619d13f5a7424429f4bf55df9e3ad5110da34d1060 | ||||
|       sha256: "47428047a0778f72af53a3c7cb5d556e1cb25e2327cc8aa40d544971dc6245b2" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "11.4.1" | ||||
|     version: "11.4.2" | ||||
|   firebase_analytics_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_analytics_platform_interface | ||||
|       sha256: a34db46c367265c4c961626e4b128bfb7b7e50958e7add4c27ba103f5f81b9b0 | ||||
|       sha256: "1076f4b041f76143e14878c70f0758f17fe5910c0cd992db9e93bd3c3584512b" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.3.1" | ||||
|     version: "4.3.2" | ||||
|   firebase_analytics_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_analytics_web | ||||
|       sha256: b6b4cef08e45e4c7d48476d9fc49fe9577081809a59026fe95b1a1b1eea165fa | ||||
|       sha256: "8f6dd64ea6d28b7f5b9e739d183a9e1c7f17027794a3e9aba1879621d42426ef" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.5.10+7" | ||||
|     version: "0.5.10+8" | ||||
|   firebase_core: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: firebase_core | ||||
|       sha256: d851c1ca98fd5a4c07c747f8c65dacc2edd84a4d9ac055d32a5f0342529069f5 | ||||
|       sha256: "93dc4dd12f9b02c5767f235307f609e61ed9211047132d07f9e02c668f0bfc33" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.10.1" | ||||
|     version: "3.11.0" | ||||
|   firebase_core_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -578,34 +578,34 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_core_web | ||||
|       sha256: fbc008cf390d909b823763064b63afefe9f02d8afdb13eb3f485b871afee956b | ||||
|       sha256: "0e13c80f0de8acaa5d0519cbe23c8b4cc138a2d5d508b5755c861bdfc9762678" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.19.0" | ||||
|     version: "2.20.0" | ||||
|   firebase_messaging: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: firebase_messaging | ||||
|       sha256: e20ea2a0ecf9b0971575ab3ab42a6e285a94e50092c555b090c1a588a81b4d54 | ||||
|       sha256: "3dee3b0cbfe719e64773cb7d1cad57c58b2235a8c136f5715fe733a54058c783" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "15.2.1" | ||||
|     version: "15.2.2" | ||||
|   firebase_messaging_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_messaging_platform_interface | ||||
|       sha256: c57a92b5ae1857ef4fe4ae2e73452b44d32e984e15ab8b53415ea1bb514bdabd | ||||
|       sha256: e9ea726b9bb864fc6223bb66422bd9877b9973ae51967754a769b0d01e201c1e | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.6.1" | ||||
|     version: "4.6.2" | ||||
|   firebase_messaging_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_messaging_web | ||||
|       sha256: "83694a990d8525d6b01039240b97757298369622ca0253ad0ebcfed221bf8ee0" | ||||
|       sha256: "5f7b40e8bf861a37f8b8196e347d8a919750421a45f0b45d1bb74e98fa72726e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.10.1" | ||||
|     version: "3.10.2" | ||||
|   fixnum: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -764,10 +764,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_markdown | ||||
|       sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487 | ||||
|       sha256: b3ff1ef5fb3924ee02b4d38b974ffae3969d50603e68787684ee9dd45f6f144a | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.7.5" | ||||
|     version: "0.7.6+1" | ||||
|   flutter_native_splash: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -938,6 +938,46 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.7.0" | ||||
|   hotkey_manager: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: hotkey_manager | ||||
|       sha256: "06f0655b76c8dd322fb7101dc615afbdbf39c3d3414df9e059c33892104479cd" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.2.3" | ||||
|   hotkey_manager_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: hotkey_manager_linux | ||||
|       sha256: "83676bda8210a3377bc6f1977f193bc1dbdd4c46f1bdd02875f44b6eff9a8473" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.2.0" | ||||
|   hotkey_manager_macos: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: hotkey_manager_macos | ||||
|       sha256: "03b5967e64357b9ac05188ea4a5df6fe4ed4205762cb80aaccf8916ee1713c96" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.2.0" | ||||
|   hotkey_manager_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: hotkey_manager_platform_interface | ||||
|       sha256: "98ffca25b8cc9081552902747b2942e3bc37855389a4218c9d50ca316b653b13" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.2.0" | ||||
|   hotkey_manager_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: hotkey_manager_windows | ||||
|       sha256: "0d03ced9fe563ed0b68f0a0e1b22c9ffe26eb8053cb960e401f68a4f070e0117" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.2.0" | ||||
|   html: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1282,6 +1322,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.2.5" | ||||
|   menu_base: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: menu_base | ||||
|       sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.1.1" | ||||
|   meta: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1334,18 +1382,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: package_info_plus | ||||
|       sha256: b15fad91c4d3d1f2b48c053dd41cb82da007c27407dc9ab5f9aa59881d0e39d4 | ||||
|       sha256: c447a3c3e7be4addf129b8f9ab6a4bd5d166b78918223e223b61fddf4d07e254 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "8.1.4" | ||||
|     version: "8.2.0" | ||||
|   package_info_plus_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: package_info_plus_platform_interface | ||||
|       sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b | ||||
|       sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.2" | ||||
|     version: "3.1.0" | ||||
|   pasteboard: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1778,6 +1826,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.1" | ||||
|   shortid: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shortid | ||||
|       sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.1.2" | ||||
|   sky_engine: | ||||
|     dependency: transitive | ||||
|     description: flutter | ||||
| @@ -1951,6 +2007,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.2" | ||||
|   tray_manager: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: tray_manager | ||||
|       sha256: "80be6c508159a6f3c57983de795209ac13453e9832fd574143b06dceee188ed2" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.3.2" | ||||
|   typed_data: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1959,6 +2023,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.4.0" | ||||
|   uni_platform: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: uni_platform | ||||
|       sha256: e02213a7ee5352212412ca026afd41d269eb00d982faa552f419ffc2debfad84 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.1.3" | ||||
|   universal_io: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2059,10 +2131,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: vector_graphics | ||||
|       sha256: "7ed22c21d7fdcc88dd6ba7860384af438cd220b251ad65dfc142ab722fabef61" | ||||
|       sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.1.16" | ||||
|     version: "1.1.18" | ||||
|   vector_graphics_codec: | ||||
|     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.2+60 | ||||
| version: 2.3.2+63 | ||||
|  | ||||
| environment: | ||||
|   sdk: ^3.5.4 | ||||
| @@ -118,6 +118,8 @@ dependencies: | ||||
|   flutter_inappwebview: ^6.1.5 | ||||
|   html: ^0.15.5 | ||||
|   xml: ^6.5.0 | ||||
|   tray_manager: ^0.3.2 | ||||
|   hotkey_manager: ^0.2.3 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
| @@ -152,6 +154,8 @@ flutter: | ||||
|     - assets/icon/icon.png | ||||
|     - assets/icon/icon-dark.png | ||||
|     - assets/icon/icon-light-radius.png | ||||
|     - assets/icon/tray-icon.ico | ||||
|     - assets/icon/tray-icon.png | ||||
|     - assets/translations/ | ||||
|  | ||||
|   # An image asset can refer to one or more resolution-specific "variants", see | ||||
|   | ||||
| @@ -15,6 +15,7 @@ | ||||
| #include <flutter_udid/flutter_udid_plugin_c_api.h> | ||||
| #include <flutter_webrtc/flutter_web_r_t_c_plugin.h> | ||||
| #include <gal/gal_plugin_c_api.h> | ||||
| #include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h> | ||||
| #include <livekit_client/live_kit_plugin.h> | ||||
| #include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h> | ||||
| #include <media_kit_video/media_kit_video_plugin_c_api.h> | ||||
| @@ -22,6 +23,7 @@ | ||||
| #include <permission_handler_windows/permission_handler_windows_plugin.h> | ||||
| #include <screen_brightness_windows/screen_brightness_windows_plugin.h> | ||||
| #include <share_plus/share_plus_windows_plugin_c_api.h> | ||||
| #include <tray_manager/tray_manager_plugin.h> | ||||
| #include <url_launcher_windows/url_launcher_windows.h> | ||||
|  | ||||
| void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||
| @@ -43,6 +45,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||
|       registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); | ||||
|   GalPluginCApiRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("GalPluginCApi")); | ||||
|   HotkeyManagerWindowsPluginCApiRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi")); | ||||
|   LiveKitPluginRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("LiveKitPlugin")); | ||||
|   MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( | ||||
| @@ -57,6 +61,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||
|       registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); | ||||
|   SharePlusWindowsPluginCApiRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); | ||||
|   TrayManagerPluginRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("TrayManagerPlugin")); | ||||
|   UrlLauncherWindowsRegisterWithRegistrar( | ||||
|       registry->GetRegistrarForPlugin("UrlLauncherWindows")); | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST | ||||
|   flutter_udid | ||||
|   flutter_webrtc | ||||
|   gal | ||||
|   hotkey_manager_windows | ||||
|   livekit_client | ||||
|   media_kit_libs_windows_video | ||||
|   media_kit_video | ||||
| @@ -19,6 +20,7 @@ list(APPEND FLUTTER_PLUGIN_LIST | ||||
|   permission_handler_windows | ||||
|   screen_brightness_windows | ||||
|   share_plus | ||||
|   tray_manager | ||||
|   url_launcher_windows | ||||
| ) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user