Compare commits
	
		
			18 Commits
		
	
	
		
			2.2.2+57
			...
			b8dcdb2315
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b8dcdb2315 | |||
| b7b921f1f4 | |||
| 319d5c7d7f | |||
| 4b5b001739 | |||
| db8871a455 | |||
| 38dcaa6066 | |||
| 03275b46ca | |||
| cf3b482fef | |||
| aa4c04d4ef | |||
| 73b82f65e4 | |||
| 9471fe40fe | |||
| 0d1e18735e | |||
| 8bb62b5992 | |||
| 1e8a6dea5b | |||
| 5c2804cc4d | |||
| 0dbb8f132a | |||
| 3395f3dbd0 | |||
| d258ba776e | 
| @@ -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 | ||||
| } | ||||
| @@ -17,6 +17,9 @@ | ||||
|   "screenAccountProfileEdit": "Edit Profile", | ||||
|   "screenAbuseReport": "Abuse Reports", | ||||
|   "screenSettings": "Settings", | ||||
|   "screenAccountSettings": "Account Settings", | ||||
|   "screenFactorSettings": "Auth Factors", | ||||
|   "screenAccountWallet": "Wallet", | ||||
|   "screenNews": "News", | ||||
|   "screenAlbum": "Album", | ||||
|   "screenChat": "Chat", | ||||
| @@ -104,8 +107,18 @@ | ||||
|   }, | ||||
|   "loginEnterPassword": "Enter the code", | ||||
|   "loginSuccess": "Logged in as {}", | ||||
|   "authFactorDelete": "Delete Auth Factor", | ||||
|   "authFactorDeleteDescription": "Are you sure you want delete auth factor {}?", | ||||
|   "authFactorPassword": "Password", | ||||
|   "authFactorPasswordDescription": "The password you set when you registered.", | ||||
|   "authFactorEmail": "Email verification code", | ||||
|   "authFactorEmailDescription": "An one-time code sent to the email address you set when you registered.", | ||||
|   "authFactorTOTP": "Time-based OTP", | ||||
|   "authFactorTOTPDescription": "A one-time code generated by a TOTP authenticator such as Google Authenticator or Authy.", | ||||
|   "authFactorInAppNotify": "In-app notification", | ||||
|   "authFactorInAppNotifyDescription": "A one-time code sent via in-app notification.", | ||||
|   "authFactorAdd": "Add a factor", | ||||
|   "authFactorAddSubtitle": "Provide another way to login your account.", | ||||
|   "accountIntroTitle": "Hello there!", | ||||
|   "accountIntroSubtitle": "Pick an option below to get started.", | ||||
|   "accountLogout": "Logout", | ||||
| @@ -114,8 +127,14 @@ | ||||
|   "accountLogoutConfirm": "You will need to re-enter your account password, even if you have already done so. This is required to login again.", | ||||
|   "accountPublishers": "Your publishers", | ||||
|   "accountPublishersSubtitle": "Manage your publish identities.", | ||||
|   "accountSettings": "Account Settings", | ||||
|   "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.", | ||||
|   "publishersNew": "New Publisher", | ||||
|   "publisherNewSubtitle": "Create a new publisher identity.", | ||||
| @@ -180,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", | ||||
| @@ -532,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", | ||||
| @@ -565,5 +590,19 @@ | ||||
|   "newsReadingFromReader": "You're reading from HyperNet.Reader", | ||||
|   "newsReadingFromOriginal": "You're reading the original article", | ||||
|   "newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.", | ||||
|   "newsToday": "Today's News" | ||||
|   "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", | ||||
|   "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" | ||||
| } | ||||
|   | ||||
| @@ -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 思考過程" | ||||
| } | ||||
|   | ||||
| @@ -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 思考過程" | ||||
| } | ||||
|   | ||||
| @@ -53,14 +53,14 @@ PODS: | ||||
|   - Firebase/Messaging (11.6.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseMessaging (~> 11.6.0) | ||||
|   - firebase_analytics (11.4.0): | ||||
|   - firebase_analytics (11.4.1): | ||||
|     - Firebase/Analytics (= 11.6.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
|   - firebase_core (3.10.0): | ||||
|   - firebase_core (3.10.1): | ||||
|     - Firebase/CoreOnly (= 11.6.0) | ||||
|     - Flutter | ||||
|   - firebase_messaging (15.2.0): | ||||
|   - firebase_messaging (15.2.1): | ||||
|     - Firebase/Messaging (= 11.6.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
| @@ -379,12 +379,12 @@ SPEC CHECKSUMS: | ||||
|   device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 | ||||
|   DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c | ||||
|   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 | ||||
|   file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 | ||||
|   file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 | ||||
|   file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 | ||||
|   Firebase: 374a441a91ead896215703a674d58cdb3e9d772b | ||||
|   firebase_analytics: 07bd7cfbac54bfcdccf2bb2530f9a65486f7ef3f | ||||
|   firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10 | ||||
|   firebase_messaging: e2f0ba891b1509668c07f5099761518a5af8fe3c | ||||
|   firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e | ||||
|   firebase_core: e2aa06dbd854d961f8ce46c2e20933bee1bf2d2b | ||||
|   firebase_messaging: 96cf6d67121b3f39746b2a4f29a26c0eee4af70e | ||||
|   FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 | ||||
|   FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa | ||||
|   FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 | ||||
|   | ||||
| @@ -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(); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										104
									
								
								lib/router.dart
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								lib/router.dart
									
									
									
									
									
								
							| @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:surface/screens/abuse_report.dart'; | ||||
| import 'package:surface/screens/account.dart'; | ||||
| import 'package:surface/screens/account/account_settings.dart'; | ||||
| import 'package:surface/screens/account/factor_settings.dart'; | ||||
| import 'package:surface/screens/account/profile_page.dart'; | ||||
| import 'package:surface/screens/account/profile_edit.dart'; | ||||
| import 'package:surface/screens/account/publishers/publisher_edit.dart'; | ||||
| @@ -31,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'; | ||||
| @@ -96,11 +99,52 @@ final _appRoutes = [ | ||||
|       ), | ||||
|     ], | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/account', | ||||
|     name: 'account', | ||||
|     builder: (context, state) => const AccountScreen(), | ||||
|   ), | ||||
|   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', | ||||
|       builder: (context, state) => AccountSettingsScreen(), | ||||
|     ), | ||||
|     GoRoute( | ||||
|       path: '/settings/factors', | ||||
|       name: 'factorSettings', | ||||
|       builder: (context, state) => FactorSettingsScreen(), | ||||
|     ), | ||||
|     GoRoute( | ||||
|       path: '/profile/edit', | ||||
|       name: 'accountProfileEdit', | ||||
|       builder: (context, state) => ProfileEditScreen(), | ||||
|     ), | ||||
|     GoRoute( | ||||
|       path: '/publishers', | ||||
|       name: 'accountPublishers', | ||||
|       builder: (context, state) => PublisherScreen(), | ||||
|     ), | ||||
|     GoRoute( | ||||
|       path: '/publishers/new', | ||||
|       name: 'accountPublisherNew', | ||||
|       builder: (context, state) => AccountPublisherNewScreen(), | ||||
|     ), | ||||
|     GoRoute( | ||||
|       path: '/publishers/edit/:name', | ||||
|       name: 'accountPublisherEdit', | ||||
|       builder: (context, state) => AccountPublisherEditScreen( | ||||
|         name: state.pathParameters['name']!, | ||||
|       ), | ||||
|     ), | ||||
|     GoRoute( | ||||
|       path: '/:name', | ||||
|       name: 'accountProfilePage', | ||||
|       pageBuilder: (context, state) => NoTransitionPage( | ||||
|         child: UserScreen(name: state.pathParameters['name']!), | ||||
|       ), | ||||
|     ), | ||||
|   ]), | ||||
|   GoRoute( | ||||
|     path: '/chat', | ||||
|     name: 'chat', | ||||
| @@ -161,20 +205,15 @@ final _appRoutes = [ | ||||
|       ), | ||||
|     ], | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/news', | ||||
|     name: 'news', | ||||
|     builder: (context, state) => const NewsScreen(), | ||||
|     routes: [ | ||||
|       GoRoute( | ||||
|         path: '/:hash', | ||||
|         name: 'newsDetail', | ||||
|         builder: (context, state) => NewsDetailScreen( | ||||
|           hash: state.pathParameters['hash']!, | ||||
|         ), | ||||
|   GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [ | ||||
|     GoRoute( | ||||
|       path: '/:hash', | ||||
|       name: 'newsDetail', | ||||
|       builder: (context, state) => NewsDetailScreen( | ||||
|         hash: state.pathParameters['hash']!, | ||||
|       ), | ||||
|     ] | ||||
|   ), | ||||
|     ), | ||||
|   ]), | ||||
|   GoRoute( | ||||
|     path: '/album', | ||||
|     name: 'album', | ||||
| @@ -205,35 +244,6 @@ final _appRoutes = [ | ||||
|     name: 'abuseReport', | ||||
|     builder: (context, state) => AbuseReportScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/account/profile/edit', | ||||
|     name: 'accountProfileEdit', | ||||
|     builder: (context, state) => ProfileEditScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/account/publishers', | ||||
|     name: 'accountPublishers', | ||||
|     builder: (context, state) => PublisherScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/account/publishers/new', | ||||
|     name: 'accountPublisherNew', | ||||
|     builder: (context, state) => AccountPublisherNewScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/account/publishers/edit/:name', | ||||
|     name: 'accountPublisherEdit', | ||||
|     builder: (context, state) => AccountPublisherEditScreen( | ||||
|       name: state.pathParameters['name']!, | ||||
|     ), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/account/:name', | ||||
|     name: 'accountProfilePage', | ||||
|     pageBuilder: (context, state) => NoTransitionPage( | ||||
|       child: UserScreen(name: state.pathParameters['name']!), | ||||
|     ), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/settings', | ||||
|     name: 'settings', | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| @@ -13,6 +15,7 @@ import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/app_bar_leading.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| class AccountScreen extends StatelessWidget { | ||||
|   const AccountScreen({super.key}); | ||||
| @@ -20,11 +23,51 @@ class AccountScreen extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final ua = context.watch<UserProvider>(); | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     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, | ||||
|                 children: [ | ||||
|                   AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), fit: BoxFit.cover), | ||||
|                   Positioned( | ||||
|                     top: 0, | ||||
|                     left: 0, | ||||
|                     right: 0, | ||||
|                     height: 56 + MediaQuery.of(context).padding.top, | ||||
|                     child: ClipRect( | ||||
|                       child: BackdropFilter( | ||||
|                         filter: ImageFilter.blur( | ||||
|                           sigmaX: 10, | ||||
|                           sigmaY: 10, | ||||
|                         ), | ||||
|                         child: Container( | ||||
|                           color: Colors.black.withOpacity( | ||||
|                             clampDouble(10 * 0.1, 0, 0.5), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ], | ||||
|               ) | ||||
|             : null, | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             icon: const Icon(Symbols.settings, fill: 1), | ||||
| @@ -83,16 +126,6 @@ class _AuthorizedAccountScreen extends StatelessWidget { | ||||
|             ); | ||||
|           }).padding(all: 20), | ||||
|         ).padding(horizontal: 8, top: 16, bottom: 4), | ||||
|         ListTile( | ||||
|           title: Text('accountProfileEdit').tr(), | ||||
|           subtitle: Text('accountProfileEditSubtitle').tr(), | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|           leading: const Icon(Symbols.contact_page), | ||||
|           trailing: const Icon(Symbols.chevron_right), | ||||
|           onTap: () { | ||||
|             GoRouter.of(context).pushNamed('accountProfileEdit'); | ||||
|           }, | ||||
|         ), | ||||
|         ListTile( | ||||
|           title: Text('accountPublishers').tr(), | ||||
|           subtitle: Text('accountPublishersSubtitle').tr(), | ||||
| @@ -113,6 +146,36 @@ class _AuthorizedAccountScreen extends StatelessWidget { | ||||
|             GoRouter.of(context).pushNamed('abuseReport'); | ||||
|           }, | ||||
|         ), | ||||
|         ListTile( | ||||
|           title: Text('factorSettings').tr(), | ||||
|           subtitle: Text('factorSettingsSubtitle').tr(), | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|           leading: const Icon(Symbols.lock), | ||||
|           trailing: const Icon(Symbols.chevron_right), | ||||
|           onTap: () { | ||||
|             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(), | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|           leading: const Icon(Symbols.manage_accounts), | ||||
|           trailing: const Icon(Symbols.chevron_right), | ||||
|           onTap: () { | ||||
|             GoRouter.of(context).pushNamed('accountSettings'); | ||||
|           }, | ||||
|         ), | ||||
|         ListTile( | ||||
|           title: Text('accountLogout').tr(), | ||||
|           subtitle: Text('accountLogoutSubtitle').tr(), | ||||
| @@ -134,33 +197,6 @@ class _AuthorizedAccountScreen extends StatelessWidget { | ||||
|             await Hive.initFlutter(); | ||||
|           }, | ||||
|         ), | ||||
|         ListTile( | ||||
|           title: Text('accountDeletion'.tr()), | ||||
|           subtitle: Text('accountDeletionActionDescription'.tr()), | ||||
|           contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|           leading: const Icon(Symbols.person_cancel), | ||||
|           trailing: const Icon(Symbols.chevron_right), | ||||
|           onTap: () { | ||||
|             context | ||||
|                 .showConfirmDialog( | ||||
|               'accountDeletion'.tr(), | ||||
|               'accountDeletionDescription'.tr(), | ||||
|             ) | ||||
|                 .then((value) { | ||||
|               if (!value || !context.mounted) return; | ||||
|               final sn = context.read<SnNetworkProvider>(); | ||||
|               sn.client.post('/cgi/id/users/me/deletion').then((value) { | ||||
|                 if (context.mounted) { | ||||
|                   context.showSnackbar('accountDeletionSubmitted'.tr()); | ||||
|                 } | ||||
|               }).catchError((err) { | ||||
|                 if (context.mounted) { | ||||
|                   context.showErrorDialog(err); | ||||
|                 } | ||||
|               }); | ||||
|             }); | ||||
|           }, | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										66
									
								
								lib/screens/account/account_settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								lib/screens/account/account_settings.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| class AccountSettingsScreen extends StatelessWidget { | ||||
|   const AccountSettingsScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: PageBackButton(), | ||||
|         title: Text('screenAccountSettings').tr(), | ||||
|       ), | ||||
|       body: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             ListTile( | ||||
|               title: Text('accountProfileEdit').tr(), | ||||
|               subtitle: Text('accountProfileEditSubtitle').tr(), | ||||
|               contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|               leading: const Icon(Symbols.contact_page), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               onTap: () { | ||||
|                 GoRouter.of(context).pushNamed('accountProfileEdit'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
|               title: Text('accountDeletion'.tr()), | ||||
|               subtitle: Text('accountDeletionActionDescription'.tr()), | ||||
|               contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|               leading: const Icon(Symbols.person_cancel), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               onTap: () { | ||||
|                 context | ||||
|                     .showConfirmDialog( | ||||
|                   'accountDeletion'.tr(), | ||||
|                   'accountDeletionDescription'.tr(), | ||||
|                 ) | ||||
|                     .then((value) { | ||||
|                   if (!value || !context.mounted) return; | ||||
|                   final sn = context.read<SnNetworkProvider>(); | ||||
|                   sn.client.post('/cgi/id/users/me/deletion').then((value) { | ||||
|                     if (context.mounted) { | ||||
|                       context.showSnackbar('accountDeletionSubmitted'.tr()); | ||||
|                     } | ||||
|                   }).catchError((err) { | ||||
|                     if (context.mounted) { | ||||
|                       context.showErrorDialog(err); | ||||
|                     } | ||||
|                   }); | ||||
|                 }); | ||||
|               }, | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										294
									
								
								lib/screens/account/factor_settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								lib/screens/account/factor_settings.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,294 @@ | ||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:qr_flutter/qr_flutter.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/auth.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| final Map<int, (String, String, IconData)> kFactorTypes = { | ||||
|   0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password), | ||||
|   1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email), | ||||
|   2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer), | ||||
|   3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active), | ||||
| }; | ||||
|  | ||||
| class FactorSettingsScreen extends StatefulWidget { | ||||
|   const FactorSettingsScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<FactorSettingsScreen> createState() => _FactorSettingsScreenState(); | ||||
| } | ||||
|  | ||||
| class _FactorSettingsScreenState extends State<FactorSettingsScreen> { | ||||
|   bool _isBusy = false; | ||||
|   List<SnAuthFactor>? _factors; | ||||
|  | ||||
|   Future<void> _fetchFactors() async { | ||||
|     try { | ||||
|       setState(() => _isBusy = true); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/id/users/me/factors'); | ||||
|       _factors = List<SnAuthFactor>.from( | ||||
|         resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [], | ||||
|       ); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _fetchFactors(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: PageBackButton(), | ||||
|         title: Text('screenFactorSettings').tr(), | ||||
|       ), | ||||
|       body: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           LoadingIndicator( | ||||
|             isActive: _isBusy, | ||||
|           ), | ||||
|           ListTile( | ||||
|             title: Text('authFactorAdd').tr(), | ||||
|             subtitle: Text('authFactorAddSubtitle').tr(), | ||||
|             contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|             leading: const Icon(Symbols.add), | ||||
|             trailing: const Icon(Symbols.chevron_right), | ||||
|             onTap: () { | ||||
|               showDialog( | ||||
|                 context: context, | ||||
|                 builder: (context) => _FactorNewDialog( | ||||
|                   currentlyHave: _factors!, | ||||
|                 ), | ||||
|               ).then((val) { | ||||
|                 if (val == true) _fetchFactors(); | ||||
|               }); | ||||
|             }, | ||||
|           ), | ||||
|           const Divider(height: 1), | ||||
|           Expanded( | ||||
|             child: MediaQuery.removePadding( | ||||
|               context: context, | ||||
|               removeTop: true, | ||||
|               child: RefreshIndicator( | ||||
|                 onRefresh: _fetchFactors, | ||||
|                 child: ListView.builder( | ||||
|                   itemCount: _factors?.length ?? 0, | ||||
|                   itemBuilder: (context, idx) { | ||||
|                     final ele = _factors![idx]; | ||||
|                     return ListTile( | ||||
|                       title: Text(kFactorTypes[ele.type]!.$1).tr(), | ||||
|                       subtitle: Text(kFactorTypes[ele.type]!.$2).tr(), | ||||
|                       contentPadding: const EdgeInsets.only(left: 24, right: 12), | ||||
|                       leading: Icon(kFactorTypes[ele.type]!.$3), | ||||
|                       trailing: IconButton( | ||||
|                         icon: const Icon(Symbols.close), | ||||
|                         onPressed: ele.type > 0 | ||||
|                             ? () { | ||||
|                                 context | ||||
|                                     .showConfirmDialog( | ||||
|                                   'authFactorDelete'.tr(), | ||||
|                                   'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]), | ||||
|                                 ) | ||||
|                                     .then((val) async { | ||||
|                                   if (!val) return; | ||||
|                                   try { | ||||
|                                     if (!context.mounted) return; | ||||
|                                     final sn = context.read<SnNetworkProvider>(); | ||||
|                                     await sn.client.delete('/cgi/id/users/me/factors/${ele.id}'); | ||||
|                                     _fetchFactors(); | ||||
|                                   } catch (err) { | ||||
|                                     if (!context.mounted) return; | ||||
|                                     context.showErrorDialog(err); | ||||
|                                   } | ||||
|                                 }); | ||||
|                               } | ||||
|                             : null, | ||||
|                       ), | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _FactorNewDialog extends StatefulWidget { | ||||
|   final List<SnAuthFactor> currentlyHave; | ||||
|  | ||||
|   const _FactorNewDialog({required this.currentlyHave}); | ||||
|  | ||||
|   @override | ||||
|   State<_FactorNewDialog> createState() => _FactorNewDialogState(); | ||||
| } | ||||
|  | ||||
| class _FactorNewDialogState extends State<_FactorNewDialog> { | ||||
|   int? _factorType; | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   Future<void> _submit() async { | ||||
|     try { | ||||
|       setState(() => _isBusy = true); | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.post('/cgi/id/users/me/factors', data: { | ||||
|         'type': _factorType, | ||||
|       }); | ||||
|       final factor = SnAuthFactor.fromJson(resp.data); | ||||
|       if (!mounted) return; | ||||
|       if (factor.type == 2) { | ||||
|         await showModalBottomSheet( | ||||
|           context: context, | ||||
|           builder: (context) => _FactorTotpFactorDialog(factor: factor), | ||||
|         ); | ||||
|       } | ||||
|       if (!mounted) return; | ||||
|       Navigator.of(context).pop(true); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AlertDialog( | ||||
|       title: Text('authFactorAdd').tr(), | ||||
|       content: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: [ | ||||
|           DropdownButtonHideUnderline( | ||||
|             child: DropdownButton2<int>( | ||||
|               hint: Text( | ||||
|                 'Select Item', | ||||
|                 style: TextStyle( | ||||
|                   fontSize: 14, | ||||
|                 ), | ||||
|                 overflow: TextOverflow.ellipsis, | ||||
|               ), | ||||
|               value: _factorType, | ||||
|               items: kFactorTypes.entries.map( | ||||
|                 (ele) { | ||||
|                   final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key); | ||||
|                   return DropdownMenuItem<int>( | ||||
|                     enabled: !contains, | ||||
|                     value: ele.key, | ||||
|                     child: Text( | ||||
|                       ele.value.$1.tr(), | ||||
|                       style: const TextStyle( | ||||
|                         fontSize: 14, | ||||
|                       ), | ||||
|                     ).opacity(contains ? 0.75 : 1), | ||||
|                   ); | ||||
|                 }, | ||||
|               ).toList(), | ||||
|               onChanged: (val) => setState(() { | ||||
|                 _factorType = val; | ||||
|               }), | ||||
|               buttonStyleData: ButtonStyleData( | ||||
|                 height: 50, | ||||
|                 padding: const EdgeInsets.only(left: 14, right: 14), | ||||
|                 decoration: BoxDecoration( | ||||
|                   borderRadius: BorderRadius.circular(14), | ||||
|                   border: Border.all( | ||||
|                     color: Theme.of(context).dividerColor, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       actions: [ | ||||
|         TextButton( | ||||
|           onPressed: _isBusy ? null : () => Navigator.of(context).pop(), | ||||
|           child: Text('dialogCancel').tr(), | ||||
|         ), | ||||
|         TextButton( | ||||
|           onPressed: _isBusy ? null : () => _submit(), | ||||
|           child: Text('dialogConfirm').tr(), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _FactorTotpFactorDialog extends StatelessWidget { | ||||
|   final SnAuthFactor factor; | ||||
|  | ||||
|   const _FactorTotpFactorDialog({required this.factor}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Center( | ||||
|       child: Column( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|         children: [ | ||||
|           Center( | ||||
|             child: Text( | ||||
|               'totpPostSetup', | ||||
|               textAlign: TextAlign.center, | ||||
|               style: Theme.of(context).textTheme.titleLarge, | ||||
|             ).tr().width(280), | ||||
|           ), | ||||
|           const Gap(4), | ||||
|           Center( | ||||
|             child: Text( | ||||
|               'totpPostSetupDescription', | ||||
|               textAlign: TextAlign.center, | ||||
|               style: Theme.of(context).textTheme.bodySmall, | ||||
|             ).tr().width(280), | ||||
|           ), | ||||
|           const Gap(16), | ||||
|           QrImageView( | ||||
|             padding: EdgeInsets.zero, | ||||
|             data: factor.config!['url'], | ||||
|             errorCorrectionLevel: QrErrorCorrectLevel.H, | ||||
|             version: QrVersions.auto, | ||||
|             size: 160, | ||||
|             gapless: true, | ||||
|             eyeStyle: QrEyeStyle( | ||||
|               eyeShape: QrEyeShape.circle, | ||||
|               color: Theme.of(context).colorScheme.onSurface, | ||||
|             ), | ||||
|             dataModuleStyle: QrDataModuleStyle( | ||||
|               dataModuleShape: QrDataModuleShape.square, | ||||
|               color: Theme.of(context).colorScheme.onSurface, | ||||
|             ), | ||||
|           ), | ||||
|           const Gap(16), | ||||
|           Center( | ||||
|             child: Text( | ||||
|               'totpNeverShare', | ||||
|               textAlign: TextAlign.center, | ||||
|               style: Theme.of(context).textTheme.bodyMedium, | ||||
|             ).tr().bold().width(280), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/screens/account/factor_settings.dart'; | ||||
| import 'package:surface/types/auth.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| @@ -14,11 +15,6 @@ import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| import '../../providers/websocket.dart'; | ||||
|  | ||||
| final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = { | ||||
|   0: ('authFactorPassword'.tr(), Symbols.password, false), | ||||
|   1: ('authFactorEmail'.tr(), Symbols.email, true), | ||||
| }; | ||||
|  | ||||
| class LoginScreen extends StatefulWidget { | ||||
|   const LoginScreen({super.key}); | ||||
|  | ||||
| @@ -212,7 +208,9 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> { | ||||
|           controller: _passwordController, | ||||
|           obscureText: true, | ||||
|           autofillHints: [ | ||||
|             (_factorLabelMap[widget.factor!.type]?.$3 ?? true) ? AutofillHints.password : AutofillHints.oneTimeCode | ||||
|             widget.factor!.type == 0 | ||||
|                 ? AutofillHints.password | ||||
|                 : AutofillHints.oneTimeCode | ||||
|           ], | ||||
|           decoration: InputDecoration( | ||||
|             isDense: true, | ||||
| @@ -267,7 +265,8 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> { | ||||
|   bool _isBusy = false; | ||||
|   int? _factorPicked; | ||||
|  | ||||
|   Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()); | ||||
|   Color get _unFocusColor => | ||||
|       Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()); | ||||
|  | ||||
|   void _performGetFactorCode() async { | ||||
|     if (_factorPicked == null) return; | ||||
| @@ -328,11 +327,11 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> { | ||||
|                           ), | ||||
|                         ), | ||||
|                         secondary: Icon( | ||||
|                           _factorLabelMap[x.type]?.$2 ?? Symbols.question_mark, | ||||
|                           kFactorTypes[x.type]?.$3 ?? Symbols.question_mark, | ||||
|                         ), | ||||
|                         title: Text( | ||||
|                           _factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(), | ||||
|                         ), | ||||
|                           kFactorTypes[x.type]?.$1 ?? 'unknown', | ||||
|                         ).tr(), | ||||
|                         enabled: !widget.ticket!.factorTrail.contains(x.id), | ||||
|                         value: _factorPicked == x.id, | ||||
|                         onChanged: (value) { | ||||
| @@ -408,11 +407,14 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> { | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final lookupResp = await sn.client.get('/cgi/id/users/lookup?probe=$username'); | ||||
|       final lookupResp = | ||||
|           await sn.client.get('/cgi/id/users/lookup?probe=$username'); | ||||
|       await sn.client.post('/cgi/id/users/me/password-reset', data: { | ||||
|         'user_id': lookupResp.data['id'], | ||||
|       }); | ||||
|       if (mounted) context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr()); | ||||
|       if (mounted) { | ||||
|         context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr()); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       if (mounted) context.showErrorDialog(err); | ||||
|     } finally { | ||||
| @@ -437,7 +439,8 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> { | ||||
|       widget.onTicket(result.ticket); | ||||
|  | ||||
|       // Pull factors | ||||
|       final factorResp = await sn.client.get('/cgi/id/auth/factors', queryParameters: { | ||||
|       final factorResp = | ||||
|           await sn.client.get('/cgi/id/auth/factors', queryParameters: { | ||||
|         'ticketId': result.ticket!.id.toString(), | ||||
|       }); | ||||
|       widget.onFactor( | ||||
| @@ -531,7 +534,10 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> { | ||||
|                     'termAcceptNextWithAgree'.tr(), | ||||
|                     textAlign: TextAlign.end, | ||||
|                     style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||
|                           color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()), | ||||
|                           color: Theme.of(context) | ||||
|                               .colorScheme | ||||
|                               .onSurface | ||||
|                               .withAlpha((255 * 0.75).round()), | ||||
|                         ), | ||||
|                   ), | ||||
|                   Material( | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/post.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/screens/post/post_detail.dart'; | ||||
| @@ -96,6 +97,8 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final cfg = context.read<ConfigProvider>(); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       floatingActionButtonLocation: ExpandableFab.location, | ||||
|       floatingActionButton: ExpandableFab( | ||||
| @@ -243,8 +246,10 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|                     ), | ||||
|                     openColor: Colors.transparent, | ||||
|                     openElevation: 0, | ||||
|                     closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75), | ||||
|                     transitionType: ContainerTransitionType.fade, | ||||
|                     closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity( | ||||
|                           cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1, | ||||
|                         ), | ||||
|                     closedShape: const RoundedRectangleBorder( | ||||
|                       borderRadius: BorderRadius.all(Radius.circular(16)), | ||||
|                     ), | ||||
|   | ||||
| @@ -51,7 +51,7 @@ class HomeScreen extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _HomeScreenState extends State<HomeScreen> { | ||||
|   static const List<HomeScreenDashEntry> kCards = [ | ||||
|   late final List<HomeScreenDashEntry> kCards = [ | ||||
|     HomeScreenDashEntry( | ||||
|       name: 'dashEntryRecommendation', | ||||
|       child: _HomeDashRecommendationPostWidget(), | ||||
| @@ -69,7 +69,7 @@ class _HomeScreenState extends State<HomeScreen> { | ||||
|     HomeScreenDashEntry( | ||||
|       name: 'dashEntryTodayNews', | ||||
|       child: _HomeDashTodayNews(), | ||||
|       cols: 2, | ||||
|       cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2, | ||||
|     ), | ||||
|   ]; | ||||
|  | ||||
| @@ -288,12 +288,13 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> { | ||||
|               child: InkWell( | ||||
|                 borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   spacing: 4, | ||||
|                   children: [ | ||||
|                     Text( | ||||
|                       _article!.title, | ||||
|                       style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18), | ||||
|                       maxLines: 2, | ||||
|                       maxLines: MediaQuery.of(context).size.width >= 640 ? 2 : 1, | ||||
|                       overflow: TextOverflow.ellipsis, | ||||
|                     ), | ||||
|                     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, | ||||
|                           ) | ||||
|                       ], | ||||
|                     ), | ||||
|             ), | ||||
|   | ||||
| @@ -175,54 +175,57 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> { | ||||
|           ), | ||||
|           if (_articleFragment != null && _isReadingFromReader) | ||||
|             Expanded( | ||||
|               child: SingleChildScrollView( | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   spacing: 8, | ||||
|                   children: [ | ||||
|                     Text(_article!.title, style: Theme.of(context).textTheme.titleLarge), | ||||
|                     Builder(builder: (context) { | ||||
|                       final htmlDescription = parse(_article!.description); | ||||
|                       return Text( | ||||
|                         htmlDescription.children.map((ele) => ele.text.trim()).join(), | ||||
|                         style: Theme.of(context).textTheme.bodyMedium, | ||||
|                       ); | ||||
|                     }), | ||||
|                     Builder(builder: (context) { | ||||
|                       final date = _article!.publishedAt ?? _article!.createdAt; | ||||
|                       return Row( | ||||
|                         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); | ||||
|                     }), | ||||
|                     Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75), | ||||
|                     const Divider(), | ||||
|                     ..._parseHtmlToWidgets(_articleFragment!.children), | ||||
|                     const Divider(), | ||||
|                     InkWell( | ||||
|                       child: Row( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         children: [ | ||||
|                           Text( | ||||
|                             'Reference from original website', | ||||
|                             style: TextStyle(decoration: TextDecoration.underline), | ||||
|                           ), | ||||
|                           const Gap(4), | ||||
|                           Icon(Icons.launch, size: 16), | ||||
|                         ], | ||||
|                       ).opacity(0.85), | ||||
|                       onTap: () { | ||||
|                         launchUrlString(_article!.url); | ||||
|                       }, | ||||
|                     ), | ||||
|                     Gap(MediaQuery.of(context).padding.bottom), | ||||
|                   ], | ||||
|                 ).padding(horizontal: 12, vertical: 16), | ||||
|               ), | ||||
|               child: Container( | ||||
|                 constraints: BoxConstraints(maxWidth: 640), | ||||
|                 child: SingleChildScrollView( | ||||
|                   child: Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     spacing: 8, | ||||
|                     children: [ | ||||
|                       Text(_article!.title, style: Theme.of(context).textTheme.titleLarge), | ||||
|                       Builder(builder: (context) { | ||||
|                         final htmlDescription = parse(_article!.description); | ||||
|                         return Text( | ||||
|                           htmlDescription.children.map((ele) => ele.text.trim()).join(), | ||||
|                           style: Theme.of(context).textTheme.bodyMedium, | ||||
|                         ); | ||||
|                       }), | ||||
|                       Builder(builder: (context) { | ||||
|                         final date = _article!.publishedAt ?? _article!.createdAt; | ||||
|                         return Row( | ||||
|                           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); | ||||
|                       }), | ||||
|                       Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75), | ||||
|                       const Divider(), | ||||
|                       ..._parseHtmlToWidgets(_articleFragment!.children), | ||||
|                       const Divider(), | ||||
|                       InkWell( | ||||
|                         child: Row( | ||||
|                           mainAxisSize: MainAxisSize.min, | ||||
|                           children: [ | ||||
|                             Text( | ||||
|                               'Reference from original website', | ||||
|                               style: TextStyle(decoration: TextDecoration.underline), | ||||
|                             ), | ||||
|                             const Gap(4), | ||||
|                             Icon(Icons.launch, size: 16), | ||||
|                           ], | ||||
|                         ).opacity(0.85), | ||||
|                         onTap: () { | ||||
|                           launchUrlString(_article!.url); | ||||
|                         }, | ||||
|                       ), | ||||
|                       Gap(MediaQuery.of(context).padding.bottom), | ||||
|                     ], | ||||
|                   ).padding(horizontal: 12, vertical: 16), | ||||
|                 ), | ||||
|               ).center(), | ||||
|             ) | ||||
|           else if (_article != null) | ||||
|             Expanded( | ||||
|   | ||||
| @@ -70,11 +70,16 @@ class _NewsScreenState extends State<NewsScreen> { | ||||
|                 sliver: SliverAppBar( | ||||
|                   leading: AutoAppBarLeading(), | ||||
|                   title: Text('screenNews').tr(), | ||||
|                   floating: true, | ||||
|                   snap: true, | ||||
|                   bottom: TabBar( | ||||
|                     isScrollable: true, | ||||
|                     tabs: [ | ||||
|                       Tab(child: Text('newsAllSources'.tr())), | ||||
|                       for (final source in _sources!) Tab(child: Text(source.label)), | ||||
|                       Tab(child: Text('newsAllSources'.tr()).textColor(Theme.of(context).appBarTheme.foregroundColor)), | ||||
|                       for (final source in _sources!) | ||||
|                         Tab( | ||||
|                           child: Text(source.label).textColor(Theme.of(context).appBarTheme.foregroundColor), | ||||
|                         ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
| @@ -146,80 +151,87 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> { | ||||
|     return MediaQuery.removePadding( | ||||
|       context: context, | ||||
|       removeTop: true, | ||||
|       child: RefreshIndicator( | ||||
|         onRefresh: _fetchArticles, | ||||
|         child: InfiniteList( | ||||
|           isLoading: _isBusy, | ||||
|           itemCount: _articles.length, | ||||
|           hasReachedMax: _totalCount != null && _articles.length >= _totalCount!, | ||||
|           onFetchData: () { | ||||
|             _fetchArticles(); | ||||
|           }, | ||||
|           itemBuilder: (context, index) { | ||||
|             final article = _articles[index]; | ||||
|       child: Center( | ||||
|         child: Container( | ||||
|           constraints: BoxConstraints(maxWidth: 640), | ||||
|           child: RefreshIndicator( | ||||
|             onRefresh: _fetchArticles, | ||||
|             child: InfiniteList( | ||||
|               isLoading: _isBusy, | ||||
|               itemCount: _articles.length, | ||||
|               hasReachedMax: _totalCount != null && _articles.length >= _totalCount!, | ||||
|               onFetchData: () { | ||||
|                 _fetchArticles(); | ||||
|               }, | ||||
|               itemBuilder: (context, index) { | ||||
|                 final article = _articles[index]; | ||||
|  | ||||
|             final baseUri = Uri.parse(article.url); | ||||
|             final baseUrl = '${baseUri.scheme}://${baseUri.host}'; | ||||
|                 final baseUri = Uri.parse(article.url); | ||||
|                 final baseUrl = '${baseUri.scheme}://${baseUri.host}'; | ||||
|  | ||||
|             final htmlDescription = parse(article.description); | ||||
|             final date = article.publishedAt ?? article.createdAt; | ||||
|                 final htmlDescription = parse(article.description); | ||||
|                 final date = article.publishedAt ?? article.createdAt; | ||||
|  | ||||
|             return Card( | ||||
|               child: InkWell( | ||||
|                 radius: 8, | ||||
|                 onTap: () { | ||||
|                   GoRouter.of(context).pushNamed( | ||||
|                     'newsDetail', | ||||
|                     pathParameters: {'hash': article.hash}, | ||||
|                   ); | ||||
|                 }, | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg')) | ||||
|                       ClipRRect( | ||||
|                         borderRadius: BorderRadius.only( | ||||
|                           topRight: Radius.circular(8), | ||||
|                           topLeft: Radius.circular(8), | ||||
|                         ), | ||||
|                         child: AspectRatio( | ||||
|                           aspectRatio: 16 / 9, | ||||
|                           child: Container( | ||||
|                             color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                             child: AutoResizeUniversalImage( | ||||
|                               article.thumbnail.startsWith('http') ? article.thumbnail : '$baseUrl/${article.thumbnail}', | ||||
|                 return Card( | ||||
|                   child: InkWell( | ||||
|                     radius: 8, | ||||
|                     onTap: () { | ||||
|                       GoRouter.of(context).pushNamed( | ||||
|                         'newsDetail', | ||||
|                         pathParameters: {'hash': article.hash}, | ||||
|                       ); | ||||
|                     }, | ||||
|                     child: Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       children: [ | ||||
|                         if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg')) | ||||
|                           ClipRRect( | ||||
|                             borderRadius: BorderRadius.only( | ||||
|                               topRight: Radius.circular(8), | ||||
|                               topLeft: Radius.circular(8), | ||||
|                             ), | ||||
|                             child: AspectRatio( | ||||
|                               aspectRatio: 16 / 9, | ||||
|                               child: Container( | ||||
|                                 color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                                 child: AutoResizeUniversalImage( | ||||
|                                   article.thumbnail.startsWith('http') | ||||
|                                       ? article.thumbnail | ||||
|                                       : '$baseUrl/${article.thumbnail}', | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     const Gap(16), | ||||
|                     Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16), | ||||
|                     const Gap(8), | ||||
|                     Text(htmlDescription.children.map((ele) => ele.text.trim()).join()) | ||||
|                         .textStyle(Theme.of(context).textTheme.bodyMedium!) | ||||
|                         .padding(horizontal: 16), | ||||
|                     const Gap(8), | ||||
|                     Row( | ||||
|                       spacing: 2, | ||||
|                       children: [ | ||||
|                         Text(widget.allSources.where((x) => x.id == article.source).first.label) | ||||
|                             .textStyle(Theme.of(context).textTheme.bodySmall!), | ||||
|                         const Gap(16), | ||||
|                         Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16), | ||||
|                         const Gap(8), | ||||
|                         Text(htmlDescription.children.map((ele) => ele.text.trim()).join()) | ||||
|                             .textStyle(Theme.of(context).textTheme.bodyMedium!) | ||||
|                             .padding(horizontal: 16), | ||||
|                         const Gap(8), | ||||
|                         Row( | ||||
|                           spacing: 2, | ||||
|                           children: [ | ||||
|                             Text(widget.allSources.where((x) => x.id == article.source).first.label) | ||||
|                                 .textStyle(Theme.of(context).textTheme.bodySmall!), | ||||
|                           ], | ||||
|                         ).opacity(0.75).padding(horizontal: 16), | ||||
|                         Row( | ||||
|                           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), | ||||
|                         const Gap(16), | ||||
|                       ], | ||||
|                     ).opacity(0.75).padding(horizontal: 16), | ||||
|                     Row( | ||||
|                       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), | ||||
|                     const Gap(16), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ); | ||||
|           }, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -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), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -15,8 +15,8 @@ class SnAccount with _$SnAccount { | ||||
|     required DateTime? deletedAt, | ||||
|     required DateTime? confirmedAt, | ||||
|     required List<SnAccountContact>? contacts, | ||||
|     required String avatar, | ||||
|     required String banner, | ||||
|     @Default("") String avatar, | ||||
|     @Default("") String banner, | ||||
|     required String description, | ||||
|     required String name, | ||||
|     required String nick, | ||||
|   | ||||
| @@ -367,8 +367,8 @@ class _$SnAccountImpl extends _SnAccount { | ||||
|       required this.deletedAt, | ||||
|       required this.confirmedAt, | ||||
|       required final List<SnAccountContact>? contacts, | ||||
|       required this.avatar, | ||||
|       required this.banner, | ||||
|       this.avatar = "", | ||||
|       this.banner = "", | ||||
|       required this.description, | ||||
|       required this.name, | ||||
|       required this.nick, | ||||
| @@ -410,8 +410,10 @@ class _$SnAccountImpl extends _SnAccount { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   @JsonKey() | ||||
|   final String avatar; | ||||
|   @override | ||||
|   @JsonKey() | ||||
|   final String banner; | ||||
|   @override | ||||
|   final String description; | ||||
| @@ -540,8 +542,8 @@ abstract class _SnAccount extends SnAccount { | ||||
|       required final DateTime? deletedAt, | ||||
|       required final DateTime? confirmedAt, | ||||
|       required final List<SnAccountContact>? contacts, | ||||
|       required final String avatar, | ||||
|       required final String banner, | ||||
|       final String avatar, | ||||
|       final String banner, | ||||
|       required final String description, | ||||
|       required final String name, | ||||
|       required final String nick, | ||||
|   | ||||
| @@ -20,8 +20,8 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) => | ||||
|       contacts: (json['contacts'] as List<dynamic>?) | ||||
|           ?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>)) | ||||
|           .toList(), | ||||
|       avatar: json['avatar'] as String, | ||||
|       banner: json['banner'] as String, | ||||
|       avatar: json['avatar'] as String? ?? "", | ||||
|       banner: json['banner'] as String? ?? "", | ||||
|       description: json['description'] as String, | ||||
|       name: json['name'] as String, | ||||
|       nick: json['nick'] as String, | ||||
|   | ||||
| @@ -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, | ||||
|     }; | ||||
| @@ -196,68 +196,71 @@ class _AttachmentListState extends State<AttachmentList> { | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         return Container( | ||||
|           constraints: BoxConstraints(maxHeight: constraints.maxHeight), | ||||
|           child: ScrollConfiguration( | ||||
|             behavior: _AttachmentListScrollBehavior(), | ||||
|             child: ListView.separated( | ||||
|               padding: widget.padding, | ||||
|               shrinkWrap: true, | ||||
|               itemCount: widget.data.length, | ||||
|               itemBuilder: (context, idx) { | ||||
|                 return Container( | ||||
|                   constraints: constraints.copyWith(maxWidth: widget.maxWidth), | ||||
|                   child: AspectRatio( | ||||
|                     aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(), | ||||
|                     child: GestureDetector( | ||||
|                       onTap: () { | ||||
|                         if (widget.data[idx]?.mediaType != SnMediaType.image) return; | ||||
|                         context.pushTransparentRoute( | ||||
|                           AttachmentZoomView( | ||||
|                             data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(), | ||||
|                             initialIndex: idx, | ||||
|                             heroTags: heroTags, | ||||
|                           ), | ||||
|                           backgroundColor: Colors.black.withOpacity(0.7), | ||||
|                           rootNavigator: true, | ||||
|                         ); | ||||
|                       }, | ||||
|                       child: Stack( | ||||
|                         fit: StackFit.expand, | ||||
|                         children: [ | ||||
|                           Container( | ||||
|                             decoration: BoxDecoration( | ||||
|                               color: backgroundColor, | ||||
|                               border: Border( | ||||
|                                 top: borderSide, | ||||
|                                 bottom: borderSide, | ||||
|                               ), | ||||
|                               borderRadius: AttachmentList.kDefaultRadius, | ||||
|         return AspectRatio( | ||||
|           aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1, | ||||
|           child: Container( | ||||
|             constraints: BoxConstraints(maxHeight: constraints.maxHeight), | ||||
|             child: ScrollConfiguration( | ||||
|               behavior: _AttachmentListScrollBehavior(), | ||||
|               child: ListView.separated( | ||||
|                 padding: widget.padding, | ||||
|                 shrinkWrap: true, | ||||
|                 itemCount: widget.data.length, | ||||
|                 itemBuilder: (context, idx) { | ||||
|                   return Container( | ||||
|                     constraints: constraints.copyWith(maxWidth: widget.maxWidth), | ||||
|                     child: AspectRatio( | ||||
|                       aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(), | ||||
|                       child: GestureDetector( | ||||
|                         onTap: () { | ||||
|                           if (widget.data[idx]?.mediaType != SnMediaType.image) return; | ||||
|                           context.pushTransparentRoute( | ||||
|                             AttachmentZoomView( | ||||
|                               data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(), | ||||
|                               initialIndex: idx, | ||||
|                               heroTags: heroTags, | ||||
|                             ), | ||||
|                             child: ClipRRect( | ||||
|                               borderRadius: AttachmentList.kDefaultRadius, | ||||
|                               child: AttachmentItem( | ||||
|                                 data: widget.data[idx], | ||||
|                                 heroTag: heroTags[idx], | ||||
|                             backgroundColor: Colors.black.withOpacity(0.7), | ||||
|                             rootNavigator: true, | ||||
|                           ); | ||||
|                         }, | ||||
|                         child: Stack( | ||||
|                           fit: StackFit.expand, | ||||
|                           children: [ | ||||
|                             Container( | ||||
|                               decoration: BoxDecoration( | ||||
|                                 color: backgroundColor, | ||||
|                                 border: Border( | ||||
|                                   top: borderSide, | ||||
|                                   bottom: borderSide, | ||||
|                                 ), | ||||
|                                 borderRadius: AttachmentList.kDefaultRadius, | ||||
|                               ), | ||||
|                               child: ClipRRect( | ||||
|                                 borderRadius: AttachmentList.kDefaultRadius, | ||||
|                                 child: AttachmentItem( | ||||
|                                   data: widget.data[idx], | ||||
|                                   heroTag: heroTags[idx], | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ), | ||||
|                           ), | ||||
|                           Positioned( | ||||
|                             right: 8, | ||||
|                             bottom: 8, | ||||
|                             child: Chip( | ||||
|                               label: Text('${idx + 1}/${widget.data.length}'), | ||||
|                             Positioned( | ||||
|                               right: 8, | ||||
|                               bottom: 8, | ||||
|                               child: Chip( | ||||
|                                 label: Text('${idx + 1}/${widget.data.length}'), | ||||
|                               ), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|               separatorBuilder: (context, index) => const Gap(8), | ||||
|               physics: const BouncingScrollPhysics(), | ||||
|               scrollDirection: Axis.horizontal, | ||||
|                   ); | ||||
|                 }, | ||||
|                 separatorBuilder: (context, index) => const Gap(8), | ||||
|                 physics: const BouncingScrollPhysics(), | ||||
|                 scrollDirection: Axis.horizontal, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ); | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -5,10 +5,9 @@ import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:flutter_animate/flutter_animate.dart'; | ||||
|  | ||||
| import 'package:surface/providers/config.dart'; | ||||
| // Keep this import to make the web image render work | ||||
| import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
|  | ||||
| class UniversalImage extends StatelessWidget { | ||||
|   final String url; | ||||
|   | ||||
| @@ -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): | ||||
| @@ -22,14 +24,14 @@ PODS: | ||||
|   - Firebase/Messaging (11.6.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseMessaging (~> 11.6.0) | ||||
|   - firebase_analytics (11.4.0): | ||||
|   - firebase_analytics (11.4.1): | ||||
|     - Firebase/Analytics (= 11.6.0) | ||||
|     - firebase_core | ||||
|     - FlutterMacOS | ||||
|   - firebase_core (3.10.0): | ||||
|   - firebase_core (3.10.1): | ||||
|     - Firebase/CoreOnly (~> 11.6.0) | ||||
|     - FlutterMacOS | ||||
|   - firebase_messaging (15.2.0): | ||||
|   - firebase_messaging (15.2.1): | ||||
|     - Firebase/CoreOnly (~> 11.6.0) | ||||
|     - Firebase/Messaging (~> 11.6.0) | ||||
|     - firebase_core | ||||
| @@ -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,12 +298,13 @@ SPEC CHECKSUMS: | ||||
|   connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 | ||||
|   croppy: 25a638bd7d05411d8c697f481568f261037694fc | ||||
|   device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 | ||||
|   file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af | ||||
|   file_saver: 44e6fbf666677faf097302460e214e977fdd977b | ||||
|   file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d | ||||
|   Firebase: 374a441a91ead896215703a674d58cdb3e9d772b | ||||
|   firebase_analytics: 5249f87da6fed852901581aab2602e0280ec2fdb | ||||
|   firebase_core: 6d9bb8b0ea817e8fe0d928177d42275b45fdba6f | ||||
|   firebase_messaging: ae8e88b586e4d50abc7cac5bacf74d21967fd226 | ||||
|   firebase_analytics: 91efc58e8e37964469fdd59ad11ba36bc97e75d6 | ||||
|   firebase_core: 75e003524565fb5bd80c9960bc5892e8475821cd | ||||
|   firebase_messaging: 082a385eb98b5bb843a566cb30404859c4bd6e25 | ||||
|   FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 | ||||
|   FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa | ||||
|   FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 | ||||
|   | ||||
							
								
								
									
										102
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										102
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -13,10 +13,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: _flutterfire_internals | ||||
|       sha256: "27899c95f9e7ec06c8310e6e0eac967707714b9f1450c4a58fa00ca011a4a8ae" | ||||
|       sha256: e4f2a7ef31b0ab2c89d2bde35ef3e6e6aff1dce5e66069c6540b0e9cfe33ee6b | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.49" | ||||
|     version: "1.3.50" | ||||
|   _macros: | ||||
|     dependency: transitive | ||||
|     description: dart | ||||
| @@ -338,10 +338,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: dart_style | ||||
|       sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" | ||||
|       sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.7" | ||||
|     version: "2.3.8" | ||||
|   dart_webrtc: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -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: | ||||
| @@ -418,10 +418,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: easy_localization | ||||
|       sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 | ||||
|       sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.7" | ||||
|     version: "3.0.7+1" | ||||
|   easy_localization_loader: | ||||
|     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: | ||||
| @@ -538,34 +538,34 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: firebase_analytics | ||||
|       sha256: "498c6cb8468e348a556709c745d92a52173ab3a9b906aa0593393f0787f201ea" | ||||
|       sha256: eac382bbcd5ae78c1d1ce5619d13f5a7424429f4bf55df9e3ad5110da34d1060 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "11.4.0" | ||||
|     version: "11.4.1" | ||||
|   firebase_analytics_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_analytics_platform_interface | ||||
|       sha256: ccbb350554e98afdb4b59852689292d194d31232a2647b5012a66622b3711df9 | ||||
|       sha256: a34db46c367265c4c961626e4b128bfb7b7e50958e7add4c27ba103f5f81b9b0 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.3.0" | ||||
|     version: "4.3.1" | ||||
|   firebase_analytics_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_analytics_web | ||||
|       sha256: "68e1f18fc16482c211c658e739c25f015b202a260d9ad8249c6d3d7963b8105f" | ||||
|       sha256: b6b4cef08e45e4c7d48476d9fc49fe9577081809a59026fe95b1a1b1eea165fa | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.5.10+6" | ||||
|     version: "0.5.10+7" | ||||
|   firebase_core: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: firebase_core | ||||
|       sha256: "0307c1fde82e2b8b97e0be2dab93612aff9a72f31ebe9bfac66ed8b37ef7c568" | ||||
|       sha256: d851c1ca98fd5a4c07c747f8c65dacc2edd84a4d9ac055d32a5f0342529069f5 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.10.0" | ||||
|     version: "3.10.1" | ||||
|   firebase_core_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -586,26 +586,26 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: firebase_messaging | ||||
|       sha256: "48a8a59197c1c5174060ba9aa1e0036e9b5a0d28a0cc22d19c1fcabc67fafe3c" | ||||
|       sha256: e20ea2a0ecf9b0971575ab3ab42a6e285a94e50092c555b090c1a588a81b4d54 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "15.2.0" | ||||
|     version: "15.2.1" | ||||
|   firebase_messaging_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_messaging_platform_interface | ||||
|       sha256: "9770a8e91f54296829dcaa61ce9b7c2f9ae9abbf99976dd3103a60470d5264dd" | ||||
|       sha256: c57a92b5ae1857ef4fe4ae2e73452b44d32e984e15ab8b53415ea1bb514bdabd | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.6.0" | ||||
|     version: "4.6.1" | ||||
|   firebase_messaging_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_messaging_web | ||||
|       sha256: "329ca4ef45ec616abe6f1d5e58feed0934a50840a65aa327052354ad3c64ed77" | ||||
|       sha256: "83694a990d8525d6b01039240b97757298369622ca0253ad0ebcfed221bf8ee0" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.10.0" | ||||
|     version: "3.10.1" | ||||
|   fixnum: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -830,10 +830,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_webrtc | ||||
|       sha256: "188401cc3275bc4f1f965babdff6cac612a4b46572f1e49f49db8af5361d5712" | ||||
|       sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.12.6" | ||||
|     version: "0.12.7" | ||||
|   freezed: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -878,18 +878,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: glob | ||||
|       sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" | ||||
|       sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.1.2" | ||||
|     version: "2.1.3" | ||||
|   go_router: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: go_router | ||||
|       sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d" | ||||
|       sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "14.6.3" | ||||
|     version: "14.7.2" | ||||
|   google_fonts: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -950,10 +950,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: http | ||||
|       sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 | ||||
|       sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.2.2" | ||||
|     version: "1.3.0" | ||||
|   http_multi_server: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1030,10 +1030,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: image_picker_macos | ||||
|       sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" | ||||
|       sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.2.1+1" | ||||
|     version: "0.2.1+2" | ||||
|   image_picker_platform_interface: | ||||
|     dependency: transitive | ||||
|     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,18 +1710,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: shared_preferences | ||||
|       sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a | ||||
|       sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.5" | ||||
|     version: "2.5.1" | ||||
|   shared_preferences_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_android | ||||
|       sha256: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5" | ||||
|       sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.2" | ||||
|     version: "2.4.4" | ||||
|   shared_preferences_foundation: | ||||
|     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: | ||||
| @@ -2171,10 +2171,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: web_socket_channel | ||||
|       sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" | ||||
|       sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.0.1" | ||||
|     version: "3.0.2" | ||||
|   webrtc_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2187,10 +2187,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: win32 | ||||
|       sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" | ||||
|       sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.10.0" | ||||
|     version: "5.10.1" | ||||
|   win32_registry: | ||||
|     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+57 | ||||
| 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