Compare commits
	
		
			12 Commits
		
	
	
		
			2.2.2+58
			...
			b8dcdb2315
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b8dcdb2315 | |||
| b7b921f1f4 | |||
| 319d5c7d7f | |||
| 4b5b001739 | |||
| db8871a455 | |||
| 38dcaa6066 | |||
| 03275b46ca | |||
| cf3b482fef | |||
| aa4c04d4ef | |||
| 73b82f65e4 | |||
| 9471fe40fe | |||
| 0d1e18735e | 
| @@ -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" | ||||
|   | ||||
| @@ -15,11 +15,11 @@ body:json { | ||||
|     "client_id": "{{third_client_id}}", | ||||
|     "client_secret":"{{third_client_tk}}", | ||||
|     "type": "general", | ||||
|     "subject": "Merry Christmas!", | ||||
|     "subject": "新年快乐!", | ||||
|     "subtitle": "一条来自 Solar Network 团队的信息", | ||||
|     "content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄", | ||||
|     "content": "今天是农历正月初一,小羊祝您新年快乐 🎉", | ||||
|     "metadata": { | ||||
|       "image": "6EqsYQwmFRCkbmhR" | ||||
|       "image": "D2EDbcrsTugs3xk5" | ||||
|     }, | ||||
|     "priority": 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 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										11
									
								
								api/WatchTower/Run Database Maintenance.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/WatchTower/Run Database Maintenance.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| meta { | ||||
|   name: Run Database Maintenance | ||||
|   type: http | ||||
|   seq: 1 | ||||
| } | ||||
|  | ||||
| post { | ||||
|   url: {{endpoint}}/wt/maintenance/database | ||||
|   body: none | ||||
|   auth: inherit | ||||
| } | ||||
| @@ -18,6 +18,8 @@ | ||||
|   "screenAbuseReport": "Abuse Reports", | ||||
|   "screenSettings": "Settings", | ||||
|   "screenAccountSettings": "Account Settings", | ||||
|   "screenFactorSettings": "Auth Factors", | ||||
|   "screenAccountWallet": "Wallet", | ||||
|   "screenNews": "News", | ||||
|   "screenAlbum": "Album", | ||||
|   "screenChat": "Chat", | ||||
| @@ -29,7 +31,6 @@ | ||||
|   "screenNotification": "Notification", | ||||
|   "screenPostSearch": "Search Posts", | ||||
|   "screenFriend": "Friends", | ||||
|   "screenFactorSettings": "Auth Factors", | ||||
|   "dialogOkay": "Okay", | ||||
|   "dialogCancel": "Cancel", | ||||
|   "dialogConfirm": "Confirm", | ||||
| @@ -130,6 +131,8 @@ | ||||
|   "accountSettingsSubtitle": "Manage your account and make it yours.", | ||||
|   "accountProfileEdit": "Edit your profile", | ||||
|   "accountProfileEditSubtitle": "Make your Solarpass account more looks like you.", | ||||
|   "accountWallet": "Wallet", | ||||
|   "accountWalletSubtitle": "View your balance and transactions.", | ||||
|   "factorSettings": "Auth Factors", | ||||
|   "factorSettingsSubtitle": "Manage your authentication factors.", | ||||
|   "accountProfileEditApplied": "Profile modification applied.", | ||||
| @@ -196,6 +199,9 @@ | ||||
|     "other": "{} comments" | ||||
|   }, | ||||
|   "settingsAppearance": "Appearance", | ||||
|   "settingsDisplayLanguage": "Display Language", | ||||
|   "settingsDisplayLanguageDescription": "Set the application language.", | ||||
|   "settingsDisplayLanguageSystem": "Follow System", | ||||
|   "settingsAppBarTransparent": "Transparent App Bar", | ||||
|   "settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.", | ||||
|   "settingsDrawerPreferCollapse": "Prefer Drawer Collapse", | ||||
| @@ -548,6 +554,9 @@ | ||||
|   "postImageShareAds": "Explore posts on the Solar Network", | ||||
|   "postShare": "Share", | ||||
|   "postShareImage": "Share via Image", | ||||
|   "postGetInsight": "Get Insight", | ||||
|   "postGetInsightTitle": "AI Insight", | ||||
|   "postGetInsightDescription": "AI may make mistakes, check important information.", | ||||
|   "appInitializing": "Initializing", | ||||
|   "poweredBy": "Powered by {}", | ||||
|   "shareIntent": "Share", | ||||
| @@ -584,5 +593,16 @@ | ||||
|   "newsToday": "Today's News", | ||||
|   "totpPostSetup": "One More Thing", | ||||
|   "totpPostSetupDescription": "Scan the QR Code below with Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden or any of kind of authenticator app which supports TOTP.", | ||||
|   "totpNeverShare": "Never share this QR Code" | ||||
|   "totpNeverShare": "Never share this QR Code", | ||||
|   "needHelp": "Need Help?", | ||||
|   "needHelpLaunch": "Check out our Goatpedia!", | ||||
|   "walletCreate": "Create a Wallet", | ||||
|   "walletCreateSubtitle": "Create a wallet to start using Source Points", | ||||
|   "walletCreatePassword": "Set a payment password for your new wallet below", | ||||
|   "walletCurrencyShort": "SRC", | ||||
|   "walletCurrency": { | ||||
|     "one": "{} Source Point", | ||||
|     "other": "{} Source Points" | ||||
|   }, | ||||
|   "aiThinkingProcess": "AI Thinking Process" | ||||
| } | ||||
|   | ||||
| @@ -16,6 +16,8 @@ | ||||
|   "screenAbuseReport": "滥用检举", | ||||
|   "screenSettings": "设置", | ||||
|   "screenAccountSettings": "账号设置", | ||||
|   "screenFactorSettings": "验证因子", | ||||
|   "screenAccountWallet": "钱包", | ||||
|   "screenNews": "新闻", | ||||
|   "screenAlbum": "相册", | ||||
|   "screenChat": "聊天", | ||||
| @@ -113,6 +115,8 @@ | ||||
|   "accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。", | ||||
|   "accountProfileEdit": "编辑资料", | ||||
|   "accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。", | ||||
|   "accountWallet": "钱包", | ||||
|   "accountWalletSubtitle": "查看你的余额和交易记录。", | ||||
|   "factorSettings": "验证因子", | ||||
|   "factorSettingsSubtitle": "管理你的登陆验证方式。", | ||||
|   "accountProfileEditApplied": "个人资料修改已被应用。", | ||||
| @@ -193,6 +197,9 @@ | ||||
|     "other": "{} 条评论" | ||||
|   }, | ||||
|   "settingsAppearance": "外观", | ||||
|   "settingsDisplayLanguage": "显示语言", | ||||
|   "settingsDisplayLanguageDescription": "设置应用程序使用的语言", | ||||
|   "settingsDisplayLanguageSystem": "跟随系统", | ||||
|   "settingsBackgroundImage": "背景图片", | ||||
|   "settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。", | ||||
|   "settingsBackgroundImageClear": "清除现存背景图", | ||||
| @@ -545,6 +552,9 @@ | ||||
|   "postImageShareAds": "来 Solar Network 探索更多有趣帖子", | ||||
|   "postShare": "分享", | ||||
|   "postShareImage": "分享帖图", | ||||
|   "postGetInsight": "获取见解", | ||||
|   "postGetInsightTitle": "AI 见解", | ||||
|   "postGetInsightDescription": "AI 可能会出错,检查信息真实性。", | ||||
|   "appInitializing": "正在初始化", | ||||
|   "poweredBy": "由 {} 提供支持", | ||||
|   "shareIntent": "分享", | ||||
| @@ -581,5 +591,16 @@ | ||||
|   "newsToday": "快讯", | ||||
|   "totpPostSetup": "还有一件事", | ||||
|   "totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的验证器扫描本 QR Code 来添加。", | ||||
|   "totpNeverShare": "永远不要分享这个 QR Code" | ||||
|   "totpNeverShare": "永远不要分享这个 QR Code", | ||||
|   "needHelp": "需要帮助?", | ||||
|   "needHelpLaunch": "查看我们的山羊维基!", | ||||
|   "walletCreate": "创建钱包", | ||||
|   "walletCreateSubtitle": "创建于一个钱包来开始使用源点。", | ||||
|   "walletCreatePassword": "在下方设置你的付款密码", | ||||
|   "walletCurrencyShort": "源点", | ||||
|   "walletCurrency": { | ||||
|     "one": "{} 源点", | ||||
|     "other": "{} 源点" | ||||
|   }, | ||||
|   "aiThinkingProcess": "AI 思考过程" | ||||
| } | ||||
|   | ||||
| @@ -15,6 +15,9 @@ | ||||
|   "screenAccountProfileEdit": "編輯資料", | ||||
|   "screenAbuseReport": "濫用檢舉", | ||||
|   "screenSettings": "設置", | ||||
|   "screenAccountSettings": "賬號設置", | ||||
|   "screenFactorSettings": "驗證因子", | ||||
|   "screenAccountWallet": "錢包", | ||||
|   "screenNews": "新聞", | ||||
|   "screenAlbum": "相冊", | ||||
|   "screenChat": "聊天", | ||||
| @@ -88,8 +91,18 @@ | ||||
|   }, | ||||
|   "loginEnterPassword": "驗證代碼", | ||||
|   "loginSuccess": "登錄為 {}", | ||||
|   "authFactorDelete": "刪除驗證因子", | ||||
|   "authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?", | ||||
|   "authFactorPassword": "密碼", | ||||
|   "authFactorPasswordDescription": "註冊時選擇設置的密碼。", | ||||
|   "authFactorEmail": "電郵一次性驗證碼", | ||||
|   "authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。", | ||||
|   "authFactorTOTP": "時序驗證碼", | ||||
|   "authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。", | ||||
|   "authFactorInAppNotify": "應用內通知驗證碼", | ||||
|   "authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。", | ||||
|   "authFactorAdd": "添加新驗證因子", | ||||
|   "authFactorAddSubtitle": "給你的帳户登陸時提供另一個方案。", | ||||
|   "accountIntroTitle": "喜歡您來!", | ||||
|   "accountIntroSubtitle": "登陸以探索更廣大的世界。", | ||||
|   "accountLogout": "退出登錄", | ||||
| @@ -98,8 +111,14 @@ | ||||
|   "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。", | ||||
|   "accountPublishers": "你的發佈者", | ||||
|   "accountPublishersSubtitle": "管理你的公共形象。", | ||||
|   "accountSettings": "帳號設置", | ||||
|   "accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。", | ||||
|   "accountProfileEdit": "編輯資料", | ||||
|   "accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。", | ||||
|   "accountWallet": "錢包", | ||||
|   "accountWalletSubtitle": "查看你的餘額和交易記錄。", | ||||
|   "factorSettings": "驗證因子", | ||||
|   "factorSettingsSubtitle": "管理你的登陸驗證方式。", | ||||
|   "accountProfileEditApplied": "個人資料修改已被應用。", | ||||
|   "publishersNew": "新發布者", | ||||
|   "publisherNewSubtitle": "創建一個新的公共身份。", | ||||
| @@ -178,6 +197,9 @@ | ||||
|     "other": "{} 條評論" | ||||
|   }, | ||||
|   "settingsAppearance": "外觀", | ||||
|   "settingsDisplayLanguage": "顯示語言", | ||||
|   "settingsDisplayLanguageDescription": "設置應用程序使用的語言", | ||||
|   "settingsDisplayLanguageSystem": "跟隨系統", | ||||
|   "settingsBackgroundImage": "背景圖片", | ||||
|   "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。", | ||||
|   "settingsBackgroundImageClear": "清除現存背景圖", | ||||
| @@ -530,6 +552,9 @@ | ||||
|   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", | ||||
|   "postShare": "分享", | ||||
|   "postShareImage": "分享帖圖", | ||||
|   "postGetInsight": "獲取見解", | ||||
|   "postGetInsightTitle": "AI 見解", | ||||
|   "postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。", | ||||
|   "appInitializing": "正在初始化", | ||||
|   "poweredBy": "由 {} 提供支持", | ||||
|   "shareIntent": "分享", | ||||
| @@ -563,5 +588,19 @@ | ||||
|   "newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章", | ||||
|   "newsReadingFromOriginal": "你正在閲讀原始文章", | ||||
|   "newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。", | ||||
|   "newsToday": "快訊" | ||||
|   "newsToday": "快訊", | ||||
|   "totpPostSetup": "還有一件事", | ||||
|   "totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。", | ||||
|   "totpNeverShare": "永遠不要分享這個 QR Code", | ||||
|   "needHelp": "需要幫助?", | ||||
|   "needHelpLaunch": "查看我們的山羊維基!", | ||||
|   "walletCreate": "創建錢包", | ||||
|   "walletCreateSubtitle": "創建於一個錢包來開始使用源點。", | ||||
|   "walletCreatePassword": "在下方設置你的付款密碼", | ||||
|   "walletCurrencyShort": "源點", | ||||
|   "walletCurrency": { | ||||
|     "one": "{} 源點", | ||||
|     "other": "{} 源點" | ||||
|   }, | ||||
|   "aiThinkingProcess": "AI 思考過程" | ||||
| } | ||||
|   | ||||
| @@ -15,6 +15,9 @@ | ||||
|   "screenAccountProfileEdit": "編輯資料", | ||||
|   "screenAbuseReport": "濫用檢舉", | ||||
|   "screenSettings": "設置", | ||||
|   "screenAccountSettings": "賬號設置", | ||||
|   "screenFactorSettings": "驗證因子", | ||||
|   "screenAccountWallet": "錢包", | ||||
|   "screenNews": "新聞", | ||||
|   "screenAlbum": "相冊", | ||||
|   "screenChat": "聊天", | ||||
| @@ -88,8 +91,18 @@ | ||||
|   }, | ||||
|   "loginEnterPassword": "驗證代碼", | ||||
|   "loginSuccess": "登錄為 {}", | ||||
|   "authFactorDelete": "刪除驗證因子", | ||||
|   "authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?", | ||||
|   "authFactorPassword": "密碼", | ||||
|   "authFactorPasswordDescription": "註冊時選擇設置的密碼。", | ||||
|   "authFactorEmail": "電郵一次性驗證碼", | ||||
|   "authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。", | ||||
|   "authFactorTOTP": "時序驗證碼", | ||||
|   "authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。", | ||||
|   "authFactorInAppNotify": "應用內通知驗證碼", | ||||
|   "authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。", | ||||
|   "authFactorAdd": "添加新驗證因子", | ||||
|   "authFactorAddSubtitle": "給你的帳戶登陸時提供另一個方案。", | ||||
|   "accountIntroTitle": "喜歡您來!", | ||||
|   "accountIntroSubtitle": "登陸以探索更廣大的世界。", | ||||
|   "accountLogout": "退出登錄", | ||||
| @@ -98,8 +111,14 @@ | ||||
|   "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。", | ||||
|   "accountPublishers": "你的發佈者", | ||||
|   "accountPublishersSubtitle": "管理你的公共形象。", | ||||
|   "accountSettings": "帳號設置", | ||||
|   "accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。", | ||||
|   "accountProfileEdit": "編輯資料", | ||||
|   "accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。", | ||||
|   "accountWallet": "錢包", | ||||
|   "accountWalletSubtitle": "查看你的餘額和交易記錄。", | ||||
|   "factorSettings": "驗證因子", | ||||
|   "factorSettingsSubtitle": "管理你的登陸驗證方式。", | ||||
|   "accountProfileEditApplied": "個人資料修改已被應用。", | ||||
|   "publishersNew": "新發布者", | ||||
|   "publisherNewSubtitle": "創建一個新的公共身份。", | ||||
| @@ -178,6 +197,9 @@ | ||||
|     "other": "{} 條評論" | ||||
|   }, | ||||
|   "settingsAppearance": "外觀", | ||||
|   "settingsDisplayLanguage": "顯示語言", | ||||
|   "settingsDisplayLanguageDescription": "設置應用程序使用的語言", | ||||
|   "settingsDisplayLanguageSystem": "跟隨系統", | ||||
|   "settingsBackgroundImage": "背景圖片", | ||||
|   "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。", | ||||
|   "settingsBackgroundImageClear": "清除現存背景圖", | ||||
| @@ -530,6 +552,9 @@ | ||||
|   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", | ||||
|   "postShare": "分享", | ||||
|   "postShareImage": "分享帖圖", | ||||
|   "postGetInsight": "獲取見解", | ||||
|   "postGetInsightTitle": "AI 見解", | ||||
|   "postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。", | ||||
|   "appInitializing": "正在初始化", | ||||
|   "poweredBy": "由 {} 提供支持", | ||||
|   "shareIntent": "分享", | ||||
| @@ -563,5 +588,19 @@ | ||||
|   "newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章", | ||||
|   "newsReadingFromOriginal": "你正在閱讀原始文章", | ||||
|   "newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。", | ||||
|   "newsToday": "快訊" | ||||
|   "newsToday": "快訊", | ||||
|   "totpPostSetup": "還有一件事", | ||||
|   "totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的驗證器掃描本 QR Code 來添加。", | ||||
|   "totpNeverShare": "永遠不要分享這個 QR Code", | ||||
|   "needHelp": "需要幫助?", | ||||
|   "needHelpLaunch": "查看我們的山羊維基!", | ||||
|   "walletCreate": "創建錢包", | ||||
|   "walletCreateSubtitle": "創建於一個錢包來開始使用源點。", | ||||
|   "walletCreatePassword": "在下方設置你的付款密碼", | ||||
|   "walletCurrencyShort": "源點", | ||||
|   "walletCurrency": { | ||||
|     "one": "{} 源點", | ||||
|     "other": "{} 源點" | ||||
|   }, | ||||
|   "aiThinkingProcess": "AI 思考過程" | ||||
| } | ||||
|   | ||||
| @@ -379,7 +379,7 @@ SPEC CHECKSUMS: | ||||
|   device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 | ||||
|   DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c | ||||
|   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 | ||||
|   file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 | ||||
|   file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 | ||||
|   file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 | ||||
|   Firebase: 374a441a91ead896215703a674d58cdb3e9d772b | ||||
|   firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e | ||||
|   | ||||
| @@ -71,22 +71,29 @@ class NotificationProvider extends ChangeNotifier { | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   int showingCount = 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++; | ||||
|         notifications.add(notification); | ||||
|         Future.delayed(const Duration(seconds: 3), () { | ||||
|           if (showingCount >= 0) showingCount--; | ||||
|           notifyListeners(); | ||||
|         }); | ||||
|         notifyListeners(); | ||||
|         final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true; | ||||
|         if (doHaptic) HapticFeedback.lightImpact(); | ||||
|         if (doHaptic) HapticFeedback.mediumImpact(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void clear() { | ||||
|     notifications.clear(); | ||||
|     showingCount = 0; | ||||
|     notifyListeners(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -33,6 +33,7 @@ import 'package:surface/screens/realm/manage.dart'; | ||||
| import 'package:surface/screens/realm/realm_detail.dart'; | ||||
| import 'package:surface/screens/settings.dart'; | ||||
| import 'package:surface/screens/sharing.dart'; | ||||
| import 'package:surface/screens/wallet.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/about.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| @@ -99,6 +100,11 @@ final _appRoutes = [ | ||||
|     ], | ||||
|   ), | ||||
|   GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [ | ||||
|     GoRoute( | ||||
|       path: '/wallet', | ||||
|       name: 'accountWallet', | ||||
|       builder: (context, state) => const WalletScreen(), | ||||
|     ), | ||||
|     GoRoute( | ||||
|       path: '/settings', | ||||
|       name: 'accountSettings', | ||||
|   | ||||
| @@ -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, | ||||
| @@ -144,6 +156,16 @@ class _AuthorizedAccountScreen extends StatelessWidget { | ||||
|             GoRouter.of(context).pushNamed('factorSettings'); | ||||
|           }, | ||||
|         ), | ||||
|         ListTile( | ||||
|           title: Text('accountWallet').tr(), | ||||
|           subtitle: Text('accountWalletSubtitle').tr(), | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|           leading: const Icon(Symbols.wallet), | ||||
|           trailing: const Icon(Symbols.chevron_right), | ||||
|           onTap: () { | ||||
|             GoRouter.of(context).pushNamed('accountWallet'); | ||||
|           }, | ||||
|         ), | ||||
|         ListTile( | ||||
|           title: Text('accountSettings').tr(), | ||||
|           subtitle: Text('accountSettingsSubtitle').tr(), | ||||
|   | ||||
| @@ -238,7 +238,7 @@ class _FactorNewDialogState extends State<_FactorNewDialog> { | ||||
| class _FactorTotpFactorDialog extends StatelessWidget { | ||||
|   final SnAuthFactor factor; | ||||
|  | ||||
|   const _FactorTotpFactorDialog({super.key, required this.factor}); | ||||
|   const _FactorTotpFactorDialog({required this.factor}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|   | ||||
| @@ -412,8 +412,9 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> { | ||||
|       await sn.client.post('/cgi/id/users/me/password-reset', data: { | ||||
|         'user_id': lookupResp.data['id'], | ||||
|       }); | ||||
|       if (mounted) | ||||
|       if (mounted) { | ||||
|         context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr()); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       if (mounted) context.showErrorDialog(err); | ||||
|     } finally { | ||||
|   | ||||
| @@ -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( | ||||
| @@ -302,20 +303,18 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> { | ||||
|                       overflow: TextOverflow.ellipsis, | ||||
|                       style: Theme.of(context).textTheme.bodyMedium, | ||||
|                     ), | ||||
|                     Builder( | ||||
|                       builder: (context) { | ||||
|                         final date = _article!.publishedAt ?? _article!.createdAt; | ||||
|                         return Row( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                           spacing: 2, | ||||
|                           children: [ | ||||
|                             Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!), | ||||
|                             Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(), | ||||
|                             Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!), | ||||
|                           ], | ||||
|                         ).opacity(0.75); | ||||
|                       } | ||||
|                     ), | ||||
|                     Builder(builder: (context) { | ||||
|                       final date = _article!.publishedAt ?? _article!.createdAt; | ||||
|                       return Row( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                         spacing: 2, | ||||
|                         children: [ | ||||
|                           Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!), | ||||
|                           Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(), | ||||
|                           Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!), | ||||
|                         ], | ||||
|                       ).opacity(0.75); | ||||
|                     }), | ||||
|                   ], | ||||
|                 ).padding(horizontal: 16), | ||||
|                 onTap: () { | ||||
| @@ -515,6 +514,11 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> { | ||||
|                           '+${_todayRecord!.resultExperience} EXP', | ||||
|                           style: Theme.of(context).textTheme.bodyLarge, | ||||
|                         ), | ||||
|                         if (_todayRecord!.resultCoin >= 0) | ||||
|                           Text( | ||||
|                             '+${_todayRecord!.resultCoin} ${'walletCurrencyShort'.tr()}', | ||||
|                             style: Theme.of(context).textTheme.bodyLarge, | ||||
|                           ) | ||||
|                       ], | ||||
|                     ), | ||||
|             ), | ||||
|   | ||||
| @@ -21,6 +21,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,13 +46,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, | ||||
|     'interactive.subscription': Symbols.subscriptions, | ||||
|     'interactive.feedback': Symbols.add_reaction, | ||||
|     'messaging.callStart': Symbols.call_received, | ||||
|   }; | ||||
|  | ||||
|   Future<void> _fetchNotifications() async { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|     if (!ua.isAuthorized) return; | ||||
|   | ||||
| @@ -82,6 +82,48 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text('settingsAppearance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), | ||||
|                 ListTile( | ||||
|                   title: Text('settingsDisplayLanguage').tr(), | ||||
|                   subtitle: Text('settingsDisplayLanguageDescription').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: ele, | ||||
|                             child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14), | ||||
|                           ); | ||||
|                         }), | ||||
|                         DropdownMenuItem<Locale?>( | ||||
|                           value: null, | ||||
|                           child: Text('settingsDisplayLanguageSystem').tr().fontSize(14), | ||||
|                         ), | ||||
|                       ], | ||||
|                       value: EasyLocalization.of(context)!.currentLocale, | ||||
|                       onChanged: (Locale? value) { | ||||
|                         if (value != null) { | ||||
|                           EasyLocalization.of(context)!.setLocale(value); | ||||
|                         } else { | ||||
|                           EasyLocalization.of(context)!.resetLocale(); | ||||
|                         } | ||||
|                       }, | ||||
|                       buttonStyleData: const ButtonStyleData( | ||||
|                         padding: EdgeInsets.symmetric( | ||||
|                           horizontal: 16, | ||||
|                           vertical: 5, | ||||
|                         ), | ||||
|                         height: 40, | ||||
|                         width: 160, | ||||
|                       ), | ||||
|                       menuItemStyleData: const MenuItemStyleData( | ||||
|                         height: 40, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|                 if (!kIsWeb) | ||||
|                   ListTile( | ||||
|                     title: Text('settingsBackgroundImage').tr(), | ||||
| @@ -147,30 +189,31 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                     Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value); | ||||
|                     final color = await showDialog<Color?>( | ||||
|                       context: context, | ||||
|                       builder: (context) => AlertDialog( | ||||
|                         content: SingleChildScrollView( | ||||
|                           child: ColorPicker( | ||||
|                             pickerColor: pickerColor, | ||||
|                             onColorChanged: (color) => pickerColor = color, | ||||
|                             enableAlpha: false, | ||||
|                             hexInputBar: true, | ||||
|                       builder: (context) => | ||||
|                           AlertDialog( | ||||
|                             content: SingleChildScrollView( | ||||
|                               child: ColorPicker( | ||||
|                                 pickerColor: pickerColor, | ||||
|                                 onColorChanged: (color) => pickerColor = color, | ||||
|                                 enableAlpha: false, | ||||
|                                 hexInputBar: true, | ||||
|                               ), | ||||
|                             ), | ||||
|                             actions: <Widget>[ | ||||
|                               TextButton( | ||||
|                                 child: const Text('dialogDismiss').tr(), | ||||
|                                 onPressed: () { | ||||
|                                   Navigator.of(context).pop(); | ||||
|                                 }, | ||||
|                               ), | ||||
|                               TextButton( | ||||
|                                 child: const Text('dialogConfirm').tr(), | ||||
|                                 onPressed: () { | ||||
|                                   Navigator.of(context).pop(pickerColor); | ||||
|                                 }, | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                         actions: <Widget>[ | ||||
|                           TextButton( | ||||
|                             child: const Text('dialogDismiss').tr(), | ||||
|                             onPressed: () { | ||||
|                               Navigator.of(context).pop(); | ||||
|                             }, | ||||
|                           ), | ||||
|                           TextButton( | ||||
|                             child: const Text('dialogConfirm').tr(), | ||||
|                             onPressed: () { | ||||
|                               Navigator.of(context).pop(pickerColor); | ||||
|                             }, | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ); | ||||
|  | ||||
|                     if (color == null || !context.mounted) return; | ||||
| @@ -206,11 +249,13 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                       value: _prefs.getInt(kAppColorSchemeStoreKey) == null | ||||
|                           ? 1 | ||||
|                           : kColorSchemes.values | ||||
|                               .toList() | ||||
|                               .indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)), | ||||
|                           .toList() | ||||
|                           .indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)), | ||||
|                       onChanged: (int? value) { | ||||
|                         if (value != null && value != -1) { | ||||
|                           _prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values.elementAt(value).value); | ||||
|                           _prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values | ||||
|                               .elementAt(value) | ||||
|                               .value); | ||||
|                           final th = context.read<ThemeProvider>(); | ||||
|                           th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value)); | ||||
|                           setState(() {}); | ||||
| @@ -342,7 +387,8 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                           ('Custom', _serverUrlController.text), | ||||
|                       ] | ||||
|                           .map( | ||||
|                             (item) => DropdownMenuItem<String>( | ||||
|                             (item) => | ||||
|                             DropdownMenuItem<String>( | ||||
|                               value: item.$2, | ||||
|                               child: Column( | ||||
|                                 mainAxisSize: MainAxisSize.max, | ||||
| @@ -354,7 +400,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                           ) | ||||
|                       ) | ||||
|                           .toList(), | ||||
|                       value: _serverUrlController.text, | ||||
|                       onChanged: (String? value) { | ||||
| @@ -409,11 +455,12 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                       isExpanded: true, | ||||
|                       items: kImageQualityLevel.entries | ||||
|                           .map( | ||||
|                             (item) => DropdownMenuItem<FilterQuality>( | ||||
|                             (item) => | ||||
|                             DropdownMenuItem<FilterQuality>( | ||||
|                               value: item.value, | ||||
|                               child: Text(item.key).tr().fontSize(14), | ||||
|                             ), | ||||
|                           ) | ||||
|                       ) | ||||
|                           .toList(), | ||||
|                       onChanged: (FilterQuality? value) { | ||||
|                         if (value == null) return; | ||||
|   | ||||
							
								
								
									
										279
									
								
								lib/screens/wallet.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								lib/screens/wallet.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,279 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/wallet.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| 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'; | ||||
|  | ||||
| class WalletScreen extends StatefulWidget { | ||||
|   const WalletScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<WalletScreen> createState() => _WalletScreenState(); | ||||
| } | ||||
|  | ||||
| class _WalletScreenState extends State<WalletScreen> { | ||||
|   bool _isBusy = false; | ||||
|   SnWallet? _wallet; | ||||
|  | ||||
|   Future<void> _fetchWallet() async { | ||||
|     try { | ||||
|       setState(() => _isBusy = true); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/wa/wallets/me'); | ||||
|       _wallet = SnWallet.fromJson(resp.data); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _fetchWallet(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: PageBackButton(), | ||||
|         title: Text('screenAccountWallet').tr(), | ||||
|       ), | ||||
|       body: Column( | ||||
|         children: [ | ||||
|           LoadingIndicator(isActive: _isBusy), | ||||
|           if (_wallet == null) | ||||
|             Expanded( | ||||
|               child: _CreateWalletWidget( | ||||
|                 onCreate: () { | ||||
|                   _fetchWallet(); | ||||
|                 }, | ||||
|               ), | ||||
|             ) | ||||
|           else | ||||
|             Card( | ||||
|               child: Column( | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   CircleAvatar( | ||||
|                     radius: 28, | ||||
|                     child: Icon(Symbols.wallet, size: 28), | ||||
|                   ), | ||||
|                   const Gap(12), | ||||
|                   SizedBox(width: double.infinity), | ||||
|                   Text( | ||||
|                     NumberFormat.compactCurrency( | ||||
|                       locale: EasyLocalization.of(context)!.currentLocale.toString(), | ||||
|                       symbol: '${'walletCurrencyShort'.tr()} ', | ||||
|                       decimalDigits: 2, | ||||
|                     ).format(double.parse(_wallet!.balance)), | ||||
|                     style: Theme.of(context).textTheme.titleLarge, | ||||
|                   ), | ||||
|                   Text('walletCurrency'.plural(double.parse(_wallet!.balance))), | ||||
|                 ], | ||||
|               ).padding(horizontal: 20, vertical: 24), | ||||
|             ).padding(horizontal: 8, top: 16, bottom: 4), | ||||
|           if (_wallet != null) Expanded(child: _WalletTransactionList(myself: _wallet!)), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _WalletTransactionList extends StatefulWidget { | ||||
|   final SnWallet myself; | ||||
|  | ||||
|   const _WalletTransactionList({required this.myself}); | ||||
|  | ||||
|   @override | ||||
|   State<_WalletTransactionList> createState() => _WalletTransactionListState(); | ||||
| } | ||||
|  | ||||
| class _WalletTransactionListState extends State<_WalletTransactionList> { | ||||
|   bool _isBusy = false; | ||||
|   int? _totalCount; | ||||
|   final List<SnTransaction> _transactions = List.empty(growable: true); | ||||
|  | ||||
|   Future<void> _fetchTransactions() async { | ||||
|     try { | ||||
|       setState(() => _isBusy = true); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/wa/transactions/me', queryParameters: { | ||||
|         'take': 10, | ||||
|         'offset': _transactions.length, | ||||
|       }); | ||||
|       _totalCount = resp.data['count']; | ||||
|       _transactions.addAll( | ||||
|         resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? [], | ||||
|       ); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _fetchTransactions(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return MediaQuery.removePadding( | ||||
|       context: context, | ||||
|       removeTop: true, | ||||
|       child: RefreshIndicator( | ||||
|         onRefresh: _fetchTransactions, | ||||
|         child: InfiniteList( | ||||
|           itemCount: _transactions.length, | ||||
|           isLoading: _isBusy, | ||||
|           hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!, | ||||
|           onFetchData: () { | ||||
|             _fetchTransactions(); | ||||
|           }, | ||||
|           itemBuilder: (context, idx) { | ||||
|             final ele = _transactions[idx]; | ||||
|             final isIncoming = ele.payeeId == widget.myself.id; | ||||
|             return ListTile( | ||||
|               leading: isIncoming ? const Icon(Symbols.call_received) : const Icon(Symbols.call_made), | ||||
|               title: Text( | ||||
|                 '${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}', | ||||
|                 style: TextStyle(color: isIncoming ? Colors.green : Colors.red), | ||||
|               ), | ||||
|               subtitle: Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   Text(ele.remark), | ||||
|                   const Gap(2), | ||||
|                   Text( | ||||
|                     DateFormat( | ||||
|                       null, | ||||
|                       EasyLocalization.of(context)!.currentLocale.toString(), | ||||
|                     ).format(ele.createdAt), | ||||
|                     style: Theme.of(context).textTheme.labelSmall, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|               contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|             ); | ||||
|           }, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _CreateWalletWidget extends StatefulWidget { | ||||
|   final Function()? onCreate; | ||||
|  | ||||
|   const _CreateWalletWidget({required this.onCreate}); | ||||
|  | ||||
|   @override | ||||
|   State<_CreateWalletWidget> createState() => _CreateWalletWidgetState(); | ||||
| } | ||||
|  | ||||
| class _CreateWalletWidgetState extends State<_CreateWalletWidget> { | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   Future<void> _createWallet() async { | ||||
|     final TextEditingController passwordController = TextEditingController(); | ||||
|     final password = await showDialog<String?>( | ||||
|       context: context, | ||||
|       builder: (ctx) => AlertDialog( | ||||
|         title: Text('walletCreate').tr(), | ||||
|         content: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: [ | ||||
|             Text('walletCreatePassword').tr(), | ||||
|             const Gap(8), | ||||
|             TextField( | ||||
|               autofocus: true, | ||||
|               obscureText: true, | ||||
|               controller: passwordController, | ||||
|               decoration: InputDecoration( | ||||
|                 labelText: 'fieldPassword'.tr(), | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|         actions: [ | ||||
|           TextButton( | ||||
|             onPressed: () => Navigator.of(ctx).pop(), | ||||
|             child: Text('cancel').tr(), | ||||
|           ), | ||||
|           TextButton( | ||||
|             onPressed: () { | ||||
|               Navigator.of(ctx).pop(passwordController.text); | ||||
|             }, | ||||
|             child: Text('next').tr(), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|     WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|       passwordController.dispose(); | ||||
|     }); | ||||
|     if (password == null || password.isEmpty) return; | ||||
|     if (!mounted) return; | ||||
|  | ||||
|     try { | ||||
|       setState(() => _isBusy = true); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.post('/cgi/wa/wallets/me', data: { | ||||
|         'password': password, | ||||
|       }); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Center( | ||||
|       child: Container( | ||||
|         constraints: const BoxConstraints(maxWidth: 380), | ||||
|         child: Card( | ||||
|           child: Column( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               CircleAvatar( | ||||
|                 radius: 28, | ||||
|                 child: Icon(Symbols.add, size: 28), | ||||
|               ), | ||||
|               const Gap(12), | ||||
|               Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(), | ||||
|               Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(), | ||||
|               const Gap(8), | ||||
|               Align( | ||||
|                 alignment: Alignment.centerRight, | ||||
|                 child: TextButton( | ||||
|                   onPressed: _isBusy ? null : () => _createWallet(), | ||||
|                   child: Text('next').tr(), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ).padding(horizontal: 20, vertical: 24), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -16,6 +16,7 @@ class SnCheckInRecord with _$SnCheckInRecord { | ||||
|     required DateTime? deletedAt, | ||||
|     required int resultTier, | ||||
|     required int resultExperience, | ||||
|     required double resultCoin, | ||||
|     required List<int> resultModifiers, | ||||
|     required int accountId, | ||||
|   }) = _SnCheckInRecord; | ||||
|   | ||||
| @@ -26,6 +26,7 @@ mixin _$SnCheckInRecord { | ||||
|   DateTime? get deletedAt => throw _privateConstructorUsedError; | ||||
|   int get resultTier => throw _privateConstructorUsedError; | ||||
|   int get resultExperience => throw _privateConstructorUsedError; | ||||
|   double get resultCoin => throw _privateConstructorUsedError; | ||||
|   List<int> get resultModifiers => throw _privateConstructorUsedError; | ||||
|   int get accountId => throw _privateConstructorUsedError; | ||||
|  | ||||
| @@ -52,6 +53,7 @@ abstract class $SnCheckInRecordCopyWith<$Res> { | ||||
|       DateTime? deletedAt, | ||||
|       int resultTier, | ||||
|       int resultExperience, | ||||
|       double resultCoin, | ||||
|       List<int> resultModifiers, | ||||
|       int accountId}); | ||||
| } | ||||
| @@ -77,6 +79,7 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord> | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? resultTier = null, | ||||
|     Object? resultExperience = null, | ||||
|     Object? resultCoin = null, | ||||
|     Object? resultModifiers = null, | ||||
|     Object? accountId = null, | ||||
|   }) { | ||||
| @@ -105,6 +108,10 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord> | ||||
|           ? _value.resultExperience | ||||
|           : resultExperience // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       resultCoin: null == resultCoin | ||||
|           ? _value.resultCoin | ||||
|           : resultCoin // ignore: cast_nullable_to_non_nullable | ||||
|               as double, | ||||
|       resultModifiers: null == resultModifiers | ||||
|           ? _value.resultModifiers | ||||
|           : resultModifiers // ignore: cast_nullable_to_non_nullable | ||||
| @@ -132,6 +139,7 @@ abstract class _$$SnCheckInRecordImplCopyWith<$Res> | ||||
|       DateTime? deletedAt, | ||||
|       int resultTier, | ||||
|       int resultExperience, | ||||
|       double resultCoin, | ||||
|       List<int> resultModifiers, | ||||
|       int accountId}); | ||||
| } | ||||
| @@ -155,6 +163,7 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res> | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? resultTier = null, | ||||
|     Object? resultExperience = null, | ||||
|     Object? resultCoin = null, | ||||
|     Object? resultModifiers = null, | ||||
|     Object? accountId = null, | ||||
|   }) { | ||||
| @@ -183,6 +192,10 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res> | ||||
|           ? _value.resultExperience | ||||
|           : resultExperience // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       resultCoin: null == resultCoin | ||||
|           ? _value.resultCoin | ||||
|           : resultCoin // ignore: cast_nullable_to_non_nullable | ||||
|               as double, | ||||
|       resultModifiers: null == resultModifiers | ||||
|           ? _value._resultModifiers | ||||
|           : resultModifiers // ignore: cast_nullable_to_non_nullable | ||||
| @@ -205,6 +218,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord { | ||||
|       required this.deletedAt, | ||||
|       required this.resultTier, | ||||
|       required this.resultExperience, | ||||
|       required this.resultCoin, | ||||
|       required final List<int> resultModifiers, | ||||
|       required this.accountId}) | ||||
|       : _resultModifiers = resultModifiers, | ||||
| @@ -225,6 +239,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord { | ||||
|   final int resultTier; | ||||
|   @override | ||||
|   final int resultExperience; | ||||
|   @override | ||||
|   final double resultCoin; | ||||
|   final List<int> _resultModifiers; | ||||
|   @override | ||||
|   List<int> get resultModifiers { | ||||
| @@ -238,7 +254,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord { | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultModifiers: $resultModifiers, accountId: $accountId)'; | ||||
|     return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultCoin: $resultCoin, resultModifiers: $resultModifiers, accountId: $accountId)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -257,6 +273,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord { | ||||
|                 other.resultTier == resultTier) && | ||||
|             (identical(other.resultExperience, resultExperience) || | ||||
|                 other.resultExperience == resultExperience) && | ||||
|             (identical(other.resultCoin, resultCoin) || | ||||
|                 other.resultCoin == resultCoin) && | ||||
|             const DeepCollectionEquality() | ||||
|                 .equals(other._resultModifiers, _resultModifiers) && | ||||
|             (identical(other.accountId, accountId) || | ||||
| @@ -273,6 +291,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord { | ||||
|       deletedAt, | ||||
|       resultTier, | ||||
|       resultExperience, | ||||
|       resultCoin, | ||||
|       const DeepCollectionEquality().hash(_resultModifiers), | ||||
|       accountId); | ||||
|  | ||||
| @@ -301,6 +320,7 @@ abstract class _SnCheckInRecord extends SnCheckInRecord { | ||||
|       required final DateTime? deletedAt, | ||||
|       required final int resultTier, | ||||
|       required final int resultExperience, | ||||
|       required final double resultCoin, | ||||
|       required final List<int> resultModifiers, | ||||
|       required final int accountId}) = _$SnCheckInRecordImpl; | ||||
|   const _SnCheckInRecord._() : super._(); | ||||
| @@ -321,6 +341,8 @@ abstract class _SnCheckInRecord extends SnCheckInRecord { | ||||
|   @override | ||||
|   int get resultExperience; | ||||
|   @override | ||||
|   double get resultCoin; | ||||
|   @override | ||||
|   List<int> get resultModifiers; | ||||
|   @override | ||||
|   int get accountId; | ||||
|   | ||||
| @@ -17,6 +17,7 @@ _$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson( | ||||
|           : DateTime.parse(json['deleted_at'] as String), | ||||
|       resultTier: (json['result_tier'] as num).toInt(), | ||||
|       resultExperience: (json['result_experience'] as num).toInt(), | ||||
|       resultCoin: (json['result_coin'] as num).toDouble(), | ||||
|       resultModifiers: (json['result_modifiers'] as List<dynamic>) | ||||
|           .map((e) => (e as num).toInt()) | ||||
|           .toList(), | ||||
| @@ -32,6 +33,7 @@ Map<String, dynamic> _$$SnCheckInRecordImplToJson( | ||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||
|       'result_tier': instance.resultTier, | ||||
|       'result_experience': instance.resultExperience, | ||||
|       'result_coin': instance.resultCoin, | ||||
|       'result_modifiers': instance.resultModifiers, | ||||
|       'account_id': instance.accountId, | ||||
|     }; | ||||
|   | ||||
							
								
								
									
										37
									
								
								lib/types/wallet.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								lib/types/wallet.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
|  | ||||
| part 'wallet.freezed.dart'; | ||||
| part 'wallet.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| class SnWallet with _$SnWallet { | ||||
|   const factory SnWallet({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     required DateTime? deletedAt, | ||||
|     required String balance, | ||||
|     required String password, | ||||
|     required int accountId, | ||||
|   }) = _SnWallet; | ||||
|  | ||||
|   factory SnWallet.fromJson(Map<String, dynamic> json) => _$SnWalletFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnTransaction with _$SnTransaction { | ||||
|   const factory SnTransaction({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     required DateTime? deletedAt, | ||||
|     required String remark, | ||||
|     required String amount, | ||||
|     required SnWallet? payer, | ||||
|     required SnWallet? payee, | ||||
|     required int? payerId, | ||||
|     required int? payeeId, | ||||
|   }) = _SnTransaction; | ||||
|  | ||||
|   factory SnTransaction.fromJson(Map<String, dynamic> json) => _$SnTransactionFromJson(json); | ||||
| } | ||||
							
								
								
									
										666
									
								
								lib/types/wallet.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										666
									
								
								lib/types/wallet.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,666 @@ | ||||
| // coverage:ignore-file | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark | ||||
|  | ||||
| part of 'wallet.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| final _privateConstructorUsedError = UnsupportedError( | ||||
|     'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); | ||||
|  | ||||
| SnWallet _$SnWalletFromJson(Map<String, dynamic> json) { | ||||
|   return _SnWallet.fromJson(json); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnWallet { | ||||
|   int get id => throw _privateConstructorUsedError; | ||||
|   DateTime get createdAt => throw _privateConstructorUsedError; | ||||
|   DateTime get updatedAt => throw _privateConstructorUsedError; | ||||
|   DateTime? get deletedAt => throw _privateConstructorUsedError; | ||||
|   String get balance => throw _privateConstructorUsedError; | ||||
|   String get password => throw _privateConstructorUsedError; | ||||
|   int get accountId => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Serializes this SnWallet to a JSON map. | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Create a copy of SnWallet | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   $SnWalletCopyWith<SnWallet> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class $SnWalletCopyWith<$Res> { | ||||
|   factory $SnWalletCopyWith(SnWallet value, $Res Function(SnWallet) then) = | ||||
|       _$SnWalletCopyWithImpl<$Res, SnWallet>; | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String balance, | ||||
|       String password, | ||||
|       int accountId}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class _$SnWalletCopyWithImpl<$Res, $Val extends SnWallet> | ||||
|     implements $SnWalletCopyWith<$Res> { | ||||
|   _$SnWalletCopyWithImpl(this._value, this._then); | ||||
|  | ||||
|   // ignore: unused_field | ||||
|   final $Val _value; | ||||
|   // ignore: unused_field | ||||
|   final $Res Function($Val) _then; | ||||
|  | ||||
|   /// Create a copy of SnWallet | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? balance = null, | ||||
|     Object? password = null, | ||||
|     Object? accountId = null, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       balance: null == balance | ||||
|           ? _value.balance | ||||
|           : balance // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       password: null == password | ||||
|           ? _value.password | ||||
|           : password // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       accountId: null == accountId | ||||
|           ? _value.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|     ) as $Val); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class _$$SnWalletImplCopyWith<$Res> | ||||
|     implements $SnWalletCopyWith<$Res> { | ||||
|   factory _$$SnWalletImplCopyWith( | ||||
|           _$SnWalletImpl value, $Res Function(_$SnWalletImpl) then) = | ||||
|       __$$SnWalletImplCopyWithImpl<$Res>; | ||||
|   @override | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String balance, | ||||
|       String password, | ||||
|       int accountId}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class __$$SnWalletImplCopyWithImpl<$Res> | ||||
|     extends _$SnWalletCopyWithImpl<$Res, _$SnWalletImpl> | ||||
|     implements _$$SnWalletImplCopyWith<$Res> { | ||||
|   __$$SnWalletImplCopyWithImpl( | ||||
|       _$SnWalletImpl _value, $Res Function(_$SnWalletImpl) _then) | ||||
|       : super(_value, _then); | ||||
|  | ||||
|   /// Create a copy of SnWallet | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? balance = null, | ||||
|     Object? password = null, | ||||
|     Object? accountId = null, | ||||
|   }) { | ||||
|     return _then(_$SnWalletImpl( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       balance: null == balance | ||||
|           ? _value.balance | ||||
|           : balance // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       password: null == password | ||||
|           ? _value.password | ||||
|           : password // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       accountId: null == accountId | ||||
|           ? _value.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
| class _$SnWalletImpl implements _SnWallet { | ||||
|   const _$SnWalletImpl( | ||||
|       {required this.id, | ||||
|       required this.createdAt, | ||||
|       required this.updatedAt, | ||||
|       required this.deletedAt, | ||||
|       required this.balance, | ||||
|       required this.password, | ||||
|       required this.accountId}); | ||||
|  | ||||
|   factory _$SnWalletImpl.fromJson(Map<String, dynamic> json) => | ||||
|       _$$SnWalletImplFromJson(json); | ||||
|  | ||||
|   @override | ||||
|   final int id; | ||||
|   @override | ||||
|   final DateTime createdAt; | ||||
|   @override | ||||
|   final DateTime updatedAt; | ||||
|   @override | ||||
|   final DateTime? deletedAt; | ||||
|   @override | ||||
|   final String balance; | ||||
|   @override | ||||
|   final String password; | ||||
|   @override | ||||
|   final int accountId; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, password: $password, accountId: $accountId)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _$SnWalletImpl && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.createdAt, createdAt) || | ||||
|                 other.createdAt == createdAt) && | ||||
|             (identical(other.updatedAt, updatedAt) || | ||||
|                 other.updatedAt == updatedAt) && | ||||
|             (identical(other.deletedAt, deletedAt) || | ||||
|                 other.deletedAt == deletedAt) && | ||||
|             (identical(other.balance, balance) || other.balance == balance) && | ||||
|             (identical(other.password, password) || | ||||
|                 other.password == password) && | ||||
|             (identical(other.accountId, accountId) || | ||||
|                 other.accountId == accountId)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt, | ||||
|       deletedAt, balance, password, accountId); | ||||
|  | ||||
|   /// Create a copy of SnWallet | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$$SnWalletImplCopyWith<_$SnWalletImpl> get copyWith => | ||||
|       __$$SnWalletImplCopyWithImpl<_$SnWalletImpl>(this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$$SnWalletImplToJson( | ||||
|       this, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _SnWallet implements SnWallet { | ||||
|   const factory _SnWallet( | ||||
|       {required final int id, | ||||
|       required final DateTime createdAt, | ||||
|       required final DateTime updatedAt, | ||||
|       required final DateTime? deletedAt, | ||||
|       required final String balance, | ||||
|       required final String password, | ||||
|       required final int accountId}) = _$SnWalletImpl; | ||||
|  | ||||
|   factory _SnWallet.fromJson(Map<String, dynamic> json) = | ||||
|       _$SnWalletImpl.fromJson; | ||||
|  | ||||
|   @override | ||||
|   int get id; | ||||
|   @override | ||||
|   DateTime get createdAt; | ||||
|   @override | ||||
|   DateTime get updatedAt; | ||||
|   @override | ||||
|   DateTime? get deletedAt; | ||||
|   @override | ||||
|   String get balance; | ||||
|   @override | ||||
|   String get password; | ||||
|   @override | ||||
|   int get accountId; | ||||
|  | ||||
|   /// Create a copy of SnWallet | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   _$$SnWalletImplCopyWith<_$SnWalletImpl> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| SnTransaction _$SnTransactionFromJson(Map<String, dynamic> json) { | ||||
|   return _SnTransaction.fromJson(json); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnTransaction { | ||||
|   int get id => throw _privateConstructorUsedError; | ||||
|   DateTime get createdAt => throw _privateConstructorUsedError; | ||||
|   DateTime get updatedAt => throw _privateConstructorUsedError; | ||||
|   DateTime? get deletedAt => throw _privateConstructorUsedError; | ||||
|   String get remark => throw _privateConstructorUsedError; | ||||
|   String get amount => throw _privateConstructorUsedError; | ||||
|   SnWallet? get payer => throw _privateConstructorUsedError; | ||||
|   SnWallet? get payee => throw _privateConstructorUsedError; | ||||
|   int? get payerId => throw _privateConstructorUsedError; | ||||
|   int? get payeeId => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Serializes this SnTransaction to a JSON map. | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Create a copy of SnTransaction | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   $SnTransactionCopyWith<SnTransaction> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class $SnTransactionCopyWith<$Res> { | ||||
|   factory $SnTransactionCopyWith( | ||||
|           SnTransaction value, $Res Function(SnTransaction) then) = | ||||
|       _$SnTransactionCopyWithImpl<$Res, SnTransaction>; | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String remark, | ||||
|       String amount, | ||||
|       SnWallet? payer, | ||||
|       SnWallet? payee, | ||||
|       int? payerId, | ||||
|       int? payeeId}); | ||||
|  | ||||
|   $SnWalletCopyWith<$Res>? get payer; | ||||
|   $SnWalletCopyWith<$Res>? get payee; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class _$SnTransactionCopyWithImpl<$Res, $Val extends SnTransaction> | ||||
|     implements $SnTransactionCopyWith<$Res> { | ||||
|   _$SnTransactionCopyWithImpl(this._value, this._then); | ||||
|  | ||||
|   // ignore: unused_field | ||||
|   final $Val _value; | ||||
|   // ignore: unused_field | ||||
|   final $Res Function($Val) _then; | ||||
|  | ||||
|   /// Create a copy of SnTransaction | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? remark = null, | ||||
|     Object? amount = null, | ||||
|     Object? payer = freezed, | ||||
|     Object? payee = freezed, | ||||
|     Object? payerId = freezed, | ||||
|     Object? payeeId = freezed, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       remark: null == remark | ||||
|           ? _value.remark | ||||
|           : remark // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       amount: null == amount | ||||
|           ? _value.amount | ||||
|           : amount // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       payer: freezed == payer | ||||
|           ? _value.payer | ||||
|           : payer // ignore: cast_nullable_to_non_nullable | ||||
|               as SnWallet?, | ||||
|       payee: freezed == payee | ||||
|           ? _value.payee | ||||
|           : payee // ignore: cast_nullable_to_non_nullable | ||||
|               as SnWallet?, | ||||
|       payerId: freezed == payerId | ||||
|           ? _value.payerId | ||||
|           : payerId // ignore: cast_nullable_to_non_nullable | ||||
|               as int?, | ||||
|       payeeId: freezed == payeeId | ||||
|           ? _value.payeeId | ||||
|           : payeeId // ignore: cast_nullable_to_non_nullable | ||||
|               as int?, | ||||
|     ) as $Val); | ||||
|   } | ||||
|  | ||||
|   /// Create a copy of SnTransaction | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   $SnWalletCopyWith<$Res>? get payer { | ||||
|     if (_value.payer == null) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     return $SnWalletCopyWith<$Res>(_value.payer!, (value) { | ||||
|       return _then(_value.copyWith(payer: value) as $Val); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /// Create a copy of SnTransaction | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   $SnWalletCopyWith<$Res>? get payee { | ||||
|     if (_value.payee == null) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     return $SnWalletCopyWith<$Res>(_value.payee!, (value) { | ||||
|       return _then(_value.copyWith(payee: value) as $Val); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class _$$SnTransactionImplCopyWith<$Res> | ||||
|     implements $SnTransactionCopyWith<$Res> { | ||||
|   factory _$$SnTransactionImplCopyWith( | ||||
|           _$SnTransactionImpl value, $Res Function(_$SnTransactionImpl) then) = | ||||
|       __$$SnTransactionImplCopyWithImpl<$Res>; | ||||
|   @override | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String remark, | ||||
|       String amount, | ||||
|       SnWallet? payer, | ||||
|       SnWallet? payee, | ||||
|       int? payerId, | ||||
|       int? payeeId}); | ||||
|  | ||||
|   @override | ||||
|   $SnWalletCopyWith<$Res>? get payer; | ||||
|   @override | ||||
|   $SnWalletCopyWith<$Res>? get payee; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class __$$SnTransactionImplCopyWithImpl<$Res> | ||||
|     extends _$SnTransactionCopyWithImpl<$Res, _$SnTransactionImpl> | ||||
|     implements _$$SnTransactionImplCopyWith<$Res> { | ||||
|   __$$SnTransactionImplCopyWithImpl( | ||||
|       _$SnTransactionImpl _value, $Res Function(_$SnTransactionImpl) _then) | ||||
|       : super(_value, _then); | ||||
|  | ||||
|   /// Create a copy of SnTransaction | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? remark = null, | ||||
|     Object? amount = null, | ||||
|     Object? payer = freezed, | ||||
|     Object? payee = freezed, | ||||
|     Object? payerId = freezed, | ||||
|     Object? payeeId = freezed, | ||||
|   }) { | ||||
|     return _then(_$SnTransactionImpl( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       remark: null == remark | ||||
|           ? _value.remark | ||||
|           : remark // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       amount: null == amount | ||||
|           ? _value.amount | ||||
|           : amount // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       payer: freezed == payer | ||||
|           ? _value.payer | ||||
|           : payer // ignore: cast_nullable_to_non_nullable | ||||
|               as SnWallet?, | ||||
|       payee: freezed == payee | ||||
|           ? _value.payee | ||||
|           : payee // ignore: cast_nullable_to_non_nullable | ||||
|               as SnWallet?, | ||||
|       payerId: freezed == payerId | ||||
|           ? _value.payerId | ||||
|           : payerId // ignore: cast_nullable_to_non_nullable | ||||
|               as int?, | ||||
|       payeeId: freezed == payeeId | ||||
|           ? _value.payeeId | ||||
|           : payeeId // ignore: cast_nullable_to_non_nullable | ||||
|               as int?, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
| class _$SnTransactionImpl implements _SnTransaction { | ||||
|   const _$SnTransactionImpl( | ||||
|       {required this.id, | ||||
|       required this.createdAt, | ||||
|       required this.updatedAt, | ||||
|       required this.deletedAt, | ||||
|       required this.remark, | ||||
|       required this.amount, | ||||
|       required this.payer, | ||||
|       required this.payee, | ||||
|       required this.payerId, | ||||
|       required this.payeeId}); | ||||
|  | ||||
|   factory _$SnTransactionImpl.fromJson(Map<String, dynamic> json) => | ||||
|       _$$SnTransactionImplFromJson(json); | ||||
|  | ||||
|   @override | ||||
|   final int id; | ||||
|   @override | ||||
|   final DateTime createdAt; | ||||
|   @override | ||||
|   final DateTime updatedAt; | ||||
|   @override | ||||
|   final DateTime? deletedAt; | ||||
|   @override | ||||
|   final String remark; | ||||
|   @override | ||||
|   final String amount; | ||||
|   @override | ||||
|   final SnWallet? payer; | ||||
|   @override | ||||
|   final SnWallet? payee; | ||||
|   @override | ||||
|   final int? payerId; | ||||
|   @override | ||||
|   final int? payeeId; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _$SnTransactionImpl && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.createdAt, createdAt) || | ||||
|                 other.createdAt == createdAt) && | ||||
|             (identical(other.updatedAt, updatedAt) || | ||||
|                 other.updatedAt == updatedAt) && | ||||
|             (identical(other.deletedAt, deletedAt) || | ||||
|                 other.deletedAt == deletedAt) && | ||||
|             (identical(other.remark, remark) || other.remark == remark) && | ||||
|             (identical(other.amount, amount) || other.amount == amount) && | ||||
|             (identical(other.payer, payer) || other.payer == payer) && | ||||
|             (identical(other.payee, payee) || other.payee == payee) && | ||||
|             (identical(other.payerId, payerId) || other.payerId == payerId) && | ||||
|             (identical(other.payeeId, payeeId) || other.payeeId == payeeId)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt, | ||||
|       deletedAt, remark, amount, payer, payee, payerId, payeeId); | ||||
|  | ||||
|   /// Create a copy of SnTransaction | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$$SnTransactionImplCopyWith<_$SnTransactionImpl> get copyWith => | ||||
|       __$$SnTransactionImplCopyWithImpl<_$SnTransactionImpl>(this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$$SnTransactionImplToJson( | ||||
|       this, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _SnTransaction implements SnTransaction { | ||||
|   const factory _SnTransaction( | ||||
|       {required final int id, | ||||
|       required final DateTime createdAt, | ||||
|       required final DateTime updatedAt, | ||||
|       required final DateTime? deletedAt, | ||||
|       required final String remark, | ||||
|       required final String amount, | ||||
|       required final SnWallet? payer, | ||||
|       required final SnWallet? payee, | ||||
|       required final int? payerId, | ||||
|       required final int? payeeId}) = _$SnTransactionImpl; | ||||
|  | ||||
|   factory _SnTransaction.fromJson(Map<String, dynamic> json) = | ||||
|       _$SnTransactionImpl.fromJson; | ||||
|  | ||||
|   @override | ||||
|   int get id; | ||||
|   @override | ||||
|   DateTime get createdAt; | ||||
|   @override | ||||
|   DateTime get updatedAt; | ||||
|   @override | ||||
|   DateTime? get deletedAt; | ||||
|   @override | ||||
|   String get remark; | ||||
|   @override | ||||
|   String get amount; | ||||
|   @override | ||||
|   SnWallet? get payer; | ||||
|   @override | ||||
|   SnWallet? get payee; | ||||
|   @override | ||||
|   int? get payerId; | ||||
|   @override | ||||
|   int? get payeeId; | ||||
|  | ||||
|   /// Create a copy of SnTransaction | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   _$$SnTransactionImplCopyWith<_$SnTransactionImpl> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
							
								
								
									
										65
									
								
								lib/types/wallet.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								lib/types/wallet.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'wallet.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnWalletImpl _$$SnWalletImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnWalletImpl( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
|       deletedAt: json['deleted_at'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['deleted_at'] as String), | ||||
|       balance: json['balance'] as String, | ||||
|       password: json['password'] as String, | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnWalletImplToJson(_$SnWalletImpl instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||
|       'balance': instance.balance, | ||||
|       'password': instance.password, | ||||
|       'account_id': instance.accountId, | ||||
|     }; | ||||
|  | ||||
| _$SnTransactionImpl _$$SnTransactionImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnTransactionImpl( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
|       deletedAt: json['deleted_at'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['deleted_at'] as String), | ||||
|       remark: json['remark'] as String, | ||||
|       amount: json['amount'] as String, | ||||
|       payer: json['payer'] == null | ||||
|           ? null | ||||
|           : SnWallet.fromJson(json['payer'] as Map<String, dynamic>), | ||||
|       payee: json['payee'] == null | ||||
|           ? null | ||||
|           : SnWallet.fromJson(json['payee'] as Map<String, dynamic>), | ||||
|       payerId: (json['payer_id'] as num?)?.toInt(), | ||||
|       payeeId: (json['payee_id'] as num?)?.toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnTransactionImplToJson(_$SnTransactionImpl instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||
|       'remark': instance.remark, | ||||
|       'amount': instance.amount, | ||||
|       'payer': instance.payer?.toJson(), | ||||
|       'payee': instance.payee?.toJson(), | ||||
|       'payer_id': instance.payerId, | ||||
|       'payee_id': instance.payeeId, | ||||
|     }; | ||||
| @@ -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( | ||||
|   | ||||
| @@ -2,7 +2,9 @@ import 'dart:math' as math; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/gestures.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| extension AppPromptExtension on BuildContext { | ||||
|   void showSnackbar(String content, {SnackBarAction? action}) { | ||||
| @@ -111,7 +113,34 @@ extension AppPromptExtension on BuildContext { | ||||
|       context: this, | ||||
|       builder: (ctx) => AlertDialog( | ||||
|         title: Text('dialogError').tr(), | ||||
|         content: content, | ||||
|         content: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           spacing: 20, | ||||
|           children: [ | ||||
|             content, | ||||
|             Text.rich( | ||||
|               TextSpan( | ||||
|                 text: 'needHelp'.tr(), | ||||
|                 children: [ | ||||
|                   TextSpan(text: ' '), | ||||
|                   TextSpan( | ||||
|                     text: 'needHelpLaunch'.tr(), | ||||
|                     style: TextStyle( | ||||
|                       color: Theme.of(ctx).colorScheme.primary, | ||||
|                       decoration: TextDecoration.underline, | ||||
|                       decorationColor: Theme.of(ctx).colorScheme.primary, | ||||
|                     ), | ||||
|                     recognizer: TapGestureRecognizer() | ||||
|                       ..onTap = () { | ||||
|                         launchUrlString('https://kb.solsynth.dev/solar-network'); | ||||
|                       }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|         actions: [ | ||||
|           TextButton( | ||||
|             onPressed: () => Navigator.pop(ctx), | ||||
| @@ -128,17 +157,7 @@ extension ByteFormatter on int { | ||||
|     if (this == 0) return '0 Bytes'; | ||||
|     const k = 1024; | ||||
|     final dm = decimals < 0 ? 0 : decimals; | ||||
|     final sizes = [ | ||||
|       'Bytes', | ||||
|       'KiB', | ||||
|       'MiB', | ||||
|       'GiB', | ||||
|       'TiB', | ||||
|       'PiB', | ||||
|       'EiB', | ||||
|       'ZiB', | ||||
|       'YiB' | ||||
|     ]; | ||||
|     final sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; | ||||
|     final i = (math.log(this) / math.log(k)).floor().toInt(); | ||||
|     return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}'; | ||||
|   } | ||||
|   | ||||
| @@ -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, | ||||
|         ), | ||||
|   | ||||
| @@ -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,184 @@ | ||||
| import 'dart:math' show min; | ||||
|  | ||||
| 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/config.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,6 +1,7 @@ | ||||
| import 'dart:io'; | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:file_saver/file_saver.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| @@ -34,6 +35,7 @@ import 'package:surface/widgets/post/post_meta_editor.dart'; | ||||
| import 'package:surface/widgets/post/post_reaction.dart'; | ||||
| import 'package:surface/widgets/post/publisher_popover.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
| import 'package:xml/xml.dart'; | ||||
|  | ||||
| class PostItem extends StatelessWidget { | ||||
|   final SnPost data; | ||||
| @@ -817,6 +819,22 @@ class _PostContentHeader extends StatelessWidget { | ||||
|                 }, | ||||
|               ), | ||||
|               const PopupMenuDivider(), | ||||
|               PopupMenuItem( | ||||
|                 child: Row( | ||||
|                   children: [ | ||||
|                     const Icon(Symbols.book_4_spark), | ||||
|                     const Gap(16), | ||||
|                     Text('postGetInsight').tr(), | ||||
|                   ], | ||||
|                 ), | ||||
|                 onTap: () { | ||||
|                   showModalBottomSheet( | ||||
|                     context: context, | ||||
|                     builder: (context) => _PostGetInsightSheet(postId: data.id), | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|               const PopupMenuDivider(), | ||||
|               PopupMenuItem( | ||||
|                 onTap: onShare, | ||||
|                 child: Row( | ||||
| @@ -1181,3 +1199,96 @@ class _PostAbuseReportDialogState extends State<_PostAbuseReportDialog> { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PostGetInsightSheet extends StatefulWidget { | ||||
|   final int postId; | ||||
|  | ||||
|   const _PostGetInsightSheet({required this.postId}); | ||||
|  | ||||
|   @override | ||||
|   State<_PostGetInsightSheet> createState() => _PostGetInsightSheetState(); | ||||
| } | ||||
|  | ||||
| class _PostGetInsightSheetState extends State<_PostGetInsightSheet> { | ||||
|   String? _response; | ||||
|   String? _thinkingProcess; | ||||
|  | ||||
|   Future<void> _fetchResponse() async { | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/co/posts/${widget.postId}/insight', | ||||
|           options: Options( | ||||
|             sendTimeout: const Duration(minutes: 10), | ||||
|             receiveTimeout: const Duration(minutes: 10), | ||||
|           )); | ||||
|       final out = resp.data['response'] as String; | ||||
|       final document = XmlDocument.parse(out); | ||||
|       _thinkingProcess = document.getElement('think')?.innerText.trim(); | ||||
|       RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>'); | ||||
|       setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim()); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _fetchResponse(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       children: [ | ||||
|         Row( | ||||
|           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|           children: [ | ||||
|             const Icon(Symbols.book_4_spark, size: 24), | ||||
|             const Gap(16), | ||||
|             Text('postGetInsightTitle', style: Theme.of(context).textTheme.titleLarge).tr(), | ||||
|           ], | ||||
|         ).padding(horizontal: 20, top: 16, bottom: 12), | ||||
|         const Gap(4), | ||||
|         Text('postGetInsightDescription', style: Theme.of(context).textTheme.bodySmall).tr().padding(horizontal: 20), | ||||
|         const Gap(4), | ||||
|         if (_response == null) | ||||
|           Expanded( | ||||
|             child: Center( | ||||
|               child: CircularProgressIndicator(), | ||||
|             ), | ||||
|           ) | ||||
|         else | ||||
|           Expanded( | ||||
|             child: SingleChildScrollView( | ||||
|               child: Column( | ||||
|                 children: [ | ||||
|                   if (_thinkingProcess != null && _thinkingProcess!.isNotEmpty) | ||||
|                     ExpansionTile( | ||||
|                       leading: const Icon(Symbols.info), | ||||
|                       title: Text('aiThinkingProcess'.tr()), | ||||
|                       tilePadding: const EdgeInsets.symmetric(horizontal: 20), | ||||
|                       collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                       minTileHeight: 32, | ||||
|                       children: [ | ||||
|                         SelectableText( | ||||
|                           _thinkingProcess!, | ||||
|                           style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontStyle: FontStyle.italic), | ||||
|                         ).padding(horizontal: 20, vertical: 8), | ||||
|                       ], | ||||
|                     ).padding(vertical: 8), | ||||
|                   SelectionArea( | ||||
|                     child: MarkdownTextContent( | ||||
|                       content: _response!, | ||||
|                     ), | ||||
|                   ).padding(horizontal: 20, top: 8), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import Foundation | ||||
| import bitsdojo_window_macos | ||||
| import connectivity_plus | ||||
| import device_info_plus | ||||
| import file_picker | ||||
| import file_saver | ||||
| import file_selector_macos | ||||
| import firebase_analytics | ||||
| @@ -36,6 +37,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | ||||
|   BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) | ||||
|   ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) | ||||
|   DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) | ||||
|   FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) | ||||
|   FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) | ||||
|   FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) | ||||
|   FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) | ||||
|   | ||||
| @@ -8,6 +8,8 @@ PODS: | ||||
|     - FlutterMacOS | ||||
|   - device_info_plus (0.0.1): | ||||
|     - FlutterMacOS | ||||
|   - file_picker (0.0.1): | ||||
|     - FlutterMacOS | ||||
|   - file_saver (0.0.1): | ||||
|     - FlutterMacOS | ||||
|   - file_selector_macos (0.0.1): | ||||
| @@ -185,6 +187,7 @@ DEPENDENCIES: | ||||
|   - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`) | ||||
|   - croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`) | ||||
|   - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) | ||||
|   - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) | ||||
|   - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) | ||||
|   - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) | ||||
|   - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`) | ||||
| @@ -237,6 +240,8 @@ EXTERNAL SOURCES: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/croppy/macos | ||||
|   device_info_plus: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos | ||||
|   file_picker: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos | ||||
|   file_saver: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos | ||||
|   file_selector_macos: | ||||
| @@ -293,6 +298,7 @@ SPEC CHECKSUMS: | ||||
|   connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 | ||||
|   croppy: 25a638bd7d05411d8c697f481568f261037694fc | ||||
|   device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 | ||||
|   file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af | ||||
|   file_saver: 44e6fbf666677faf097302460e214e977fdd977b | ||||
|   file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d | ||||
|   Firebase: 374a441a91ead896215703a674d58cdb3e9d772b | ||||
|   | ||||
							
								
								
									
										34
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -362,10 +362,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: device_info_plus | ||||
|       sha256: b37d37c2f912ad4e8ec694187de87d05de2a3cb82b465ff1f65f65a2d05de544 | ||||
|       sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "11.2.1" | ||||
|     version: "11.2.2" | ||||
|   device_info_plus_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -378,10 +378,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: dio | ||||
|       sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" | ||||
|       sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.7.0" | ||||
|     version: "5.8.0+1" | ||||
|   dio_smart_retry: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -394,10 +394,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: dio_web_adapter | ||||
|       sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" | ||||
|       sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.0" | ||||
|     version: "2.1.0" | ||||
|   dismissible_page: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -490,10 +490,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: file_picker | ||||
|       sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204 | ||||
|       sha256: c9943dd7d702ab4199d199bc151a2d79c86db031a02ad84566dab58c494d2adc | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "8.1.7" | ||||
|     version: "8.3.1" | ||||
|   file_saver: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -886,10 +886,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: go_router | ||||
|       sha256: daf3ff5570f55396b2d2c9bf8136d7db3a8acf208ac0cef92a3ae2beb9a81550 | ||||
|       sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "14.7.1" | ||||
|     version: "14.7.2" | ||||
|   google_fonts: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1334,10 +1334,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: package_info_plus | ||||
|       sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790" | ||||
|       sha256: b15fad91c4d3d1f2b48c053dd41cb82da007c27407dc9ab5f9aa59881d0e39d4 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "8.1.3" | ||||
|     version: "8.1.4" | ||||
|   package_info_plus_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1710,10 +1710,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: shared_preferences | ||||
|       sha256: c59819dacc6669a1165d54d2735a9543f136f9b3cec94ca65cea6ab8dffc422e | ||||
|       sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.0" | ||||
|     version: "2.5.1" | ||||
|   shared_preferences_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2059,10 +2059,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: vector_graphics | ||||
|       sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" | ||||
|       sha256: "7ed22c21d7fdcc88dd6ba7860384af438cd220b251ad65dfc142ab722fabef61" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.1.15" | ||||
|     version: "1.1.16" | ||||
|   vector_graphics_codec: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2216,7 +2216,7 @@ packages: | ||||
|     source: hosted | ||||
|     version: "1.1.0" | ||||
|   xml: | ||||
|     dependency: transitive | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: xml | ||||
|       sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 | ||||
|   | ||||
| @@ -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+58 | ||||
| version: 2.2.2+60 | ||||
|  | ||||
| environment: | ||||
|   sdk: ^3.5.4 | ||||
| @@ -117,6 +117,7 @@ dependencies: | ||||
|   cached_network_image: ^3.4.1 | ||||
|   flutter_inappwebview: ^6.1.5 | ||||
|   html: ^0.15.5 | ||||
|   xml: ^6.5.0 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user