Compare commits
	
		
			83 Commits
		
	
	
		
			2.2.1+44
			...
			b8dcdb2315
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b8dcdb2315 | |||
| b7b921f1f4 | |||
| 319d5c7d7f | |||
| 4b5b001739 | |||
| db8871a455 | |||
| 38dcaa6066 | |||
| 03275b46ca | |||
| cf3b482fef | |||
| aa4c04d4ef | |||
| 73b82f65e4 | |||
| 9471fe40fe | |||
| 0d1e18735e | |||
| 8bb62b5992 | |||
| 1e8a6dea5b | |||
| 5c2804cc4d | |||
| 0dbb8f132a | |||
| 3395f3dbd0 | |||
| d258ba776e | |||
| 0dcfcaad56 | |||
| 687e720956 | |||
| 180876949e | |||
| 9718965809 | |||
| 5377161fb0 | |||
| 963e538ae5 | |||
| a355e3bf90 | |||
| cb4a2598c8 | |||
| 950612dc07 | |||
| cbd1eaf1af | |||
| ac41cbd99f | |||
| 9f9c90abc4 | |||
| 87029e3538 | |||
| 127d9adc09 | |||
| c82dc7ad85 | |||
| 36bcff7a7c | |||
| 38201b547a | |||
| ed0334fcda | |||
| fbb486b90b | |||
| 9b34f385d5 | |||
| bb7b731602 | |||
| 19076f8136 | |||
| dc77a936ce | |||
| 7f58710c6f | |||
| 068ddcdcdc | |||
| f4e9252ca0 | |||
| 3b1e918117 | |||
| ed7981fdaf | |||
| 9698ca53e4 | |||
| ddc1dc7daf | |||
| 1625a957f8 | |||
| 2dc50d627e | |||
| 2ffde9a3dd | |||
| 5967a91ae1 | |||
| 32c1effcb5 | |||
| 9d0e19c56f | |||
| acf4e634fe | |||
| 25942c2338 | |||
| a4f81f6ba1 | |||
| c1b9090e51 | |||
| f494f70003 | |||
| fb2a55a909 | |||
| 4edfa7fd50 | |||
| d699cac9b1 | |||
| c0428e12c1 | |||
| 55f434ff05 | |||
| f2b3bdda2d | |||
| 1f6bf33b0e | |||
| e2027b1a32 | |||
| 2b3a58b55e | |||
| 6ac536412a | |||
| 52f8ffe4e4 | |||
| aca81431aa | |||
| 1fadd850b7 | |||
| ed2a9a21b6 | |||
| 57279eb3e4 | |||
| c403a2914a | |||
| bcb176344c | |||
| ecf362cffc | |||
| f4ab7671d8 | |||
| a2a3018917 | |||
| 0bdb664000 | |||
| 9c3b61ce57 | |||
| d06df3d278 | |||
| 547ba19e61 | 
| @@ -1,12 +1,12 @@ | ||||
| { | ||||
|   "sync": { | ||||
|     "region": "solian-next", | ||||
|     "region": "solian", | ||||
|     "configPath": "roadsign.toml" | ||||
|   }, | ||||
|   "deployments": [ | ||||
|     { | ||||
|       "region": "solian-next", | ||||
|       "site": "solian-next-web", | ||||
|       "region": "solian", | ||||
|       "site": "solian-web", | ||||
|       "path": "build/web" | ||||
|     } | ||||
|   ] | ||||
|   | ||||
| @@ -17,11 +17,16 @@ | ||||
|         android:label="Solian" | ||||
|         android:name="${applicationName}" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:enableOnBackInvokedCallback="true" | ||||
|         android:requestLegacyExternalStorage="true"> | ||||
|         <meta-data | ||||
|             android:name="flutterEmbedding" | ||||
|             android:value="2" /> | ||||
|  | ||||
|         <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" | ||||
|   | ||||
| @@ -7,11 +7,7 @@ meta { | ||||
| post { | ||||
|   url: {{endpoint}}/cgi/uc/boosts/1/activate | ||||
|   body: none | ||||
|   auth: bearer | ||||
| } | ||||
|  | ||||
| auth:bearer { | ||||
|   token: {{atk}} | ||||
|   auth: inherit | ||||
| } | ||||
|  | ||||
| body:json { | ||||
|   | ||||
							
								
								
									
										19
									
								
								api/Paperclip/Stickers/Create Sticker Pack.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								api/Paperclip/Stickers/Create Sticker Pack.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| meta { | ||||
|   name: Create Sticker Pack | ||||
|   type: http | ||||
|   seq: 1 | ||||
| } | ||||
|  | ||||
| post { | ||||
|   url: {{endpoint}}/cgi/uc/stickers/packs | ||||
|   body: json | ||||
|   auth: inherit | ||||
| } | ||||
|  | ||||
| body:json { | ||||
|   { | ||||
|     "prefix": "cat", | ||||
|     "name": "Solar Network full of Cats!", | ||||
|     "description": "The sticker packs is full of stickers which related with cats!" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										20
									
								
								api/Paperclip/Stickers/Create Sticker.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/Paperclip/Stickers/Create Sticker.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| meta { | ||||
|   name: Create Sticker | ||||
|   type: http | ||||
|   seq: 2 | ||||
| } | ||||
|  | ||||
| post { | ||||
|   url: {{endpoint}}/cgi/uc/stickers | ||||
|   body: json | ||||
|   auth: inherit | ||||
| } | ||||
|  | ||||
| body:json { | ||||
|   { | ||||
|     "alias": "AteChip", | ||||
|     "name": "Cat ate chips", | ||||
|     "attachment_id": "d0b692cc64054463", | ||||
|     "pack_id": 2 | ||||
|   } | ||||
| } | ||||
| @@ -7,11 +7,7 @@ meta { | ||||
| post { | ||||
|   url: {{endpoint}}/cgi/id/dev/notify/all | ||||
|   body: json | ||||
|   auth: bearer | ||||
| } | ||||
|  | ||||
| auth:bearer { | ||||
|   token: {{atk}} | ||||
|   auth: inherit | ||||
| } | ||||
|  | ||||
| body:json { | ||||
| @@ -19,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/Reader/List News Sources.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/Reader/List News Sources.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| meta { | ||||
|   name: List News Sources | ||||
|   type: http | ||||
|   seq: 3 | ||||
| } | ||||
|  | ||||
| get { | ||||
|   url: {{endpoint}}/cgi/re/well-known/sources | ||||
|   body: none | ||||
|   auth: none | ||||
| } | ||||
							
								
								
									
										17
									
								
								api/Reader/List News.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								api/Reader/List News.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| meta { | ||||
|   name: List News | ||||
|   type: http | ||||
|   seq: 2 | ||||
| } | ||||
|  | ||||
| get { | ||||
|   url: {{endpoint}}/cgi/re/news?take=10&offset=0&source=shadiao | ||||
|   body: none | ||||
|   auth: none | ||||
| } | ||||
|  | ||||
| params:query { | ||||
|   take: 10 | ||||
|   offset: 0 | ||||
|   source: shadiao | ||||
| } | ||||
							
								
								
									
										18
									
								
								api/Reader/Trigger Scan News.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/Reader/Trigger Scan News.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| meta { | ||||
|   name: Trigger Scan News | ||||
|   type: http | ||||
|   seq: 1 | ||||
| } | ||||
|  | ||||
| post { | ||||
|   url: {{endpoint}}/cgi/re/admin/scan | ||||
|   body: json | ||||
|   auth: inherit | ||||
| } | ||||
|  | ||||
| body:json { | ||||
|   { | ||||
|     "sources": ["taiwan-ltn"], | ||||
|     "eager": true | ||||
|   } | ||||
| } | ||||
							
								
								
									
										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 | ||||
| } | ||||
							
								
								
									
										7
									
								
								api/collection.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								api/collection.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| auth { | ||||
|   mode: bearer | ||||
| } | ||||
|  | ||||
| auth:bearer { | ||||
|   token: {{atk}} | ||||
| } | ||||
| @@ -17,6 +17,10 @@ | ||||
|   "screenAccountProfileEdit": "Edit Profile", | ||||
|   "screenAbuseReport": "Abuse Reports", | ||||
|   "screenSettings": "Settings", | ||||
|   "screenAccountSettings": "Account Settings", | ||||
|   "screenFactorSettings": "Auth Factors", | ||||
|   "screenAccountWallet": "Wallet", | ||||
|   "screenNews": "News", | ||||
|   "screenAlbum": "Album", | ||||
|   "screenChat": "Chat", | ||||
|   "screenChatManage": "Edit Channel", | ||||
| @@ -103,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", | ||||
| @@ -113,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.", | ||||
| @@ -179,8 +199,13 @@ | ||||
|     "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", | ||||
|   "settingsDrawerPreferCollapseDescription": "Make the drawer to collapse even when the screen is wide enough.", | ||||
|   "settingsBackgroundImage": "Background Image", | ||||
|   "settingsBackgroundImageDescription": "Set the background image that will be applied globally.", | ||||
|   "settingsBackgroundImageClear": "Clear Existing Background Image", | ||||
| @@ -191,6 +216,13 @@ | ||||
|   "settingsColorSchemeDescription": "Set the application primary color.", | ||||
|   "settingsColorSeed": "Color Seed", | ||||
|   "settingsColorSeedDescription": "Select one of the present color schemes.", | ||||
|   "settingsFeatures": "Features", | ||||
|   "settingsNotifyWithHaptic": "Haptic when Notified", | ||||
|   "settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.", | ||||
|   "settingsExpandPostLink": "Expand Post Link", | ||||
|   "settingsExpandPostLinkDescription": "Expand the post link in the post list.", | ||||
|   "settingsExpandChatLink": "Expand Chat Link", | ||||
|   "settingsExpandChatLinkDescription": "Expand the chat link in the chat list.", | ||||
|   "settingsNetwork": "Network", | ||||
|   "settingsNetworkServer": "HyperNet Server", | ||||
|   "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.", | ||||
| @@ -213,8 +245,9 @@ | ||||
|   "sensitiveContentCollapsed": "Sensitive content has been collapsed.", | ||||
|   "sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.", | ||||
|   "sensitiveContentReveal": "Reveal", | ||||
|   "serverConnecting": "Connecting to server...", | ||||
|   "serverDisconnected": "Lost connection from server", | ||||
|   "serverConnecting": "Connecting...", | ||||
|   "serverDisconnected": "Connection Lost", | ||||
|   "serverConnected": "Connected", | ||||
|   "fieldChatAlias": "Channel Alias", | ||||
|   "fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.", | ||||
|   "fieldChatName": "Name", | ||||
| @@ -292,6 +325,7 @@ | ||||
|   "addAttachmentFromCameraPhoto": "Take photo", | ||||
|   "addAttachmentFromCameraVideo": "Take video", | ||||
|   "addAttachmentFromRandomId": "Link via RID", | ||||
|   "attachmentDetailInfo": "Attachment details", | ||||
|   "attachmentPastedImage": "Pasted Image", | ||||
|   "attachmentInsertLink": "Insert Link", | ||||
|   "attachmentSetAsPostThumbnail": "Set as post thumbnail", | ||||
| @@ -414,6 +448,9 @@ | ||||
|   "celebrateBirthday": "Happy birthday, {}!", | ||||
|   "celebrateMerryXmas": "Merry christmas, {}!", | ||||
|   "celebrateNewYear": "Happy new year, {}!", | ||||
|   "celebrateLunarNewYear": "Happy lunar new year, {}!", | ||||
|   "celebrateMidAutumn": "Happy mid-autumn festival, {}!", | ||||
|   "celebrateDragonBoat": "Happy dragon boat festival, {}!", | ||||
|   "celebrateValentineDay": "Today is valentine's day, {}!", | ||||
|   "celebrateLaborDay": "Today is labor day, {}.", | ||||
|   "celebrateMotherDay": "Today is mother's day, {}.", | ||||
| @@ -423,6 +460,9 @@ | ||||
|   "celebrateThanksgiving": "Today is thanksgiving day, {}!", | ||||
|   "pendingBirthday": "Birthday in {}", | ||||
|   "pendingMerryXmas": "Christmas in {}", | ||||
|   "pendingLunarNewYear": "Lunar new year in {}", | ||||
|   "pendingMidAutumn": "Mid-autumn festival in {}", | ||||
|   "pendingDragonBoat": "Dragon boat festival in {}", | ||||
|   "pendingNewYear": "New year in {}", | ||||
|   "pendingValentineDay": "Valentine's day in {}", | ||||
|   "pendingLaborDay": "Labor day in {}", | ||||
| @@ -514,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", | ||||
| @@ -541,5 +584,25 @@ | ||||
|   "postCategoryKnowledge": "Knowledge", | ||||
|   "postCategoryLiterature": "Literature", | ||||
|   "postCategoryFunny": "Funny", | ||||
|   "postCategoryUncategorized": "Uncategorized" | ||||
|   "postCategoryUncategorized": "Uncategorized", | ||||
|   "newsAllSources": "All News", | ||||
|   "newsReadingProviderSwap": "Swap", | ||||
|   "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", | ||||
|   "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,10 @@ | ||||
|   "screenAccountProfileEdit": "编辑资料", | ||||
|   "screenAbuseReport": "滥用检举", | ||||
|   "screenSettings": "设置", | ||||
|   "screenAccountSettings": "账号设置", | ||||
|   "screenFactorSettings": "验证因子", | ||||
|   "screenAccountWallet": "钱包", | ||||
|   "screenNews": "新闻", | ||||
|   "screenAlbum": "相册", | ||||
|   "screenChat": "聊天", | ||||
|   "screenChatManage": "编辑聊天频道", | ||||
| @@ -87,8 +91,18 @@ | ||||
|   }, | ||||
|   "loginEnterPassword": "验证代码", | ||||
|   "loginSuccess": "登录为 {}", | ||||
|   "authFactorDelete": "删除验证因子", | ||||
|   "authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?", | ||||
|   "authFactorPassword": "密码", | ||||
|   "authFactorPasswordDescription": "注册时选择设置的密码。", | ||||
|   "authFactorEmail": "电邮一次性验证码", | ||||
|   "authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。", | ||||
|   "authFactorTOTP": "时序验证码", | ||||
|   "authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。", | ||||
|   "authFactorInAppNotify": "应用内通知验证码", | ||||
|   "authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。", | ||||
|   "authFactorAdd": "添加新验证因子", | ||||
|   "authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。", | ||||
|   "accountIntroTitle": "喜欢您来!", | ||||
|   "accountIntroSubtitle": "登陆以探索更广大的世界。", | ||||
|   "accountLogout": "退出登录", | ||||
| @@ -97,8 +111,14 @@ | ||||
|   "accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。", | ||||
|   "accountPublishers": "你的发布者", | ||||
|   "accountPublishersSubtitle": "管理你的公共形象。", | ||||
|   "accountSettings": "帐号设置", | ||||
|   "accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。", | ||||
|   "accountProfileEdit": "编辑资料", | ||||
|   "accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。", | ||||
|   "accountWallet": "钱包", | ||||
|   "accountWalletSubtitle": "查看你的余额和交易记录。", | ||||
|   "factorSettings": "验证因子", | ||||
|   "factorSettingsSubtitle": "管理你的登陆验证方式。", | ||||
|   "accountProfileEditApplied": "个人资料修改已被应用。", | ||||
|   "publishersNew": "新发布者", | ||||
|   "publisherNewSubtitle": "创建一个新的公共身份。", | ||||
| @@ -177,6 +197,9 @@ | ||||
|     "other": "{} 条评论" | ||||
|   }, | ||||
|   "settingsAppearance": "外观", | ||||
|   "settingsDisplayLanguage": "显示语言", | ||||
|   "settingsDisplayLanguageDescription": "设置应用程序使用的语言", | ||||
|   "settingsDisplayLanguageSystem": "跟随系统", | ||||
|   "settingsBackgroundImage": "背景图片", | ||||
|   "settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。", | ||||
|   "settingsBackgroundImageClear": "清除现存背景图", | ||||
| @@ -185,10 +208,19 @@ | ||||
|   "settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。", | ||||
|   "settingsAppBarTransparent": "透明顶栏", | ||||
|   "settingsAppBarTransparentDescription": "为顶栏启用透明效果。", | ||||
|   "settingsDrawerPreferCollapse": "侧边栏偏好折叠", | ||||
|   "settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。", | ||||
|   "settingsColorScheme": "主题色", | ||||
|   "settingsColorSchemeDescription": "设置应用主题色。", | ||||
|   "settingsColorSeed": "预设色彩主题", | ||||
|   "settingsColorSeedDescription": "选择一个预设色彩主题。", | ||||
|   "settingsFeatures": "功能", | ||||
|   "settingsNotifyWithHaptic": "新通知时振动", | ||||
|   "settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。", | ||||
|   "settingsExpandPostLink": "展开帖子链接", | ||||
|   "settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。", | ||||
|   "settingsExpandChatLink": "展开聊天链接", | ||||
|   "settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。", | ||||
|   "settingsNetwork": "网络", | ||||
|   "settingsNetworkServer": "HyperNet 服务器", | ||||
|   "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。", | ||||
| @@ -211,8 +243,9 @@ | ||||
|   "sensitiveContentCollapsed": "敏感内容已折叠。", | ||||
|   "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。", | ||||
|   "sensitiveContentReveal": "显示内容", | ||||
|   "serverConnecting": "正在连接服务器…", | ||||
|   "serverDisconnected": "已与服务器断开连接", | ||||
|   "serverConnecting": "正在连接…", | ||||
|   "serverDisconnected": "已断开连接", | ||||
|   "serverConnected": "已连接", | ||||
|   "fieldChatAlias": "频道别名", | ||||
|   "fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。", | ||||
|   "fieldChatName": "名称", | ||||
| @@ -290,6 +323,7 @@ | ||||
|   "addAttachmentFromCameraPhoto": "拍摄照片", | ||||
|   "addAttachmentFromCameraVideo": "拍摄视频", | ||||
|   "addAttachmentFromRandomId": "通过访问 ID 链接", | ||||
|   "attachmentDetailInfo": "附件详细信息", | ||||
|   "attachmentPastedImage": "粘贴的图片", | ||||
|   "attachmentInsertLink": "插入连接", | ||||
|   "attachmentSetAsPostThumbnail": "设置为帖子缩略图", | ||||
| @@ -410,6 +444,9 @@ | ||||
|   "dailyCheckNegativeHint6": "出门", | ||||
|   "dailyCheckNegativeHint6Description": "忘带伞遇上大雨", | ||||
|   "celebrateBirthday": "生日快乐,{}!", | ||||
|   "celebrateLunarNewYear": "春节快乐,{}!", | ||||
|   "celebrateMidAutumn": "中秋节快乐,{}!", | ||||
|   "celebrateDragonBoat": "端午节快乐,{}!", | ||||
|   "celebrateMerryXmas": "圣诞快乐,{}!", | ||||
|   "celebrateNewYear": "新年快乐,{}!", | ||||
|   "celebrateValentineDay": "今天是情人节,{}!", | ||||
| @@ -419,6 +456,9 @@ | ||||
|   "celebrateFatherDay": "今天是父亲节,{}。", | ||||
|   "celebrateHalloween": "快乐在圣诞节,{}!", | ||||
|   "celebrateThanksgiving": "今天是感恩节,{}!", | ||||
|   "pendingLunarNewYear": "{} 过春节", | ||||
|   "pendingMidAutumn": "{} 过中秋节", | ||||
|   "pendingDragonBoat": "{} 过端午节", | ||||
|   "pendingBirthday": "{} 过生日", | ||||
|   "pendingMerryXmas": "{} 过圣诞节", | ||||
|   "pendingNewYear": "{} 跨年", | ||||
| @@ -512,6 +552,9 @@ | ||||
|   "postImageShareAds": "来 Solar Network 探索更多有趣帖子", | ||||
|   "postShare": "分享", | ||||
|   "postShareImage": "分享帖图", | ||||
|   "postGetInsight": "获取见解", | ||||
|   "postGetInsightTitle": "AI 见解", | ||||
|   "postGetInsightDescription": "AI 可能会出错,检查信息真实性。", | ||||
|   "appInitializing": "正在初始化", | ||||
|   "poweredBy": "由 {} 提供支持", | ||||
|   "shareIntent": "分享", | ||||
| @@ -539,5 +582,25 @@ | ||||
|   "postCategoryKnowledge": "知识", | ||||
|   "postCategoryLiterature": "文学", | ||||
|   "postCategoryFunny": "搞笑", | ||||
|   "postCategoryUncategorized": "未分类" | ||||
|   "postCategoryUncategorized": "未分类", | ||||
|   "newsAllSources": "所有新闻", | ||||
|   "newsReadingProviderSwap": "切换", | ||||
|   "newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章", | ||||
|   "newsReadingFromOriginal": "你正在阅读原始文章", | ||||
|   "newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。", | ||||
|   "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,10 @@ | ||||
|   "screenAccountProfileEdit": "編輯資料", | ||||
|   "screenAbuseReport": "濫用檢舉", | ||||
|   "screenSettings": "設置", | ||||
|   "screenAccountSettings": "賬號設置", | ||||
|   "screenFactorSettings": "驗證因子", | ||||
|   "screenAccountWallet": "錢包", | ||||
|   "screenNews": "新聞", | ||||
|   "screenAlbum": "相冊", | ||||
|   "screenChat": "聊天", | ||||
|   "screenChatManage": "編輯聊天頻道", | ||||
| @@ -87,8 +91,18 @@ | ||||
|   }, | ||||
|   "loginEnterPassword": "驗證代碼", | ||||
|   "loginSuccess": "登錄為 {}", | ||||
|   "authFactorDelete": "刪除驗證因子", | ||||
|   "authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?", | ||||
|   "authFactorPassword": "密碼", | ||||
|   "authFactorPasswordDescription": "註冊時選擇設置的密碼。", | ||||
|   "authFactorEmail": "電郵一次性驗證碼", | ||||
|   "authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。", | ||||
|   "authFactorTOTP": "時序驗證碼", | ||||
|   "authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。", | ||||
|   "authFactorInAppNotify": "應用內通知驗證碼", | ||||
|   "authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。", | ||||
|   "authFactorAdd": "添加新驗證因子", | ||||
|   "authFactorAddSubtitle": "給你的帳户登陸時提供另一個方案。", | ||||
|   "accountIntroTitle": "喜歡您來!", | ||||
|   "accountIntroSubtitle": "登陸以探索更廣大的世界。", | ||||
|   "accountLogout": "退出登錄", | ||||
| @@ -97,8 +111,14 @@ | ||||
|   "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。", | ||||
|   "accountPublishers": "你的發佈者", | ||||
|   "accountPublishersSubtitle": "管理你的公共形象。", | ||||
|   "accountSettings": "帳號設置", | ||||
|   "accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。", | ||||
|   "accountProfileEdit": "編輯資料", | ||||
|   "accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。", | ||||
|   "accountWallet": "錢包", | ||||
|   "accountWalletSubtitle": "查看你的餘額和交易記錄。", | ||||
|   "factorSettings": "驗證因子", | ||||
|   "factorSettingsSubtitle": "管理你的登陸驗證方式。", | ||||
|   "accountProfileEditApplied": "個人資料修改已被應用。", | ||||
|   "publishersNew": "新發布者", | ||||
|   "publisherNewSubtitle": "創建一個新的公共身份。", | ||||
| @@ -177,6 +197,9 @@ | ||||
|     "other": "{} 條評論" | ||||
|   }, | ||||
|   "settingsAppearance": "外觀", | ||||
|   "settingsDisplayLanguage": "顯示語言", | ||||
|   "settingsDisplayLanguageDescription": "設置應用程序使用的語言", | ||||
|   "settingsDisplayLanguageSystem": "跟隨系統", | ||||
|   "settingsBackgroundImage": "背景圖片", | ||||
|   "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。", | ||||
|   "settingsBackgroundImageClear": "清除現存背景圖", | ||||
| @@ -185,10 +208,19 @@ | ||||
|   "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。", | ||||
|   "settingsAppBarTransparent": "透明頂欄", | ||||
|   "settingsAppBarTransparentDescription": "為頂欄啓用透明效果。", | ||||
|   "settingsDrawerPreferCollapse": "側邊欄偏好摺疊", | ||||
|   "settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。", | ||||
|   "settingsColorScheme": "主題色", | ||||
|   "settingsColorSchemeDescription": "設置應用主題色。", | ||||
|   "settingsColorSeed": "預設色彩主題", | ||||
|   "settingsColorSeedDescription": "選擇一個預設色彩主題。", | ||||
|   "settingsFeatures": "功能", | ||||
|   "settingsNotifyWithHaptic": "新通知時振動", | ||||
|   "settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。", | ||||
|   "settingsExpandPostLink": "展開帖子鏈接", | ||||
|   "settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。", | ||||
|   "settingsExpandChatLink": "展開聊天鏈接", | ||||
|   "settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。", | ||||
|   "settingsNetwork": "網絡", | ||||
|   "settingsNetworkServer": "HyperNet 服務器", | ||||
|   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", | ||||
| @@ -211,8 +243,9 @@ | ||||
|   "sensitiveContentCollapsed": "敏感內容已摺疊。", | ||||
|   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", | ||||
|   "sensitiveContentReveal": "顯示內容", | ||||
|   "serverConnecting": "正在連接服務器…", | ||||
|   "serverDisconnected": "已與服務器斷開連接", | ||||
|   "serverConnecting": "正在連接…", | ||||
|   "serverDisconnected": "已斷開連接", | ||||
|   "serverConnected": "已連接", | ||||
|   "fieldChatAlias": "頻道別名", | ||||
|   "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", | ||||
|   "fieldChatName": "名稱", | ||||
| @@ -279,6 +312,10 @@ | ||||
|     "one": "{} 個附件", | ||||
|     "other": "{} 個附件" | ||||
|   }, | ||||
|   "messageTyping": { | ||||
|     "one": "{} 正在輸入", | ||||
|     "other": "{} 正在輸入" | ||||
|   }, | ||||
|   "fieldAttachmentRandomId": "訪問 ID", | ||||
|   "fieldAttachmentAlt": "概述文字", | ||||
|   "addAttachmentFromAlbum": "從相冊中添加附件", | ||||
| @@ -286,6 +323,7 @@ | ||||
|   "addAttachmentFromCameraPhoto": "拍攝照片", | ||||
|   "addAttachmentFromCameraVideo": "拍攝視頻", | ||||
|   "addAttachmentFromRandomId": "通過訪問 ID 鏈接", | ||||
|   "attachmentDetailInfo": "附件詳細信息", | ||||
|   "attachmentPastedImage": "粘貼的圖片", | ||||
|   "attachmentInsertLink": "插入連接", | ||||
|   "attachmentSetAsPostThumbnail": "設置為帖子縮略圖", | ||||
| @@ -406,6 +444,9 @@ | ||||
|   "dailyCheckNegativeHint6": "出門", | ||||
|   "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", | ||||
|   "celebrateBirthday": "生日快樂,{}!", | ||||
|   "celebrateLunarNewYear": "春節快樂,{}!", | ||||
|   "celebrateMidAutumn": "中秋節快樂,{}!", | ||||
|   "celebrateDragonBoat": "端午節快樂,{}!", | ||||
|   "celebrateMerryXmas": "聖誕快樂,{}!", | ||||
|   "celebrateNewYear": "新年快樂,{}!", | ||||
|   "celebrateValentineDay": "今天是情人節,{}!", | ||||
| @@ -415,6 +456,9 @@ | ||||
|   "celebrateFatherDay": "今天是父親節,{}。", | ||||
|   "celebrateHalloween": "快樂在聖誕節,{}!", | ||||
|   "celebrateThanksgiving": "今天是感恩節,{}!", | ||||
|   "pendingLunarNewYear": "{} 過春節", | ||||
|   "pendingMidAutumn": "{} 過中秋節", | ||||
|   "pendingDragonBoat": "{} 過端午節", | ||||
|   "pendingBirthday": "{} 過生日", | ||||
|   "pendingMerryXmas": "{} 過聖誕節", | ||||
|   "pendingNewYear": "{} 跨年", | ||||
| @@ -508,6 +552,9 @@ | ||||
|   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", | ||||
|   "postShare": "分享", | ||||
|   "postShareImage": "分享帖圖", | ||||
|   "postGetInsight": "獲取見解", | ||||
|   "postGetInsightTitle": "AI 見解", | ||||
|   "postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。", | ||||
|   "appInitializing": "正在初始化", | ||||
|   "poweredBy": "由 {} 提供支持", | ||||
|   "shareIntent": "分享", | ||||
| @@ -535,5 +582,25 @@ | ||||
|   "postCategoryKnowledge": "知識", | ||||
|   "postCategoryLiterature": "文學", | ||||
|   "postCategoryFunny": "搞笑", | ||||
|   "postCategoryUncategorized": "未分類" | ||||
|   "postCategoryUncategorized": "未分類", | ||||
|   "newsAllSources": "所有新聞", | ||||
|   "newsReadingProviderSwap": "切換", | ||||
|   "newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章", | ||||
|   "newsReadingFromOriginal": "你正在閲讀原始文章", | ||||
|   "newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。", | ||||
|   "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,10 @@ | ||||
|   "screenAccountProfileEdit": "編輯資料", | ||||
|   "screenAbuseReport": "濫用檢舉", | ||||
|   "screenSettings": "設置", | ||||
|   "screenAccountSettings": "賬號設置", | ||||
|   "screenFactorSettings": "驗證因子", | ||||
|   "screenAccountWallet": "錢包", | ||||
|   "screenNews": "新聞", | ||||
|   "screenAlbum": "相冊", | ||||
|   "screenChat": "聊天", | ||||
|   "screenChatManage": "編輯聊天頻道", | ||||
| @@ -87,8 +91,18 @@ | ||||
|   }, | ||||
|   "loginEnterPassword": "驗證代碼", | ||||
|   "loginSuccess": "登錄為 {}", | ||||
|   "authFactorDelete": "刪除驗證因子", | ||||
|   "authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?", | ||||
|   "authFactorPassword": "密碼", | ||||
|   "authFactorPasswordDescription": "註冊時選擇設置的密碼。", | ||||
|   "authFactorEmail": "電郵一次性驗證碼", | ||||
|   "authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。", | ||||
|   "authFactorTOTP": "時序驗證碼", | ||||
|   "authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。", | ||||
|   "authFactorInAppNotify": "應用內通知驗證碼", | ||||
|   "authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。", | ||||
|   "authFactorAdd": "添加新驗證因子", | ||||
|   "authFactorAddSubtitle": "給你的帳戶登陸時提供另一個方案。", | ||||
|   "accountIntroTitle": "喜歡您來!", | ||||
|   "accountIntroSubtitle": "登陸以探索更廣大的世界。", | ||||
|   "accountLogout": "退出登錄", | ||||
| @@ -97,8 +111,14 @@ | ||||
|   "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。", | ||||
|   "accountPublishers": "你的發佈者", | ||||
|   "accountPublishersSubtitle": "管理你的公共形象。", | ||||
|   "accountSettings": "帳號設置", | ||||
|   "accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。", | ||||
|   "accountProfileEdit": "編輯資料", | ||||
|   "accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。", | ||||
|   "accountWallet": "錢包", | ||||
|   "accountWalletSubtitle": "查看你的餘額和交易記錄。", | ||||
|   "factorSettings": "驗證因子", | ||||
|   "factorSettingsSubtitle": "管理你的登陸驗證方式。", | ||||
|   "accountProfileEditApplied": "個人資料修改已被應用。", | ||||
|   "publishersNew": "新發布者", | ||||
|   "publisherNewSubtitle": "創建一個新的公共身份。", | ||||
| @@ -177,6 +197,9 @@ | ||||
|     "other": "{} 條評論" | ||||
|   }, | ||||
|   "settingsAppearance": "外觀", | ||||
|   "settingsDisplayLanguage": "顯示語言", | ||||
|   "settingsDisplayLanguageDescription": "設置應用程序使用的語言", | ||||
|   "settingsDisplayLanguageSystem": "跟隨系統", | ||||
|   "settingsBackgroundImage": "背景圖片", | ||||
|   "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。", | ||||
|   "settingsBackgroundImageClear": "清除現存背景圖", | ||||
| @@ -185,10 +208,19 @@ | ||||
|   "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。", | ||||
|   "settingsAppBarTransparent": "透明頂欄", | ||||
|   "settingsAppBarTransparentDescription": "為頂欄啟用透明效果。", | ||||
|   "settingsDrawerPreferCollapse": "側邊欄偏好摺疊", | ||||
|   "settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。", | ||||
|   "settingsColorScheme": "主題色", | ||||
|   "settingsColorSchemeDescription": "設置應用主題色。", | ||||
|   "settingsColorSeed": "預設色彩主題", | ||||
|   "settingsColorSeedDescription": "選擇一個預設色彩主題。", | ||||
|   "settingsFeatures": "功能", | ||||
|   "settingsNotifyWithHaptic": "新通知時振動", | ||||
|   "settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。", | ||||
|   "settingsExpandPostLink": "展開帖子鏈接", | ||||
|   "settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。", | ||||
|   "settingsExpandChatLink": "展開聊天鏈接", | ||||
|   "settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。", | ||||
|   "settingsNetwork": "網絡", | ||||
|   "settingsNetworkServer": "HyperNet 服務器", | ||||
|   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", | ||||
| @@ -211,8 +243,9 @@ | ||||
|   "sensitiveContentCollapsed": "敏感內容已摺疊。", | ||||
|   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", | ||||
|   "sensitiveContentReveal": "顯示內容", | ||||
|   "serverConnecting": "正在連接服務器…", | ||||
|   "serverDisconnected": "已與服務器斷開連接", | ||||
|   "serverConnecting": "正在連接…", | ||||
|   "serverDisconnected": "已斷開連接", | ||||
|   "serverConnected": "已連接", | ||||
|   "fieldChatAlias": "頻道別名", | ||||
|   "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", | ||||
|   "fieldChatName": "名稱", | ||||
| @@ -279,6 +312,10 @@ | ||||
|     "one": "{} 個附件", | ||||
|     "other": "{} 個附件" | ||||
|   }, | ||||
|   "messageTyping": { | ||||
|     "one": "{} 正在輸入", | ||||
|     "other": "{} 正在輸入" | ||||
|   }, | ||||
|   "fieldAttachmentRandomId": "訪問 ID", | ||||
|   "fieldAttachmentAlt": "概述文字", | ||||
|   "addAttachmentFromAlbum": "從相冊中添加附件", | ||||
| @@ -286,6 +323,7 @@ | ||||
|   "addAttachmentFromCameraPhoto": "拍攝照片", | ||||
|   "addAttachmentFromCameraVideo": "拍攝視頻", | ||||
|   "addAttachmentFromRandomId": "通過訪問 ID 鏈接", | ||||
|   "attachmentDetailInfo": "附件詳細信息", | ||||
|   "attachmentPastedImage": "粘貼的圖片", | ||||
|   "attachmentInsertLink": "插入連接", | ||||
|   "attachmentSetAsPostThumbnail": "設置為帖子縮略圖", | ||||
| @@ -406,6 +444,9 @@ | ||||
|   "dailyCheckNegativeHint6": "出門", | ||||
|   "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", | ||||
|   "celebrateBirthday": "生日快樂,{}!", | ||||
|   "celebrateLunarNewYear": "春節快樂,{}!", | ||||
|   "celebrateMidAutumn": "中秋節快樂,{}!", | ||||
|   "celebrateDragonBoat": "端午節快樂,{}!", | ||||
|   "celebrateMerryXmas": "聖誕快樂,{}!", | ||||
|   "celebrateNewYear": "新年快樂,{}!", | ||||
|   "celebrateValentineDay": "今天是情人節,{}!", | ||||
| @@ -415,6 +456,9 @@ | ||||
|   "celebrateFatherDay": "今天是父親節,{}。", | ||||
|   "celebrateHalloween": "快樂在聖誕節,{}!", | ||||
|   "celebrateThanksgiving": "今天是感恩節,{}!", | ||||
|   "pendingLunarNewYear": "{} 過春節", | ||||
|   "pendingMidAutumn": "{} 過中秋節", | ||||
|   "pendingDragonBoat": "{} 過端午節", | ||||
|   "pendingBirthday": "{} 過生日", | ||||
|   "pendingMerryXmas": "{} 過聖誕節", | ||||
|   "pendingNewYear": "{} 跨年", | ||||
| @@ -508,6 +552,9 @@ | ||||
|   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", | ||||
|   "postShare": "分享", | ||||
|   "postShareImage": "分享帖圖", | ||||
|   "postGetInsight": "獲取見解", | ||||
|   "postGetInsightTitle": "AI 見解", | ||||
|   "postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。", | ||||
|   "appInitializing": "正在初始化", | ||||
|   "poweredBy": "由 {} 提供支持", | ||||
|   "shareIntent": "分享", | ||||
| @@ -535,5 +582,25 @@ | ||||
|   "postCategoryKnowledge": "知識", | ||||
|   "postCategoryLiterature": "文學", | ||||
|   "postCategoryFunny": "搞笑", | ||||
|   "postCategoryUncategorized": "未分類" | ||||
|   "postCategoryUncategorized": "未分類", | ||||
|   "newsAllSources": "所有新聞", | ||||
|   "newsReadingProviderSwap": "切換", | ||||
|   "newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章", | ||||
|   "newsReadingFromOriginal": "你正在閱讀原始文章", | ||||
|   "newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。", | ||||
|   "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 思考過程" | ||||
| } | ||||
|   | ||||
							
								
								
									
										104
									
								
								ios/Podfile.lock
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								ios/Podfile.lock
									
									
									
									
									
								
							| @@ -43,58 +43,58 @@ PODS: | ||||
|     - Flutter | ||||
|   - file_saver (0.0.1): | ||||
|     - Flutter | ||||
|   - Firebase/Analytics (11.4.0): | ||||
|   - Firebase/Analytics (11.6.0): | ||||
|     - Firebase/Core | ||||
|   - Firebase/Core (11.4.0): | ||||
|   - Firebase/Core (11.6.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseAnalytics (~> 11.4.0) | ||||
|   - Firebase/CoreOnly (11.4.0): | ||||
|     - FirebaseCore (= 11.4.0) | ||||
|   - Firebase/Messaging (11.4.0): | ||||
|     - FirebaseAnalytics (~> 11.6.0) | ||||
|   - Firebase/CoreOnly (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|   - Firebase/Messaging (11.6.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseMessaging (~> 11.4.0) | ||||
|   - firebase_analytics (11.3.6): | ||||
|     - Firebase/Analytics (= 11.4.0) | ||||
|     - FirebaseMessaging (~> 11.6.0) | ||||
|   - firebase_analytics (11.4.1): | ||||
|     - Firebase/Analytics (= 11.6.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
|   - firebase_core (3.9.0): | ||||
|     - Firebase/CoreOnly (= 11.4.0) | ||||
|   - firebase_core (3.10.1): | ||||
|     - Firebase/CoreOnly (= 11.6.0) | ||||
|     - Flutter | ||||
|   - firebase_messaging (15.1.6): | ||||
|     - Firebase/Messaging (= 11.4.0) | ||||
|   - firebase_messaging (15.2.1): | ||||
|     - Firebase/Messaging (= 11.6.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
|   - FirebaseAnalytics (11.4.0): | ||||
|     - FirebaseAnalytics/AdIdSupport (= 11.4.0) | ||||
|     - FirebaseCore (~> 11.0) | ||||
|   - FirebaseAnalytics (11.6.0): | ||||
|     - FirebaseAnalytics/AdIdSupport (= 11.6.0) | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseAnalytics/AdIdSupport (11.4.0): | ||||
|     - FirebaseCore (~> 11.0) | ||||
|   - FirebaseAnalytics/AdIdSupport (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleAppMeasurement (= 11.4.0) | ||||
|     - GoogleAppMeasurement (= 11.6.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseCore (11.4.0): | ||||
|     - FirebaseCoreInternal (~> 11.0) | ||||
|   - FirebaseCore (11.6.0): | ||||
|     - FirebaseCoreInternal (~> 11.6.0) | ||||
|     - GoogleUtilities/Environment (~> 8.0) | ||||
|     - GoogleUtilities/Logger (~> 8.0) | ||||
|   - FirebaseCoreInternal (11.6.0): | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|   - FirebaseInstallations (11.4.0): | ||||
|     - FirebaseCore (~> 11.0) | ||||
|   - FirebaseInstallations (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|     - GoogleUtilities/Environment (~> 8.0) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.0) | ||||
|     - PromisesObjC (~> 2.4) | ||||
|   - FirebaseMessaging (11.4.0): | ||||
|     - FirebaseCore (~> 11.0) | ||||
|   - FirebaseMessaging (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleDataTransport (~> 10.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
| @@ -105,32 +105,39 @@ PODS: | ||||
|   - Flutter (1.0.0) | ||||
|   - flutter_app_update (0.0.1): | ||||
|     - Flutter | ||||
|   - flutter_inappwebview_ios (0.0.1): | ||||
|     - Flutter | ||||
|     - flutter_inappwebview_ios/Core (= 0.0.1) | ||||
|     - OrderedSet (~> 6.0.3) | ||||
|   - flutter_inappwebview_ios/Core (0.0.1): | ||||
|     - Flutter | ||||
|     - OrderedSet (~> 6.0.3) | ||||
|   - flutter_native_splash (2.4.3): | ||||
|     - Flutter | ||||
|   - flutter_udid (0.0.1): | ||||
|     - Flutter | ||||
|     - SAMKeychain | ||||
|   - flutter_webrtc (0.12.2): | ||||
|   - flutter_webrtc (0.12.6): | ||||
|     - Flutter | ||||
|     - WebRTC-SDK (= 125.6422.06) | ||||
|   - gal (1.0.0): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - GoogleAppMeasurement (11.4.0): | ||||
|     - GoogleAppMeasurement/AdIdSupport (= 11.4.0) | ||||
|   - GoogleAppMeasurement (11.6.0): | ||||
|     - GoogleAppMeasurement/AdIdSupport (= 11.6.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/AdIdSupport (11.4.0): | ||||
|     - GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0) | ||||
|   - GoogleAppMeasurement/AdIdSupport (11.6.0): | ||||
|     - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/WithoutAdIdSupport (11.4.0): | ||||
|   - GoogleAppMeasurement/WithoutAdIdSupport (11.6.0): | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
| @@ -173,7 +180,7 @@ PODS: | ||||
|   - in_app_review (2.0.0): | ||||
|     - Flutter | ||||
|   - Kingfisher (8.1.3) | ||||
|   - livekit_client (2.3.4): | ||||
|   - livekit_client (2.3.5): | ||||
|     - Flutter | ||||
|     - flutter_webrtc | ||||
|     - WebRTC-SDK (= 125.6422.06) | ||||
| @@ -188,6 +195,7 @@ PODS: | ||||
|     - nanopb/encode (= 3.30910.0) | ||||
|   - nanopb/decode (3.30910.0) | ||||
|   - nanopb/encode (3.30910.0) | ||||
|   - OrderedSet (6.0.3) | ||||
|   - package_info_plus (0.4.5): | ||||
|     - Flutter | ||||
|   - pasteboard (0.0.1): | ||||
| @@ -239,6 +247,7 @@ DEPENDENCIES: | ||||
|   - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) | ||||
|   - Flutter (from `Flutter`) | ||||
|   - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`) | ||||
|   - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) | ||||
|   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) | ||||
|   - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) | ||||
|   - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) | ||||
| @@ -282,6 +291,7 @@ SPEC REPOS: | ||||
|     - GoogleUtilities | ||||
|     - Kingfisher | ||||
|     - nanopb | ||||
|     - OrderedSet | ||||
|     - PromisesObjC | ||||
|     - SAMKeychain | ||||
|     - SDWebImage | ||||
| @@ -309,6 +319,8 @@ EXTERNAL SOURCES: | ||||
|     :path: Flutter | ||||
|   flutter_app_update: | ||||
|     :path: ".symlinks/plugins/flutter_app_update/ios" | ||||
|   flutter_inappwebview_ios: | ||||
|     :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" | ||||
|   flutter_native_splash: | ||||
|     :path: ".symlinks/plugins/flutter_native_splash/ios" | ||||
|   flutter_udid: | ||||
| @@ -367,35 +379,37 @@ SPEC CHECKSUMS: | ||||
|   device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 | ||||
|   DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c | ||||
|   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 | ||||
|   file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 | ||||
|   file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 | ||||
|   file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 | ||||
|   Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 | ||||
|   firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75 | ||||
|   firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10 | ||||
|   firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9 | ||||
|   FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49 | ||||
|   FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 | ||||
|   Firebase: 374a441a91ead896215703a674d58cdb3e9d772b | ||||
|   firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e | ||||
|   firebase_core: e2aa06dbd854d961f8ce46c2e20933bee1bf2d2b | ||||
|   firebase_messaging: 96cf6d67121b3f39746b2a4f29a26c0eee4af70e | ||||
|   FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 | ||||
|   FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa | ||||
|   FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 | ||||
|   FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 | ||||
|   FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 | ||||
|   FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c | ||||
|   FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021 | ||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||
|   flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc | ||||
|   flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a | ||||
|   flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 | ||||
|   flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a | ||||
|   flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab | ||||
|   flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563 | ||||
|   flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1 | ||||
|   gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 | ||||
|   GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e | ||||
|   GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3 | ||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||
|   GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d | ||||
|   home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 | ||||
|   image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 | ||||
|   in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 | ||||
|   Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef | ||||
|   livekit_client: 4eaa7a2968fc7e7c57888f43d90394547cc8d9e9 | ||||
|   livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976 | ||||
|   media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 | ||||
|   media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a | ||||
|   media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e | ||||
|   nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 | ||||
|   OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 | ||||
|   package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 | ||||
|   pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 | ||||
|   path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 | ||||
|   | ||||
| @@ -74,6 +74,7 @@ class ChatMessageController extends ChangeNotifier { | ||||
|     _wsSubscription = _ws.stream.stream.listen((event) { | ||||
|       switch (event.method) { | ||||
|         case 'events.new': | ||||
|           if (event.payload?['channel_id'] != channel?.id) break; | ||||
|           final payload = SnChatMessage.fromJson(event.payload!); | ||||
|           _addMessage(payload); | ||||
|           break; | ||||
| @@ -83,7 +84,6 @@ class ChatMessageController extends ChangeNotifier { | ||||
|           if (member.id == profile?.id) break; | ||||
|           if (!typingMembers.any((x) => x.id == member.id)) { | ||||
|             typingMembers.add(member); | ||||
|             print('Typing member: ${typingMembers.map((ele) => member.id)}'); | ||||
|             notifyListeners(); | ||||
|           } | ||||
|           typingInactiveTimer[member.id]?.cancel(); | ||||
|   | ||||
| @@ -154,7 +154,10 @@ class PostWriteController extends ChangeNotifier { | ||||
|   final TextEditingController descriptionController = TextEditingController(); | ||||
|   final TextEditingController aliasController = TextEditingController(); | ||||
|  | ||||
|   PostWriteController() { | ||||
|   bool _temporarySaveActive = false; | ||||
|  | ||||
|   PostWriteController({bool doLoadFromTemporary = true}) { | ||||
|     _temporarySaveActive = doLoadFromTemporary; | ||||
|     titleController.addListener(() { | ||||
|       _temporaryPlanSave(); | ||||
|       notifyListeners(); | ||||
| @@ -166,7 +169,7 @@ class PostWriteController extends ChangeNotifier { | ||||
|     contentController.addListener(() { | ||||
|       _temporaryPlanSave(); | ||||
|     }); | ||||
|     _temporaryLoad(); | ||||
|     if (doLoadFromTemporary) _temporaryLoad(); | ||||
|   } | ||||
|  | ||||
|   String mode = kTitleMap.keys.first; | ||||
| @@ -317,6 +320,7 @@ class PostWriteController extends ChangeNotifier { | ||||
|   Timer? _temporarySaveTimer; | ||||
|  | ||||
|   void _temporaryPlanSave() { | ||||
|     if (!_temporarySaveActive) return; | ||||
|     _temporarySaveTimer?.cancel(); | ||||
|     _temporarySaveTimer = Timer(const Duration(seconds: 1), () { | ||||
|       _temporarySave(); | ||||
| @@ -623,13 +627,15 @@ class PostWriteController extends ChangeNotifier { | ||||
|   void reset() { | ||||
|     publishedAt = null; | ||||
|     publishedUntil = null; | ||||
|     thumbnail = null; | ||||
|     visibility = 0; | ||||
|     titleController.clear(); | ||||
|     descriptionController.clear(); | ||||
|     contentController.clear(); | ||||
|     aliasController.clear(); | ||||
|     tags.clear(); | ||||
|     categories.clear(); | ||||
|     attachments.clear(); | ||||
|     tags = List.empty(growable: true); | ||||
|     categories = List.empty(growable: true); | ||||
|     attachments = List.empty(growable: true); | ||||
|     editingPost = null; | ||||
|     replyingPost = null; | ||||
|     repostingPost = null; | ||||
|   | ||||
| @@ -10,7 +10,6 @@ import 'package:easy_localization_loader/easy_localization_loader.dart'; | ||||
| import 'package:firebase_core/firebase_core.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hive_flutter/hive_flutter.dart'; | ||||
| import 'package:package_info_plus/package_info_plus.dart'; | ||||
| @@ -18,7 +17,6 @@ import 'package:provider/provider.dart'; | ||||
| import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:responsive_framework/responsive_framework.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/firebase_options.dart'; | ||||
| import 'package:surface/providers/channel.dart'; | ||||
| import 'package:surface/providers/chat_call.dart'; | ||||
| @@ -30,6 +28,7 @@ import 'package:surface/providers/post.dart'; | ||||
| import 'package:surface/providers/relationship.dart'; | ||||
| import 'package:surface/providers/sn_attachment.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/sn_sticker.dart'; | ||||
| import 'package:surface/providers/special_day.dart'; | ||||
| import 'package:surface/providers/theme.dart'; | ||||
| import 'package:surface/providers/user_directory.dart'; | ||||
| @@ -41,7 +40,6 @@ import 'package:surface/types/chat.dart'; | ||||
| import 'package:surface/types/realm.dart'; | ||||
| import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/version_label.dart'; | ||||
| import 'package:version/version.dart'; | ||||
| import 'package:workmanager/workmanager.dart'; | ||||
| import 'package:in_app_review/in_app_review.dart'; | ||||
| @@ -144,6 +142,7 @@ class SolianApp extends StatelessWidget { | ||||
|             Provider(create: (ctx) => SnPostContentProvider(ctx)), | ||||
|             Provider(create: (ctx) => SnRelationshipProvider(ctx)), | ||||
|             Provider(create: (ctx) => SnLinkPreviewProvider(ctx)), | ||||
|             Provider(create: (ctx) => SnStickerProvider(ctx)), | ||||
|             ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)), | ||||
|             ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)), | ||||
|             ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)), | ||||
| @@ -208,8 +207,6 @@ class _AppSplashScreen extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||
|   bool _isReady = false; | ||||
|  | ||||
|   void _tryRequestRating() async { | ||||
|     final prefs = await SharedPreferences.getInstance(); | ||||
|     if (prefs.containsKey('first_boot_time')) { | ||||
| @@ -261,6 +258,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||
|  | ||||
|   Future<void> _initialize() async { | ||||
|     try { | ||||
|       final cfg = context.read<ConfigProvider>(); | ||||
|       WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|         cfg.calcDrawerSize(context); | ||||
|       }); | ||||
|       final home = context.read<HomeWidgetProvider>(); | ||||
|       await home.initialize(); | ||||
|       if (!mounted) return; | ||||
| @@ -278,12 +279,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||
|       await ws.tryConnect(); | ||||
|       if (!mounted) return; | ||||
|       final notify = context.read<NotificationProvider>(); | ||||
|       notify.listen(); | ||||
|       await notify.registerPushNotifications(); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       await context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isReady = true); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -303,32 +303,17 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (!_isReady) { | ||||
|       return Scaffold( | ||||
|         backgroundColor: Theme.of(context).colorScheme.surface, | ||||
|         body: Container( | ||||
|           constraints: const BoxConstraints(maxWidth: 180), | ||||
|           child: Column( | ||||
|             mainAxisAlignment: MainAxisAlignment.center, | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: [ | ||||
|               if (MediaQuery.of(context).platformBrightness == Brightness.dark) | ||||
|                 Image.asset("assets/icon/icon-dark.png", width: 64, height: 64) | ||||
|               else | ||||
|                 Image.asset("assets/icon/icon.png", width: 64, height: 64), | ||||
|               const Gap(6), | ||||
|               LinearProgressIndicator( | ||||
|                 backgroundColor: Theme.of(context).colorScheme.surfaceContainer, | ||||
|               ), | ||||
|               const Gap(20), | ||||
|               Text('appInitializing'.tr(), textAlign: TextAlign.center), | ||||
|               AppVersionLabel(), | ||||
|             ], | ||||
|           ), | ||||
|         ).center(), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return widget.child; | ||||
|     final cfg = context.read<ConfigProvider>(); | ||||
|     return NotificationListener<SizeChangedLayoutNotification>( | ||||
|       onNotification: (notification) { | ||||
|         WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|           cfg.calcDrawerSize(context); | ||||
|         }); | ||||
|         return false; | ||||
|       }, | ||||
|       child: SizeChangedLayoutNotifier( | ||||
|         child: widget.child, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:responsive_framework/responsive_framework.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:surface/providers/widget.dart'; | ||||
|  | ||||
| @@ -12,6 +13,10 @@ const kNetworkServerStoreKey = 'app_server_url'; | ||||
| const kAppbarTransparentStoreKey = 'app_bar_transparent'; | ||||
| const kAppBackgroundStoreKey = 'app_has_background'; | ||||
| const kAppColorSchemeStoreKey = 'app_color_scheme'; | ||||
| const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse'; | ||||
| const kAppNotifyWithHaptic = 'app_notify_with_haptic'; | ||||
| const kAppExpandPostLink = 'app_expand_post_link'; | ||||
| const kAppExpandChatLink = 'app_expand_chat_link'; | ||||
|  | ||||
| const Map<String, FilterQuality> kImageQualityLevel = { | ||||
|   'settingsImageQualityLowest': FilterQuality.none, | ||||
| @@ -33,6 +38,32 @@ class ConfigProvider extends ChangeNotifier { | ||||
|     prefs = await SharedPreferences.getInstance(); | ||||
|   } | ||||
|  | ||||
|   bool drawerIsCollapsed = false; | ||||
|   bool drawerIsExpanded = false; | ||||
|  | ||||
|   void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) { | ||||
|     bool newDrawerIsCollapsed = false; | ||||
|     bool newDrawerIsExpanded = false; | ||||
|     if (withMediaQuery) { | ||||
|       newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450; | ||||
|       newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451; | ||||
|     } else { | ||||
|       final rpb = ResponsiveBreakpoints.of(context); | ||||
|       newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE); | ||||
|       newDrawerIsExpanded = rpb.largerThan(TABLET) | ||||
|           ? (prefs.getBool(kAppDrawerPreferCollapse) ?? false) | ||||
|               ? false | ||||
|               : true | ||||
|           : false; | ||||
|     } | ||||
|  | ||||
|     if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) { | ||||
|       drawerIsExpanded = newDrawerIsExpanded; | ||||
|       drawerIsCollapsed = newDrawerIsCollapsed; | ||||
|       notifyListeners(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   FilterQuality get imageQuality { | ||||
|     return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high; | ||||
|   } | ||||
|   | ||||
| @@ -58,6 +58,11 @@ class NavigationProvider extends ChangeNotifier { | ||||
|       screen: 'realm', | ||||
|       label: 'screenRealm', | ||||
|     ), | ||||
|     AppNavDestination( | ||||
|       icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20), | ||||
|       screen: 'news', | ||||
|       label: 'screenNews', | ||||
|     ), | ||||
|     AppNavDestination( | ||||
|       icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20), | ||||
|       screen: 'album', | ||||
| @@ -83,8 +88,7 @@ class NavigationProvider extends ChangeNotifier { | ||||
|  | ||||
|   List<AppNavDestination> destinations = []; | ||||
|  | ||||
|   int get pinnedDestinationCount => | ||||
|       destinations.where((ele) => ele.isPinned).length; | ||||
|   int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length; | ||||
|  | ||||
|   NavigationProvider() { | ||||
|     buildDestinations(kDefaultPinnedDestination); | ||||
| @@ -113,17 +117,13 @@ class NavigationProvider extends ChangeNotifier { | ||||
|   } | ||||
|  | ||||
|   bool isIndexInRange(int min, int max) { | ||||
|     return _currentIndex != null && | ||||
|         _currentIndex! >= min && | ||||
|         _currentIndex! < max; | ||||
|     return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max; | ||||
|   } | ||||
|  | ||||
|   void autoDetectIndex(GoRouter? state) { | ||||
|     if (state == null) return; | ||||
|     final idx = destinations.indexWhere( | ||||
|       (ele) => | ||||
|           ele.screen == | ||||
|           state.routerDelegate.currentConfiguration.last.route.name, | ||||
|       (ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name, | ||||
|     ); | ||||
|     _currentIndex = idx == -1 ? null : idx; | ||||
|     notifyListeners(); | ||||
|   | ||||
| @@ -4,18 +4,26 @@ import 'dart:io'; | ||||
| import 'package:firebase_messaging/firebase_messaging.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_udid/flutter_udid.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/providers/websocket.dart'; | ||||
| import 'package:surface/types/notification.dart'; | ||||
|  | ||||
| class NotificationProvider extends ChangeNotifier { | ||||
|   late final SnNetworkProvider _sn; | ||||
|   late final UserProvider _ua; | ||||
|   late final WebSocketProvider _ws; | ||||
|   late final ConfigProvider _cfg; | ||||
|  | ||||
|   NotificationProvider(BuildContext context) { | ||||
|     _sn = context.read<SnNetworkProvider>(); | ||||
|     _ua = context.read<UserProvider>(); | ||||
|     _ws = context.read<WebSocketProvider>(); | ||||
|     _cfg = context.read<ConfigProvider>(); | ||||
|   } | ||||
|  | ||||
|   Future<void> registerPushNotifications() async { | ||||
| @@ -62,4 +70,30 @@ 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.mediumImpact(); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   void clear() { | ||||
|     showingCount = 0; | ||||
|     notifyListeners(); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										38
									
								
								lib/providers/sn_sticker.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								lib/providers/sn_sticker.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
|  | ||||
| class SnStickerProvider { | ||||
|   late final SnNetworkProvider _sn; | ||||
|   final Map<String, SnSticker?> _cache = {}; | ||||
|  | ||||
|   SnStickerProvider(BuildContext context) { | ||||
|     _sn = context.read<SnNetworkProvider>(); | ||||
|   } | ||||
|  | ||||
|   bool hasNotSticker(String alias) { | ||||
|     return _cache.containsKey(alias) && _cache[alias] == null; | ||||
|   } | ||||
|  | ||||
|   Future<SnSticker?> lookupSticker(String alias) async { | ||||
|     if (_cache.containsKey(alias)) { | ||||
|       return _cache[alias]; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias'); | ||||
|       final sticker = SnSticker.fromJson(resp.data); | ||||
|       _cache[alias] = sticker; | ||||
|  | ||||
|       return sticker; | ||||
|     } catch (err) { | ||||
|       _cache[alias] = null; | ||||
|       log('[Sticker] Failed to lookup sticker $alias: $err'); | ||||
|     } | ||||
|  | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
| @@ -3,9 +3,12 @@ import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
|  | ||||
| // Stored as key: month, day | ||||
| const Map<String, (int, int)> kSpecialDays = { | ||||
| final Map<String, (int, int)> kSpecialDays = { | ||||
|   // Birthday is dynamically generated according to the user's profile | ||||
|   'NewYear': (1, 1), | ||||
|   'LunarNewYear': (lunarToGregorian(null, 1, 1).month, lunarToGregorian(null, 1, 1).day), | ||||
|   'MidAutumn': (lunarToGregorian(null, 8, 15).month, lunarToGregorian(null, 8, 15).day), | ||||
|   'DragonBoat': (lunarToGregorian(null, 5, 5).month, lunarToGregorian(null, 5, 5).day), | ||||
|   'ValentineDay': (2, 14), | ||||
|   'LaborDay': (5, 1), | ||||
|   'MotherDay': (5, 11), | ||||
| @@ -19,6 +22,9 @@ const Map<String, (int, int)> kSpecialDays = { | ||||
| const Map<String, String> kSpecialDaysSymbol = { | ||||
|   'Birthday': '🎂', | ||||
|   'NewYear': '🎉', | ||||
|   'LunarNewYear': '🎉', | ||||
|   'MidAutumn': '🥮', | ||||
|   'DragonBoat': '🐲', | ||||
|   'MerryXmas': '🎄', | ||||
|   'ValentineDay': '💑', | ||||
|   'LaborDay': '🏋️', | ||||
| @@ -134,3 +140,45 @@ class SpecialDayProvider { | ||||
|     return (elapsedDuration / totalDuration).clamp(0.0, 1.0); | ||||
|   } | ||||
| } | ||||
|  | ||||
| final Map<int, LunarYear> lunarYearData = { | ||||
|   2025: LunarYear( | ||||
|     startDate: DateTime(2025, 1, 29), | ||||
|     months: [29, 30, 30, 29, 30, 29, 29, 30, 30, 29, 30, 29], | ||||
|     leapMonth: 0, | ||||
|   ), | ||||
| }; | ||||
|  | ||||
| class LunarYear { | ||||
|   final DateTime startDate; | ||||
|   final List<int> months; | ||||
|   final int leapMonth; | ||||
|  | ||||
|   LunarYear({required this.startDate, required this.months, required this.leapMonth}); | ||||
| } | ||||
|  | ||||
| DateTime lunarToGregorian(int? year, int month, int day, {bool isLeapMonth = false}) { | ||||
|   year = year ?? DateTime.now().year; | ||||
|   final lunarYear = lunarYearData[year]; | ||||
|   if (lunarYear == null) { | ||||
|     throw Exception('Lunar data for year $year not found'); | ||||
|   } | ||||
|  | ||||
|   int leapMonth = lunarYear.leapMonth; | ||||
|   if (isLeapMonth && (leapMonth == 0 || leapMonth != month)) { | ||||
|     throw Exception('Invalid leap month for year $year'); | ||||
|   } | ||||
|  | ||||
|   int daysFromStart = 0; | ||||
|   for (int i = 0; i < month - 1; i++) { | ||||
|     daysFromStart += lunarYear.months[i]; | ||||
|   } | ||||
|  | ||||
|   if (isLeapMonth) { | ||||
|     daysFromStart += lunarYear.months[month - 1]; | ||||
|   } | ||||
|  | ||||
|   daysFromStart += day - 1; | ||||
|  | ||||
|   return lunarYear.startDate.add(Duration(days: daysFromStart)); | ||||
| } | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| class StickerProvider { | ||||
|  | ||||
| } | ||||
| @@ -35,7 +35,7 @@ class WebSocketProvider extends ChangeNotifier { | ||||
|  | ||||
|   Future<void> connect({noRetry = false}) async { | ||||
|     if (!_ua.isAuthorized) return; | ||||
|     if (isConnected) { | ||||
|     if (isConnected || conn != null) { | ||||
|       disconnect(); | ||||
|     } | ||||
|  | ||||
| @@ -97,7 +97,7 @@ class WebSocketProvider extends ChangeNotifier { | ||||
|       onError: (err) { | ||||
|         isConnected = false; | ||||
|         notifyListeners(); | ||||
|         Future.delayed(const Duration(seconds: 11), () => connect()); | ||||
|         Future.delayed(const Duration(seconds: 1), () => connect()); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										424
									
								
								lib/router.dart
									
									
									
									
									
								
							
							
						
						
									
										424
									
								
								lib/router.dart
									
									
									
									
									
								
							| @@ -3,7 +3,9 @@ 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/pfp.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'; | ||||
| import 'package:surface/screens/account/publishers/publisher_new.dart'; | ||||
| @@ -19,6 +21,8 @@ import 'package:surface/screens/chat/room.dart'; | ||||
| import 'package:surface/screens/explore.dart'; | ||||
| import 'package:surface/screens/friend.dart'; | ||||
| import 'package:surface/screens/home.dart'; | ||||
| import 'package:surface/screens/news/news_detail.dart'; | ||||
| import 'package:surface/screens/news/news_list.dart'; | ||||
| import 'package:surface/screens/notification.dart'; | ||||
| import 'package:surface/screens/post/post_detail.dart'; | ||||
| import 'package:surface/screens/post/post_editor.dart'; | ||||
| @@ -29,290 +33,226 @@ 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_background.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| Widget _fadeThroughTransition( | ||||
|     BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { | ||||
|   return FadeThroughTransition( | ||||
|     animation: animation, | ||||
|     secondaryAnimation: secondaryAnimation, | ||||
|     fillColor: Colors.transparent, | ||||
|     child: child, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| final _appRoutes = [ | ||||
|   ShellRoute( | ||||
|     builder: (context, state, child) => AppPageScaffold( | ||||
|       body: child, | ||||
|       showAppBar: false, | ||||
|     ), | ||||
|   GoRoute( | ||||
|     path: '/', | ||||
|     name: 'home', | ||||
|     builder: (context, state) => const HomeScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/posts', | ||||
|     name: 'explore', | ||||
|     builder: (context, state) => const ExploreScreen(), | ||||
|     routes: [ | ||||
|       GoRoute( | ||||
|         path: '/', | ||||
|         name: 'home', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|           child: const HomeScreen(), | ||||
|         path: '/write/:mode', | ||||
|         name: 'postEditor', | ||||
|         builder: (context, state) => PostEditorScreen( | ||||
|           mode: state.pathParameters['mode']!, | ||||
|           postEditId: int.tryParse( | ||||
|             state.uri.queryParameters['editing'] ?? '', | ||||
|           ), | ||||
|           postReplyId: int.tryParse( | ||||
|             state.uri.queryParameters['replying'] ?? '', | ||||
|           ), | ||||
|           postRepostId: int.tryParse( | ||||
|             state.uri.queryParameters['reposting'] ?? '', | ||||
|           ), | ||||
|           extraProps: state.extra as PostEditorExtraProps?, | ||||
|         ), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/posts', | ||||
|         name: 'explore', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|           child: const ExploreScreen(), | ||||
|         ), | ||||
|         routes: [ | ||||
|           GoRoute( | ||||
|             path: '/write/:mode', | ||||
|             name: 'postEditor', | ||||
|             builder: (context, state) => AppBackground( | ||||
|               child: PostEditorScreen( | ||||
|                 mode: state.pathParameters['mode']!, | ||||
|                 postEditId: int.tryParse( | ||||
|                   state.uri.queryParameters['editing'] ?? '', | ||||
|                 ), | ||||
|                 postReplyId: int.tryParse( | ||||
|                   state.uri.queryParameters['replying'] ?? '', | ||||
|                 ), | ||||
|                 postRepostId: int.tryParse( | ||||
|                   state.uri.queryParameters['reposting'] ?? '', | ||||
|                 ), | ||||
|                 extraProps: state.extra as PostEditorExtraProps?, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/search', | ||||
|             name: 'postSearch', | ||||
|             builder: (context, state) => AppBackground( | ||||
|               child: PostSearchScreen( | ||||
|                 initialTags: state.uri.queryParameters['tags']?.split(','), | ||||
|                 initialCategories: state.uri.queryParameters['categories']?.split(','), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/publishers/:name', | ||||
|             name: 'postPublisher', | ||||
|             builder: (context, state) => AppBackground( | ||||
|               child: PostPublisherScreen(name: state.pathParameters['name']!), | ||||
|             ), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/:slug', | ||||
|             name: 'postDetail', | ||||
|             builder: (context, state) => AppBackground( | ||||
|               child: PostDetailScreen( | ||||
|                 slug: state.pathParameters['slug']!, | ||||
|                 preload: state.extra as SnPost?, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/account', | ||||
|         name: 'account', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|           child: const AccountScreen(), | ||||
|         ), | ||||
|         routes: [], | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/chat', | ||||
|         name: 'chat', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|           child: const ChatScreen(), | ||||
|         ), | ||||
|         routes: [ | ||||
|           GoRoute( | ||||
|             path: '/:scope/:alias', | ||||
|             name: 'chatRoom', | ||||
|             builder: (context, state) => AppBackground( | ||||
|               child: ChatRoomScreen( | ||||
|                 scope: state.pathParameters['scope']!, | ||||
|                 alias: state.pathParameters['alias']!, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/:scope/:alias/call', | ||||
|             name: 'chatCallRoom', | ||||
|             builder: (context, state) => AppBackground( | ||||
|               child: CallRoomScreen( | ||||
|                 scope: state.pathParameters['scope']!, | ||||
|                 alias: state.pathParameters['alias']!, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/:scope/:alias/detail', | ||||
|             name: 'channelDetail', | ||||
|             builder: (context, state) => AppBackground( | ||||
|               child: ChannelDetailScreen( | ||||
|                 scope: state.pathParameters['scope']!, | ||||
|                 alias: state.pathParameters['alias']!, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/manage', | ||||
|             name: 'chatManage', | ||||
|             pageBuilder: (context, state) => CustomTransitionPage( | ||||
|               child: ChatManageScreen( | ||||
|                 editingChannelAlias: state.uri.queryParameters['editing'], | ||||
|               ), | ||||
|               transitionsBuilder: (context, animation, secondaryAnimation, child) { | ||||
|                 return FadeThroughTransition( | ||||
|                   animation: animation, | ||||
|                   secondaryAnimation: secondaryAnimation, | ||||
|                   fillColor: Colors.transparent, | ||||
|                   child: AppBackground( | ||||
|                     child: child, | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           ), | ||||
|           GoRoute( | ||||
|             path: '/:alias', | ||||
|             name: 'realmDetail', | ||||
|             builder: (context, state) => AppBackground( | ||||
|               child: RealmDetailScreen(alias: state.pathParameters['alias']!), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/realm', | ||||
|         name: 'realm', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|           child: const RealmScreen(), | ||||
|         ), | ||||
|         routes: [ | ||||
|           GoRoute( | ||||
|             path: '/manage', | ||||
|             name: 'realmManage', | ||||
|             pageBuilder: (context, state) => CustomTransitionPage( | ||||
|               child: RealmManageScreen( | ||||
|                 editingRealmAlias: state.uri.queryParameters['editing'], | ||||
|               ), | ||||
|               transitionsBuilder: (context, animation, secondaryAnimation, child) { | ||||
|                 return FadeThroughTransition( | ||||
|                   animation: animation, | ||||
|                   secondaryAnimation: secondaryAnimation, | ||||
|                   fillColor: Colors.transparent, | ||||
|                   child: AppBackground( | ||||
|                     child: child, | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/album', | ||||
|         name: 'album', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|           child: const AlbumScreen(), | ||||
|         path: '/search', | ||||
|         name: 'postSearch', | ||||
|         builder: (context, state) => PostSearchScreen( | ||||
|           initialTags: state.uri.queryParameters['tags']?.split(','), | ||||
|           initialCategories: state.uri.queryParameters['categories']?.split(','), | ||||
|         ), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/friend', | ||||
|         name: 'friend', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|           child: const FriendScreen(), | ||||
|         ), | ||||
|         path: '/publishers/:name', | ||||
|         name: 'postPublisher', | ||||
|         builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/notification', | ||||
|         name: 'notification', | ||||
|         pageBuilder: (context, state) => NoTransitionPage( | ||||
|           child: const NotificationScreen(), | ||||
|         path: '/:slug', | ||||
|         name: 'postDetail', | ||||
|         builder: (context, state) => PostDetailScreen( | ||||
|           slug: state.pathParameters['slug']!, | ||||
|           preload: state.extra as SnPost?, | ||||
|         ), | ||||
|       ), | ||||
|     ], | ||||
|   ), | ||||
|   ShellRoute( | ||||
|     builder: (context, state, child) => AppPageScaffold(body: child), | ||||
|   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', | ||||
|     builder: (context, state) => const ChatScreen(), | ||||
|     routes: [ | ||||
|       GoRoute( | ||||
|         path: '/auth/login', | ||||
|         name: 'authLogin', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: LoginScreen(), | ||||
|         path: '/:scope/:alias', | ||||
|         name: 'chatRoom', | ||||
|         builder: (context, state) => ChatRoomScreen( | ||||
|           scope: state.pathParameters['scope']!, | ||||
|           alias: state.pathParameters['alias']!, | ||||
|         ), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/auth/register', | ||||
|         name: 'authRegister', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: RegisterScreen(), | ||||
|         path: '/:scope/:alias/call', | ||||
|         name: 'chatCallRoom', | ||||
|         builder: (context, state) => CallRoomScreen( | ||||
|           scope: state.pathParameters['scope']!, | ||||
|           alias: state.pathParameters['alias']!, | ||||
|         ), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/reports', | ||||
|         name: 'abuseReport', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: AbuseReportScreen(), | ||||
|         path: '/:scope/:alias/detail', | ||||
|         name: 'channelDetail', | ||||
|         builder: (context, state) => ChannelDetailScreen( | ||||
|           scope: state.pathParameters['scope']!, | ||||
|           alias: state.pathParameters['alias']!, | ||||
|         ), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/account/profile/edit', | ||||
|         name: 'accountProfileEdit', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: ProfileEditScreen(), | ||||
|         ), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/account/publishers', | ||||
|         name: 'accountPublishers', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: PublisherScreen(), | ||||
|         ), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/account/publishers/new', | ||||
|         name: 'accountPublisherNew', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: AccountPublisherNewScreen(), | ||||
|         ), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/account/publishers/edit/:name', | ||||
|         name: 'accountPublisherEdit', | ||||
|         builder: (context, state) => AppBackground( | ||||
|           child: AccountPublisherEditScreen( | ||||
|             name: state.pathParameters['name']!, | ||||
|           ), | ||||
|         path: '/manage', | ||||
|         name: 'chatManage', | ||||
|         builder: (context, state) => ChatManageScreen( | ||||
|           editingChannelAlias: state.uri.queryParameters['editing'], | ||||
|         ), | ||||
|       ), | ||||
|     ], | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/account/:name', | ||||
|     name: 'accountProfilePage', | ||||
|     pageBuilder: (context, state) => NoTransitionPage( | ||||
|       child: UserScreen(name: state.pathParameters['name']!), | ||||
|     path: '/realm', | ||||
|     name: 'realm', | ||||
|     pageBuilder: (context, state) => CustomTransitionPage( | ||||
|       transitionsBuilder: _fadeThroughTransition, | ||||
|       child: const RealmScreen(), | ||||
|     ), | ||||
|   ), | ||||
|   ShellRoute( | ||||
|     builder: (context, state, child) => AppPageScaffold(body: child), | ||||
|     routes: [ | ||||
|       GoRoute( | ||||
|         path: '/settings', | ||||
|         name: 'settings', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: SettingsScreen(), | ||||
|         path: '/:alias', | ||||
|         name: 'realmDetail', | ||||
|         builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!), | ||||
|       ), | ||||
|       GoRoute( | ||||
|         path: '/manage', | ||||
|         name: 'realmManage', | ||||
|         builder: (context, state) => RealmManageScreen( | ||||
|           editingRealmAlias: state.uri.queryParameters['editing'], | ||||
|         ), | ||||
|       ), | ||||
|     ], | ||||
|   ), | ||||
|   ShellRoute( | ||||
|     builder: (context, state, child) => AppPageScaffold(body: child), | ||||
|     routes: [ | ||||
|       GoRoute( | ||||
|         path: '/about', | ||||
|         name: 'about', | ||||
|         builder: (context, state) => const AppBackground( | ||||
|           child: AboutScreen(), | ||||
|         ), | ||||
|   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', | ||||
|     builder: (context, state) => const AlbumScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/friend', | ||||
|     name: 'friend', | ||||
|     builder: (context, state) => const FriendScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/notification', | ||||
|     name: 'notification', | ||||
|     builder: (context, state) => const NotificationScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/auth/login', | ||||
|     name: 'authLogin', | ||||
|     builder: (context, state) => LoginScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/auth/register', | ||||
|     name: 'authRegister', | ||||
|     builder: (context, state) => RegisterScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/reports', | ||||
|     name: 'abuseReport', | ||||
|     builder: (context, state) => AbuseReportScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/settings', | ||||
|     name: 'settings', | ||||
|     builder: (context, state) => SettingsScreen(), | ||||
|   ), | ||||
|   GoRoute( | ||||
|     path: '/about', | ||||
|     name: 'about', | ||||
|     builder: (context, state) => AboutScreen(), | ||||
|   ), | ||||
| ]; | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| import '../types/account.dart'; | ||||
|  | ||||
| @@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAbuseReport').tr(), | ||||
|       ), | ||||
|       body: Column( | ||||
|         children: [ | ||||
|           ListTile( | ||||
| @@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> { | ||||
|           else | ||||
|             Expanded( | ||||
|               child: ListView.builder( | ||||
|                 padding: EdgeInsets.only(top: 8), | ||||
|                 itemCount: _reports.length, | ||||
|                 itemBuilder: (context, idx) { | ||||
|                   return ListTile( | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| @@ -12,6 +14,8 @@ import 'package:surface/providers/websocket.dart'; | ||||
| 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}); | ||||
| @@ -19,11 +23,51 @@ class AccountScreen extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final ua = context.watch<UserProvider>(); | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|     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), | ||||
| @@ -82,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(), | ||||
| @@ -112,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(), | ||||
| @@ -133,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), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -18,6 +18,7 @@ import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| class ProfileEditScreen extends StatefulWidget { | ||||
| @@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|             onDateTimeChanged: (DateTime newDate) { | ||||
|               setState(() { | ||||
|                 _birthday = newDate; | ||||
|                 _birthdayController.text = | ||||
|                     DateFormat(_kDateFormat).format(_birthday!); | ||||
|                 _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!); | ||||
|               }); | ||||
|             }, | ||||
|           ), | ||||
| @@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|     if (image == null) return; | ||||
|     if (!mounted) return; | ||||
|  | ||||
|     final ImageProvider imageProvider = | ||||
|         kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); | ||||
|     final aspectRatios = place == 'banner' | ||||
|         ? [CropAspectRatio(width: 16, height: 7)] | ||||
|         : [CropAspectRatio(width: 1, height: 1)]; | ||||
|     final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); | ||||
|     final aspectRatios = | ||||
|         place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; | ||||
|     final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) | ||||
|         ? await showCupertinoImageCropper( | ||||
|             // ignore: use_build_context_synchronously | ||||
| @@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|  | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     final rawBytes = | ||||
|         (await result.uiImage.toByteData(format: ImageByteFormat.png))! | ||||
|             .buffer | ||||
|             .asUint8List(); | ||||
|     final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); | ||||
|  | ||||
|     try { | ||||
|       final attachment = await attach.directUploadOne( | ||||
| @@ -212,136 +207,141 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> { | ||||
|  | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return SingleChildScrollView( | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           LoadingIndicator(isActive: _isBusy), | ||||
|           const Gap(24), | ||||
|           Stack( | ||||
|             clipBehavior: Clip.none, | ||||
|             children: [ | ||||
|               Material( | ||||
|                 elevation: 0, | ||||
|                 child: InkWell( | ||||
|                   child: ClipRRect( | ||||
|                     borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                     child: AspectRatio( | ||||
|                       aspectRatio: 16 / 9, | ||||
|                       child: Container( | ||||
|                         color: | ||||
|                             Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                         child: _banner != null | ||||
|                             ? AutoResizeUniversalImage( | ||||
|                                 sn.getAttachmentUrl(_banner!), | ||||
|                                 fit: BoxFit.cover, | ||||
|                               ) | ||||
|                             : const SizedBox.shrink(), | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAccountProfileEdit').tr(), | ||||
|       ), | ||||
|       body: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             LoadingIndicator(isActive: _isBusy), | ||||
|             const Gap(24), | ||||
|             Stack( | ||||
|               clipBehavior: Clip.none, | ||||
|               children: [ | ||||
|                 Material( | ||||
|                   elevation: 0, | ||||
|                   child: InkWell( | ||||
|                     child: ClipRRect( | ||||
|                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                       child: AspectRatio( | ||||
|                         aspectRatio: 16 / 9, | ||||
|                         child: Container( | ||||
|                           color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                           child: _banner != null | ||||
|                               ? AutoResizeUniversalImage( | ||||
|                                   sn.getAttachmentUrl(_banner!), | ||||
|                                   fit: BoxFit.cover, | ||||
|                                 ) | ||||
|                               : const SizedBox.shrink(), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                   onTap: () { | ||||
|                     _updateImage('banner'); | ||||
|                   }, | ||||
|                 ), | ||||
|               ), | ||||
|               Positioned( | ||||
|                 bottom: -28, | ||||
|                 left: 16, | ||||
|                 child: Material( | ||||
|                   elevation: 2, | ||||
|                   borderRadius: const BorderRadius.all(Radius.circular(40)), | ||||
|                   child: InkWell( | ||||
|                     child: AccountImage(content: _avatar, radius: 40), | ||||
|                     onTap: () { | ||||
|                       _updateImage('avatar'); | ||||
|                       _updateImage('banner'); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ).padding(horizontal: padding), | ||||
|           const Gap(8 + 28), | ||||
|           Column( | ||||
|             children: [ | ||||
|               TextField( | ||||
|                 readOnly: true, | ||||
|                 controller: _usernameController, | ||||
|                 decoration: InputDecoration( | ||||
|                   border: const UnderlineInputBorder(), | ||||
|                   labelText: 'fieldUsername'.tr(), | ||||
|                   helperText: 'fieldUsernameCannotEditHint'.tr(), | ||||
|                 ), | ||||
|               ), | ||||
|               const Gap(4), | ||||
|               TextField( | ||||
|                 controller: _nicknameController, | ||||
|                 decoration: InputDecoration( | ||||
|                   border: const UnderlineInputBorder(), | ||||
|                   labelText: 'fieldNickname'.tr(), | ||||
|                 ), | ||||
|               ), | ||||
|               const Gap(4), | ||||
|               Row( | ||||
|                 children: [ | ||||
|                   Flexible( | ||||
|                     flex: 1, | ||||
|                     child: TextField( | ||||
|                       controller: _firstNameController, | ||||
|                       decoration: InputDecoration( | ||||
|                         border: const UnderlineInputBorder(), | ||||
|                         labelText: 'fieldFirstName'.tr(), | ||||
|                       ), | ||||
|                 Positioned( | ||||
|                   bottom: -28, | ||||
|                   left: 16, | ||||
|                   child: Material( | ||||
|                     elevation: 2, | ||||
|                     borderRadius: const BorderRadius.all(Radius.circular(40)), | ||||
|                     child: InkWell( | ||||
|                       child: AccountImage(content: _avatar, radius: 40), | ||||
|                       onTap: () { | ||||
|                         _updateImage('avatar'); | ||||
|                       }, | ||||
|                     ), | ||||
|                   ), | ||||
|                   const Gap(8), | ||||
|                   Flexible( | ||||
|                     flex: 1, | ||||
|                     child: TextField( | ||||
|                       controller: _lastNameController, | ||||
|                       decoration: InputDecoration( | ||||
|                         border: const UnderlineInputBorder(), | ||||
|                         labelText: 'fieldLastName'.tr(), | ||||
|                 ), | ||||
|               ], | ||||
|             ).padding(horizontal: padding), | ||||
|             const Gap(8 + 28), | ||||
|             Column( | ||||
|               children: [ | ||||
|                 TextField( | ||||
|                   readOnly: true, | ||||
|                   controller: _usernameController, | ||||
|                   decoration: InputDecoration( | ||||
|                     border: const UnderlineInputBorder(), | ||||
|                     labelText: 'fieldUsername'.tr(), | ||||
|                     helperText: 'fieldUsernameCannotEditHint'.tr(), | ||||
|                   ), | ||||
|                 ), | ||||
|                 const Gap(4), | ||||
|                 TextField( | ||||
|                   controller: _nicknameController, | ||||
|                   decoration: InputDecoration( | ||||
|                     border: const UnderlineInputBorder(), | ||||
|                     labelText: 'fieldNickname'.tr(), | ||||
|                   ), | ||||
|                 ), | ||||
|                 const Gap(4), | ||||
|                 Row( | ||||
|                   children: [ | ||||
|                     Flexible( | ||||
|                       flex: 1, | ||||
|                       child: TextField( | ||||
|                         controller: _firstNameController, | ||||
|                         decoration: InputDecoration( | ||||
|                           border: const UnderlineInputBorder(), | ||||
|                           labelText: 'fieldFirstName'.tr(), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                     const Gap(8), | ||||
|                     Flexible( | ||||
|                       flex: 1, | ||||
|                       child: TextField( | ||||
|                         controller: _lastNameController, | ||||
|                         decoration: InputDecoration( | ||||
|                           border: const UnderlineInputBorder(), | ||||
|                           labelText: 'fieldLastName'.tr(), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 const Gap(4), | ||||
|                 TextField( | ||||
|                   controller: _descriptionController, | ||||
|                   keyboardType: TextInputType.multiline, | ||||
|                   maxLines: null, | ||||
|                   minLines: 3, | ||||
|                   decoration: InputDecoration( | ||||
|                     border: const UnderlineInputBorder(), | ||||
|                     labelText: 'fieldDescription'.tr(), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|               const Gap(4), | ||||
|               TextField( | ||||
|                 controller: _descriptionController, | ||||
|                 keyboardType: TextInputType.multiline, | ||||
|                 maxLines: null, | ||||
|                 minLines: 3, | ||||
|                 decoration: InputDecoration( | ||||
|                   border: const UnderlineInputBorder(), | ||||
|                   labelText: 'fieldDescription'.tr(), | ||||
|                 ), | ||||
|               ), | ||||
|               const Gap(4), | ||||
|               TextField( | ||||
|                 controller: _birthdayController, | ||||
|                 readOnly: true, | ||||
|                 decoration: InputDecoration( | ||||
|                   border: const UnderlineInputBorder(), | ||||
|                   labelText: 'fieldBirthday'.tr(), | ||||
|                 const Gap(4), | ||||
|                 TextField( | ||||
|                   controller: _birthdayController, | ||||
|                   readOnly: true, | ||||
|                   decoration: InputDecoration( | ||||
|                     border: const UnderlineInputBorder(), | ||||
|                     labelText: 'fieldBirthday'.tr(), | ||||
|                   ), | ||||
|                   onTap: () => _selectBirthday(), | ||||
|                 ), | ||||
|                 onTap: () => _selectBirthday(), | ||||
|               ), | ||||
|             ], | ||||
|           ).padding(horizontal: padding + 8), | ||||
|           const Gap(12), | ||||
|           Row( | ||||
|             mainAxisAlignment: MainAxisAlignment.end, | ||||
|             children: [ | ||||
|               ElevatedButton.icon( | ||||
|                 onPressed: _isBusy ? null : _updateUserInfo, | ||||
|                 icon: const Icon(Symbols.save), | ||||
|                 label: Text('apply').tr(), | ||||
|               ), | ||||
|             ], | ||||
|           ).padding(horizontal: padding), | ||||
|         ], | ||||
|               ], | ||||
|             ).padding(horizontal: padding + 8), | ||||
|             const Gap(12), | ||||
|             Row( | ||||
|               mainAxisAlignment: MainAxisAlignment.end, | ||||
|               children: [ | ||||
|                 ElevatedButton.icon( | ||||
|                   onPressed: _isBusy ? null : _updateUserInfo, | ||||
|                   icon: const Icon(Symbols.save), | ||||
|                   label: Text('apply').tr(), | ||||
|                 ), | ||||
|               ], | ||||
|             ).padding(horizontal: padding), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -241,6 +241,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
| 
 | ||||
|     return Scaffold( | ||||
|       backgroundColor: Colors.transparent, | ||||
|       body: CustomScrollView( | ||||
|         controller: _scrollController, | ||||
|         slivers: [ | ||||
| @@ -594,7 +595,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM | ||||
|                 subtitle: Text('@${ele.name}'), | ||||
|                 trailing: const Icon(Symbols.chevron_right), | ||||
|                 onTap: () { | ||||
|                   GoRouter.of(context).pushNamed( | ||||
|                   GoRouter.of(context).goNamed( | ||||
|                     'postPublisher', | ||||
|                     pathParameters: {'name': ele.name}, | ||||
|                   ); | ||||
| @@ -18,6 +18,7 @@ import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| class AccountPublisherEditScreen extends StatefulWidget { | ||||
| @@ -176,7 +177,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> | ||||
|   Widget build(BuildContext context) { | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       body: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           children: [ | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/realm.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| class AccountPublisherNewScreen extends StatefulWidget { | ||||
|   const AccountPublisherNewScreen({super.key}); | ||||
| @@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     return  AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAccountPublisherNew').tr(), | ||||
|       ), | ||||
|       body: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           children: [ | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| class PublisherScreen extends StatefulWidget { | ||||
|   const PublisherScreen({super.key}); | ||||
| @@ -32,8 +33,7 @@ class _PublisherScreenState extends State<PublisherScreen> { | ||||
|  | ||||
|     try { | ||||
|       final resp = await sn.client.get('/cgi/co/publishers/me'); | ||||
|       final List<SnPublisher> out = List<SnPublisher>.from( | ||||
|           resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []); | ||||
|       final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []); | ||||
|  | ||||
|       if (!mounted) return; | ||||
|  | ||||
| @@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAccountPublishers').tr(), | ||||
|       ), | ||||
|       body: Column( | ||||
|         children: [ | ||||
|           ListTile( | ||||
| @@ -62,9 +66,7 @@ class _PublisherScreenState extends State<PublisherScreen> { | ||||
|             contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|             leading: const Icon(Symbols.add_circle), | ||||
|             onTap: () { | ||||
|               GoRouter.of(context) | ||||
|                   .pushNamed('accountPublisherNew') | ||||
|                   .then((value) { | ||||
|               GoRouter.of(context).pushNamed('accountPublisherNew').then((value) { | ||||
|                 if (value == true) { | ||||
|                   _publishers.clear(); | ||||
|                   _fetchPublishers(); | ||||
| @@ -75,48 +77,52 @@ class _PublisherScreenState extends State<PublisherScreen> { | ||||
|           const Divider(height: 1), | ||||
|           LoadingIndicator(isActive: _isBusy), | ||||
|           Expanded( | ||||
|             child: RefreshIndicator( | ||||
|               onRefresh: () { | ||||
|                 _publishers.clear(); | ||||
|                 return _fetchPublishers(); | ||||
|               }, | ||||
|               child: ListView.builder( | ||||
|                 itemCount: _publishers.length, | ||||
|                 itemBuilder: (context, idx) { | ||||
|                   final publisher = _publishers[idx]; | ||||
|                   return ListTile( | ||||
|                     title: Text(publisher.nick), | ||||
|                     subtitle: Text('@${publisher.name}'), | ||||
|                     contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                     leading: AccountImage(content: publisher.avatar), | ||||
|                     trailing: PopupMenuButton( | ||||
|                       itemBuilder: (BuildContext context) => [ | ||||
|                         PopupMenuItem( | ||||
|                           child: Row( | ||||
|                             children: [ | ||||
|                               const Icon(Symbols.edit), | ||||
|                               const Gap(16), | ||||
|                               Text('edit').tr(), | ||||
|                             ], | ||||
|                           ), | ||||
|                           onTap: () { | ||||
|                             GoRouter.of(context).pushNamed( | ||||
|                               'accountPublisherEdit', | ||||
|                               pathParameters: { | ||||
|                                 'name': publisher.name, | ||||
|                               }, | ||||
|                             ).then((value) { | ||||
|                               if (value == true) { | ||||
|                                 _publishers.clear(); | ||||
|                                 _fetchPublishers(); | ||||
|                               } | ||||
|                             }); | ||||
|                           }, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ); | ||||
|             child: MediaQuery.removePadding( | ||||
|               context: context, | ||||
|               removeTop: true, | ||||
|               child: RefreshIndicator( | ||||
|                 onRefresh: () { | ||||
|                   _publishers.clear(); | ||||
|                   return _fetchPublishers(); | ||||
|                 }, | ||||
|                 child: ListView.builder( | ||||
|                   itemCount: _publishers.length, | ||||
|                   itemBuilder: (context, idx) { | ||||
|                     final publisher = _publishers[idx]; | ||||
|                     return ListTile( | ||||
|                       title: Text(publisher.nick), | ||||
|                       subtitle: Text('@${publisher.name}'), | ||||
|                       contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                       leading: AccountImage(content: publisher.avatar), | ||||
|                       trailing: PopupMenuButton( | ||||
|                         itemBuilder: (BuildContext context) => [ | ||||
|                           PopupMenuItem( | ||||
|                             child: Row( | ||||
|                               children: [ | ||||
|                                 const Icon(Symbols.edit), | ||||
|                                 const Gap(16), | ||||
|                                 Text('edit').tr(), | ||||
|                               ], | ||||
|                             ), | ||||
|                             onTap: () { | ||||
|                               GoRouter.of(context).pushNamed( | ||||
|                                 'accountPublisherEdit', | ||||
|                                 pathParameters: { | ||||
|                                   'name': publisher.name, | ||||
|                                 }, | ||||
|                               ).then((value) { | ||||
|                                 if (value == true) { | ||||
|                                   _publishers.clear(); | ||||
|                                   _fetchPublishers(); | ||||
|                                 } | ||||
|                               }); | ||||
|                             }, | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import 'package:surface/widgets/app_bar_leading.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_zoom.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_item.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| class AlbumScreen extends StatefulWidget { | ||||
| @@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       body: CustomScrollView( | ||||
|         controller: _scrollController, | ||||
|         slivers: [ | ||||
|   | ||||
| @@ -7,17 +7,14 @@ 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'; | ||||
| 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}); | ||||
|  | ||||
| @@ -35,67 +32,73 @@ class _LoginScreenState extends State<LoginScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Theme( | ||||
|       data: Theme.of(context).copyWith(canvasColor: Colors.transparent), | ||||
|       child: SingleChildScrollView( | ||||
|         child: PageTransitionSwitcher( | ||||
|           transitionBuilder: ( | ||||
|             Widget child, | ||||
|             Animation<double> primaryAnimation, | ||||
|             Animation<double> secondaryAnimation, | ||||
|           ) { | ||||
|             return SharedAxisTransition( | ||||
|               animation: primaryAnimation, | ||||
|               secondaryAnimation: secondaryAnimation, | ||||
|               transitionType: SharedAxisTransitionType.horizontal, | ||||
|               child: Container( | ||||
|                 constraints: BoxConstraints(maxWidth: 380), | ||||
|                 child: child, | ||||
|               ), | ||||
|             ); | ||||
|           }, | ||||
|           child: switch (_period % 3) { | ||||
|             1 => _LoginPickerScreen( | ||||
|                 key: const ValueKey(1), | ||||
|                 ticket: _currentTicket, | ||||
|                 factors: _factors, | ||||
|                 onTicket: (p0) => setState(() { | ||||
|                   _currentTicket = p0; | ||||
|                 }), | ||||
|                 onPickFactor: (p0) => setState(() { | ||||
|                   _factorPicked = p0; | ||||
|                 }), | ||||
|                 onNext: () => setState(() { | ||||
|                   _period++; | ||||
|                 }), | ||||
|               ), | ||||
|             2 => _LoginCheckScreen( | ||||
|                 key: const ValueKey(2), | ||||
|                 ticket: _currentTicket, | ||||
|                 factor: _factorPicked, | ||||
|                 onTicket: (p0) => setState(() { | ||||
|                   _currentTicket = p0; | ||||
|                 }), | ||||
|                 onNext: () => setState(() { | ||||
|                   _period = 1; | ||||
|                 }), | ||||
|               ), | ||||
|             _ => _LoginLookupScreen( | ||||
|                 key: const ValueKey(0), | ||||
|                 ticket: _currentTicket, | ||||
|                 onTicket: (p0) => setState(() { | ||||
|                   _currentTicket = p0; | ||||
|                 }), | ||||
|                 onFactor: (p0) => setState(() { | ||||
|                   _factors = p0; | ||||
|                 }), | ||||
|                 onNext: () => setState(() { | ||||
|                   _period++; | ||||
|                 }), | ||||
|               ), | ||||
|           }, | ||||
|         ).padding(all: 24), | ||||
|       ).center(), | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAuthLogin').tr(), | ||||
|       ), | ||||
|       body: Theme( | ||||
|         data: Theme.of(context).copyWith(canvasColor: Colors.transparent), | ||||
|         child: SingleChildScrollView( | ||||
|           child: PageTransitionSwitcher( | ||||
|             transitionBuilder: ( | ||||
|               Widget child, | ||||
|               Animation<double> primaryAnimation, | ||||
|               Animation<double> secondaryAnimation, | ||||
|             ) { | ||||
|               return SharedAxisTransition( | ||||
|                 animation: primaryAnimation, | ||||
|                 secondaryAnimation: secondaryAnimation, | ||||
|                 transitionType: SharedAxisTransitionType.horizontal, | ||||
|                 child: Container( | ||||
|                   constraints: BoxConstraints(maxWidth: 380), | ||||
|                   child: child, | ||||
|                 ), | ||||
|               ); | ||||
|             }, | ||||
|             child: switch (_period % 3) { | ||||
|               1 => _LoginPickerScreen( | ||||
|                   key: const ValueKey(1), | ||||
|                   ticket: _currentTicket, | ||||
|                   factors: _factors, | ||||
|                   onTicket: (p0) => setState(() { | ||||
|                     _currentTicket = p0; | ||||
|                   }), | ||||
|                   onPickFactor: (p0) => setState(() { | ||||
|                     _factorPicked = p0; | ||||
|                   }), | ||||
|                   onNext: () => setState(() { | ||||
|                     _period++; | ||||
|                   }), | ||||
|                 ), | ||||
|               2 => _LoginCheckScreen( | ||||
|                   key: const ValueKey(2), | ||||
|                   ticket: _currentTicket, | ||||
|                   factor: _factorPicked, | ||||
|                   onTicket: (p0) => setState(() { | ||||
|                     _currentTicket = p0; | ||||
|                   }), | ||||
|                   onNext: () => setState(() { | ||||
|                     _period = 1; | ||||
|                   }), | ||||
|                 ), | ||||
|               _ => _LoginLookupScreen( | ||||
|                   key: const ValueKey(0), | ||||
|                   ticket: _currentTicket, | ||||
|                   onTicket: (p0) => setState(() { | ||||
|                     _currentTicket = p0; | ||||
|                   }), | ||||
|                   onFactor: (p0) => setState(() { | ||||
|                     _factors = p0; | ||||
|                   }), | ||||
|                   onNext: () => setState(() { | ||||
|                     _period++; | ||||
|                   }), | ||||
|                 ), | ||||
|             }, | ||||
|           ).padding(all: 24), | ||||
|         ).center(), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -205,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, | ||||
| @@ -260,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; | ||||
| @@ -321,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) { | ||||
| @@ -401,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 { | ||||
| @@ -430,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( | ||||
| @@ -441,7 +451,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> { | ||||
|  | ||||
|       widget.onNext(); | ||||
|     } catch (err) { | ||||
|       if(mounted) context.showErrorDialog(err); | ||||
|       if (mounted) context.showErrorDialog(err); | ||||
|       return; | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
| @@ -524,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( | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| class RegisterScreen extends StatefulWidget { | ||||
| @@ -54,175 +55,178 @@ class _RegisterScreenState extends State<RegisterScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return StyledWidget(Container( | ||||
|       constraints: const BoxConstraints(maxWidth: 380), | ||||
|       child: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             Align( | ||||
|               alignment: Alignment.centerLeft, | ||||
|               child: CircleAvatar( | ||||
|                 radius: 26, | ||||
|                 child: const Icon( | ||||
|                   Symbols.person_add, | ||||
|                   size: 28, | ||||
|                 ), | ||||
|               ).padding(bottom: 8), | ||||
|             ), | ||||
|             Text( | ||||
|               'screenAuthRegister', | ||||
|               style: const TextStyle( | ||||
|                 fontSize: 28, | ||||
|                 fontWeight: FontWeight.w900, | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAuthRegister').tr(), | ||||
|       ), | ||||
|       body: StyledWidget(Container( | ||||
|         constraints: const BoxConstraints(maxWidth: 380), | ||||
|         child: SingleChildScrollView( | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               Align( | ||||
|                 alignment: Alignment.centerLeft, | ||||
|                 child: CircleAvatar( | ||||
|                   radius: 26, | ||||
|                   child: const Icon( | ||||
|                     Symbols.person_add, | ||||
|                     size: 28, | ||||
|                   ), | ||||
|                 ).padding(bottom: 8), | ||||
|               ), | ||||
|             ).tr().padding(left: 4, bottom: 16), | ||||
|             Form( | ||||
|               key: _formKey, | ||||
|               autovalidateMode: AutovalidateMode.onUserInteraction, | ||||
|               child: Column( | ||||
|                 children: [ | ||||
|                   TextFormField( | ||||
|                     validator: (value) { | ||||
|                       if (value == null || value.length < 4 || value.length > 32) { | ||||
|                         return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]); | ||||
|                       } | ||||
|                       if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) { | ||||
|                         return 'fieldUsernameAlphanumOnly'.tr(); | ||||
|                       } | ||||
|                       return null; | ||||
|                     }, | ||||
|                     autocorrect: false, | ||||
|                     enableSuggestions: false, | ||||
|                     controller: _usernameController, | ||||
|                     autofillHints: const [AutofillHints.username], | ||||
|                     decoration: InputDecoration( | ||||
|                       isDense: true, | ||||
|                       border: const UnderlineInputBorder(), | ||||
|                       labelText: 'fieldUsername'.tr(), | ||||
|                     ), | ||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   ), | ||||
|                   const Gap(12), | ||||
|                   TextFormField( | ||||
|                     validator: (value) { | ||||
|                       if (value == null || value.length < 4 || value.length > 32) { | ||||
|                         return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]); | ||||
|                       } | ||||
|                       return null; | ||||
|                     }, | ||||
|                     autocorrect: false, | ||||
|                     enableSuggestions: false, | ||||
|                     controller: _nicknameController, | ||||
|                     autofillHints: const [AutofillHints.nickname], | ||||
|                     decoration: InputDecoration( | ||||
|                       isDense: true, | ||||
|                       border: const UnderlineInputBorder(), | ||||
|                       labelText: 'fieldNickname'.tr(), | ||||
|                     ), | ||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   ), | ||||
|                   const Gap(12), | ||||
|                   TextFormField( | ||||
|                     validator: (value) { | ||||
|                       if (value == null || value.isEmpty) { | ||||
|                         return 'fieldCannotBeEmpty'.tr(); | ||||
|                       } | ||||
|                       if (!EmailValidator.validate(value)) { | ||||
|                         return 'fieldEmailAddressMustBeValid'.tr(); | ||||
|                       } | ||||
|                       return null; | ||||
|                     }, | ||||
|                     autocorrect: false, | ||||
|                     enableSuggestions: false, | ||||
|                     controller: _emailController, | ||||
|                     autofillHints: const [AutofillHints.email], | ||||
|                     decoration: InputDecoration( | ||||
|                       isDense: true, | ||||
|                       border: const UnderlineInputBorder(), | ||||
|                       labelText: 'fieldEmail'.tr(), | ||||
|                     ), | ||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   ), | ||||
|                   const Gap(12), | ||||
|                   TextFormField( | ||||
|                     validator: (value) { | ||||
|                       if (value == null || value.isEmpty) { | ||||
|                         return 'fieldCannotBeEmpty'.tr(); | ||||
|                       } | ||||
|                       return null; | ||||
|                     }, | ||||
|                     obscureText: true, | ||||
|                     autocorrect: false, | ||||
|                     enableSuggestions: false, | ||||
|                     autofillHints: const [AutofillHints.password], | ||||
|                     controller: _passwordController, | ||||
|                     decoration: InputDecoration( | ||||
|                       isDense: true, | ||||
|                       border: const UnderlineInputBorder(), | ||||
|                       labelText: 'fieldPassword'.tr(), | ||||
|                     ), | ||||
|                     onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   ), | ||||
|                 ], | ||||
|               ).padding(horizontal: 7), | ||||
|             ), | ||||
|             const Gap(16), | ||||
|             Align( | ||||
|               alignment: Alignment.centerRight, | ||||
|               child: StyledWidget( | ||||
|                 Container( | ||||
|                   constraints: const BoxConstraints(maxWidth: 290), | ||||
|                   child: Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.end, | ||||
|                     children: [ | ||||
|                       Text( | ||||
|                         'termAcceptNextWithAgree'.tr(), | ||||
|                         textAlign: TextAlign.end, | ||||
|                         style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||
|                           color: Theme.of(context) | ||||
|                               .colorScheme | ||||
|                               .onSurface | ||||
|                               .withAlpha((255 * 0.75).round()), | ||||
|                         ), | ||||
|               Text( | ||||
|                 'screenAuthRegister', | ||||
|                 style: const TextStyle( | ||||
|                   fontSize: 28, | ||||
|                   fontWeight: FontWeight.w900, | ||||
|                 ), | ||||
|               ).tr().padding(left: 4, bottom: 16), | ||||
|               Form( | ||||
|                 key: _formKey, | ||||
|                 autovalidateMode: AutovalidateMode.onUserInteraction, | ||||
|                 child: Column( | ||||
|                   children: [ | ||||
|                     TextFormField( | ||||
|                       validator: (value) { | ||||
|                         if (value == null || value.length < 4 || value.length > 32) { | ||||
|                           return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]); | ||||
|                         } | ||||
|                         if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) { | ||||
|                           return 'fieldUsernameAlphanumOnly'.tr(); | ||||
|                         } | ||||
|                         return null; | ||||
|                       }, | ||||
|                       autocorrect: false, | ||||
|                       enableSuggestions: false, | ||||
|                       controller: _usernameController, | ||||
|                       autofillHints: const [AutofillHints.username], | ||||
|                       decoration: InputDecoration( | ||||
|                         isDense: true, | ||||
|                         border: const UnderlineInputBorder(), | ||||
|                         labelText: 'fieldUsername'.tr(), | ||||
|                       ), | ||||
|                       Material( | ||||
|                         color: Colors.transparent, | ||||
|                         child: InkWell( | ||||
|                           child: Row( | ||||
|                             mainAxisSize: MainAxisSize.min, | ||||
|                             children: [ | ||||
|                               Text('termAcceptLink'.tr()), | ||||
|                               const Gap(4), | ||||
|                               const Icon(Symbols.launch, size: 14), | ||||
|                             ], | ||||
|                       onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     ), | ||||
|                     const Gap(12), | ||||
|                     TextFormField( | ||||
|                       validator: (value) { | ||||
|                         if (value == null || value.length < 4 || value.length > 32) { | ||||
|                           return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]); | ||||
|                         } | ||||
|                         return null; | ||||
|                       }, | ||||
|                       autocorrect: false, | ||||
|                       enableSuggestions: false, | ||||
|                       controller: _nicknameController, | ||||
|                       autofillHints: const [AutofillHints.nickname], | ||||
|                       decoration: InputDecoration( | ||||
|                         isDense: true, | ||||
|                         border: const UnderlineInputBorder(), | ||||
|                         labelText: 'fieldNickname'.tr(), | ||||
|                       ), | ||||
|                       onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     ), | ||||
|                     const Gap(12), | ||||
|                     TextFormField( | ||||
|                       validator: (value) { | ||||
|                         if (value == null || value.isEmpty) { | ||||
|                           return 'fieldCannotBeEmpty'.tr(); | ||||
|                         } | ||||
|                         if (!EmailValidator.validate(value)) { | ||||
|                           return 'fieldEmailAddressMustBeValid'.tr(); | ||||
|                         } | ||||
|                         return null; | ||||
|                       }, | ||||
|                       autocorrect: false, | ||||
|                       enableSuggestions: false, | ||||
|                       controller: _emailController, | ||||
|                       autofillHints: const [AutofillHints.email], | ||||
|                       decoration: InputDecoration( | ||||
|                         isDense: true, | ||||
|                         border: const UnderlineInputBorder(), | ||||
|                         labelText: 'fieldEmail'.tr(), | ||||
|                       ), | ||||
|                       onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     ), | ||||
|                     const Gap(12), | ||||
|                     TextFormField( | ||||
|                       validator: (value) { | ||||
|                         if (value == null || value.isEmpty) { | ||||
|                           return 'fieldCannotBeEmpty'.tr(); | ||||
|                         } | ||||
|                         return null; | ||||
|                       }, | ||||
|                       obscureText: true, | ||||
|                       autocorrect: false, | ||||
|                       enableSuggestions: false, | ||||
|                       autofillHints: const [AutofillHints.password], | ||||
|                       controller: _passwordController, | ||||
|                       decoration: InputDecoration( | ||||
|                         isDense: true, | ||||
|                         border: const UnderlineInputBorder(), | ||||
|                         labelText: 'fieldPassword'.tr(), | ||||
|                       ), | ||||
|                       onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ).padding(horizontal: 7), | ||||
|               ), | ||||
|               const Gap(16), | ||||
|               Align( | ||||
|                 alignment: Alignment.centerRight, | ||||
|                 child: StyledWidget( | ||||
|                   Container( | ||||
|                     constraints: const BoxConstraints(maxWidth: 290), | ||||
|                     child: Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.end, | ||||
|                       children: [ | ||||
|                         Text( | ||||
|                           'termAcceptNextWithAgree'.tr(), | ||||
|                           textAlign: TextAlign.end, | ||||
|                           style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||
|                                 color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()), | ||||
|                               ), | ||||
|                         ), | ||||
|                         Material( | ||||
|                           color: Colors.transparent, | ||||
|                           child: InkWell( | ||||
|                             child: Row( | ||||
|                               mainAxisSize: MainAxisSize.min, | ||||
|                               children: [ | ||||
|                                 Text('termAcceptLink'.tr()), | ||||
|                                 const Gap(4), | ||||
|                                 const Icon(Symbols.launch, size: 14), | ||||
|                               ], | ||||
|                             ), | ||||
|                             onTap: () { | ||||
|                               launchUrlString('https://solsynth.dev/terms'); | ||||
|                             }, | ||||
|                           ), | ||||
|                           onTap: () { | ||||
|                             launchUrlString('https://solsynth.dev/terms'); | ||||
|                           }, | ||||
|                         ), | ||||
|                       ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|                 ).padding(horizontal: 16), | ||||
|               ), | ||||
|               Align( | ||||
|                 alignment: Alignment.centerRight, | ||||
|                 child: TextButton( | ||||
|                   onPressed: () => _performAction(context), | ||||
|                   child: Row( | ||||
|                     mainAxisSize: MainAxisSize.min, | ||||
|                     children: [ | ||||
|                       Text('next').tr(), | ||||
|                       const Icon(Symbols.chevron_right), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|               ).padding(horizontal: 16), | ||||
|             ), | ||||
|             Align( | ||||
|               alignment: Alignment.centerRight, | ||||
|               child: TextButton( | ||||
|                 onPressed: () => _performAction(context), | ||||
|                 child: Row( | ||||
|                   mainAxisSize: MainAxisSize.min, | ||||
|                   children: [ | ||||
|                     Text('next').tr(), | ||||
|                     const Icon(Symbols.chevron_right), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     )).padding(all: 24).center(); | ||||
|       )).padding(all: 24).center(), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import 'package:surface/widgets/account/account_select.dart'; | ||||
| import 'package:surface/widgets/app_bar_leading.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/unauthorized_hint.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| @@ -120,7 +121,7 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|  | ||||
|     if (!ua.isAuthorized) { | ||||
|       return Scaffold( | ||||
|       return AppScaffold( | ||||
|         appBar: AppBar( | ||||
|           leading: AutoAppBarLeading(), | ||||
|           title: Text('screenChat').tr(), | ||||
| @@ -131,7 +132,7 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: AutoAppBarLeading(), | ||||
|         title: Text('screenChat').tr(), | ||||
| @@ -195,22 +196,58 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|         children: [ | ||||
|           LoadingIndicator(isActive: _isBusy), | ||||
|           Expanded( | ||||
|             child: RefreshIndicator( | ||||
|               onRefresh: () => Future.sync(() => _refreshChannels()), | ||||
|               child: ListView.builder( | ||||
|                 itemCount: _channels?.length ?? 0, | ||||
|                 itemBuilder: (context, idx) { | ||||
|                   final channel = _channels![idx]; | ||||
|                   final lastMessage = _lastMessages?[channel.id]; | ||||
|             child: MediaQuery.removePadding( | ||||
|               context: context, | ||||
|               removeTop: true, | ||||
|               child: RefreshIndicator( | ||||
|                 onRefresh: () => Future.sync(() => _refreshChannels()), | ||||
|                 child: ListView.builder( | ||||
|                   itemCount: _channels?.length ?? 0, | ||||
|                   itemBuilder: (context, idx) { | ||||
|                     final channel = _channels![idx]; | ||||
|                     final lastMessage = _lastMessages?[channel.id]; | ||||
|  | ||||
|                   if (channel.type == 1) { | ||||
|                     final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere( | ||||
|                           (ele) => ele?.accountId != ua.user?.id, | ||||
|                           orElse: () => null, | ||||
|                         ); | ||||
|                     if (channel.type == 1) { | ||||
|                       final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere( | ||||
|                             (ele) => ele?.accountId != ua.user?.id, | ||||
|                             orElse: () => null, | ||||
|                           ); | ||||
|  | ||||
|                       return ListTile( | ||||
|                         title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name), | ||||
|                         subtitle: lastMessage != null | ||||
|                             ? Text( | ||||
|                                 '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||
|                                 maxLines: 1, | ||||
|                                 overflow: TextOverflow.ellipsis, | ||||
|                               ) | ||||
|                             : Text( | ||||
|                                 'channelDirectMessageDescription'.tr(args: [ | ||||
|                                   '@${ud.getAccountFromCache(otherMember?.accountId)?.name}', | ||||
|                                 ]), | ||||
|                                 maxLines: 1, | ||||
|                                 overflow: TextOverflow.ellipsis, | ||||
|                               ), | ||||
|                         contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                         leading: AccountImage( | ||||
|                           content: ud.getAccountFromCache(otherMember?.accountId)?.avatar, | ||||
|                         ), | ||||
|                         onTap: () { | ||||
|                           GoRouter.of(context).pushNamed( | ||||
|                             'chatRoom', | ||||
|                             pathParameters: { | ||||
|                               'scope': channel.realm?.alias ?? 'global', | ||||
|                               'alias': channel.alias, | ||||
|                             }, | ||||
|                           ).then((value) { | ||||
|                             if (mounted) _refreshChannels(); | ||||
|                           }); | ||||
|                         }, | ||||
|                       ); | ||||
|                     } | ||||
|  | ||||
|                     return ListTile( | ||||
|                       title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name), | ||||
|                       title: Text(channel.name), | ||||
|                       subtitle: lastMessage != null | ||||
|                           ? Text( | ||||
|                               '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||
| @@ -218,15 +255,14 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|                               overflow: TextOverflow.ellipsis, | ||||
|                             ) | ||||
|                           : Text( | ||||
|                               'channelDirectMessageDescription'.tr(args: [ | ||||
|                                 '@${ud.getAccountFromCache(otherMember?.accountId)?.name}', | ||||
|                               ]), | ||||
|                               channel.description, | ||||
|                               maxLines: 1, | ||||
|                               overflow: TextOverflow.ellipsis, | ||||
|                             ), | ||||
|                       contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                       leading: AccountImage( | ||||
|                         content: ud.getAccountFromCache(otherMember?.accountId)?.avatar, | ||||
|                         content: null, | ||||
|                         fallbackWidget: const Icon(Symbols.chat, size: 20), | ||||
|                       ), | ||||
|                       onTap: () { | ||||
|                         GoRouter.of(context).pushNamed( | ||||
| @@ -240,39 +276,8 @@ class _ChatScreenState extends State<ChatScreen> { | ||||
|                         }); | ||||
|                       }, | ||||
|                     ); | ||||
|                   } | ||||
|  | ||||
|                   return ListTile( | ||||
|                     title: Text(channel.name), | ||||
|                     subtitle: lastMessage != null | ||||
|                         ? Text( | ||||
|                             '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||
|                             maxLines: 1, | ||||
|                             overflow: TextOverflow.ellipsis, | ||||
|                           ) | ||||
|                         : Text( | ||||
|                             channel.description, | ||||
|                             maxLines: 1, | ||||
|                             overflow: TextOverflow.ellipsis, | ||||
|                           ), | ||||
|                     contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                     leading: AccountImage( | ||||
|                       content: null, | ||||
|                       fallbackWidget: const Icon(Symbols.chat, size: 20), | ||||
|                     ), | ||||
|                     onTap: () { | ||||
|                       GoRouter.of(context).pushNamed( | ||||
|                         'chatRoom', | ||||
|                         pathParameters: { | ||||
|                           'scope': channel.realm?.alias ?? 'global', | ||||
|                           'alias': channel.alias, | ||||
|                         }, | ||||
|                       ).then((value) { | ||||
|                         if (value == true) _refreshChannels(); | ||||
|                       }); | ||||
|                     }, | ||||
|                   ); | ||||
|                 }, | ||||
|                   }, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|   | ||||
| @@ -9,10 +9,12 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/chat_call.dart'; | ||||
| import 'package:surface/widgets/chat/call/call_controls.dart'; | ||||
| import 'package:surface/widgets/chat/call/call_participant.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| class CallRoomScreen extends StatefulWidget { | ||||
|   final String scope; | ||||
|   final String alias; | ||||
|  | ||||
|   const CallRoomScreen({super.key, required this.scope, required this.alias}); | ||||
|  | ||||
|   @override | ||||
| @@ -35,8 +37,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> { | ||||
|     return Stack( | ||||
|       children: [ | ||||
|         Container( | ||||
|           color: | ||||
|               Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75), | ||||
|           color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75), | ||||
|           child: call.focusTrack != null | ||||
|               ? InteractiveParticipantWidget( | ||||
|                   isFixedAvatar: false, | ||||
| @@ -71,8 +72,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> { | ||||
|                       color: Theme.of(context).cardColor, | ||||
|                       participant: track, | ||||
|                       onTap: () { | ||||
|                         if (track.participant.sid != | ||||
|                             call.focusTrack?.participant.sid) { | ||||
|                         if (track.participant.sid != call.focusTrack?.participant.sid) { | ||||
|                           call.setFocusTrack(track); | ||||
|                         } | ||||
|                       }, | ||||
| @@ -114,14 +114,10 @@ class _CallRoomScreenState extends State<CallRoomScreen> { | ||||
|             child: ClipRRect( | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|               child: InteractiveParticipantWidget( | ||||
|                 color: Theme.of(context) | ||||
|                     .colorScheme | ||||
|                     .surfaceContainerHigh | ||||
|                     .withOpacity(0.75), | ||||
|                 color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75), | ||||
|                 participant: track, | ||||
|                 onTap: () { | ||||
|                   if (track.participant.sid != | ||||
|                       call.focusTrack?.participant.sid) { | ||||
|                   if (track.participant.sid != call.focusTrack?.participant.sid) { | ||||
|                     call.setFocusTrack(track); | ||||
|                   } | ||||
|                 }, | ||||
| @@ -152,157 +148,134 @@ class _CallRoomScreenState extends State<CallRoomScreen> { | ||||
|     return ListenableBuilder( | ||||
|         listenable: call, | ||||
|         builder: (context, _) { | ||||
|           return Scaffold( | ||||
|           return AppScaffold( | ||||
|             appBar: AppBar( | ||||
|               title: RichText( | ||||
|                 textAlign: TextAlign.center, | ||||
|                 text: TextSpan(children: [ | ||||
|                   TextSpan( | ||||
|                     text: 'call'.tr(), | ||||
|                     style: Theme.of(context) | ||||
|                         .textTheme | ||||
|                         .titleLarge! | ||||
|                         .copyWith(color: Colors.white), | ||||
|                     style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white), | ||||
|                   ), | ||||
|                   const TextSpan(text: '\n'), | ||||
|                   TextSpan( | ||||
|                     text: call.lastDuration.toString(), | ||||
|                     style: Theme.of(context) | ||||
|                         .textTheme | ||||
|                         .bodySmall! | ||||
|                         .copyWith(color: Colors.white), | ||||
|                     style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white), | ||||
|                   ), | ||||
|                 ]), | ||||
|               ), | ||||
|             ), | ||||
|             body: SafeArea( | ||||
|               child: GestureDetector( | ||||
|                 behavior: HitTestBehavior.translucent, | ||||
|                 child: Column( | ||||
|                   children: [ | ||||
|             body: GestureDetector( | ||||
|               behavior: HitTestBehavior.translucent, | ||||
|               child: Column( | ||||
|                 children: [ | ||||
|                   SizedBox( | ||||
|                     width: MediaQuery.of(context).size.width, | ||||
|                     height: 64, | ||||
|                     child: Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                       children: [ | ||||
|                         Builder(builder: (context) { | ||||
|                           final call = context.read<ChatCallProvider>(); | ||||
|                           final connectionQuality = | ||||
|                               call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown; | ||||
|                           return Expanded( | ||||
|                             child: Column( | ||||
|                               mainAxisSize: MainAxisSize.min, | ||||
|                               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                               children: [ | ||||
|                                 Row( | ||||
|                                   children: [ | ||||
|                                     Text( | ||||
|                                       call.channel?.name ?? 'unknown'.tr(), | ||||
|                                       style: const TextStyle( | ||||
|                                         fontWeight: FontWeight.bold, | ||||
|                                       ), | ||||
|                                     ), | ||||
|                                     const Gap(6), | ||||
|                                     Text(call.lastDuration.toString()) | ||||
|                                   ], | ||||
|                                 ), | ||||
|                                 Row( | ||||
|                                   children: [ | ||||
|                                     Text( | ||||
|                                       { | ||||
|                                         livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(), | ||||
|                                         livekit.ConnectionState.connected: 'callStatusConnected'.tr(), | ||||
|                                         livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(), | ||||
|                                         livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(), | ||||
|                                       }[call.room.connectionState]!, | ||||
|                                     ), | ||||
|                                     const Gap(6), | ||||
|                                     if (connectionQuality != livekit.ConnectionQuality.unknown) | ||||
|                                       Icon( | ||||
|                                         { | ||||
|                                           livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt, | ||||
|                                           livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar, | ||||
|                                           livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar, | ||||
|                                         }[connectionQuality], | ||||
|                                         color: { | ||||
|                                           livekit.ConnectionQuality.excellent: Colors.green, | ||||
|                                           livekit.ConnectionQuality.good: Colors.orange, | ||||
|                                           livekit.ConnectionQuality.poor: Colors.red, | ||||
|                                         }[connectionQuality], | ||||
|                                         size: 16, | ||||
|                                       ) | ||||
|                                     else | ||||
|                                       const SizedBox( | ||||
|                                         width: 12, | ||||
|                                         height: 12, | ||||
|                                         child: CircularProgressIndicator( | ||||
|                                           color: Colors.white, | ||||
|                                           strokeWidth: 2, | ||||
|                                         ), | ||||
|                                       ).padding(all: 3), | ||||
|                                   ], | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ), | ||||
|                           ); | ||||
|                         }), | ||||
|                         Row( | ||||
|                           children: [ | ||||
|                             IconButton( | ||||
|                               icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view), | ||||
|                               onPressed: () { | ||||
|                                 _switchLayout(); | ||||
|                               }, | ||||
|                             ), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ], | ||||
|                     ).padding(left: 20, right: 16), | ||||
|                   ), | ||||
|                   Expanded( | ||||
|                     child: Material( | ||||
|                       color: Theme.of(context).colorScheme.surfaceContainerLow, | ||||
|                       child: Builder( | ||||
|                         builder: (context) { | ||||
|                           switch (_layoutMode) { | ||||
|                             case 1: | ||||
|                               return _buildGridLayout(); | ||||
|                             default: | ||||
|                               return _buildListLayout(); | ||||
|                           } | ||||
|                         }, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                   if (call.room.localParticipant != null) | ||||
|                     SizedBox( | ||||
|                       width: MediaQuery.of(context).size.width, | ||||
|                       height: 64, | ||||
|                       child: Row( | ||||
|                         mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                         children: [ | ||||
|                           Builder(builder: (context) { | ||||
|                             final call = context.read<ChatCallProvider>(); | ||||
|                             final connectionQuality = | ||||
|                                 call.room.localParticipant?.connectionQuality ?? | ||||
|                                     livekit.ConnectionQuality.unknown; | ||||
|                             return Expanded( | ||||
|                               child: Column( | ||||
|                                 mainAxisSize: MainAxisSize.min, | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                                 children: [ | ||||
|                                   Row( | ||||
|                                     children: [ | ||||
|                                       Text( | ||||
|                                         call.channel?.name ?? 'unknown'.tr(), | ||||
|                                         style: const TextStyle( | ||||
|                                           fontWeight: FontWeight.bold, | ||||
|                                         ), | ||||
|                                       ), | ||||
|                                       const Gap(6), | ||||
|                                       Text(call.lastDuration.toString()) | ||||
|                                     ], | ||||
|                                   ), | ||||
|                                   Row( | ||||
|                                     children: [ | ||||
|                                       Text( | ||||
|                                         { | ||||
|                                           livekit.ConnectionState.disconnected: | ||||
|                                               'callStatusDisconnected'.tr(), | ||||
|                                           livekit.ConnectionState.connected: | ||||
|                                               'callStatusConnected'.tr(), | ||||
|                                           livekit.ConnectionState.connecting: | ||||
|                                               'callStatusConnecting'.tr(), | ||||
|                                           livekit.ConnectionState.reconnecting: | ||||
|                                               'callStatusReconnecting'.tr(), | ||||
|                                         }[call.room.connectionState]!, | ||||
|                                       ), | ||||
|                                       const Gap(6), | ||||
|                                       if (connectionQuality != | ||||
|                                           livekit.ConnectionQuality.unknown) | ||||
|                                         Icon( | ||||
|                                           { | ||||
|                                             livekit.ConnectionQuality.excellent: | ||||
|                                                 Icons.signal_cellular_alt, | ||||
|                                             livekit.ConnectionQuality.good: | ||||
|                                                 Icons.signal_cellular_alt_2_bar, | ||||
|                                             livekit.ConnectionQuality.poor: | ||||
|                                                 Icons.signal_cellular_alt_1_bar, | ||||
|                                           }[connectionQuality], | ||||
|                                           color: { | ||||
|                                             livekit.ConnectionQuality.excellent: | ||||
|                                                 Colors.green, | ||||
|                                             livekit.ConnectionQuality.good: | ||||
|                                                 Colors.orange, | ||||
|                                             livekit.ConnectionQuality.poor: | ||||
|                                                 Colors.red, | ||||
|                                           }[connectionQuality], | ||||
|                                           size: 16, | ||||
|                                         ) | ||||
|                                       else | ||||
|                                         const SizedBox( | ||||
|                                           width: 12, | ||||
|                                           height: 12, | ||||
|                                           child: CircularProgressIndicator( | ||||
|                                             color: Colors.white, | ||||
|                                             strokeWidth: 2, | ||||
|                                           ), | ||||
|                                         ).padding(all: 3), | ||||
|                                     ], | ||||
|                                   ), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ); | ||||
|                           }), | ||||
|                           Row( | ||||
|                             children: [ | ||||
|                               IconButton( | ||||
|                                 icon: _layoutMode == 0 | ||||
|                                     ? const Icon(Icons.view_list) | ||||
|                                     : const Icon(Icons.grid_view), | ||||
|                                 onPressed: () { | ||||
|                                   _switchLayout(); | ||||
|                                 }, | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ], | ||||
|                       ).padding(left: 20, right: 16), | ||||
|                     ), | ||||
|                     Expanded( | ||||
|                       child: Material( | ||||
|                         color: | ||||
|                             Theme.of(context).colorScheme.surfaceContainerLow, | ||||
|                         child: Builder( | ||||
|                           builder: (context) { | ||||
|                             switch (_layoutMode) { | ||||
|                               case 1: | ||||
|                                 return _buildGridLayout(); | ||||
|                               default: | ||||
|                                 return _buildListLayout(); | ||||
|                             } | ||||
|                           }, | ||||
|                         ), | ||||
|                       child: ControlsWidget( | ||||
|                         call.room, | ||||
|                         call.room.localParticipant!, | ||||
|                       ), | ||||
|                     ), | ||||
|                     if (call.room.localParticipant != null) | ||||
|                       SizedBox( | ||||
|                         width: MediaQuery.of(context).size.width, | ||||
|                         child: ControlsWidget( | ||||
|                           call.room, | ||||
|                           call.room.localParticipant!, | ||||
|                         ), | ||||
|                       ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 onTap: () {}, | ||||
|                 ], | ||||
|               ), | ||||
|               onTap: () {}, | ||||
|             ), | ||||
|           ); | ||||
|         }); | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import 'package:surface/types/chat.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
|  | ||||
| class ChannelDetailScreen extends StatefulWidget { | ||||
| @@ -189,7 +190,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | ||||
|  | ||||
|     final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id; | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: _channel != null ? Text(_channel!.name) : Text('loading').tr(), | ||||
|       ), | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import 'package:surface/types/realm.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| class ChatManageScreen extends StatefulWidget { | ||||
| @@ -121,7 +122,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: widget.editingChannelAlias != null | ||||
|             ? Text('screenChatManage').tr() | ||||
|   | ||||
| @@ -20,6 +20,7 @@ import 'package:surface/widgets/chat/chat_message_input.dart'; | ||||
| import 'package:surface/widgets/chat/chat_typing_indicator.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
|  | ||||
| import '../../providers/user_directory.dart'; | ||||
| @@ -211,7 +212,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|     final call = context.watch<ChatCallProvider>(); | ||||
|     final ud = context.read<UserDirectoryProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text( | ||||
|           _channel?.type == 1 | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import 'package:animations/animations.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; | ||||
| @@ -6,11 +7,14 @@ 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'; | ||||
| import 'package:surface/types/post.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/post/post_item.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
|  | ||||
| @@ -93,7 +97,9 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     final cfg = context.read<ConfigProvider>(); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       floatingActionButtonLocation: ExpandableFab.location, | ||||
|       floatingActionButton: ExpandableFab( | ||||
|         key: _fabKey, | ||||
| @@ -210,6 +216,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             const SliverGap(12), | ||||
|             SliverInfiniteList( | ||||
|               itemCount: _posts.length, | ||||
|               isLoading: _isBusy, | ||||
| @@ -217,27 +224,39 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|               hasReachedMax: _postCount != null && _posts.length >= _postCount!, | ||||
|               onFetchData: _fetchPosts, | ||||
|               itemBuilder: (context, idx) { | ||||
|                 return GestureDetector( | ||||
|                   child: PostItem( | ||||
|                     data: _posts[idx], | ||||
|                     maxWidth: 640, | ||||
|                     onChanged: (data) { | ||||
|                       setState(() => _posts[idx] = data); | ||||
|                     }, | ||||
|                     onDeleted: () { | ||||
|                       _refreshPosts(); | ||||
|                     }, | ||||
|                 return Center( | ||||
|                   child: OpenContainer( | ||||
|                     closedBuilder: (_, __) => Container( | ||||
|                       constraints: const BoxConstraints(maxWidth: 640), | ||||
|                       child: PostItem( | ||||
|                         data: _posts[idx], | ||||
|                         maxWidth: 640, | ||||
|                         onChanged: (data) { | ||||
|                           setState(() => _posts[idx] = data); | ||||
|                         }, | ||||
|                         onDeleted: () { | ||||
|                           _refreshPosts(); | ||||
|                         }, | ||||
|                       ), | ||||
|                     ), | ||||
|                     openBuilder: (_, close) => PostDetailScreen( | ||||
|                       slug: _posts[idx].id.toString(), | ||||
|                       preload: _posts[idx], | ||||
|                       onBack: close, | ||||
|                     ), | ||||
|                     openColor: Colors.transparent, | ||||
|                     openElevation: 0, | ||||
|                     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)), | ||||
|                     ), | ||||
|                   ), | ||||
|                   onTap: () { | ||||
|                     GoRouter.of(context).pushNamed( | ||||
|                       'postDetail', | ||||
|                       pathParameters: {'slug': _posts[idx].id.toString()}, | ||||
|                       extra: _posts[idx], | ||||
|                     ); | ||||
|                   }, | ||||
|                 ); | ||||
|               }, | ||||
|               separatorBuilder: (context, index) => const Divider(height: 1), | ||||
|               separatorBuilder: (_, __) => const Gap(8), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|   | ||||
| @@ -11,6 +11,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/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| import '../providers/userinfo.dart'; | ||||
| import '../widgets/unauthorized_hint.dart'; | ||||
| @@ -180,7 +181,7 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|  | ||||
|     if (!ua.isAuthorized) { | ||||
|       return Scaffold( | ||||
|       return AppScaffold( | ||||
|         appBar: AppBar( | ||||
|           leading: AutoAppBarLeading(), | ||||
|           title: Text('screenFriend').tr(), | ||||
| @@ -191,7 +192,7 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: AutoAppBarLeading(), | ||||
|         title: Text('screenFriend').tr(), | ||||
| @@ -233,52 +234,56 @@ class _FriendScreenState extends State<FriendScreen> { | ||||
|           if (_requests.isNotEmpty || _blocks.isNotEmpty) | ||||
|             const Divider(height: 1), | ||||
|           Expanded( | ||||
|             child: RefreshIndicator( | ||||
|               onRefresh: () => Future.wait([ | ||||
|                 _fetchRelations(), | ||||
|                 _fetchRequests(), | ||||
|               ]), | ||||
|               child: ListView.builder( | ||||
|                 itemCount: _relations.length, | ||||
|                 itemBuilder: (context, index) { | ||||
|                   final relation = _relations[index]; | ||||
|                   final other = relation.related; | ||||
|                   return ListTile( | ||||
|                     contentPadding: const EdgeInsets.only(right: 24, left: 16), | ||||
|                     leading: AccountImage(content: other?.avatar), | ||||
|                     title: Text(other?.nick ?? 'unknown'), | ||||
|                     subtitle: Text(other?.nick ?? 'unknown'), | ||||
|                     trailing: SizedBox( | ||||
|                       height: 48, | ||||
|                       width: 120, | ||||
|                       child: Column( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         mainAxisAlignment: MainAxisAlignment.center, | ||||
|                         crossAxisAlignment: CrossAxisAlignment.end, | ||||
|                         children: [ | ||||
|                           Row( | ||||
|                             mainAxisAlignment: MainAxisAlignment.end, | ||||
|                             children: [ | ||||
|                               InkWell( | ||||
|                                 onTap: _isUpdating | ||||
|                                     ? null | ||||
|                                     : () => _changeRelation(relation, 2), | ||||
|                                 child: Text('friendBlock').tr(), | ||||
|                               ), | ||||
|                               const Gap(8), | ||||
|                               InkWell( | ||||
|                                 onTap: _isUpdating | ||||
|                                     ? null | ||||
|                                     : () => _deleteRelation(relation), | ||||
|                                 child: Text('friendDeleteAction').tr(), | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ], | ||||
|             child: MediaQuery.removePadding( | ||||
|               context: context, | ||||
|               removeTop: true, | ||||
|               child: RefreshIndicator( | ||||
|                 onRefresh: () => Future.wait([ | ||||
|                   _fetchRelations(), | ||||
|                   _fetchRequests(), | ||||
|                 ]), | ||||
|                 child: ListView.builder( | ||||
|                   itemCount: _relations.length, | ||||
|                   itemBuilder: (context, index) { | ||||
|                     final relation = _relations[index]; | ||||
|                     final other = relation.related; | ||||
|                     return ListTile( | ||||
|                       contentPadding: const EdgeInsets.only(right: 24, left: 16), | ||||
|                       leading: AccountImage(content: other?.avatar), | ||||
|                       title: Text(other?.nick ?? 'unknown'), | ||||
|                       subtitle: Text(other?.nick ?? 'unknown'), | ||||
|                       trailing: SizedBox( | ||||
|                         height: 48, | ||||
|                         width: 120, | ||||
|                         child: Column( | ||||
|                           mainAxisSize: MainAxisSize.min, | ||||
|                           mainAxisAlignment: MainAxisAlignment.center, | ||||
|                           crossAxisAlignment: CrossAxisAlignment.end, | ||||
|                           children: [ | ||||
|                             Row( | ||||
|                               mainAxisAlignment: MainAxisAlignment.end, | ||||
|                               children: [ | ||||
|                                 InkWell( | ||||
|                                   onTap: _isUpdating | ||||
|                                       ? null | ||||
|                                       : () => _changeRelation(relation, 2), | ||||
|                                   child: Text('friendBlock').tr(), | ||||
|                                 ), | ||||
|                                 const Gap(8), | ||||
|                                 InkWell( | ||||
|                                   onTap: _isUpdating | ||||
|                                       ? null | ||||
|                                       : () => _deleteRelation(relation), | ||||
|                                   child: Text('friendDeleteAction').tr(), | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ); | ||||
|                 }, | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:relative_time/relative_time.dart'; | ||||
| @@ -22,9 +23,11 @@ import 'package:surface/providers/special_day.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/providers/widget.dart'; | ||||
| import 'package:surface/types/check_in.dart'; | ||||
| import 'package:surface/types/news.dart'; | ||||
| import 'package:surface/types/post.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/post/post_item.dart'; | ||||
|  | ||||
| class HomeScreenDashEntry { | ||||
| @@ -48,12 +51,12 @@ class HomeScreen extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _HomeScreenState extends State<HomeScreen> { | ||||
|   static const List<HomeScreenDashEntry> kCards = [ | ||||
|   late final List<HomeScreenDashEntry> kCards = [ | ||||
|     HomeScreenDashEntry( | ||||
|       name: 'dashEntryRecommendation', | ||||
|       cols: 2, | ||||
|       rows: 2, | ||||
|       child: _HomeDashRecommendationPostWidget(), | ||||
|       rows: 2, | ||||
|       cols: 2, | ||||
|     ), | ||||
|     HomeScreenDashEntry( | ||||
|       name: 'dashEntryCheckIn', | ||||
| @@ -63,11 +66,16 @@ class _HomeScreenState extends State<HomeScreen> { | ||||
|       name: 'dashEntryNotification', | ||||
|       child: _HomeDashNotificationWidget(), | ||||
|     ), | ||||
|     HomeScreenDashEntry( | ||||
|       name: 'dashEntryTodayNews', | ||||
|       child: _HomeDashTodayNews(), | ||||
|       cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2, | ||||
|     ), | ||||
|   ]; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: AutoAppBarLeading(), | ||||
|         title: Text("screenHome").tr(), | ||||
| @@ -197,7 +205,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> { | ||||
|       return Card( | ||||
|         child: ListTile( | ||||
|           leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24), | ||||
|           title: Text('pending$name').tr(args: [RelativeTime(context).format(date)]), | ||||
|           title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]), | ||||
|           subtitle: Row( | ||||
|             crossAxisAlignment: CrossAxisAlignment.center, | ||||
|             children: [ | ||||
| @@ -229,6 +237,106 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _HomeDashTodayNews extends StatefulWidget { | ||||
|   const _HomeDashTodayNews(); | ||||
|  | ||||
|   @override | ||||
|   State<_HomeDashTodayNews> createState() => _HomeDashTodayNewsState(); | ||||
| } | ||||
|  | ||||
| class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> { | ||||
|   SnNewsArticle? _article; | ||||
|  | ||||
|   Future<void> _fetchArticle() async { | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/re/news/today'); | ||||
|       _article = SnNewsArticle.fromJson(resp.data['data']); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|       rethrow; | ||||
|     } finally { | ||||
|       setState(() {}); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   initState() { | ||||
|     super.initState(); | ||||
|     _fetchArticle(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Card( | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Row( | ||||
|             children: [ | ||||
|               const Icon(Symbols.newspaper), | ||||
|               const Gap(8), | ||||
|               Text( | ||||
|                 'newsToday', | ||||
|                 style: Theme.of(context).textTheme.titleLarge, | ||||
|               ).tr() | ||||
|             ], | ||||
|           ).padding(horizontal: 18, top: 12, bottom: 8), | ||||
|           if (_article != null) | ||||
|             Expanded( | ||||
|               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: MediaQuery.of(context).size.width >= 640 ? 2 : 1, | ||||
|                       overflow: TextOverflow.ellipsis, | ||||
|                     ), | ||||
|                     Text( | ||||
|                       parse(_article!.description).children.map((e) => e.text.trim()).join(), | ||||
|                       maxLines: 3, | ||||
|                       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); | ||||
|                     }), | ||||
|                   ], | ||||
|                 ).padding(horizontal: 16), | ||||
|                 onTap: () { | ||||
|                   GoRouter.of(context).pushNamed( | ||||
|                     'newsDetail', | ||||
|                     pathParameters: {'hash': _article!.hash}, | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|             ) | ||||
|           else | ||||
|             Expanded( | ||||
|               child: Center( | ||||
|                 child: CircularProgressIndicator(), | ||||
|               ), | ||||
|             ) | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _HomeDashCheckInWidget extends StatefulWidget { | ||||
|   const _HomeDashCheckInWidget(); | ||||
|  | ||||
| @@ -387,6 +495,8 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> { | ||||
|                         Text( | ||||
|                           'dailyCheckInNone', | ||||
|                           style: Theme.of(context).textTheme.bodyLarge, | ||||
|                           maxLines: 2, | ||||
|                           overflow: TextOverflow.ellipsis, | ||||
|                         ).tr(), | ||||
|                       ], | ||||
|                     ) | ||||
| @@ -404,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, | ||||
|                           ) | ||||
|                       ], | ||||
|                     ), | ||||
|             ), | ||||
|   | ||||
							
								
								
									
										241
									
								
								lib/screens/news/news_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								lib/screens/news/news_detail.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,241 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/gestures.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:html/dom.dart' as dom; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/news.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:flutter_inappwebview/flutter_inappwebview.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| class NewsDetailScreen extends StatefulWidget { | ||||
|   final String hash; | ||||
|  | ||||
|   const NewsDetailScreen({super.key, required this.hash}); | ||||
|  | ||||
|   @override | ||||
|   State<NewsDetailScreen> createState() => _NewsDetailScreenState(); | ||||
| } | ||||
|  | ||||
| class _NewsDetailScreenState extends State<NewsDetailScreen> { | ||||
|   SnNewsArticle? _article; | ||||
|   dom.Document? _articleFragment; | ||||
|  | ||||
|   Future<void> _fetchArticle() async { | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/re/news/${widget.hash}'); | ||||
|       _article = SnNewsArticle.fromJson(resp.data); | ||||
|       _articleFragment = parse(_article!.content); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err).then((_) { | ||||
|         if (!mounted) return; | ||||
|         Navigator.pop(context); | ||||
|       }); | ||||
|     } finally { | ||||
|       setState(() {}); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   List<Widget> _parseHtmlToWidgets(Iterable<dom.Element>? elements) { | ||||
|     if (elements == null) return []; | ||||
|  | ||||
|     final List<Widget> widgets = []; | ||||
|  | ||||
|     for (final node in elements) { | ||||
|       switch (node.localName) { | ||||
|         case 'h1': | ||||
|         case 'h2': | ||||
|         case 'h3': | ||||
|         case 'h4': | ||||
|         case 'h5': | ||||
|         case 'h6': | ||||
|           widgets.add(Text(node.text.trim(), style: Theme.of(context).textTheme.titleMedium)); | ||||
|           break; | ||||
|         case 'p': | ||||
|           if (node.text.trim().isEmpty) continue; | ||||
|           widgets.add( | ||||
|             Text.rich( | ||||
|               TextSpan( | ||||
|                 text: node.text.trim(), | ||||
|                 children: [ | ||||
|                   for (final child in node.children) | ||||
|                     switch (child.localName) { | ||||
|                       'a' => TextSpan( | ||||
|                           text: child.text.trim(), | ||||
|                           style: const TextStyle(decoration: TextDecoration.underline), | ||||
|                           recognizer: TapGestureRecognizer() | ||||
|                             ..onTap = () { | ||||
|                               launchUrlString(child.attributes['href']!); | ||||
|                             }, | ||||
|                         ), | ||||
|                       _ => TextSpan(text: child.text.trim()), | ||||
|                     }, | ||||
|                 ], | ||||
|               ), | ||||
|               style: Theme.of(context).textTheme.bodyLarge, | ||||
|             ), | ||||
|           ); | ||||
|           break; | ||||
|         case 'a': | ||||
|           // drop single link | ||||
|           break; | ||||
|         case 'div': | ||||
|           // ignore div text, normally it is not meaningful | ||||
|           widgets.addAll(_parseHtmlToWidgets(node.children)); | ||||
|           break; | ||||
|         case 'hr': | ||||
|           widgets.add(const Divider()); | ||||
|           break; | ||||
|         case 'img': | ||||
|           var src = node.attributes['src']; | ||||
|           if (src == null) break; | ||||
|           final width = double.tryParse(node.attributes['width'] ?? 'null'); | ||||
|           final height = double.tryParse(node.attributes['height'] ?? 'null'); | ||||
|           final ratio = width != null && height != null ? width / height : 1.0; | ||||
|           if (src.startsWith('//')) { | ||||
|             src = 'https:$src'; | ||||
|           } else if (!src.startsWith('http')) { | ||||
|             final baseUri = Uri.parse(_article!.url); | ||||
|             final baseUrl = '${baseUri.scheme}://${baseUri.host}'; | ||||
|             src = '$baseUrl/$src'; | ||||
|           } | ||||
|           widgets.add( | ||||
|             AspectRatio( | ||||
|               aspectRatio: ratio, | ||||
|               child: Container( | ||||
|                 decoration: BoxDecoration( | ||||
|                   borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|                   border: Border.all( | ||||
|                     color: Theme.of(context).dividerColor, | ||||
|                     width: 1, | ||||
|                   ), | ||||
|                 ), | ||||
|                 height: height ?? double.infinity, | ||||
|                 child: ClipRRect( | ||||
|                   borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|                   child: Container( | ||||
|                     color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                     child: AutoResizeUniversalImage( | ||||
|                       src, | ||||
|                       fit: width != null && height != null ? BoxFit.cover : BoxFit.contain, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ); | ||||
|           break; | ||||
|         default: | ||||
|           widgets.addAll(_parseHtmlToWidgets(node.children)); | ||||
|           break; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return widgets; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _fetchArticle(); | ||||
|   } | ||||
|  | ||||
|   bool _isReadingFromReader = true; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text(_article?.title ?? 'loading'.tr()), | ||||
|       ), | ||||
|       body: Column( | ||||
|         children: [ | ||||
|           MaterialBanner( | ||||
|             dividerColor: Colors.transparent, | ||||
|             leading: const Icon(Icons.info), | ||||
|             content: Text(_isReadingFromReader ? 'newsReadingFromReader'.tr() : 'newsReadingFromOriginal'.tr()), | ||||
|             actions: [ | ||||
|               TextButton( | ||||
|                 child: Text('newsReadingProviderSwap').tr(), | ||||
|                 onPressed: () { | ||||
|                   setState(() => _isReadingFromReader = !_isReadingFromReader); | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|           if (_articleFragment != null && _isReadingFromReader) | ||||
|             Expanded( | ||||
|               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( | ||||
|               child: InAppWebView( | ||||
|                 key: GlobalKey(), | ||||
|                 initialUrlRequest: URLRequest(url: WebUri(_article!.url)), | ||||
|               ), | ||||
|             ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										239
									
								
								lib/screens/news/news_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								lib/screens/news/news_list.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,239 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:html/parser.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/news.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'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
|  | ||||
| class NewsScreen extends StatefulWidget { | ||||
|   const NewsScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<NewsScreen> createState() => _NewsScreenState(); | ||||
| } | ||||
|  | ||||
| class _NewsScreenState extends State<NewsScreen> { | ||||
|   List<SnNewsSource>? _sources; | ||||
|  | ||||
|   @override | ||||
|   initState() { | ||||
|     super.initState(); | ||||
|     _fetchSources(); | ||||
|   } | ||||
|  | ||||
|   Future<void> _fetchSources() async { | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/re/well-known/sources'); | ||||
|       _sources = List<SnNewsSource>.from( | ||||
|         resp.data?.map((e) => SnNewsSource.fromJson(e)) ?? [], | ||||
|       ); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() {}); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (_sources == null) { | ||||
|       return AppScaffold( | ||||
|         appBar: AppBar( | ||||
|           leading: AutoAppBarLeading(), | ||||
|           title: Text('screenNews').tr(), | ||||
|         ), | ||||
|         body: Center( | ||||
|           child: CircularProgressIndicator(), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return DefaultTabController( | ||||
|       length: _sources!.length + 1, | ||||
|       child: AppScaffold( | ||||
|         body: NestedScrollView( | ||||
|           headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { | ||||
|             return <Widget>[ | ||||
|               SliverOverlapAbsorber( | ||||
|                 handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), | ||||
|                 sliver: SliverAppBar( | ||||
|                   leading: AutoAppBarLeading(), | ||||
|                   title: Text('screenNews').tr(), | ||||
|                   floating: true, | ||||
|                   snap: true, | ||||
|                   bottom: TabBar( | ||||
|                     isScrollable: true, | ||||
|                     tabs: [ | ||||
|                       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), | ||||
|                         ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ]; | ||||
|           }, | ||||
|           body: TabBarView( | ||||
|             children: [ | ||||
|               _NewsArticleListWidget(allSources: _sources!), | ||||
|               for (final source in _sources!) | ||||
|                 _NewsArticleListWidget( | ||||
|                   source: source.id, | ||||
|                   allSources: _sources!, | ||||
|                 ), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _NewsArticleListWidget extends StatefulWidget { | ||||
|   final String? source; | ||||
|   final List<SnNewsSource> allSources; | ||||
|  | ||||
|   const _NewsArticleListWidget({this.source, required this.allSources}); | ||||
|  | ||||
|   @override | ||||
|   State<_NewsArticleListWidget> createState() => _NewsArticleListWidgetState(); | ||||
| } | ||||
|  | ||||
| class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> { | ||||
|   bool _isBusy = false; | ||||
|  | ||||
|   int? _totalCount; | ||||
|   final List<SnNewsArticle> _articles = List.empty(growable: true); | ||||
|  | ||||
|   Future<void> _fetchArticles() async { | ||||
|     setState(() => _isBusy = true); | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get('/cgi/re/news', queryParameters: { | ||||
|         'take': 10, | ||||
|         'offset': _articles.length, | ||||
|         if (widget.source != null) 'source': widget.source, | ||||
|       }); | ||||
|       _totalCount = resp.data['count']; | ||||
|       _articles.addAll(List<SnNewsArticle>.from( | ||||
|         resp.data['data']?.map((e) => SnNewsArticle.fromJson(e)) ?? [], | ||||
|       )); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isBusy = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _fetchArticles(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return MediaQuery.removePadding( | ||||
|       context: context, | ||||
|       removeTop: true, | ||||
|       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 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}', | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ), | ||||
|                           ), | ||||
|                         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), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -14,12 +14,23 @@ import 'package:surface/widgets/app_bar_leading.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/markdown_content.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_item.dart'; | ||||
| 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}); | ||||
|  | ||||
| @@ -35,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; | ||||
| @@ -137,7 +141,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|  | ||||
|     if (!ua.isAuthorized) { | ||||
|       return Scaffold( | ||||
|       return AppScaffold( | ||||
|         appBar: AppBar( | ||||
|           leading: AutoAppBarLeading(), | ||||
|           title: Text('screenNotification').tr(), | ||||
| @@ -148,7 +152,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: AutoAppBarLeading(), | ||||
|         title: Text('screenNotification').tr(), | ||||
| @@ -206,10 +210,11 @@ class _NotificationScreenState extends State<NotificationScreen> { | ||||
|                                 style: Theme.of(context).textTheme.titleSmall, | ||||
|                               ), | ||||
|                             if (nty.subtitle != null) const Gap(4), | ||||
|                             MarkdownTextContent( | ||||
|                               content: nty.body, | ||||
|                               isAutoWarp: true, | ||||
|                               isSelectable: true, | ||||
|                             SelectionArea( | ||||
|                               child: MarkdownTextContent( | ||||
|                                 content: nty.body, | ||||
|                                 isAutoWarp: true, | ||||
|                               ), | ||||
|                             ), | ||||
|                             if ([ | ||||
|                                   'interactive.feedback', | ||||
|   | ||||
| @@ -13,6 +13,8 @@ import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_background.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_comment_list.dart'; | ||||
| import 'package:surface/widgets/post/post_item.dart'; | ||||
| import 'package:surface/widgets/post/post_mini_editor.dart'; | ||||
| @@ -20,12 +22,9 @@ import 'package:surface/widgets/post/post_mini_editor.dart'; | ||||
| class PostDetailScreen extends StatefulWidget { | ||||
|   final String slug; | ||||
|   final SnPost? preload; | ||||
|   final Function? onBack; | ||||
|  | ||||
|   const PostDetailScreen({ | ||||
|     super.key, | ||||
|     required this.slug, | ||||
|     this.preload, | ||||
|   }); | ||||
|   const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack}); | ||||
|  | ||||
|   @override | ||||
|   State<PostDetailScreen> createState() => _PostDetailScreenState(); | ||||
| @@ -67,121 +66,129 @@ class _PostDetailScreenState extends State<PostDetailScreen> { | ||||
|     final ua = context.watch<UserProvider>(); | ||||
|     final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: BackButton( | ||||
|           onPressed: () { | ||||
|             if (GoRouter.of(context).canPop()) { | ||||
|               GoRouter.of(context).pop(context); | ||||
|               return; | ||||
|             } | ||||
|             GoRouter.of(context).replaceNamed('explore'); | ||||
|           }, | ||||
|         ), | ||||
|         title: _data?.body['title'] != null | ||||
|             ? RichText( | ||||
|                 textAlign: TextAlign.center, | ||||
|                 text: TextSpan(children: [ | ||||
|                   TextSpan( | ||||
|                     text: _data?.body['title'] ?? 'postNoun'.tr(), | ||||
|                     style: Theme.of(context).textTheme.titleLarge!.copyWith( | ||||
|                           color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                         ), | ||||
|                   ), | ||||
|                   const TextSpan(text: '\n'), | ||||
|                   TextSpan( | ||||
|                     text: 'postDetail'.tr(), | ||||
|                     style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||
|                           color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                         ), | ||||
|                   ), | ||||
|                 ]), | ||||
|               ) | ||||
|             : Text('postDetail').tr(), | ||||
|       ), | ||||
|       body: CustomScrollView( | ||||
|         slivers: [ | ||||
|           SliverToBoxAdapter( | ||||
|             child: LoadingIndicator(isActive: _isBusy), | ||||
|     return AppBackground( | ||||
|       isRoot: widget.onBack != null, | ||||
|       child: AppScaffold( | ||||
|         appBar: AppBar( | ||||
|           leading: BackButton( | ||||
|             onPressed: () { | ||||
|               if (widget.onBack != null) { | ||||
|                 widget.onBack!.call(); | ||||
|               } | ||||
|               if (GoRouter.of(context).canPop()) { | ||||
|                 GoRouter.of(context).pop(context); | ||||
|                 return; | ||||
|               } | ||||
|               GoRouter.of(context).replaceNamed('explore'); | ||||
|             }, | ||||
|           ), | ||||
|           if (_data != null) | ||||
|             SliverToBoxAdapter( | ||||
|               child: PostItem( | ||||
|                 data: _data!, | ||||
|                 maxWidth: 640, | ||||
|                 showComments: false, | ||||
|                 showFullPost: true, | ||||
|                 onChanged: (data) { | ||||
|                   setState(() => _data = data); | ||||
|                 }, | ||||
|                 onDeleted: () { | ||||
|                   Navigator.pop(context); | ||||
|                 }, | ||||
|               ), | ||||
|             ), | ||||
|           const SliverToBoxAdapter(child: Divider(height: 1)), | ||||
|           if (_data != null) | ||||
|             SliverToBoxAdapter( | ||||
|               child: Container( | ||||
|                 constraints: const BoxConstraints(maxWidth: 640), | ||||
|                 child: Row( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     const Icon(Symbols.comment, size: 24), | ||||
|                     const Gap(16), | ||||
|                     Text('postCommentsDetailed') | ||||
|                         .plural(_data!.metric.replyCount) | ||||
|                         .textStyle(Theme.of(context).textTheme.titleLarge!), | ||||
|                   ], | ||||
|                 ).padding(horizontal: 20, vertical: 12).center(), | ||||
|               ), | ||||
|             ), | ||||
|           if (_data != null && ua.isAuthorized) | ||||
|             SliverToBoxAdapter( | ||||
|               child: Container( | ||||
|                 height: 240, | ||||
|                 constraints: const BoxConstraints(maxWidth: 640), | ||||
|                 margin: | ||||
|                     ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.all(8) : EdgeInsets.zero, | ||||
|                 decoration: BoxDecoration( | ||||
|                   borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE) | ||||
|                       ? const BorderRadius.all(Radius.circular(8)) | ||||
|                       : BorderRadius.zero, | ||||
|                   border: ResponsiveBreakpoints.of(context).largerThan(MOBILE) | ||||
|                       ? Border.all( | ||||
|                           color: Theme.of(context).dividerColor, | ||||
|                           width: 1 / devicePixelRatio, | ||||
|                         ) | ||||
|                       : Border.symmetric( | ||||
|                           horizontal: BorderSide( | ||||
|                             color: Theme.of(context).dividerColor, | ||||
|                             width: 1 / devicePixelRatio, | ||||
|           title: _data?.body['title'] != null | ||||
|               ? RichText( | ||||
|                   textAlign: TextAlign.center, | ||||
|                   text: TextSpan(children: [ | ||||
|                     TextSpan( | ||||
|                       text: _data?.body['title'] ?? 'postNoun'.tr(), | ||||
|                       style: Theme.of(context).textTheme.titleLarge!.copyWith( | ||||
|                             color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                           ), | ||||
|                         ), | ||||
|                 ), | ||||
|                 child: PostMiniEditor( | ||||
|                   postReplyId: _data!.id, | ||||
|                   onPost: () { | ||||
|                     setState(() { | ||||
|                       _data = _data!.copyWith( | ||||
|                         metric: _data!.metric.copyWith( | ||||
|                           replyCount: _data!.metric.replyCount + 1, | ||||
|                         ), | ||||
|                       ); | ||||
|                     }); | ||||
|                     _childListKey.currentState!.refresh(); | ||||
|                     ), | ||||
|                     const TextSpan(text: '\n'), | ||||
|                     TextSpan( | ||||
|                       text: 'postDetail'.tr(), | ||||
|                       style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||
|                             color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                           ), | ||||
|                     ), | ||||
|                   ]), | ||||
|                   maxLines: 2, | ||||
|                   overflow: TextOverflow.ellipsis, | ||||
|                 ) | ||||
|               : Text('postDetail').tr(), | ||||
|         ), | ||||
|         body: CustomScrollView( | ||||
|           slivers: [ | ||||
|             SliverToBoxAdapter( | ||||
|               child: LoadingIndicator(isActive: _isBusy), | ||||
|             ), | ||||
|             if (_data != null) | ||||
|               SliverToBoxAdapter( | ||||
|                 child: PostItem( | ||||
|                   data: _data!, | ||||
|                   maxWidth: 640, | ||||
|                   showComments: false, | ||||
|                   showFullPost: true, | ||||
|                   onChanged: (data) { | ||||
|                     setState(() => _data = data); | ||||
|                   }, | ||||
|                   onDeleted: () { | ||||
|                     Navigator.pop(context); | ||||
|                   }, | ||||
|                 ), | ||||
|               ).center(), | ||||
|             ), | ||||
|           if (_data != null) | ||||
|             PostCommentSliverList( | ||||
|               key: _childListKey, | ||||
|               parentPostId: _data!.id, | ||||
|               maxWidth: 640, | ||||
|             ), | ||||
|           SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)), | ||||
|         ], | ||||
|               ), | ||||
|             const SliverToBoxAdapter(child: Divider(height: 1)), | ||||
|             if (_data != null) | ||||
|               SliverToBoxAdapter( | ||||
|                 child: Container( | ||||
|                   constraints: const BoxConstraints(maxWidth: 640), | ||||
|                   child: Row( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                     children: [ | ||||
|                       const Icon(Symbols.comment, size: 24), | ||||
|                       const Gap(16), | ||||
|                       Text('postCommentsDetailed') | ||||
|                           .plural(_data!.metric.replyCount) | ||||
|                           .textStyle(Theme.of(context).textTheme.titleLarge!), | ||||
|                     ], | ||||
|                   ).padding(horizontal: 20, vertical: 12).center(), | ||||
|                 ), | ||||
|               ), | ||||
|             if (_data != null && ua.isAuthorized) | ||||
|               SliverToBoxAdapter( | ||||
|                 child: Container( | ||||
|                   height: 240, | ||||
|                   constraints: const BoxConstraints(maxWidth: 640), | ||||
|                   margin: | ||||
|                       ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.all(8) : EdgeInsets.zero, | ||||
|                   decoration: BoxDecoration( | ||||
|                     borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE) | ||||
|                         ? const BorderRadius.all(Radius.circular(8)) | ||||
|                         : BorderRadius.zero, | ||||
|                     border: ResponsiveBreakpoints.of(context).largerThan(MOBILE) | ||||
|                         ? Border.all( | ||||
|                             color: Theme.of(context).dividerColor, | ||||
|                             width: 1 / devicePixelRatio, | ||||
|                           ) | ||||
|                         : Border.symmetric( | ||||
|                             horizontal: BorderSide( | ||||
|                               color: Theme.of(context).dividerColor, | ||||
|                               width: 1 / devicePixelRatio, | ||||
|                             ), | ||||
|                           ), | ||||
|                   ), | ||||
|                   child: PostMiniEditor( | ||||
|                     postReplyId: _data!.id, | ||||
|                     onPost: () { | ||||
|                       setState(() { | ||||
|                         _data = _data!.copyWith( | ||||
|                           metric: _data!.metric.copyWith( | ||||
|                             replyCount: _data!.metric.replyCount + 1, | ||||
|                           ), | ||||
|                         ); | ||||
|                       }); | ||||
|                       _childListKey.currentState!.refresh(); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ).center(), | ||||
|               ), | ||||
|             if (_data != null) | ||||
|               PostCommentSliverList( | ||||
|                 key: _childListKey, | ||||
|                 parentPostId: _data!.id, | ||||
|                 maxWidth: 640, | ||||
|               ), | ||||
|             SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_item.dart'; | ||||
| import 'package:surface/widgets/post/post_media_pending_list.dart'; | ||||
| import 'package:surface/widgets/post/post_meta_editor.dart'; | ||||
| @@ -54,7 +55,9 @@ class PostEditorScreen extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|   final PostWriteController _writeController = PostWriteController(); | ||||
|   late final PostWriteController _writeController = PostWriteController( | ||||
|     doLoadFromTemporary: widget.postEditId == null, | ||||
|   ); | ||||
|  | ||||
|   bool _isFetching = false; | ||||
|  | ||||
| @@ -126,7 +129,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|     return ListenableBuilder( | ||||
|       listenable: _writeController, | ||||
|       builder: (context, _) { | ||||
|         return Scaffold( | ||||
|         return AppScaffold( | ||||
|           appBar: AppBar( | ||||
|             leading: BackButton( | ||||
|               onPressed: () { | ||||
| @@ -301,19 +304,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                           ], | ||||
|                         ), | ||||
|                       // Content Input Area | ||||
|                       TextField( | ||||
|                         controller: _writeController.contentController, | ||||
|                         maxLines: null, | ||||
|                         decoration: InputDecoration( | ||||
|                           hintText: 'fieldPostContent'.tr(), | ||||
|                           hintStyle: TextStyle(fontSize: 14), | ||||
|                           isCollapsed: true, | ||||
|                           contentPadding: const EdgeInsets.symmetric( | ||||
|                             horizontal: 16, | ||||
|                       Container( | ||||
|                         constraints: const BoxConstraints(maxWidth: 640), | ||||
|                         child: TextField( | ||||
|                           controller: _writeController.contentController, | ||||
|                           maxLines: null, | ||||
|                           decoration: InputDecoration( | ||||
|                             hintText: 'fieldPostContent'.tr(), | ||||
|                             hintStyle: TextStyle(fontSize: 14), | ||||
|                             isCollapsed: true, | ||||
|                             contentPadding: const EdgeInsets.symmetric( | ||||
|                               horizontal: 16, | ||||
|                             ), | ||||
|                             border: InputBorder.none, | ||||
|                           ), | ||||
|                           border: InputBorder.none, | ||||
|                           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                         ), | ||||
|                         onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                       ), | ||||
|                     ] | ||||
|                         .expandIndexed( | ||||
| @@ -364,6 +370,15 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     LoadingIndicator(isActive: _isLoading), | ||||
|                     if (_writeController.isBusy && _writeController.progress != null) | ||||
|                       TweenAnimationBuilder<double>( | ||||
|                         tween: Tween(begin: 0, end: _writeController.progress), | ||||
|                         duration: Duration(milliseconds: 300), | ||||
|                         builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2), | ||||
|                       ) | ||||
|                     else if (_writeController.isBusy) | ||||
|                       const LinearProgressIndicator(value: null, minHeight: 2), | ||||
|                     Container( | ||||
|                       child: _writeController.temporaryRestored | ||||
|                           ? Container( | ||||
| @@ -394,15 +409,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | ||||
|                     ) | ||||
|                         .height(_writeController.temporaryRestored ? 32 : 0, animate: true) | ||||
|                         .animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn), | ||||
|                     LoadingIndicator(isActive: _isLoading), | ||||
|                     if (_writeController.isBusy && _writeController.progress != null) | ||||
|                       TweenAnimationBuilder<double>( | ||||
|                         tween: Tween(begin: 0, end: _writeController.progress), | ||||
|                         duration: Duration(milliseconds: 300), | ||||
|                         builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2), | ||||
|                       ) | ||||
|                     else if (_writeController.isBusy) | ||||
|                       const LinearProgressIndicator(value: null, minHeight: 2), | ||||
|                     Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                       children: [ | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/post.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_item.dart'; | ||||
| import 'package:surface/widgets/post/post_tags_field.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
| @@ -119,7 +120,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> { | ||||
|       ), | ||||
|     ]; | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text('screenPostSearch').tr(), | ||||
|         actions: [ | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/types/realm.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/post/post_item.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
| @@ -274,7 +275,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi | ||||
|  | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       body: NestedScrollView( | ||||
|         controller: _scrollController, | ||||
|         headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { | ||||
|   | ||||
| @@ -12,6 +12,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/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/unauthorized_hint.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| @@ -83,7 +84,7 @@ class _RealmScreenState extends State<RealmScreen> { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|  | ||||
|     if (!ua.isAuthorized) { | ||||
|       return Scaffold( | ||||
|       return AppScaffold( | ||||
|         appBar: AppBar( | ||||
|           leading: AutoAppBarLeading(), | ||||
|           title: Text('screenRealm').tr(), | ||||
| @@ -94,7 +95,7 @@ class _RealmScreenState extends State<RealmScreen> { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: AutoAppBarLeading(), | ||||
|         title: Text('screenRealm').tr(), | ||||
| @@ -118,113 +119,61 @@ class _RealmScreenState extends State<RealmScreen> { | ||||
|         children: [ | ||||
|           LoadingIndicator(isActive: _isBusy), | ||||
|           Expanded( | ||||
|             child: RefreshIndicator( | ||||
|               onRefresh: _fetchRealms, | ||||
|               child: ListView.builder( | ||||
|                 itemCount: _realms?.length ?? 0, | ||||
|                 itemBuilder: (context, idx) { | ||||
|                   final realm = _realms![idx]; | ||||
|                   if (_isCompactView) { | ||||
|                     return ListTile( | ||||
|                       contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                       leading: AccountImage( | ||||
|                         content: realm.avatar, | ||||
|                         fallbackWidget: const Icon(Symbols.group, size: 20), | ||||
|                       ), | ||||
|                       title: Text(realm.name), | ||||
|                       subtitle: Text( | ||||
|                         realm.description, | ||||
|                         maxLines: 1, | ||||
|                         overflow: TextOverflow.ellipsis, | ||||
|                       ), | ||||
|                       trailing: PopupMenuButton( | ||||
|                         itemBuilder: (BuildContext context) => [ | ||||
|                           PopupMenuItem( | ||||
|                             child: Row( | ||||
|                               children: [ | ||||
|                                 const Icon(Symbols.edit), | ||||
|                                 const Gap(16), | ||||
|                                 Text('edit').tr(), | ||||
|                               ], | ||||
|                             ), | ||||
|                             onTap: () { | ||||
|                               GoRouter.of(context).pushNamed( | ||||
|                                 'realmManage', | ||||
|                                 queryParameters: {'editing': realm.alias}, | ||||
|                               ).then((value) { | ||||
|                                 if (value != null) { | ||||
|                                   _fetchRealms(); | ||||
|                                 } | ||||
|                               }); | ||||
|                             }, | ||||
|                           ), | ||||
|                           PopupMenuItem( | ||||
|                             child: Row( | ||||
|                               children: [ | ||||
|                                 const Icon(Symbols.delete), | ||||
|                                 const Gap(16), | ||||
|                                 Text('delete').tr(), | ||||
|                               ], | ||||
|                             ), | ||||
|                             onTap: () { | ||||
|                               _deleteRealm(realm); | ||||
|                             }, | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                       onTap: () { | ||||
|                         GoRouter.of(context).pushNamed( | ||||
|                           'realmDetail', | ||||
|                           pathParameters: {'alias': realm.alias}, | ||||
|                         ); | ||||
|                       }, | ||||
|                     ); | ||||
|                   } | ||||
|  | ||||
|                   return Container( | ||||
|                     constraints: BoxConstraints(maxWidth: 640), | ||||
|                     child: Card( | ||||
|                       margin: const EdgeInsets.all(12), | ||||
|                       child: InkWell( | ||||
|                         borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                         child: Column( | ||||
|                           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                           children: [ | ||||
|                             AspectRatio( | ||||
|                               aspectRatio: 16 / 7, | ||||
|                               child: Stack( | ||||
|                                 clipBehavior: Clip.none, | ||||
|                                 fit: StackFit.expand, | ||||
|             child: MediaQuery.removePadding( | ||||
|               context: context, | ||||
|               removeTop: true, | ||||
|               child: RefreshIndicator( | ||||
|                 onRefresh: _fetchRealms, | ||||
|                 child: ListView.builder( | ||||
|                   itemCount: _realms?.length ?? 0, | ||||
|                   itemBuilder: (context, idx) { | ||||
|                     final realm = _realms![idx]; | ||||
|                     if (_isCompactView) { | ||||
|                       return ListTile( | ||||
|                         contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||
|                         leading: AccountImage( | ||||
|                           content: realm.avatar, | ||||
|                           fallbackWidget: const Icon(Symbols.group, size: 20), | ||||
|                         ), | ||||
|                         title: Text(realm.name), | ||||
|                         subtitle: Text( | ||||
|                           realm.description, | ||||
|                           maxLines: 1, | ||||
|                           overflow: TextOverflow.ellipsis, | ||||
|                         ), | ||||
|                         trailing: PopupMenuButton( | ||||
|                           itemBuilder: (BuildContext context) => [ | ||||
|                             PopupMenuItem( | ||||
|                               child: Row( | ||||
|                                 children: [ | ||||
|                                   Container( | ||||
|                                     color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                                     child: (realm.banner?.isEmpty ?? true) | ||||
|                                         ? const SizedBox.shrink() | ||||
|                                         : AutoResizeUniversalImage( | ||||
|                                             sn.getAttachmentUrl(realm.banner!), | ||||
|                                             fit: BoxFit.cover, | ||||
|                                           ), | ||||
|                                   ), | ||||
|                                   Positioned( | ||||
|                                     bottom: -30, | ||||
|                                     left: 18, | ||||
|                                     child: AccountImage( | ||||
|                                       content: realm.avatar, | ||||
|                                       radius: 24, | ||||
|                                       fallbackWidget: const Icon(Symbols.group, size: 24), | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                   const Icon(Symbols.edit), | ||||
|                                   const Gap(16), | ||||
|                                   Text('edit').tr(), | ||||
|                                 ], | ||||
|                               ), | ||||
|                               onTap: () { | ||||
|                                 GoRouter.of(context).pushNamed( | ||||
|                                   'realmManage', | ||||
|                                   queryParameters: {'editing': realm.alias}, | ||||
|                                 ).then((value) { | ||||
|                                   if (value != null) { | ||||
|                                     _fetchRealms(); | ||||
|                                   } | ||||
|                                 }); | ||||
|                               }, | ||||
|                             ), | ||||
|                             PopupMenuItem( | ||||
|                               child: Row( | ||||
|                                 children: [ | ||||
|                                   const Icon(Symbols.delete), | ||||
|                                   const Gap(16), | ||||
|                                   Text('delete').tr(), | ||||
|                                 ], | ||||
|                               ), | ||||
|                               onTap: () { | ||||
|                                 _deleteRealm(realm); | ||||
|                               }, | ||||
|                             ), | ||||
|                             const Gap(20 + 12), | ||||
|                             Column( | ||||
|                               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                               children: [ | ||||
|                                 Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!), | ||||
|                                 Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!), | ||||
|                               ], | ||||
|                             ).padding(horizontal: 24, bottom: 14), | ||||
|                           ], | ||||
|                         ), | ||||
|                         onTap: () { | ||||
| @@ -233,10 +182,69 @@ class _RealmScreenState extends State<RealmScreen> { | ||||
|                             pathParameters: {'alias': realm.alias}, | ||||
|                           ); | ||||
|                         }, | ||||
|                       ); | ||||
|                     } | ||||
|  | ||||
|                     return Container( | ||||
|                       constraints: BoxConstraints(maxWidth: 640), | ||||
|                       child: Card( | ||||
|                         margin: const EdgeInsets.all(12), | ||||
|                         child: InkWell( | ||||
|                           borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                           child: Column( | ||||
|                             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                             children: [ | ||||
|                               AspectRatio( | ||||
|                                 aspectRatio: 16 / 7, | ||||
|                                 child: Stack( | ||||
|                                   clipBehavior: Clip.none, | ||||
|                                   fit: StackFit.expand, | ||||
|                                   children: [ | ||||
|                                     ClipRRect( | ||||
|                                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                                       child: Container( | ||||
|                                         color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                                         child: (realm.banner?.isEmpty ?? true) | ||||
|                                             ? const SizedBox.shrink() | ||||
|                                             : AutoResizeUniversalImage( | ||||
|                                                 sn.getAttachmentUrl(realm.banner!), | ||||
|                                                 fit: BoxFit.cover, | ||||
|                                               ), | ||||
|                                       ), | ||||
|                                     ), | ||||
|                                     Positioned( | ||||
|                                       bottom: -30, | ||||
|                                       left: 18, | ||||
|                                       child: AccountImage( | ||||
|                                         content: realm.avatar, | ||||
|                                         radius: 24, | ||||
|                                         fallbackWidget: const Icon(Symbols.group, size: 24), | ||||
|                                       ), | ||||
|                                     ), | ||||
|                                   ], | ||||
|                                 ), | ||||
|                               ), | ||||
|                               const Gap(20 + 12), | ||||
|                               Column( | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                                 children: [ | ||||
|                                   Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!), | ||||
|                                   Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!), | ||||
|                                 ], | ||||
|                               ).padding(horizontal: 24, bottom: 14), | ||||
|                             ], | ||||
|                           ), | ||||
|                           onTap: () { | ||||
|                             GoRouter.of(context).pushNamed( | ||||
|                               'realmDetail', | ||||
|                               pathParameters: {'alias': realm.alias}, | ||||
|                             ); | ||||
|                           }, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ).center(); | ||||
|                 }, | ||||
|                     ).center(); | ||||
|                   }, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import 'package:surface/types/realm.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/loading_indicator.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| @@ -179,7 +180,7 @@ class _RealmManageScreenState extends State<RealmManageScreen> { | ||||
|   Widget build(BuildContext context) { | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: widget.editingRealmAlias != null | ||||
|             ? Text('screenRealmManage').tr() | ||||
|   | ||||
| @@ -8,13 +8,13 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/user_directory.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/types/realm.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | ||||
|  | ||||
| import '../../types/post.dart'; | ||||
|  | ||||
| class RealmDetailScreen extends StatefulWidget { | ||||
|   final String alias; | ||||
|  | ||||
| @@ -70,19 +70,11 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> { | ||||
|   Widget build(BuildContext context) { | ||||
|     return DefaultTabController( | ||||
|       length: 3, | ||||
|       child: Scaffold( | ||||
|       child: AppScaffold( | ||||
|         body: NestedScrollView( | ||||
|           headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { | ||||
|             // These are the slivers that show up in the "outer" scroll view. | ||||
|             return <Widget>[ | ||||
|               SliverOverlapAbsorber( | ||||
|                 // This widget takes the overlapping behavior of the SliverAppBar, | ||||
|                 // and redirects it to the SliverOverlapInjector below. If it is | ||||
|                 // missing, then it is possible for the nested "inner" scroll view | ||||
|                 // below to end up under the SliverAppBar even when the inner | ||||
|                 // scroll view thinks it has not been scrolled. | ||||
|                 // This is not necessary if the "headerSliverBuilder" only builds | ||||
|                 // widgets that do not overlap the next sliver. | ||||
|                 handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), | ||||
|                 sliver: SliverAppBar( | ||||
|                   title: Text(_realm?.name ?? 'loading'.tr()), | ||||
| @@ -428,7 +420,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> { | ||||
|  | ||||
|     return Column( | ||||
|       children: [ | ||||
|         const Gap(16), | ||||
|         const Gap(8), | ||||
|         ListTile( | ||||
|           leading: const Icon(Symbols.edit), | ||||
|           trailing: const Icon(Symbols.chevron_right), | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/theme.dart'; | ||||
| import 'package:surface/theme.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| const Map<String, Color> kColorSchemes = { | ||||
|   'colorSchemeIndigo': Colors.indigo, | ||||
| @@ -67,7 +68,11 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|   Widget build(BuildContext context) { | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenSettings').tr(), | ||||
|       ), | ||||
|       body: SingleChildScrollView( | ||||
|         child: Column( | ||||
|           spacing: 16, | ||||
| @@ -77,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(), | ||||
| @@ -120,7 +167,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                   subtitle: Text('settingsThemeMaterial3Description').tr(), | ||||
|                   contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|                   secondary: const Icon(Symbols.new_releases), | ||||
|                   value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false, | ||||
|                   value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? true, | ||||
|                   onChanged: (value) { | ||||
|                     setState(() { | ||||
|                       _prefs.setBool( | ||||
| @@ -142,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; | ||||
| @@ -201,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(() {}); | ||||
| @@ -240,6 +290,61 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                     setState(() {}); | ||||
|                   }, | ||||
|                 ), | ||||
|                 CheckboxListTile( | ||||
|                   secondary: const Icon(Symbols.left_panel_close), | ||||
|                   title: Text('settingsDrawerPreferCollapse').tr(), | ||||
|                   subtitle: Text('settingsDrawerPreferCollapseDescription').tr(), | ||||
|                   contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|                   value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false, | ||||
|                   onChanged: (value) { | ||||
|                     _prefs.setBool(kAppDrawerPreferCollapse, value ?? false); | ||||
|                     final cfg = context.read<ConfigProvider>(); | ||||
|                     cfg.calcDrawerSize(context); | ||||
|                     setState(() {}); | ||||
|                   }, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text('settingsFeatures').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), | ||||
|                 CheckboxListTile( | ||||
|                   secondary: const Icon(Symbols.vibration), | ||||
|                   contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|                   title: Text('settingsNotifyWithHaptic').tr(), | ||||
|                   subtitle: Text('settingsNotifyWithHapticDescription').tr(), | ||||
|                   value: _prefs.getBool(kAppNotifyWithHaptic) ?? true, | ||||
|                   onChanged: (value) { | ||||
|                     setState(() { | ||||
|                       _prefs.setBool(kAppNotifyWithHaptic, value ?? false); | ||||
|                     }); | ||||
|                   }, | ||||
|                 ), | ||||
|                 CheckboxListTile( | ||||
|                   secondary: const Icon(Symbols.link), | ||||
|                   title: Text('settingsExpandPostLink').tr(), | ||||
|                   subtitle: Text('settingsExpandPostLinkDescription').tr(), | ||||
|                   contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|                   value: _prefs.getBool(kAppExpandPostLink) ?? true, | ||||
|                   onChanged: (value) { | ||||
|                     setState(() { | ||||
|                       _prefs.setBool(kAppExpandPostLink, value ?? false); | ||||
|                     }); | ||||
|                   }, | ||||
|                 ), | ||||
|                 CheckboxListTile( | ||||
|                   secondary: const Icon(Symbols.chat), | ||||
|                   title: Text('settingsExpandChatLink').tr(), | ||||
|                   subtitle: Text('settingsExpandChatLinkDescription').tr(), | ||||
|                   contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|                   value: _prefs.getBool(kAppExpandChatLink) ?? true, | ||||
|                   onChanged: (value) { | ||||
|                     setState(() { | ||||
|                       _prefs.setBool(kAppExpandChatLink, value ?? false); | ||||
|                     }); | ||||
|                   }, | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|             Column( | ||||
| @@ -282,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, | ||||
| @@ -294,7 +400,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                           ) | ||||
|                       ) | ||||
|                           .toList(), | ||||
|                       value: _serverUrlController.text, | ||||
|                       onChanged: (String? value) { | ||||
| @@ -349,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), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -20,7 +20,7 @@ Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3 | ||||
|  | ||||
| Future<ThemeData> createAppTheme( | ||||
|   Brightness brightness, { | ||||
|     Color? seedColorOverride, | ||||
|   Color? seedColorOverride, | ||||
|   bool? useMaterial3, | ||||
| }) async { | ||||
|   final prefs = await SharedPreferences.getInstance(); | ||||
| @@ -34,9 +34,10 @@ Future<ThemeData> createAppTheme( | ||||
|   ); | ||||
|  | ||||
|   final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false; | ||||
|   final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true); | ||||
|  | ||||
|   return ThemeData( | ||||
|     useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false), | ||||
|     useMaterial3: useM3, | ||||
|     colorScheme: colorScheme, | ||||
|     brightness: brightness, | ||||
|     iconTheme: IconThemeData( | ||||
| @@ -45,12 +46,24 @@ Future<ThemeData> createAppTheme( | ||||
|       opticalSize: 20, | ||||
|       color: colorScheme.onSurface, | ||||
|     ), | ||||
|     snackBarTheme: SnackBarThemeData( | ||||
|       behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed, | ||||
|     ), | ||||
|     appBarTheme: AppBarTheme( | ||||
|       centerTitle: true, | ||||
|       elevation: hasAppBarBlurry ? 0 : null, | ||||
|       backgroundColor: hasAppBarBlurry ? colorScheme.surfaceContainer.withAlpha(200) : colorScheme.primary, | ||||
|       backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary, | ||||
|       foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary, | ||||
|     ), | ||||
|     scaffoldBackgroundColor: Colors.transparent, | ||||
|     pageTransitionsTheme: PageTransitionsTheme( | ||||
|       builders: { | ||||
|         TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), | ||||
|         TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), | ||||
|         TargetPlatform.macOS: ZoomPageTransitionsBuilder(), | ||||
|         TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(), | ||||
|         TargetPlatform.linux: ZoomPageTransitionsBuilder(), | ||||
|         TargetPlatform.windows: ZoomPageTransitionsBuilder(), | ||||
|       }, | ||||
|     ), | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -141,3 +141,39 @@ class SnAttachmentBoost with _$SnAttachmentBoost { | ||||
|  | ||||
|   factory SnAttachmentBoost.fromJson(Map<String, Object?> json) => _$SnAttachmentBoostFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnSticker with _$SnSticker { | ||||
|   const factory SnSticker({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     required DateTime? deletedAt, | ||||
|     required String alias, | ||||
|     required String name, | ||||
|     required int attachmentId, | ||||
|     required SnAttachment attachment, | ||||
|     required int packId, | ||||
|     required SnStickerPack pack, | ||||
|     required int accountId, | ||||
|   }) = _SnSticker; | ||||
|  | ||||
|   factory SnSticker.fromJson(Map<String, Object?> json) => _$SnStickerFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnStickerPack with _$SnStickerPack { | ||||
|   const factory SnStickerPack({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     required DateTime? deletedAt, | ||||
|     required String prefix, | ||||
|     required String name, | ||||
|     required String description, | ||||
|     required List<SnSticker>? stickers, | ||||
|     required int accountId, | ||||
|   }) = _SnStickerPack; | ||||
|  | ||||
|   factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json); | ||||
| } | ||||
|   | ||||
| @@ -2272,3 +2272,738 @@ abstract class _SnAttachmentBoost implements SnAttachmentBoost { | ||||
|   _$$SnAttachmentBoostImplCopyWith<_$SnAttachmentBoostImpl> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| SnSticker _$SnStickerFromJson(Map<String, dynamic> json) { | ||||
|   return _SnSticker.fromJson(json); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnSticker { | ||||
|   int get id => throw _privateConstructorUsedError; | ||||
|   DateTime get createdAt => throw _privateConstructorUsedError; | ||||
|   DateTime get updatedAt => throw _privateConstructorUsedError; | ||||
|   DateTime? get deletedAt => throw _privateConstructorUsedError; | ||||
|   String get alias => throw _privateConstructorUsedError; | ||||
|   String get name => throw _privateConstructorUsedError; | ||||
|   int get attachmentId => throw _privateConstructorUsedError; | ||||
|   SnAttachment get attachment => throw _privateConstructorUsedError; | ||||
|   int get packId => throw _privateConstructorUsedError; | ||||
|   SnStickerPack get pack => throw _privateConstructorUsedError; | ||||
|   int get accountId => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Serializes this SnSticker to a JSON map. | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Create a copy of SnSticker | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   $SnStickerCopyWith<SnSticker> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class $SnStickerCopyWith<$Res> { | ||||
|   factory $SnStickerCopyWith(SnSticker value, $Res Function(SnSticker) then) = | ||||
|       _$SnStickerCopyWithImpl<$Res, SnSticker>; | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String alias, | ||||
|       String name, | ||||
|       int attachmentId, | ||||
|       SnAttachment attachment, | ||||
|       int packId, | ||||
|       SnStickerPack pack, | ||||
|       int accountId}); | ||||
|  | ||||
|   $SnAttachmentCopyWith<$Res> get attachment; | ||||
|   $SnStickerPackCopyWith<$Res> get pack; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class _$SnStickerCopyWithImpl<$Res, $Val extends SnSticker> | ||||
|     implements $SnStickerCopyWith<$Res> { | ||||
|   _$SnStickerCopyWithImpl(this._value, this._then); | ||||
|  | ||||
|   // ignore: unused_field | ||||
|   final $Val _value; | ||||
|   // ignore: unused_field | ||||
|   final $Res Function($Val) _then; | ||||
|  | ||||
|   /// Create a copy of SnSticker | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? alias = null, | ||||
|     Object? name = null, | ||||
|     Object? attachmentId = null, | ||||
|     Object? attachment = null, | ||||
|     Object? packId = null, | ||||
|     Object? pack = null, | ||||
|     Object? accountId = null, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       alias: null == alias | ||||
|           ? _value.alias | ||||
|           : alias // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       name: null == name | ||||
|           ? _value.name | ||||
|           : name // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       attachmentId: null == attachmentId | ||||
|           ? _value.attachmentId | ||||
|           : attachmentId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       attachment: null == attachment | ||||
|           ? _value.attachment | ||||
|           : attachment // ignore: cast_nullable_to_non_nullable | ||||
|               as SnAttachment, | ||||
|       packId: null == packId | ||||
|           ? _value.packId | ||||
|           : packId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       pack: null == pack | ||||
|           ? _value.pack | ||||
|           : pack // ignore: cast_nullable_to_non_nullable | ||||
|               as SnStickerPack, | ||||
|       accountId: null == accountId | ||||
|           ? _value.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|     ) as $Val); | ||||
|   } | ||||
|  | ||||
|   /// Create a copy of SnSticker | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   $SnAttachmentCopyWith<$Res> get attachment { | ||||
|     return $SnAttachmentCopyWith<$Res>(_value.attachment, (value) { | ||||
|       return _then(_value.copyWith(attachment: value) as $Val); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /// Create a copy of SnSticker | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   $SnStickerPackCopyWith<$Res> get pack { | ||||
|     return $SnStickerPackCopyWith<$Res>(_value.pack, (value) { | ||||
|       return _then(_value.copyWith(pack: value) as $Val); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class _$$SnStickerImplCopyWith<$Res> | ||||
|     implements $SnStickerCopyWith<$Res> { | ||||
|   factory _$$SnStickerImplCopyWith( | ||||
|           _$SnStickerImpl value, $Res Function(_$SnStickerImpl) then) = | ||||
|       __$$SnStickerImplCopyWithImpl<$Res>; | ||||
|   @override | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String alias, | ||||
|       String name, | ||||
|       int attachmentId, | ||||
|       SnAttachment attachment, | ||||
|       int packId, | ||||
|       SnStickerPack pack, | ||||
|       int accountId}); | ||||
|  | ||||
|   @override | ||||
|   $SnAttachmentCopyWith<$Res> get attachment; | ||||
|   @override | ||||
|   $SnStickerPackCopyWith<$Res> get pack; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class __$$SnStickerImplCopyWithImpl<$Res> | ||||
|     extends _$SnStickerCopyWithImpl<$Res, _$SnStickerImpl> | ||||
|     implements _$$SnStickerImplCopyWith<$Res> { | ||||
|   __$$SnStickerImplCopyWithImpl( | ||||
|       _$SnStickerImpl _value, $Res Function(_$SnStickerImpl) _then) | ||||
|       : super(_value, _then); | ||||
|  | ||||
|   /// Create a copy of SnSticker | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? alias = null, | ||||
|     Object? name = null, | ||||
|     Object? attachmentId = null, | ||||
|     Object? attachment = null, | ||||
|     Object? packId = null, | ||||
|     Object? pack = null, | ||||
|     Object? accountId = null, | ||||
|   }) { | ||||
|     return _then(_$SnStickerImpl( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       alias: null == alias | ||||
|           ? _value.alias | ||||
|           : alias // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       name: null == name | ||||
|           ? _value.name | ||||
|           : name // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       attachmentId: null == attachmentId | ||||
|           ? _value.attachmentId | ||||
|           : attachmentId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       attachment: null == attachment | ||||
|           ? _value.attachment | ||||
|           : attachment // ignore: cast_nullable_to_non_nullable | ||||
|               as SnAttachment, | ||||
|       packId: null == packId | ||||
|           ? _value.packId | ||||
|           : packId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       pack: null == pack | ||||
|           ? _value.pack | ||||
|           : pack // ignore: cast_nullable_to_non_nullable | ||||
|               as SnStickerPack, | ||||
|       accountId: null == accountId | ||||
|           ? _value.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
| class _$SnStickerImpl implements _SnSticker { | ||||
|   const _$SnStickerImpl( | ||||
|       {required this.id, | ||||
|       required this.createdAt, | ||||
|       required this.updatedAt, | ||||
|       required this.deletedAt, | ||||
|       required this.alias, | ||||
|       required this.name, | ||||
|       required this.attachmentId, | ||||
|       required this.attachment, | ||||
|       required this.packId, | ||||
|       required this.pack, | ||||
|       required this.accountId}); | ||||
|  | ||||
|   factory _$SnStickerImpl.fromJson(Map<String, dynamic> json) => | ||||
|       _$$SnStickerImplFromJson(json); | ||||
|  | ||||
|   @override | ||||
|   final int id; | ||||
|   @override | ||||
|   final DateTime createdAt; | ||||
|   @override | ||||
|   final DateTime updatedAt; | ||||
|   @override | ||||
|   final DateTime? deletedAt; | ||||
|   @override | ||||
|   final String alias; | ||||
|   @override | ||||
|   final String name; | ||||
|   @override | ||||
|   final int attachmentId; | ||||
|   @override | ||||
|   final SnAttachment attachment; | ||||
|   @override | ||||
|   final int packId; | ||||
|   @override | ||||
|   final SnStickerPack pack; | ||||
|   @override | ||||
|   final int accountId; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnSticker(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, alias: $alias, name: $name, attachmentId: $attachmentId, attachment: $attachment, packId: $packId, pack: $pack, accountId: $accountId)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _$SnStickerImpl && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.createdAt, createdAt) || | ||||
|                 other.createdAt == createdAt) && | ||||
|             (identical(other.updatedAt, updatedAt) || | ||||
|                 other.updatedAt == updatedAt) && | ||||
|             (identical(other.deletedAt, deletedAt) || | ||||
|                 other.deletedAt == deletedAt) && | ||||
|             (identical(other.alias, alias) || other.alias == alias) && | ||||
|             (identical(other.name, name) || other.name == name) && | ||||
|             (identical(other.attachmentId, attachmentId) || | ||||
|                 other.attachmentId == attachmentId) && | ||||
|             (identical(other.attachment, attachment) || | ||||
|                 other.attachment == attachment) && | ||||
|             (identical(other.packId, packId) || other.packId == packId) && | ||||
|             (identical(other.pack, pack) || other.pack == pack) && | ||||
|             (identical(other.accountId, accountId) || | ||||
|                 other.accountId == accountId)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   int get hashCode => Object.hash( | ||||
|       runtimeType, | ||||
|       id, | ||||
|       createdAt, | ||||
|       updatedAt, | ||||
|       deletedAt, | ||||
|       alias, | ||||
|       name, | ||||
|       attachmentId, | ||||
|       attachment, | ||||
|       packId, | ||||
|       pack, | ||||
|       accountId); | ||||
|  | ||||
|   /// Create a copy of SnSticker | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$$SnStickerImplCopyWith<_$SnStickerImpl> get copyWith => | ||||
|       __$$SnStickerImplCopyWithImpl<_$SnStickerImpl>(this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$$SnStickerImplToJson( | ||||
|       this, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _SnSticker implements SnSticker { | ||||
|   const factory _SnSticker( | ||||
|       {required final int id, | ||||
|       required final DateTime createdAt, | ||||
|       required final DateTime updatedAt, | ||||
|       required final DateTime? deletedAt, | ||||
|       required final String alias, | ||||
|       required final String name, | ||||
|       required final int attachmentId, | ||||
|       required final SnAttachment attachment, | ||||
|       required final int packId, | ||||
|       required final SnStickerPack pack, | ||||
|       required final int accountId}) = _$SnStickerImpl; | ||||
|  | ||||
|   factory _SnSticker.fromJson(Map<String, dynamic> json) = | ||||
|       _$SnStickerImpl.fromJson; | ||||
|  | ||||
|   @override | ||||
|   int get id; | ||||
|   @override | ||||
|   DateTime get createdAt; | ||||
|   @override | ||||
|   DateTime get updatedAt; | ||||
|   @override | ||||
|   DateTime? get deletedAt; | ||||
|   @override | ||||
|   String get alias; | ||||
|   @override | ||||
|   String get name; | ||||
|   @override | ||||
|   int get attachmentId; | ||||
|   @override | ||||
|   SnAttachment get attachment; | ||||
|   @override | ||||
|   int get packId; | ||||
|   @override | ||||
|   SnStickerPack get pack; | ||||
|   @override | ||||
|   int get accountId; | ||||
|  | ||||
|   /// Create a copy of SnSticker | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   _$$SnStickerImplCopyWith<_$SnStickerImpl> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| SnStickerPack _$SnStickerPackFromJson(Map<String, dynamic> json) { | ||||
|   return _SnStickerPack.fromJson(json); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnStickerPack { | ||||
|   int get id => throw _privateConstructorUsedError; | ||||
|   DateTime get createdAt => throw _privateConstructorUsedError; | ||||
|   DateTime get updatedAt => throw _privateConstructorUsedError; | ||||
|   DateTime? get deletedAt => throw _privateConstructorUsedError; | ||||
|   String get prefix => throw _privateConstructorUsedError; | ||||
|   String get name => throw _privateConstructorUsedError; | ||||
|   String get description => throw _privateConstructorUsedError; | ||||
|   List<SnSticker>? get stickers => throw _privateConstructorUsedError; | ||||
|   int get accountId => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Serializes this SnStickerPack to a JSON map. | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Create a copy of SnStickerPack | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   $SnStickerPackCopyWith<SnStickerPack> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class $SnStickerPackCopyWith<$Res> { | ||||
|   factory $SnStickerPackCopyWith( | ||||
|           SnStickerPack value, $Res Function(SnStickerPack) then) = | ||||
|       _$SnStickerPackCopyWithImpl<$Res, SnStickerPack>; | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String prefix, | ||||
|       String name, | ||||
|       String description, | ||||
|       List<SnSticker>? stickers, | ||||
|       int accountId}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class _$SnStickerPackCopyWithImpl<$Res, $Val extends SnStickerPack> | ||||
|     implements $SnStickerPackCopyWith<$Res> { | ||||
|   _$SnStickerPackCopyWithImpl(this._value, this._then); | ||||
|  | ||||
|   // ignore: unused_field | ||||
|   final $Val _value; | ||||
|   // ignore: unused_field | ||||
|   final $Res Function($Val) _then; | ||||
|  | ||||
|   /// Create a copy of SnStickerPack | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? prefix = null, | ||||
|     Object? name = null, | ||||
|     Object? description = null, | ||||
|     Object? stickers = freezed, | ||||
|     Object? accountId = null, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       prefix: null == prefix | ||||
|           ? _value.prefix | ||||
|           : prefix // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       name: null == name | ||||
|           ? _value.name | ||||
|           : name // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       description: null == description | ||||
|           ? _value.description | ||||
|           : description // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       stickers: freezed == stickers | ||||
|           ? _value.stickers | ||||
|           : stickers // ignore: cast_nullable_to_non_nullable | ||||
|               as List<SnSticker>?, | ||||
|       accountId: null == accountId | ||||
|           ? _value.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|     ) as $Val); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class _$$SnStickerPackImplCopyWith<$Res> | ||||
|     implements $SnStickerPackCopyWith<$Res> { | ||||
|   factory _$$SnStickerPackImplCopyWith( | ||||
|           _$SnStickerPackImpl value, $Res Function(_$SnStickerPackImpl) then) = | ||||
|       __$$SnStickerPackImplCopyWithImpl<$Res>; | ||||
|   @override | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       DateTime? deletedAt, | ||||
|       String prefix, | ||||
|       String name, | ||||
|       String description, | ||||
|       List<SnSticker>? stickers, | ||||
|       int accountId}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class __$$SnStickerPackImplCopyWithImpl<$Res> | ||||
|     extends _$SnStickerPackCopyWithImpl<$Res, _$SnStickerPackImpl> | ||||
|     implements _$$SnStickerPackImplCopyWith<$Res> { | ||||
|   __$$SnStickerPackImplCopyWithImpl( | ||||
|       _$SnStickerPackImpl _value, $Res Function(_$SnStickerPackImpl) _then) | ||||
|       : super(_value, _then); | ||||
|  | ||||
|   /// Create a copy of SnStickerPack | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? createdAt = null, | ||||
|     Object? updatedAt = null, | ||||
|     Object? deletedAt = freezed, | ||||
|     Object? prefix = null, | ||||
|     Object? name = null, | ||||
|     Object? description = null, | ||||
|     Object? stickers = freezed, | ||||
|     Object? accountId = null, | ||||
|   }) { | ||||
|     return _then(_$SnStickerPackImpl( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       createdAt: null == createdAt | ||||
|           ? _value.createdAt | ||||
|           : createdAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       updatedAt: null == updatedAt | ||||
|           ? _value.updatedAt | ||||
|           : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime, | ||||
|       deletedAt: freezed == deletedAt | ||||
|           ? _value.deletedAt | ||||
|           : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|       prefix: null == prefix | ||||
|           ? _value.prefix | ||||
|           : prefix // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       name: null == name | ||||
|           ? _value.name | ||||
|           : name // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       description: null == description | ||||
|           ? _value.description | ||||
|           : description // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       stickers: freezed == stickers | ||||
|           ? _value._stickers | ||||
|           : stickers // ignore: cast_nullable_to_non_nullable | ||||
|               as List<SnSticker>?, | ||||
|       accountId: null == accountId | ||||
|           ? _value.accountId | ||||
|           : accountId // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
| class _$SnStickerPackImpl implements _SnStickerPack { | ||||
|   const _$SnStickerPackImpl( | ||||
|       {required this.id, | ||||
|       required this.createdAt, | ||||
|       required this.updatedAt, | ||||
|       required this.deletedAt, | ||||
|       required this.prefix, | ||||
|       required this.name, | ||||
|       required this.description, | ||||
|       required final List<SnSticker>? stickers, | ||||
|       required this.accountId}) | ||||
|       : _stickers = stickers; | ||||
|  | ||||
|   factory _$SnStickerPackImpl.fromJson(Map<String, dynamic> json) => | ||||
|       _$$SnStickerPackImplFromJson(json); | ||||
|  | ||||
|   @override | ||||
|   final int id; | ||||
|   @override | ||||
|   final DateTime createdAt; | ||||
|   @override | ||||
|   final DateTime updatedAt; | ||||
|   @override | ||||
|   final DateTime? deletedAt; | ||||
|   @override | ||||
|   final String prefix; | ||||
|   @override | ||||
|   final String name; | ||||
|   @override | ||||
|   final String description; | ||||
|   final List<SnSticker>? _stickers; | ||||
|   @override | ||||
|   List<SnSticker>? get stickers { | ||||
|     final value = _stickers; | ||||
|     if (value == null) return null; | ||||
|     if (_stickers is EqualUnmodifiableListView) return _stickers; | ||||
|     // ignore: implicit_dynamic_type | ||||
|     return EqualUnmodifiableListView(value); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   final int accountId; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnStickerPack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, prefix: $prefix, name: $name, description: $description, stickers: $stickers, accountId: $accountId)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _$SnStickerPackImpl && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.createdAt, createdAt) || | ||||
|                 other.createdAt == createdAt) && | ||||
|             (identical(other.updatedAt, updatedAt) || | ||||
|                 other.updatedAt == updatedAt) && | ||||
|             (identical(other.deletedAt, deletedAt) || | ||||
|                 other.deletedAt == deletedAt) && | ||||
|             (identical(other.prefix, prefix) || other.prefix == prefix) && | ||||
|             (identical(other.name, name) || other.name == name) && | ||||
|             (identical(other.description, description) || | ||||
|                 other.description == description) && | ||||
|             const DeepCollectionEquality().equals(other._stickers, _stickers) && | ||||
|             (identical(other.accountId, accountId) || | ||||
|                 other.accountId == accountId)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   int get hashCode => Object.hash( | ||||
|       runtimeType, | ||||
|       id, | ||||
|       createdAt, | ||||
|       updatedAt, | ||||
|       deletedAt, | ||||
|       prefix, | ||||
|       name, | ||||
|       description, | ||||
|       const DeepCollectionEquality().hash(_stickers), | ||||
|       accountId); | ||||
|  | ||||
|   /// Create a copy of SnStickerPack | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$$SnStickerPackImplCopyWith<_$SnStickerPackImpl> get copyWith => | ||||
|       __$$SnStickerPackImplCopyWithImpl<_$SnStickerPackImpl>(this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$$SnStickerPackImplToJson( | ||||
|       this, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _SnStickerPack implements SnStickerPack { | ||||
|   const factory _SnStickerPack( | ||||
|       {required final int id, | ||||
|       required final DateTime createdAt, | ||||
|       required final DateTime updatedAt, | ||||
|       required final DateTime? deletedAt, | ||||
|       required final String prefix, | ||||
|       required final String name, | ||||
|       required final String description, | ||||
|       required final List<SnSticker>? stickers, | ||||
|       required final int accountId}) = _$SnStickerPackImpl; | ||||
|  | ||||
|   factory _SnStickerPack.fromJson(Map<String, dynamic> json) = | ||||
|       _$SnStickerPackImpl.fromJson; | ||||
|  | ||||
|   @override | ||||
|   int get id; | ||||
|   @override | ||||
|   DateTime get createdAt; | ||||
|   @override | ||||
|   DateTime get updatedAt; | ||||
|   @override | ||||
|   DateTime? get deletedAt; | ||||
|   @override | ||||
|   String get prefix; | ||||
|   @override | ||||
|   String get name; | ||||
|   @override | ||||
|   String get description; | ||||
|   @override | ||||
|   List<SnSticker>? get stickers; | ||||
|   @override | ||||
|   int get accountId; | ||||
|  | ||||
|   /// Create a copy of SnStickerPack | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   _$$SnStickerPackImplCopyWith<_$SnStickerPackImpl> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|   | ||||
| @@ -218,3 +218,66 @@ Map<String, dynamic> _$$SnAttachmentBoostImplToJson( | ||||
|       'attachment': instance.attachment.toJson(), | ||||
|       'account': instance.account, | ||||
|     }; | ||||
|  | ||||
| _$SnStickerImpl _$$SnStickerImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnStickerImpl( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
|       deletedAt: json['deleted_at'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['deleted_at'] as String), | ||||
|       alias: json['alias'] as String, | ||||
|       name: json['name'] as String, | ||||
|       attachmentId: (json['attachment_id'] as num).toInt(), | ||||
|       attachment: | ||||
|           SnAttachment.fromJson(json['attachment'] as Map<String, dynamic>), | ||||
|       packId: (json['pack_id'] as num).toInt(), | ||||
|       pack: SnStickerPack.fromJson(json['pack'] as Map<String, dynamic>), | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnStickerImplToJson(_$SnStickerImpl instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||
|       'alias': instance.alias, | ||||
|       'name': instance.name, | ||||
|       'attachment_id': instance.attachmentId, | ||||
|       'attachment': instance.attachment.toJson(), | ||||
|       'pack_id': instance.packId, | ||||
|       'pack': instance.pack.toJson(), | ||||
|       'account_id': instance.accountId, | ||||
|     }; | ||||
|  | ||||
| _$SnStickerPackImpl _$$SnStickerPackImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnStickerPackImpl( | ||||
|       id: (json['id'] as num).toInt(), | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
|       deletedAt: json['deleted_at'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['deleted_at'] as String), | ||||
|       prefix: json['prefix'] as String, | ||||
|       name: json['name'] as String, | ||||
|       description: json['description'] as String, | ||||
|       stickers: (json['stickers'] as List<dynamic>?) | ||||
|           ?.map((e) => SnSticker.fromJson(e as Map<String, dynamic>)) | ||||
|           .toList(), | ||||
|       accountId: (json['account_id'] as num).toInt(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnStickerPackImplToJson(_$SnStickerPackImpl instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||
|       'prefix': instance.prefix, | ||||
|       'name': instance.name, | ||||
|       'description': instance.description, | ||||
|       'stickers': instance.stickers?.map((e) => e.toJson()).toList(), | ||||
|       'account_id': instance.accountId, | ||||
|     }; | ||||
|   | ||||
| @@ -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, | ||||
|     }; | ||||
|   | ||||
							
								
								
									
										38
									
								
								lib/types/news.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								lib/types/news.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
|  | ||||
| part 'news.freezed.dart'; | ||||
| part 'news.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| class SnNewsSource with _$SnNewsSource { | ||||
|   const factory SnNewsSource({ | ||||
|     required String id, | ||||
|     required String label, | ||||
|     required String type, | ||||
|     required String source, | ||||
|     required int depth, | ||||
|     required bool enabled, | ||||
|   }) = _SnNewsSource; | ||||
|  | ||||
|   factory SnNewsSource.fromJson(Map<String, dynamic> json) => _$SnNewsSourceFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| class SnNewsArticle with _$SnNewsArticle { | ||||
|   const factory SnNewsArticle({ | ||||
|     required int id, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     required dynamic deletedAt, | ||||
|     required String thumbnail, | ||||
|     required String title, | ||||
|     required String description, | ||||
|     required String content, | ||||
|     required String url, | ||||
|     required String hash, | ||||
|     required String source, | ||||
|     required DateTime? publishedAt, | ||||
|   }) = _SnNewsArticle; | ||||
|  | ||||
|   factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json); | ||||
| } | ||||
							
								
								
									
										660
									
								
								lib/types/news.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										660
									
								
								lib/types/news.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,660 @@ | ||||
| // 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 'news.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'); | ||||
|  | ||||
| SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> json) { | ||||
|   return _SnNewsSource.fromJson(json); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnNewsSource { | ||||
|   String get id => throw _privateConstructorUsedError; | ||||
|   String get label => throw _privateConstructorUsedError; | ||||
|   String get type => throw _privateConstructorUsedError; | ||||
|   String get source => throw _privateConstructorUsedError; | ||||
|   int get depth => throw _privateConstructorUsedError; | ||||
|   bool get enabled => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Serializes this SnNewsSource to a JSON map. | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Create a copy of SnNewsSource | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   $SnNewsSourceCopyWith<SnNewsSource> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class $SnNewsSourceCopyWith<$Res> { | ||||
|   factory $SnNewsSourceCopyWith( | ||||
|           SnNewsSource value, $Res Function(SnNewsSource) then) = | ||||
|       _$SnNewsSourceCopyWithImpl<$Res, SnNewsSource>; | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {String id, | ||||
|       String label, | ||||
|       String type, | ||||
|       String source, | ||||
|       int depth, | ||||
|       bool enabled}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class _$SnNewsSourceCopyWithImpl<$Res, $Val extends SnNewsSource> | ||||
|     implements $SnNewsSourceCopyWith<$Res> { | ||||
|   _$SnNewsSourceCopyWithImpl(this._value, this._then); | ||||
|  | ||||
|   // ignore: unused_field | ||||
|   final $Val _value; | ||||
|   // ignore: unused_field | ||||
|   final $Res Function($Val) _then; | ||||
|  | ||||
|   /// Create a copy of SnNewsSource | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? label = null, | ||||
|     Object? type = null, | ||||
|     Object? source = null, | ||||
|     Object? depth = null, | ||||
|     Object? enabled = null, | ||||
|   }) { | ||||
|     return _then(_value.copyWith( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       label: null == label | ||||
|           ? _value.label | ||||
|           : label // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       type: null == type | ||||
|           ? _value.type | ||||
|           : type // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       source: null == source | ||||
|           ? _value.source | ||||
|           : source // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       depth: null == depth | ||||
|           ? _value.depth | ||||
|           : depth // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       enabled: null == enabled | ||||
|           ? _value.enabled | ||||
|           : enabled // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|     ) as $Val); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class _$$SnNewsSourceImplCopyWith<$Res> | ||||
|     implements $SnNewsSourceCopyWith<$Res> { | ||||
|   factory _$$SnNewsSourceImplCopyWith( | ||||
|           _$SnNewsSourceImpl value, $Res Function(_$SnNewsSourceImpl) then) = | ||||
|       __$$SnNewsSourceImplCopyWithImpl<$Res>; | ||||
|   @override | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {String id, | ||||
|       String label, | ||||
|       String type, | ||||
|       String source, | ||||
|       int depth, | ||||
|       bool enabled}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class __$$SnNewsSourceImplCopyWithImpl<$Res> | ||||
|     extends _$SnNewsSourceCopyWithImpl<$Res, _$SnNewsSourceImpl> | ||||
|     implements _$$SnNewsSourceImplCopyWith<$Res> { | ||||
|   __$$SnNewsSourceImplCopyWithImpl( | ||||
|       _$SnNewsSourceImpl _value, $Res Function(_$SnNewsSourceImpl) _then) | ||||
|       : super(_value, _then); | ||||
|  | ||||
|   /// Create a copy of SnNewsSource | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @pragma('vm:prefer-inline') | ||||
|   @override | ||||
|   $Res call({ | ||||
|     Object? id = null, | ||||
|     Object? label = null, | ||||
|     Object? type = null, | ||||
|     Object? source = null, | ||||
|     Object? depth = null, | ||||
|     Object? enabled = null, | ||||
|   }) { | ||||
|     return _then(_$SnNewsSourceImpl( | ||||
|       id: null == id | ||||
|           ? _value.id | ||||
|           : id // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       label: null == label | ||||
|           ? _value.label | ||||
|           : label // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       type: null == type | ||||
|           ? _value.type | ||||
|           : type // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       source: null == source | ||||
|           ? _value.source | ||||
|           : source // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       depth: null == depth | ||||
|           ? _value.depth | ||||
|           : depth // ignore: cast_nullable_to_non_nullable | ||||
|               as int, | ||||
|       enabled: null == enabled | ||||
|           ? _value.enabled | ||||
|           : enabled // ignore: cast_nullable_to_non_nullable | ||||
|               as bool, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
| class _$SnNewsSourceImpl implements _SnNewsSource { | ||||
|   const _$SnNewsSourceImpl( | ||||
|       {required this.id, | ||||
|       required this.label, | ||||
|       required this.type, | ||||
|       required this.source, | ||||
|       required this.depth, | ||||
|       required this.enabled}); | ||||
|  | ||||
|   factory _$SnNewsSourceImpl.fromJson(Map<String, dynamic> json) => | ||||
|       _$$SnNewsSourceImplFromJson(json); | ||||
|  | ||||
|   @override | ||||
|   final String id; | ||||
|   @override | ||||
|   final String label; | ||||
|   @override | ||||
|   final String type; | ||||
|   @override | ||||
|   final String source; | ||||
|   @override | ||||
|   final int depth; | ||||
|   @override | ||||
|   final bool enabled; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _$SnNewsSourceImpl && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.label, label) || other.label == label) && | ||||
|             (identical(other.type, type) || other.type == type) && | ||||
|             (identical(other.source, source) || other.source == source) && | ||||
|             (identical(other.depth, depth) || other.depth == depth) && | ||||
|             (identical(other.enabled, enabled) || other.enabled == enabled)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   int get hashCode => | ||||
|       Object.hash(runtimeType, id, label, type, source, depth, enabled); | ||||
|  | ||||
|   /// Create a copy of SnNewsSource | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith => | ||||
|       __$$SnNewsSourceImplCopyWithImpl<_$SnNewsSourceImpl>(this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$$SnNewsSourceImplToJson( | ||||
|       this, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _SnNewsSource implements SnNewsSource { | ||||
|   const factory _SnNewsSource( | ||||
|       {required final String id, | ||||
|       required final String label, | ||||
|       required final String type, | ||||
|       required final String source, | ||||
|       required final int depth, | ||||
|       required final bool enabled}) = _$SnNewsSourceImpl; | ||||
|  | ||||
|   factory _SnNewsSource.fromJson(Map<String, dynamic> json) = | ||||
|       _$SnNewsSourceImpl.fromJson; | ||||
|  | ||||
|   @override | ||||
|   String get id; | ||||
|   @override | ||||
|   String get label; | ||||
|   @override | ||||
|   String get type; | ||||
|   @override | ||||
|   String get source; | ||||
|   @override | ||||
|   int get depth; | ||||
|   @override | ||||
|   bool get enabled; | ||||
|  | ||||
|   /// Create a copy of SnNewsSource | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   _$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| SnNewsArticle _$SnNewsArticleFromJson(Map<String, dynamic> json) { | ||||
|   return _SnNewsArticle.fromJson(json); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnNewsArticle { | ||||
|   int get id => throw _privateConstructorUsedError; | ||||
|   DateTime get createdAt => throw _privateConstructorUsedError; | ||||
|   DateTime get updatedAt => throw _privateConstructorUsedError; | ||||
|   dynamic get deletedAt => throw _privateConstructorUsedError; | ||||
|   String get thumbnail => throw _privateConstructorUsedError; | ||||
|   String get title => throw _privateConstructorUsedError; | ||||
|   String get description => throw _privateConstructorUsedError; | ||||
|   String get content => throw _privateConstructorUsedError; | ||||
|   String get url => throw _privateConstructorUsedError; | ||||
|   String get hash => throw _privateConstructorUsedError; | ||||
|   String get source => throw _privateConstructorUsedError; | ||||
|   DateTime? get publishedAt => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Serializes this SnNewsArticle to a JSON map. | ||||
|   Map<String, dynamic> toJson() => throw _privateConstructorUsedError; | ||||
|  | ||||
|   /// Create a copy of SnNewsArticle | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   $SnNewsArticleCopyWith<SnNewsArticle> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class $SnNewsArticleCopyWith<$Res> { | ||||
|   factory $SnNewsArticleCopyWith( | ||||
|           SnNewsArticle value, $Res Function(SnNewsArticle) then) = | ||||
|       _$SnNewsArticleCopyWithImpl<$Res, SnNewsArticle>; | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       dynamic deletedAt, | ||||
|       String thumbnail, | ||||
|       String title, | ||||
|       String description, | ||||
|       String content, | ||||
|       String url, | ||||
|       String hash, | ||||
|       String source, | ||||
|       DateTime? publishedAt}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class _$SnNewsArticleCopyWithImpl<$Res, $Val extends SnNewsArticle> | ||||
|     implements $SnNewsArticleCopyWith<$Res> { | ||||
|   _$SnNewsArticleCopyWithImpl(this._value, this._then); | ||||
|  | ||||
|   // ignore: unused_field | ||||
|   final $Val _value; | ||||
|   // ignore: unused_field | ||||
|   final $Res Function($Val) _then; | ||||
|  | ||||
|   /// Create a copy of SnNewsArticle | ||||
|   /// 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? thumbnail = null, | ||||
|     Object? title = null, | ||||
|     Object? description = null, | ||||
|     Object? content = null, | ||||
|     Object? url = null, | ||||
|     Object? hash = null, | ||||
|     Object? source = null, | ||||
|     Object? publishedAt = 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 dynamic, | ||||
|       thumbnail: null == thumbnail | ||||
|           ? _value.thumbnail | ||||
|           : thumbnail // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       title: null == title | ||||
|           ? _value.title | ||||
|           : title // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       description: null == description | ||||
|           ? _value.description | ||||
|           : description // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       content: null == content | ||||
|           ? _value.content | ||||
|           : content // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       url: null == url | ||||
|           ? _value.url | ||||
|           : url // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       hash: null == hash | ||||
|           ? _value.hash | ||||
|           : hash // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       source: null == source | ||||
|           ? _value.source | ||||
|           : source // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       publishedAt: freezed == publishedAt | ||||
|           ? _value.publishedAt | ||||
|           : publishedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|     ) as $Val); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract class _$$SnNewsArticleImplCopyWith<$Res> | ||||
|     implements $SnNewsArticleCopyWith<$Res> { | ||||
|   factory _$$SnNewsArticleImplCopyWith( | ||||
|           _$SnNewsArticleImpl value, $Res Function(_$SnNewsArticleImpl) then) = | ||||
|       __$$SnNewsArticleImplCopyWithImpl<$Res>; | ||||
|   @override | ||||
|   @useResult | ||||
|   $Res call( | ||||
|       {int id, | ||||
|       DateTime createdAt, | ||||
|       DateTime updatedAt, | ||||
|       dynamic deletedAt, | ||||
|       String thumbnail, | ||||
|       String title, | ||||
|       String description, | ||||
|       String content, | ||||
|       String url, | ||||
|       String hash, | ||||
|       String source, | ||||
|       DateTime? publishedAt}); | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| class __$$SnNewsArticleImplCopyWithImpl<$Res> | ||||
|     extends _$SnNewsArticleCopyWithImpl<$Res, _$SnNewsArticleImpl> | ||||
|     implements _$$SnNewsArticleImplCopyWith<$Res> { | ||||
|   __$$SnNewsArticleImplCopyWithImpl( | ||||
|       _$SnNewsArticleImpl _value, $Res Function(_$SnNewsArticleImpl) _then) | ||||
|       : super(_value, _then); | ||||
|  | ||||
|   /// Create a copy of SnNewsArticle | ||||
|   /// 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? thumbnail = null, | ||||
|     Object? title = null, | ||||
|     Object? description = null, | ||||
|     Object? content = null, | ||||
|     Object? url = null, | ||||
|     Object? hash = null, | ||||
|     Object? source = null, | ||||
|     Object? publishedAt = freezed, | ||||
|   }) { | ||||
|     return _then(_$SnNewsArticleImpl( | ||||
|       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 dynamic, | ||||
|       thumbnail: null == thumbnail | ||||
|           ? _value.thumbnail | ||||
|           : thumbnail // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       title: null == title | ||||
|           ? _value.title | ||||
|           : title // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       description: null == description | ||||
|           ? _value.description | ||||
|           : description // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       content: null == content | ||||
|           ? _value.content | ||||
|           : content // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       url: null == url | ||||
|           ? _value.url | ||||
|           : url // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       hash: null == hash | ||||
|           ? _value.hash | ||||
|           : hash // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       source: null == source | ||||
|           ? _value.source | ||||
|           : source // ignore: cast_nullable_to_non_nullable | ||||
|               as String, | ||||
|       publishedAt: freezed == publishedAt | ||||
|           ? _value.publishedAt | ||||
|           : publishedAt // ignore: cast_nullable_to_non_nullable | ||||
|               as DateTime?, | ||||
|     )); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
| class _$SnNewsArticleImpl implements _SnNewsArticle { | ||||
|   const _$SnNewsArticleImpl( | ||||
|       {required this.id, | ||||
|       required this.createdAt, | ||||
|       required this.updatedAt, | ||||
|       required this.deletedAt, | ||||
|       required this.thumbnail, | ||||
|       required this.title, | ||||
|       required this.description, | ||||
|       required this.content, | ||||
|       required this.url, | ||||
|       required this.hash, | ||||
|       required this.source, | ||||
|       required this.publishedAt}); | ||||
|  | ||||
|   factory _$SnNewsArticleImpl.fromJson(Map<String, dynamic> json) => | ||||
|       _$$SnNewsArticleImplFromJson(json); | ||||
|  | ||||
|   @override | ||||
|   final int id; | ||||
|   @override | ||||
|   final DateTime createdAt; | ||||
|   @override | ||||
|   final DateTime updatedAt; | ||||
|   @override | ||||
|   final dynamic deletedAt; | ||||
|   @override | ||||
|   final String thumbnail; | ||||
|   @override | ||||
|   final String title; | ||||
|   @override | ||||
|   final String description; | ||||
|   @override | ||||
|   final String content; | ||||
|   @override | ||||
|   final String url; | ||||
|   @override | ||||
|   final String hash; | ||||
|   @override | ||||
|   final String source; | ||||
|   @override | ||||
|   final DateTime? publishedAt; | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return 'SnNewsArticle(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, source: $source, publishedAt: $publishedAt)'; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return identical(this, other) || | ||||
|         (other.runtimeType == runtimeType && | ||||
|             other is _$SnNewsArticleImpl && | ||||
|             (identical(other.id, id) || other.id == id) && | ||||
|             (identical(other.createdAt, createdAt) || | ||||
|                 other.createdAt == createdAt) && | ||||
|             (identical(other.updatedAt, updatedAt) || | ||||
|                 other.updatedAt == updatedAt) && | ||||
|             const DeepCollectionEquality().equals(other.deletedAt, deletedAt) && | ||||
|             (identical(other.thumbnail, thumbnail) || | ||||
|                 other.thumbnail == thumbnail) && | ||||
|             (identical(other.title, title) || other.title == title) && | ||||
|             (identical(other.description, description) || | ||||
|                 other.description == description) && | ||||
|             (identical(other.content, content) || other.content == content) && | ||||
|             (identical(other.url, url) || other.url == url) && | ||||
|             (identical(other.hash, hash) || other.hash == hash) && | ||||
|             (identical(other.source, source) || other.source == source) && | ||||
|             (identical(other.publishedAt, publishedAt) || | ||||
|                 other.publishedAt == publishedAt)); | ||||
|   } | ||||
|  | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   int get hashCode => Object.hash( | ||||
|       runtimeType, | ||||
|       id, | ||||
|       createdAt, | ||||
|       updatedAt, | ||||
|       const DeepCollectionEquality().hash(deletedAt), | ||||
|       thumbnail, | ||||
|       title, | ||||
|       description, | ||||
|       content, | ||||
|       url, | ||||
|       hash, | ||||
|       source, | ||||
|       publishedAt); | ||||
|  | ||||
|   /// Create a copy of SnNewsArticle | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   @override | ||||
|   @pragma('vm:prefer-inline') | ||||
|   _$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith => | ||||
|       __$$SnNewsArticleImplCopyWithImpl<_$SnNewsArticleImpl>(this, _$identity); | ||||
|  | ||||
|   @override | ||||
|   Map<String, dynamic> toJson() { | ||||
|     return _$$SnNewsArticleImplToJson( | ||||
|       this, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| abstract class _SnNewsArticle implements SnNewsArticle { | ||||
|   const factory _SnNewsArticle( | ||||
|       {required final int id, | ||||
|       required final DateTime createdAt, | ||||
|       required final DateTime updatedAt, | ||||
|       required final dynamic deletedAt, | ||||
|       required final String thumbnail, | ||||
|       required final String title, | ||||
|       required final String description, | ||||
|       required final String content, | ||||
|       required final String url, | ||||
|       required final String hash, | ||||
|       required final String source, | ||||
|       required final DateTime? publishedAt}) = _$SnNewsArticleImpl; | ||||
|  | ||||
|   factory _SnNewsArticle.fromJson(Map<String, dynamic> json) = | ||||
|       _$SnNewsArticleImpl.fromJson; | ||||
|  | ||||
|   @override | ||||
|   int get id; | ||||
|   @override | ||||
|   DateTime get createdAt; | ||||
|   @override | ||||
|   DateTime get updatedAt; | ||||
|   @override | ||||
|   dynamic get deletedAt; | ||||
|   @override | ||||
|   String get thumbnail; | ||||
|   @override | ||||
|   String get title; | ||||
|   @override | ||||
|   String get description; | ||||
|   @override | ||||
|   String get content; | ||||
|   @override | ||||
|   String get url; | ||||
|   @override | ||||
|   String get hash; | ||||
|   @override | ||||
|   String get source; | ||||
|   @override | ||||
|   DateTime? get publishedAt; | ||||
|  | ||||
|   /// Create a copy of SnNewsArticle | ||||
|   /// with the given fields replaced by the non-null parameter values. | ||||
|   @override | ||||
|   @JsonKey(includeFromJson: false, includeToJson: false) | ||||
|   _$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith => | ||||
|       throw _privateConstructorUsedError; | ||||
| } | ||||
							
								
								
									
										61
									
								
								lib/types/news.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								lib/types/news.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'news.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _$SnNewsSourceImpl _$$SnNewsSourceImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnNewsSourceImpl( | ||||
|       id: json['id'] as String, | ||||
|       label: json['label'] as String, | ||||
|       type: json['type'] as String, | ||||
|       source: json['source'] as String, | ||||
|       depth: (json['depth'] as num).toInt(), | ||||
|       enabled: json['enabled'] as bool, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnNewsSourceImplToJson(_$SnNewsSourceImpl instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'label': instance.label, | ||||
|       'type': instance.type, | ||||
|       'source': instance.source, | ||||
|       'depth': instance.depth, | ||||
|       'enabled': instance.enabled, | ||||
|     }; | ||||
|  | ||||
| _$SnNewsArticleImpl _$$SnNewsArticleImplFromJson(Map<String, dynamic> json) => | ||||
|     _$SnNewsArticleImpl( | ||||
|       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'], | ||||
|       thumbnail: json['thumbnail'] as String, | ||||
|       title: json['title'] as String, | ||||
|       description: json['description'] as String, | ||||
|       content: json['content'] as String, | ||||
|       url: json['url'] as String, | ||||
|       hash: json['hash'] as String, | ||||
|       source: json['source'] as String, | ||||
|       publishedAt: json['published_at'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['published_at'] as String), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'deleted_at': instance.deletedAt, | ||||
|       'thumbnail': instance.thumbnail, | ||||
|       'title': instance.title, | ||||
|       'description': instance.description, | ||||
|       'content': instance.content, | ||||
|       'url': instance.url, | ||||
|       'hash': instance.hash, | ||||
|       'source': instance.source, | ||||
|       'published_at': instance.publishedAt?.toIso8601String(), | ||||
|     }; | ||||
							
								
								
									
										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, | ||||
|     }; | ||||
| @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:package_info_plus/package_info_plus.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| class AboutScreen extends StatelessWidget { | ||||
| @@ -12,97 +13,103 @@ class AboutScreen extends StatelessWidget { | ||||
|   Widget build(BuildContext context) { | ||||
|     const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4)); | ||||
|  | ||||
|     return SizedBox( | ||||
|       width: double.infinity, | ||||
|       child: Column( | ||||
|         mainAxisAlignment: MainAxisAlignment.center, | ||||
|         children: [ | ||||
|           ClipRRect( | ||||
|             borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|             child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120), | ||||
|           ), | ||||
|           const Gap(8), | ||||
|           Text( | ||||
|             'Solian', | ||||
|             style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36), | ||||
|           ), | ||||
|           const Text( | ||||
|             'The Solar Network', | ||||
|             style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), | ||||
|           ), | ||||
|           const Gap(8), | ||||
|           FutureBuilder( | ||||
|             future: PackageInfo.fromPlatform(), | ||||
|             builder: (context, snapshot) { | ||||
|               if (!snapshot.hasData) { | ||||
|                 return const SizedBox.shrink(); | ||||
|               } | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: const PageBackButton(), | ||||
|         title: Text('screenAbout').tr(), | ||||
|       ), | ||||
|       body: SizedBox( | ||||
|         width: double.infinity, | ||||
|         child: Column( | ||||
|           mainAxisAlignment: MainAxisAlignment.center, | ||||
|           children: [ | ||||
|             ClipRRect( | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|               child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120), | ||||
|             ), | ||||
|             const Gap(8), | ||||
|             Text( | ||||
|               'Solian', | ||||
|               style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36), | ||||
|             ), | ||||
|             const Text( | ||||
|               'The Solar Network', | ||||
|               style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), | ||||
|             ), | ||||
|             const Gap(8), | ||||
|             FutureBuilder( | ||||
|               future: PackageInfo.fromPlatform(), | ||||
|               builder: (context, snapshot) { | ||||
|                 if (!snapshot.hasData) { | ||||
|                   return const SizedBox.shrink(); | ||||
|                 } | ||||
|  | ||||
|               return Text( | ||||
|                 'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}', | ||||
|                 style: const TextStyle(fontFamily: 'monospace'), | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|           Text('Copyright © ${DateTime.now().year} Solsynth LLC'), | ||||
|           const Gap(16), | ||||
|           Container( | ||||
|             constraints: const BoxConstraints(maxWidth: 280), | ||||
|             child: Wrap( | ||||
|               spacing: 4, | ||||
|               runSpacing: 4, | ||||
|               alignment: WrapAlignment.center, | ||||
|               children: [ | ||||
|                 TextButton( | ||||
|                   style: denseButtonStyle, | ||||
|                   child: Text('appDetails').tr(), | ||||
|                   onPressed: () async { | ||||
|                     final info = await PackageInfo.fromPlatform(); | ||||
|                 return Text( | ||||
|                   'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}', | ||||
|                   style: const TextStyle(fontFamily: 'monospace'), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|             Text('Copyright © ${DateTime.now().year} Solsynth LLC'), | ||||
|             const Gap(16), | ||||
|             Container( | ||||
|               constraints: const BoxConstraints(maxWidth: 280), | ||||
|               child: Wrap( | ||||
|                 spacing: 4, | ||||
|                 runSpacing: 4, | ||||
|                 alignment: WrapAlignment.center, | ||||
|                 children: [ | ||||
|                   TextButton( | ||||
|                     style: denseButtonStyle, | ||||
|                     child: Text('appDetails').tr(), | ||||
|                     onPressed: () async { | ||||
|                       final info = await PackageInfo.fromPlatform(); | ||||
|  | ||||
|                     if (!context.mounted) return; | ||||
|                     showAboutDialog( | ||||
|                       context: context, | ||||
|                       applicationName: 'Solian', | ||||
|                       applicationVersion: '${info.version}+${info.buildNumber}', | ||||
|                       applicationLegalese: | ||||
|                           'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.', | ||||
|                       applicationIcon: ClipRRect( | ||||
|                         borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|                         child: Image.asset( | ||||
|                           'assets/icon/icon-light-radius.png', | ||||
|                           width: 60, | ||||
|                           height: 60, | ||||
|                       if (!context.mounted) return; | ||||
|                       showAboutDialog( | ||||
|                         context: context, | ||||
|                         applicationName: 'Solian', | ||||
|                         applicationVersion: '${info.version}+${info.buildNumber}', | ||||
|                         applicationLegalese: | ||||
|                             'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.', | ||||
|                         applicationIcon: ClipRRect( | ||||
|                           borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|                           child: Image.asset( | ||||
|                             'assets/icon/icon-light-radius.png', | ||||
|                             width: 60, | ||||
|                             height: 60, | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|                 TextButton( | ||||
|                   style: denseButtonStyle, | ||||
|                   child: Text('termRelated').tr(), | ||||
|                   onPressed: () { | ||||
|                     launchUrlString('https://solsynth.dev/terms'); | ||||
|                   }, | ||||
|                 ), | ||||
|                 TextButton( | ||||
|                   style: denseButtonStyle, | ||||
|                   child: Text('serviceStatus').tr(), | ||||
|                   onPressed: () { | ||||
|                     launchUrlString('https://status.solsynth.dev'); | ||||
|                   }, | ||||
|                 ), | ||||
|               ], | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                   TextButton( | ||||
|                     style: denseButtonStyle, | ||||
|                     child: Text('termRelated').tr(), | ||||
|                     onPressed: () { | ||||
|                       launchUrlString('https://solsynth.dev/terms'); | ||||
|                     }, | ||||
|                   ), | ||||
|                   TextButton( | ||||
|                     style: denseButtonStyle, | ||||
|                     child: Text('serviceStatus').tr(), | ||||
|                     onPressed: () { | ||||
|                       launchUrlString('https://status.solsynth.dev'); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ).center(), | ||||
|             const Gap(16), | ||||
|             const Text( | ||||
|               'Open-sourced under AGPLv3', | ||||
|               style: TextStyle( | ||||
|                 fontWeight: FontWeight.w300, | ||||
|                 fontSize: 12, | ||||
|               ), | ||||
|             ), | ||||
|           ).center(), | ||||
|           const Gap(16), | ||||
|           const Text( | ||||
|             'Open-sourced under AGPLv3', | ||||
|             style: TextStyle( | ||||
|               fontWeight: FontWeight.w300, | ||||
|               fontSize: 12, | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										164
									
								
								lib/widgets/account/account_popover.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								lib/widgets/account/account_popover.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/experience.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/screens/account/profile_page.dart'; | ||||
| import 'package:surface/types/account.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
|  | ||||
| class AccountPopoverCard extends StatelessWidget { | ||||
|   final SnAccount data; | ||||
|  | ||||
|   const AccountPopoverCard({super.key, required this.data}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|  | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       mainAxisSize: MainAxisSize.min, | ||||
|       children: [ | ||||
|         if (data.banner.isNotEmpty) | ||||
|           Container( | ||||
|             color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|             child: AspectRatio( | ||||
|               aspectRatio: 16 / 7, | ||||
|               child: AutoResizeUniversalImage( | ||||
|                 sn.getAttachmentUrl(data.banner), | ||||
|                 fit: BoxFit.cover, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         // Top padding | ||||
|         Gap(16), | ||||
|         Row( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             AccountImage( | ||||
|               content: data.avatar, | ||||
|               radius: 20, | ||||
|             ), | ||||
|             Gap(16), | ||||
|             Expanded( | ||||
|               child: Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 mainAxisSize: MainAxisSize.min, | ||||
|                 children: [ | ||||
|                   Text(data.nick).bold(), | ||||
|                   Text('@${data.name}').fontSize(13).opacity(0.75), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|             IconButton( | ||||
|               onPressed: () { | ||||
|                 Navigator.pop(context); | ||||
|                 GoRouter.of(context).pushNamed( | ||||
|                   'accountProfilePage', | ||||
|                   pathParameters: {'name': data.name}, | ||||
|                 ); | ||||
|               }, | ||||
|               icon: const Icon(Symbols.chevron_right), | ||||
|               padding: EdgeInsets.zero, | ||||
|               visualDensity: const VisualDensity(horizontal: -4, vertical: -4), | ||||
|             ), | ||||
|             const Gap(8) | ||||
|           ], | ||||
|         ).padding(horizontal: 16), | ||||
|         const Gap(16), | ||||
|         Wrap( | ||||
|           children: data.badges | ||||
|               .map( | ||||
|                 (ele) => Tooltip( | ||||
|               richMessage: TextSpan( | ||||
|                 children: [ | ||||
|                   TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()), | ||||
|                   if (ele.metadata['title'] != null) | ||||
|                     TextSpan( | ||||
|                       text: '\n${ele.metadata['title']}', | ||||
|                       style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|                     ), | ||||
|                   TextSpan(text: '\n'), | ||||
|                   TextSpan( | ||||
|                     text: DateFormat.yMEd().format(ele.createdAt), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|               child: Icon( | ||||
|                 kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark, | ||||
|                 color: kBadgesMeta[ele.type]?.$3, | ||||
|                 fill: 1, | ||||
|               ), | ||||
|             ), | ||||
|           ) | ||||
|               .toList(), | ||||
|         ).padding(horizontal: 24), | ||||
|         const Gap(8), | ||||
|         Row( | ||||
|           crossAxisAlignment: CrossAxisAlignment.center, | ||||
|           children: [ | ||||
|             const Icon(Symbols.star), | ||||
|             const Gap(8), | ||||
|             Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'), | ||||
|             const Gap(8), | ||||
|             Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0)).fontSize(11).opacity(0.5), | ||||
|             const Gap(8), | ||||
|             Container( | ||||
|               width: double.infinity, | ||||
|               constraints: const BoxConstraints(maxWidth: 160), | ||||
|               child: LinearProgressIndicator( | ||||
|                 value: calcLevelUpProgress(data.profile?.experience ?? 0), | ||||
|                 borderRadius: BorderRadius.circular(8), | ||||
|                 backgroundColor: Theme.of(context).colorScheme.surfaceContainer, | ||||
|               ).alignment(Alignment.centerLeft), | ||||
|             ), | ||||
|           ], | ||||
|         ).padding(horizontal: 24), | ||||
|         FutureBuilder( | ||||
|           future: sn.client.get('/cgi/id/users/${data.name}/status'), | ||||
|           builder: (context, snapshot) { | ||||
|             final SnAccountStatusInfo? status = | ||||
|                 snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null; | ||||
|             return Row( | ||||
|                 children: [ | ||||
|                   Icon( | ||||
|                     Symbols.circle, | ||||
|                     fill: 1, | ||||
|                     size: 16, | ||||
|                     color: (status?.isOnline ?? false) ? Colors.green : Colors.grey, | ||||
|                   ).padding(all: 4), | ||||
|                   const Gap(8), | ||||
|                   Text( | ||||
|                     status != null | ||||
|                         ? status.isOnline | ||||
|                             ? 'accountStatusOnline'.tr() | ||||
|                             : 'accountStatusOffline'.tr() | ||||
|                         : 'loading'.tr(), | ||||
|                   ), | ||||
|                   if (status != null && !status.isOnline && status.lastSeenAt != null) | ||||
|                     Text( | ||||
|                       'accountStatusLastSeen'.tr(args: [ | ||||
|                         status.lastSeenAt != null | ||||
|                             ? RelativeTime(context).format( | ||||
|                                 status.lastSeenAt!.toLocal(), | ||||
|                               ) | ||||
|                             : 'unknown', | ||||
|                       ]), | ||||
|                     ).padding(left: 6).opacity(0.75), | ||||
|                 ], | ||||
|               ).padding(horizontal: 24); | ||||
|           }, | ||||
|         ), | ||||
|         // Bottom padding | ||||
|         const Gap(16), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -15,6 +15,7 @@ class AttachmentList extends StatefulWidget { | ||||
|   final List<SnAttachment?> data; | ||||
|   final bool bordered; | ||||
|   final bool gridded; | ||||
|   final bool columned; | ||||
|   final BoxFit fit; | ||||
|   final double? maxHeight; | ||||
|   final double? minWidth; | ||||
| @@ -26,6 +27,7 @@ class AttachmentList extends StatefulWidget { | ||||
|     required this.data, | ||||
|     this.bordered = false, | ||||
|     this.gridded = false, | ||||
|     this.columned = false, | ||||
|     this.fit = BoxFit.cover, | ||||
|     this.maxHeight, | ||||
|     this.minWidth, | ||||
| @@ -105,7 +107,10 @@ class _AttachmentListState extends State<AttachmentList> { | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         if (widget.gridded) { | ||||
|         final fullOfImage = | ||||
|             widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length; | ||||
|  | ||||
|         if (widget.gridded && fullOfImage) { | ||||
|           return Container( | ||||
|             margin: widget.padding ?? EdgeInsets.zero, | ||||
|             decoration: BoxDecoration( | ||||
| @@ -153,68 +158,109 @@ 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, | ||||
|         if ((!fullOfImage && widget.gridded) || widget.columned) { | ||||
|           return Container( | ||||
|             margin: widget.padding ?? EdgeInsets.zero, | ||||
|             decoration: BoxDecoration( | ||||
|               color: backgroundColor, | ||||
|               border: Border( | ||||
|                 top: borderSide, | ||||
|                 bottom: borderSide, | ||||
|               ), | ||||
|               borderRadius: AttachmentList.kDefaultRadius, | ||||
|             ), | ||||
|             child: ClipRRect( | ||||
|               borderRadius: AttachmentList.kDefaultRadius, | ||||
|               child: Column( | ||||
|                 children: widget.data | ||||
|                     .mapIndexed( | ||||
|                       (idx, ele) => GestureDetector( | ||||
|                         child: AspectRatio( | ||||
|                           aspectRatio: ele?.data['ratio']?.toDouble() ?? 1, | ||||
|                           child: Container( | ||||
|                             constraints: constraints, | ||||
|                             child: AttachmentItem( | ||||
|                               data: ele, | ||||
|                               heroTag: heroTags[idx], | ||||
|                               fit: BoxFit.cover, | ||||
|                             ), | ||||
|                             child: ClipRRect( | ||||
|                               borderRadius: AttachmentList.kDefaultRadius, | ||||
|                               child: AttachmentItem( | ||||
|                                 data: widget.data[idx], | ||||
|                                 heroTag: heroTags[idx], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ) | ||||
|                     .expand((ele) => [ele, const Divider(height: 1)]) | ||||
|                     .toList() | ||||
|                   ..removeLast(), | ||||
|               ), | ||||
|             ), | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|         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, | ||||
|                             ), | ||||
|                             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, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|         ); | ||||
|   | ||||
| @@ -129,6 +129,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> { | ||||
|  | ||||
|   Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75); | ||||
|  | ||||
|   bool _showDetail = false; | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
| @@ -144,223 +146,350 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> { | ||||
|       onDismissed: () { | ||||
|         Navigator.of(context).pop(); | ||||
|       }, | ||||
|       direction: DismissiblePageDismissDirection.down, | ||||
|       direction: DismissiblePageDismissDirection.none, | ||||
|       backgroundColor: Colors.transparent, | ||||
|       isFullScreen: true, | ||||
|       child: Scaffold( | ||||
|         body: Stack( | ||||
|           children: [ | ||||
|             Builder(builder: (context) { | ||||
|               if (widget.data.length == 1) { | ||||
|                 final heroTag = widget.heroTags?.first ?? uuid.v4(); | ||||
|                 return Hero( | ||||
|                   tag: 'attachment-${widget.data.first.rid}-$heroTag', | ||||
|                   child: PhotoView( | ||||
|                     key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'), | ||||
|                     backgroundDecoration: BoxDecoration(color: Colors.transparent), | ||||
|                     imageProvider: UniversalImage.provider( | ||||
|                       sn.getAttachmentUrl(widget.data.first.rid), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ); | ||||
|               } | ||||
|  | ||||
|               return PhotoViewGallery.builder( | ||||
|                 pageController: _pageController, | ||||
|                 scrollPhysics: const BouncingScrollPhysics(), | ||||
|                 builder: (context, idx) { | ||||
|                   final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4(); | ||||
|                   return PhotoViewGalleryPageOptions( | ||||
|                     imageProvider: UniversalImage.provider( | ||||
|                       sn.getAttachmentUrl(widget.data.elementAt(idx).rid), | ||||
|                     ), | ||||
|                     heroAttributes: PhotoViewHeroAttributes( | ||||
|                       tag: 'attachment-${widget.data.first.rid}-$heroTag', | ||||
|       child: GestureDetector( | ||||
|         behavior: HitTestBehavior.translucent, | ||||
|         child: Scaffold( | ||||
|           body: Stack( | ||||
|             children: [ | ||||
|               Builder(builder: (context) { | ||||
|                 if (widget.data.length == 1) { | ||||
|                   final heroTag = widget.heroTags?.first ?? uuid.v4(); | ||||
|                   return Hero( | ||||
|                     tag: 'attachment-${widget.data.first.rid}-$heroTag', | ||||
|                     child: PhotoView( | ||||
|                       key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'), | ||||
|                       backgroundDecoration: BoxDecoration(color: Colors.transparent), | ||||
|                       imageProvider: UniversalImage.provider( | ||||
|                         sn.getAttachmentUrl(widget.data.first.rid), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ); | ||||
|                 }, | ||||
|                 itemCount: widget.data.length, | ||||
|                 loadingBuilder: (context, event) => Center( | ||||
|                   child: SizedBox( | ||||
|                     width: 20.0, | ||||
|                     height: 20.0, | ||||
|                     child: CircularProgressIndicator( | ||||
|                       value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1), | ||||
|                 } | ||||
|  | ||||
|                 return PhotoViewGallery.builder( | ||||
|                   pageController: _pageController, | ||||
|                   scrollPhysics: const BouncingScrollPhysics(), | ||||
|                   builder: (context, idx) { | ||||
|                     final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4(); | ||||
|                     return PhotoViewGalleryPageOptions( | ||||
|                       imageProvider: UniversalImage.provider( | ||||
|                         sn.getAttachmentUrl(widget.data.elementAt(idx).rid), | ||||
|                       ), | ||||
|                       heroAttributes: PhotoViewHeroAttributes( | ||||
|                         tag: 'attachment-${widget.data.first.rid}-$heroTag', | ||||
|                       ), | ||||
|                     ); | ||||
|                   }, | ||||
|                   itemCount: widget.data.length, | ||||
|                   loadingBuilder: (context, event) => Center( | ||||
|                     child: SizedBox( | ||||
|                       width: 20.0, | ||||
|                       height: 20.0, | ||||
|                       child: CircularProgressIndicator( | ||||
|                         value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|                 backgroundDecoration: BoxDecoration(color: Colors.transparent), | ||||
|               ); | ||||
|             }), | ||||
|             Align( | ||||
|               alignment: Alignment.bottomCenter, | ||||
|               child: IgnorePointer( | ||||
|                 child: Container( | ||||
|                   height: 300, | ||||
|                   decoration: BoxDecoration( | ||||
|                     gradient: LinearGradient( | ||||
|                       begin: Alignment.bottomCenter, | ||||
|                       end: Alignment.topCenter, | ||||
|                       colors: [ | ||||
|                         Theme.of(context).colorScheme.surface, | ||||
|                         Colors.transparent, | ||||
|                       ], | ||||
|                   backgroundDecoration: BoxDecoration(color: Colors.transparent), | ||||
|                 ); | ||||
|               }), | ||||
|               Align( | ||||
|                 alignment: Alignment.bottomCenter, | ||||
|                 child: IgnorePointer( | ||||
|                   child: Container( | ||||
|                     height: 300, | ||||
|                     decoration: BoxDecoration( | ||||
|                       gradient: LinearGradient( | ||||
|                         begin: Alignment.bottomCenter, | ||||
|                         end: Alignment.topCenter, | ||||
|                         colors: [ | ||||
|                           Theme.of(context).colorScheme.surface, | ||||
|                           Colors.transparent, | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             Positioned( | ||||
|               left: 16, | ||||
|               right: 16, | ||||
|               bottom: 16 + MediaQuery.of(context).padding.bottom, | ||||
|               child: Material( | ||||
|                 color: Colors.transparent, | ||||
|                 child: Builder(builder: (context) { | ||||
|                   final ud = context.read<UserDirectoryProvider>(); | ||||
|                   final item = widget.data.elementAt( | ||||
|                     widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0, | ||||
|                   ); | ||||
|                   final account = ud.getAccountFromCache(item.accountId); | ||||
|               Positioned( | ||||
|                 left: 16, | ||||
|                 right: 16, | ||||
|                 bottom: 16 + MediaQuery.of(context).padding.bottom, | ||||
|                 child: Material( | ||||
|                   color: Colors.transparent, | ||||
|                   child: Builder(builder: (context) { | ||||
|                     final ud = context.read<UserDirectoryProvider>(); | ||||
|                     final item = widget.data.elementAt( | ||||
|                       widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0, | ||||
|                     ); | ||||
|                     final account = ud.getAccountFromCache(item.accountId); | ||||
|  | ||||
|                   return Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       if (item.accountId > 0) | ||||
|                         Row( | ||||
|                           children: [ | ||||
|                             IgnorePointer( | ||||
|                               child: AccountImage( | ||||
|                                 content: account?.avatar, | ||||
|                                 radius: 19, | ||||
|                               ), | ||||
|                             ), | ||||
|                             const Gap(8), | ||||
|                             Expanded( | ||||
|                               child: IgnorePointer( | ||||
|                                 child: Column( | ||||
|                                   crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                                   children: [ | ||||
|                                     Text( | ||||
|                                       'attachmentUploadBy'.tr(), | ||||
|                                       style: Theme.of(context).textTheme.bodySmall, | ||||
|                                     ), | ||||
|                                     Text( | ||||
|                                       account?.nick ?? 'unknown'.tr(), | ||||
|                                       style: Theme.of(context).textTheme.bodyMedium, | ||||
|                                     ), | ||||
|                                   ], | ||||
|                     return Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                       children: [ | ||||
|                         if (item.accountId > 0) | ||||
|                           Row( | ||||
|                             children: [ | ||||
|                               IgnorePointer( | ||||
|                                 child: AccountImage( | ||||
|                                   content: account?.avatar, | ||||
|                                   radius: 19, | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ), | ||||
|                             if (widget.data.length > 1) | ||||
|                               IgnorePointer( | ||||
|                                 child: Text( | ||||
|                                   '${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}', | ||||
|                                   style: GoogleFonts.robotoMono(fontSize: 13), | ||||
|                                 ).padding(right: 8), | ||||
|                               ), | ||||
|                             InkWell( | ||||
|                               borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|                               onTap: _isDownloading | ||||
|                                   ? null | ||||
|                                   : () => _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0), | ||||
|                               child: Container( | ||||
|                                 padding: const EdgeInsets.all(6), | ||||
|                                 child: !_isDownloading | ||||
|                                     ? !_isCompletedDownload | ||||
|                                         ? const Icon(Symbols.save_alt) | ||||
|                                         : const Icon(Symbols.download_done) | ||||
|                                     : SizedBox( | ||||
|                                         width: 24, | ||||
|                                         height: 24, | ||||
|                                         child: CircularProgressIndicator( | ||||
|                                           value: _progressOfDownload, | ||||
|                                           strokeWidth: 3, | ||||
|                                         ), | ||||
|                               const Gap(8), | ||||
|                               Expanded( | ||||
|                                 child: IgnorePointer( | ||||
|                                   child: Column( | ||||
|                                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                                     children: [ | ||||
|                                       Text( | ||||
|                                         'attachmentUploadBy'.tr(), | ||||
|                                         style: Theme.of(context).textTheme.bodySmall, | ||||
|                                       ), | ||||
|                                       Text( | ||||
|                                         account?.nick ?? 'unknown'.tr(), | ||||
|                                         style: Theme.of(context).textTheme.bodyMedium, | ||||
|                                       ), | ||||
|                                     ], | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                               if (widget.data.length > 1) | ||||
|                                 IgnorePointer( | ||||
|                                   child: Text( | ||||
|                                     '${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}', | ||||
|                                     style: GoogleFonts.robotoMono(fontSize: 13), | ||||
|                                   ).padding(right: 8), | ||||
|                                 ), | ||||
|                               InkWell( | ||||
|                                 borderRadius: const BorderRadius.all(Radius.circular(16)), | ||||
|                                 onTap: _isDownloading | ||||
|                                     ? null | ||||
|                                     : () => | ||||
|                                         _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0), | ||||
|                                 child: Container( | ||||
|                                   padding: const EdgeInsets.all(6), | ||||
|                                   child: !_isDownloading | ||||
|                                       ? !_isCompletedDownload | ||||
|                                           ? const Icon(Symbols.save_alt) | ||||
|                                           : const Icon(Symbols.download_done) | ||||
|                                       : SizedBox( | ||||
|                                           width: 24, | ||||
|                                           height: 24, | ||||
|                                           child: CircularProgressIndicator( | ||||
|                                             value: _progressOfDownload, | ||||
|                                             strokeWidth: 3, | ||||
|                                           ), | ||||
|                                         ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         const Gap(4), | ||||
|                         IgnorePointer( | ||||
|                           child: Text( | ||||
|                             item.alt, | ||||
|                             maxLines: 2, | ||||
|                             overflow: TextOverflow.ellipsis, | ||||
|                             style: const TextStyle( | ||||
|                               fontSize: 15, | ||||
|                               fontWeight: FontWeight.w500, | ||||
|                             ), | ||||
|                           ], | ||||
|                         ), | ||||
|                       const Gap(4), | ||||
|                       IgnorePointer( | ||||
|                         child: Text( | ||||
|                           item.alt, | ||||
|                           maxLines: 2, | ||||
|                           overflow: TextOverflow.ellipsis, | ||||
|                           style: const TextStyle( | ||||
|                             fontSize: 15, | ||||
|                             fontWeight: FontWeight.w500, | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                       const Gap(2), | ||||
|                       IgnorePointer( | ||||
|                         child: Wrap( | ||||
|                           spacing: 6, | ||||
|                           children: [ | ||||
|                             if (item.metadata['exif'] == null) | ||||
|                         const Gap(2), | ||||
|                         IgnorePointer( | ||||
|                           child: Wrap( | ||||
|                             spacing: 6, | ||||
|                             children: [ | ||||
|                               if (item.metadata['exif'] == null) | ||||
|                                 Text( | ||||
|                                   '#${item.rid}', | ||||
|                                   style: metaTextStyle, | ||||
|                                 ), | ||||
|                               if (item.metadata['exif']?['Model'] != null) | ||||
|                                 Text( | ||||
|                                   'attachmentShotOn'.tr(args: [ | ||||
|                                     item.metadata['exif']?['Model'], | ||||
|                                   ]), | ||||
|                                   style: metaTextStyle, | ||||
|                                 ).padding(right: 2), | ||||
|                               if (item.metadata['exif']?['ISO'] != null) | ||||
|                                 Text( | ||||
|                                   'ISO${item.metadata['exif']?['ISO']}', | ||||
|                                   style: metaTextStyle, | ||||
|                                 ).padding(right: 2), | ||||
|                               if (item.metadata['exif']?['Aperture'] != null) | ||||
|                                 Text( | ||||
|                                   'f/${item.metadata['exif']?['Aperture']}', | ||||
|                                   style: metaTextStyle, | ||||
|                                 ).padding(right: 2), | ||||
|                               if (item.metadata['exif']?['Megapixels'] != null && | ||||
|                                   item.metadata['exif']?['Model'] != null) | ||||
|                                 Text( | ||||
|                                   '${item.metadata['exif']?['Megapixels']}MP', | ||||
|                                   style: metaTextStyle, | ||||
|                                 ) | ||||
|                               else | ||||
|                                 Text( | ||||
|                                   item.size.formatBytes(), | ||||
|                                   style: metaTextStyle, | ||||
|                                 ), | ||||
|                               if (item.metadata['width'] != null && item.metadata['height'] != null) | ||||
|                                 Text( | ||||
|                                   '${item.metadata['width']}x${item.metadata['height']}', | ||||
|                                   style: metaTextStyle, | ||||
|                                 ), | ||||
|                               if (item.metadata['ratio'] != null) | ||||
|                                 Text( | ||||
|                                   (item.metadata['ratio'] as num).toStringAsFixed(2), | ||||
|                                   style: metaTextStyle, | ||||
|                                 ), | ||||
|                               Text( | ||||
|                                 '#${item.rid}', | ||||
|                                 item.mimetype, | ||||
|                                 style: metaTextStyle, | ||||
|                               ), | ||||
|                             if (item.metadata['exif']?['Model'] != null) | ||||
|                               Text( | ||||
|                                 'attachmentShotOn'.tr(args: [ | ||||
|                                   item.metadata['exif']?['Model'], | ||||
|                                 ]), | ||||
|                                 style: metaTextStyle, | ||||
|                               ).padding(right: 2), | ||||
|                             if (item.metadata['exif']?['ShutterSpeed'] != null) | ||||
|                               Text( | ||||
|                                 item.metadata['exif']?['ShutterSpeed'], | ||||
|                                 style: metaTextStyle, | ||||
|                               ).padding(right: 2), | ||||
|                             if (item.metadata['exif']?['ISO'] != null) | ||||
|                               Text( | ||||
|                                 'ISO${item.metadata['exif']?['ISO']}', | ||||
|                                 style: metaTextStyle, | ||||
|                               ).padding(right: 2), | ||||
|                             if (item.metadata['exif']?['Aperture'] != null) | ||||
|                               Text( | ||||
|                                 'f/${item.metadata['exif']?['Aperture']}', | ||||
|                                 style: metaTextStyle, | ||||
|                               ).padding(right: 2), | ||||
|                             if (item.metadata['exif']?['Megapixels'] != null && item.metadata['exif']?['Model'] != null) | ||||
|                               Text( | ||||
|                                 '${item.metadata['exif']?['Megapixels']}MP', | ||||
|                                 style: metaTextStyle, | ||||
|                               ) | ||||
|                             else | ||||
|                               Text( | ||||
|                                 '${item.size} Bytes', | ||||
|                                 style: metaTextStyle, | ||||
|                               ), | ||||
|                             if (item.metadata['width'] != null && item.metadata['height'] != null) | ||||
|                               Text( | ||||
|                                 '${item.metadata['width']}x${item.metadata['height']}', | ||||
|                                 style: metaTextStyle, | ||||
|                               ), | ||||
|                             if (item.metadata['ratio'] != null) | ||||
|                               Text( | ||||
|                                 (item.metadata['ratio'] as num).toStringAsFixed(2), | ||||
|                                 style: metaTextStyle, | ||||
|                               ), | ||||
|                             Text( | ||||
|                               item.mimetype, | ||||
|                               style: metaTextStyle, | ||||
|                             ), | ||||
|                           ], | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ); | ||||
|                 }), | ||||
|                       ], | ||||
|                     ); | ||||
|                   }), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|         onVerticalDragUpdate: (details) { | ||||
|           if (_showDetail) return; | ||||
|           if (details.delta.dy <= -40) { | ||||
|             _showDetail = true; | ||||
|             showModalBottomSheet( | ||||
|               context: context, | ||||
|               builder: (context) => _AttachmentZoomDetailPopup( | ||||
|                 data: widget.data.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0), | ||||
|               ), | ||||
|             ).then((_) { | ||||
|               _showDetail = false; | ||||
|             }); | ||||
|           } | ||||
|         }, | ||||
|         onTap: () { | ||||
|           Navigator.of(context).pop(); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _AttachmentZoomDetailPopup extends StatelessWidget { | ||||
|   final SnAttachment data; | ||||
|  | ||||
|   const _AttachmentZoomDetailPopup({required this.data}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final ud = context.read<UserDirectoryProvider>(); | ||||
|     final account = ud.getAccountFromCache(data.accountId); | ||||
|  | ||||
|     const tableGap = TableRow( | ||||
|       children: [ | ||||
|         TableCell(child: SizedBox(height: 16)), | ||||
|         TableCell(child: SizedBox(height: 16)), | ||||
|       ], | ||||
|     ); | ||||
|  | ||||
|     return SizedBox( | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Row( | ||||
|             crossAxisAlignment: CrossAxisAlignment.center, | ||||
|             children: [ | ||||
|               const Icon(Symbols.info, size: 24), | ||||
|               const Gap(16), | ||||
|               Text('attachmentDetailInfo').tr().textStyle(Theme.of(context).textTheme.titleLarge!), | ||||
|             ], | ||||
|           ).padding(horizontal: 20, top: 16, bottom: 12), | ||||
|           Expanded( | ||||
|             child: SingleChildScrollView( | ||||
|               child: Table( | ||||
|                 columnWidths: { | ||||
|                   0: IntrinsicColumnWidth(), | ||||
|                   1: FlexColumnWidth(), | ||||
|                 }, | ||||
|                 children: [ | ||||
|                   TableRow( | ||||
|                     children: [ | ||||
|                       TableCell( | ||||
|                         child: Text('attachmentUploadBy').tr().padding(right: 16), | ||||
|                       ), | ||||
|                       TableCell( | ||||
|                         child: Row( | ||||
|                           children: [ | ||||
|                             if (data.accountId > 0) | ||||
|                               AccountImage( | ||||
|                                 content: account?.avatar, | ||||
|                                 radius: 8, | ||||
|                               ), | ||||
|                             const Gap(8), | ||||
|                             Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()), | ||||
|                             const Gap(8), | ||||
|                             Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75), | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   tableGap, | ||||
|                   TableRow( | ||||
|                     children: [ | ||||
|                       TableCell(child: Text('Mimetype').padding(right: 16)), | ||||
|                       TableCell(child: Text(data.mimetype)), | ||||
|                     ], | ||||
|                   ), | ||||
|                   TableRow( | ||||
|                     children: [ | ||||
|                       TableCell(child: Text('Size').padding(right: 16)), | ||||
|                       TableCell( | ||||
|                           child: Row( | ||||
|                         children: [ | ||||
|                           Text(data.size.formatBytes()), | ||||
|                           const Gap(12), | ||||
|                           Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75), | ||||
|                         ], | ||||
|                       )), | ||||
|                     ], | ||||
|                   ), | ||||
|                   TableRow( | ||||
|                     children: [ | ||||
|                       TableCell(child: Text('Name').padding(right: 16)), | ||||
|                       TableCell(child: Text(data.name)), | ||||
|                     ], | ||||
|                   ), | ||||
|                   if (data.hash.isNotEmpty) | ||||
|                     TableRow( | ||||
|                       children: [ | ||||
|                         TableCell(child: Text('Hash').padding(right: 16)), | ||||
|                         TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)), | ||||
|                       ], | ||||
|                     ), | ||||
|                   tableGap, | ||||
|                   ...(data.metadata['exif']?.keys.map((k) => TableRow( | ||||
|                         children: [ | ||||
|                           TableCell(child: Text(k).padding(right: 16)), | ||||
|                           TableCell(child: Text(data.metadata['exif'][k].toString())), | ||||
|                         ], | ||||
|                       )) ?? | ||||
|                       []), | ||||
|                 ], | ||||
|               ).padding(horizontal: 20, vertical: 8), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -1,14 +1,19 @@ | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_context_menu/flutter_context_menu.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:popover/popover.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/user_directory.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/chat.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| import 'package:surface/widgets/account/account_popover.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_list.dart'; | ||||
| import 'package:surface/widgets/context_menu.dart'; | ||||
| import 'package:surface/widgets/link_preview.dart'; | ||||
| @@ -49,6 +54,8 @@ class ChatMessage extends StatelessWidget { | ||||
|  | ||||
|     final dateFormatter = DateFormat('MM/dd HH:mm'); | ||||
|  | ||||
|     final cfg = context.read<ConfigProvider>(); | ||||
|  | ||||
|     return SwipeTo( | ||||
|       key: Key('chat-message-${data.id}'), | ||||
|       iconOnLeftSwipe: Symbols.reply, | ||||
| @@ -95,8 +102,28 @@ class ChatMessage extends StatelessWidget { | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   if (!isMerged && !isCompact) | ||||
|                     AccountImage( | ||||
|                       content: user?.avatar, | ||||
|                     GestureDetector( | ||||
|                       child: AccountImage( | ||||
|                         content: user?.avatar, | ||||
|                       ), | ||||
|                       onTap: () { | ||||
|                         if (user == null) return; | ||||
|                         showPopover( | ||||
|                           backgroundColor: Theme.of(context).colorScheme.surface, | ||||
|                           context: context, | ||||
|                           transition: PopoverTransition.other, | ||||
|                           bodyBuilder: (context) => SizedBox( | ||||
|                             width: math.min(400, MediaQuery.of(context).size.width - 10), | ||||
|                             child: AccountPopoverCard( | ||||
|                               data: user, | ||||
|                             ), | ||||
|                           ), | ||||
|                           direction: PopoverDirection.bottom, | ||||
|                           arrowHeight: 5, | ||||
|                           arrowWidth: 15, | ||||
|                           arrowDxOffset: -190, | ||||
|                         ); | ||||
|                       }, | ||||
|                     ) | ||||
|                   else if (isMerged) | ||||
|                     const Gap(40), | ||||
| @@ -124,10 +151,13 @@ class ChatMessage extends StatelessWidget { | ||||
|                                   dateFormatter.format(data.createdAt.toLocal()), | ||||
|                                 ).fontSize(13), | ||||
|                               ], | ||||
|                             ), | ||||
|                             ).height(21), | ||||
|                           if (isCompact) const Gap(8), | ||||
|                           if (data.preload?.quoteEvent != null) | ||||
|                             StyledWidget(Container( | ||||
|                               constraints: BoxConstraints( | ||||
|                                 maxWidth: 480, | ||||
|                               ), | ||||
|                               decoration: BoxDecoration( | ||||
|                                 borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                                 border: Border.all( | ||||
| @@ -150,7 +180,12 @@ class ChatMessage extends StatelessWidget { | ||||
|                               ), | ||||
|                             )).padding(bottom: 4, top: 4), | ||||
|                           switch (data.type) { | ||||
|                             'messages.new' => _ChatMessageText(data: data), | ||||
|                             'messages.new' => _ChatMessageText( | ||||
|                                 data: data, | ||||
|                                 onReply: onReply, | ||||
|                                 onEdit: onEdit, | ||||
|                                 onDelete: onDelete, | ||||
|                               ), | ||||
|                             _ => _ChatMessageSystemNotify(data: data), | ||||
|                           }, | ||||
|                         ], | ||||
| @@ -160,7 +195,10 @@ class ChatMessage extends StatelessWidget { | ||||
|                 ], | ||||
|               ).opacity(isPending ? 0.5 : 1), | ||||
|             ), | ||||
|             if (data.body['text'] != null && data.type == 'messages.new' && (data.body['text']?.isNotEmpty ?? false)) | ||||
|             if (data.body['text'] != null && | ||||
|                 data.type == 'messages.new' && | ||||
|                 (data.body['text']?.isNotEmpty ?? false) && | ||||
|                 (cfg.prefs.getBool(kAppExpandChatLink) ?? true)) | ||||
|               LinkPreviewWidget(text: data.body['text']!), | ||||
|             if (data.preload?.attachments?.isNotEmpty ?? false) | ||||
|               AttachmentList( | ||||
| @@ -181,19 +219,75 @@ class ChatMessage extends StatelessWidget { | ||||
|  | ||||
| class _ChatMessageText extends StatelessWidget { | ||||
|   final SnChatMessage data; | ||||
|   final Function(SnChatMessage)? onReply; | ||||
|   final Function(SnChatMessage)? onEdit; | ||||
|   final Function(SnChatMessage)? onDelete; | ||||
|  | ||||
|   const _ChatMessageText({required this.data}); | ||||
|   const _ChatMessageText({required this.data, this.onReply, this.onEdit, this.onDelete}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final ua = context.read<UserProvider>(); | ||||
|  | ||||
|     final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id; | ||||
|  | ||||
|     if (data.body['text'] != null && data.body['text'].isNotEmpty) { | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           MarkdownTextContent( | ||||
|             content: data.body['text'], | ||||
|             isSelectable: true, | ||||
|             isAutoWarp: true, | ||||
|           SelectionArea( | ||||
|             contextMenuBuilder: (context, editableTextState) { | ||||
|               final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems; | ||||
|  | ||||
|               if (onReply != null) { | ||||
|                 items.insert( | ||||
|                   0, | ||||
|                   ContextMenuButtonItem( | ||||
|                     label: 'reply'.tr(), | ||||
|                     onPressed: () { | ||||
|                       ContextMenuController.removeAny(); | ||||
|                       onReply?.call(data); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ); | ||||
|               } | ||||
|               if (isOwner && onEdit != null) { | ||||
|                 items.insert( | ||||
|                   1, | ||||
|                   ContextMenuButtonItem( | ||||
|                     label: 'edit'.tr(), | ||||
|                     onPressed: () { | ||||
|                       ContextMenuController.removeAny(); | ||||
|                       onEdit?.call(data); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ); | ||||
|               } | ||||
|               if (isOwner && onDelete != null) { | ||||
|                 items.insert( | ||||
|                   2, | ||||
|                   ContextMenuButtonItem( | ||||
|                     label: 'delete'.tr(), | ||||
|                     onPressed: () { | ||||
|                       ContextMenuController.removeAny(); | ||||
|                       onDelete?.call(data); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ); | ||||
|               } | ||||
|  | ||||
|               return AdaptiveTextSelectionToolbar.buttonItems( | ||||
|                 anchors: editableTextState.contextMenuAnchors, | ||||
|                 buttonItems: items, | ||||
|               ); | ||||
|             }, | ||||
|             child: Container( | ||||
|               constraints: const BoxConstraints(maxWidth: 480), | ||||
|               child: MarkdownTextContent( | ||||
|                 content: data.body['text'], | ||||
|                 isAutoWarp: true, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           if (data.updatedAt != data.createdAt) | ||||
|             Text( | ||||
|   | ||||
| @@ -48,6 +48,8 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|  | ||||
|   void setEdit(SnChatMessage? value) { | ||||
|     _contentController.text = value?.body['text'] ?? ''; | ||||
|     _attachments.clear(); | ||||
|     _attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []); | ||||
|     setState(() => _editingMessage = value); | ||||
|   } | ||||
|  | ||||
| @@ -101,7 +103,9 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|           }, | ||||
|         ); | ||||
|  | ||||
|         _attachments[i] = PostWriteMedia(item); | ||||
|         setState(() { | ||||
|           _attachments[i] = PostWriteMedia(item); | ||||
|         }); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
| @@ -113,7 +117,7 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|     // Send the message | ||||
|     // NOTICE This future should not be awaited, so that the message can be sent in the background and the user can continue to type | ||||
|     widget.controller.sendMessage( | ||||
|       'messages.new', | ||||
|       _editingMessage != null ? 'messages.edit' : 'messages.new', | ||||
|       _contentController.text, | ||||
|       attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(), | ||||
|       relatedId: _editingMessage?.id, | ||||
| @@ -197,6 +201,7 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|                       InkWell( | ||||
|                         child: Text('cancel'.tr()), | ||||
|                         onTap: () { | ||||
|                           _attachments.clear(); | ||||
|                           setState(() => _replyingMessage = null); | ||||
|                         }, | ||||
|                       ), | ||||
| @@ -236,6 +241,7 @@ class ChatMessageInputState extends State<ChatMessageInput> { | ||||
|                       InkWell( | ||||
|                         child: Text('cancel'.tr()), | ||||
|                         onTap: () { | ||||
|                           _attachments.clear(); | ||||
|                           _contentController.clear(); | ||||
|                           setState(() => _editingMessage = null); | ||||
|                         }, | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| 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:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/providers/websocket.dart'; | ||||
|  | ||||
| @@ -11,50 +14,62 @@ 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, | ||||
|       builder: (context, _) { | ||||
|         final ua = context.read<UserProvider>(); | ||||
|         final show = (ws.isBusy || !ws.isConnected) && ua.isAuthorized; | ||||
|  | ||||
|         return GestureDetector( | ||||
|           child: Container( | ||||
|             padding: EdgeInsets.only( | ||||
|               bottom: 8, | ||||
|               top: MediaQuery.of(context).padding.top + 8, | ||||
|               left: 24, | ||||
|               right: 24, | ||||
|         return IgnorePointer( | ||||
|           ignoring: !show, | ||||
|           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(); | ||||
|                 } | ||||
|               }, | ||||
|             ), | ||||
|             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), | ||||
|                     ], | ||||
|                   ) | ||||
|                 : const SizedBox.shrink(), | ||||
|           ) | ||||
|               .height( | ||||
|                   (ws.isBusy || !ws.isConnected) && ua.isAuthorized | ||||
|                       ? MediaQuery.of(context).padding.top + 36 | ||||
|                       : 0, | ||||
|                   animate: true) | ||||
|               .animate( | ||||
|                 const Duration(milliseconds: 300), | ||||
|                 Curves.easeInOut, | ||||
|               ), | ||||
|           onTap: () { | ||||
|             if (!ws.isConnected && !ws.isBusy) { | ||||
|               ws.connect(); | ||||
|             } | ||||
|           }, | ||||
|           ).padding(left: marginLeft), | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_animate/flutter_animate.dart'; | ||||
| import 'package:flutter_context_menu/flutter_context_menu.dart'; | ||||
| import 'package:responsive_framework/responsive_framework.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
|  | ||||
| class ContextMenuArea extends StatelessWidget { | ||||
|   final ContextMenu contextMenu; | ||||
| @@ -22,13 +23,12 @@ class ContextMenuArea extends StatelessWidget { | ||||
|     return Listener( | ||||
|       onPointerDown: (event) { | ||||
|         mousePosition = event.position; | ||||
|         final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE); | ||||
|         if (!isCollapseDrawer) { | ||||
|           final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET); | ||||
|         final cfg = context.read<ConfigProvider>(); | ||||
|         if (!cfg.drawerIsCollapsed) { | ||||
|           // Leave padding for side navigation | ||||
|           mousePosition = isExpandDrawer | ||||
|           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]}'; | ||||
|   } | ||||
|   | ||||
| @@ -7,12 +7,11 @@ import 'package:marquee/marquee.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:responsive_framework/responsive_framework.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/link_preview.dart'; | ||||
| import 'package:surface/types/link.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| import '../providers/link_preview.dart'; | ||||
|  | ||||
| class LinkPreviewWidget extends StatefulWidget { | ||||
|   final String text; | ||||
|  | ||||
| @@ -81,8 +80,9 @@ class _LinkPreviewEntry extends StatelessWidget { | ||||
|                   child: AspectRatio( | ||||
|                     aspectRatio: 16 / 9, | ||||
|                     child: ClipRRect( | ||||
|                       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                       child: AutoResizeUniversalImage( | ||||
|                         meta.image!, | ||||
|                         meta.image!.startsWith('//') ? 'https:${meta.image}' : meta.image!, | ||||
|                         fit: BoxFit.contain, | ||||
|                       ), | ||||
|                     ), | ||||
| @@ -94,11 +94,14 @@ class _LinkPreviewEntry extends StatelessWidget { | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     if (meta.icon?.isNotEmpty ?? false) | ||||
|                       StyledWidget( | ||||
|                         meta.icon!.endsWith('.svg') | ||||
|                             ? SvgPicture.network(meta.icon!) | ||||
|                       SizedBox( | ||||
|                         width: 36, | ||||
|                         height: 36, | ||||
|                         child: meta.icon!.endsWith('.svg') | ||||
|                             ? SvgPicture.network(meta.icon!, width: 36, height: 36) | ||||
|                             : UniversalImage( | ||||
|                                 meta.icon!, | ||||
|                                 noErrorWidget: true, | ||||
|                                 width: 36, | ||||
|                                 height: 36, | ||||
|                                 cacheHeight: 36, | ||||
|   | ||||
| @@ -1,39 +1,40 @@ | ||||
| import 'dart:ui'; | ||||
|  | ||||
| import 'package:dismissible_page/dismissible_page.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_markdown/flutter_markdown.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:markdown/markdown.dart' as markdown; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/sn_sticker.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
| import 'package:surface/widgets/attachment/attachment_item.dart'; | ||||
| import 'package:surface/widgets/universal_image.dart'; | ||||
| import 'package:syntax_highlight/syntax_highlight.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
| import 'package:path/path.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| import 'attachment/attachment_zoom.dart'; | ||||
|  | ||||
| class MarkdownTextContent extends StatelessWidget { | ||||
|   final String content; | ||||
|   final bool isSelectable; | ||||
|   final bool isAutoWarp; | ||||
|   final bool isEnlargeSticker; | ||||
|   final TextScaler? textScaler; | ||||
|   final Color? textColor; | ||||
|   final List<SnAttachment?>? attachments; | ||||
|  | ||||
|   const MarkdownTextContent({ | ||||
|     super.key, | ||||
|     required this.content, | ||||
|     this.isSelectable = false, | ||||
|     this.isAutoWarp = false, | ||||
|     this.isEnlargeSticker = false, | ||||
|     this.textScaler, | ||||
|     this.textColor, | ||||
|     this.attachments, | ||||
|   }); | ||||
|  | ||||
|   Widget _buildContent(BuildContext context) { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Markdown( | ||||
|       shrinkWrap: true, | ||||
|       physics: const NeverScrollableScrollPhysics(), | ||||
| @@ -42,33 +43,34 @@ class MarkdownTextContent extends StatelessWidget { | ||||
|       styleSheet: MarkdownStyleSheet.fromTheme( | ||||
|         Theme.of(context), | ||||
|       ).copyWith( | ||||
|           textScaler: textScaler, | ||||
|           blockquote: TextStyle( | ||||
|             color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|           ), | ||||
|           blockquoteDecoration: BoxDecoration( | ||||
|             color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|             borderRadius: const BorderRadius.all(Radius.circular(4)), | ||||
|           ), | ||||
|           horizontalRuleDecoration: BoxDecoration( | ||||
|             border: Border( | ||||
|               top: BorderSide( | ||||
|                 width: 1.0, | ||||
|                 color: Theme.of(context).dividerColor, | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           codeblockDecoration: BoxDecoration( | ||||
|             border: Border.all( | ||||
|         textScaler: textScaler, | ||||
|         p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null, | ||||
|         blockquote: TextStyle( | ||||
|           color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||
|         ), | ||||
|         blockquoteDecoration: BoxDecoration( | ||||
|           color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|           borderRadius: const BorderRadius.all(Radius.circular(4)), | ||||
|         ), | ||||
|         horizontalRuleDecoration: BoxDecoration( | ||||
|           border: Border( | ||||
|             top: BorderSide( | ||||
|               width: 1.0, | ||||
|               color: Theme.of(context).dividerColor, | ||||
|               width: 0.3, | ||||
|             ), | ||||
|             borderRadius: const BorderRadius.all(Radius.circular(4)), | ||||
|             color: Theme.of(context).colorScheme.surface.withOpacity(0.5), | ||||
|           )), | ||||
|       builders: { | ||||
|         'code': _MarkdownTextCodeElement(), | ||||
|       }, | ||||
|           ), | ||||
|         ), | ||||
|         codeblockDecoration: BoxDecoration( | ||||
|           border: Border.all( | ||||
|             color: Theme.of(context).dividerColor, | ||||
|             width: 0.3, | ||||
|           ), | ||||
|           borderRadius: const BorderRadius.all(Radius.circular(4)), | ||||
|           color: Theme.of(context).colorScheme.surface.withOpacity(0.5), | ||||
|         ), | ||||
|         code: GoogleFonts.robotoMono(height: 1), | ||||
|       ), | ||||
|       builders: {}, | ||||
|       softLineBreak: true, | ||||
|       extensionSet: markdown.ExtensionSet( | ||||
|         <markdown.BlockSyntax>[ | ||||
| @@ -78,6 +80,7 @@ class MarkdownTextContent extends StatelessWidget { | ||||
|         <markdown.InlineSyntax>[ | ||||
|           if (isAutoWarp) markdown.LineBreakSyntax(), | ||||
|           _UserNameCardInlineSyntax(), | ||||
|           _CustomEmoteInlineSyntax(context), | ||||
|           markdown.AutolinkSyntax(), | ||||
|           markdown.AutolinkExtensionSyntax(), | ||||
|           markdown.CodeSyntax(), | ||||
| @@ -108,9 +111,41 @@ class MarkdownTextContent extends StatelessWidget { | ||||
|         if (url.startsWith('solink://')) { | ||||
|           final segments = url.replaceFirst('solink://', '').split('/'); | ||||
|           switch (segments[0]) { | ||||
|             case 'stickers': | ||||
|               final alias = segments[1]; | ||||
|               final st = context.read<SnStickerProvider>(); | ||||
|               final sn = context.read<SnNetworkProvider>(); | ||||
|               final double size = isEnlargeSticker ? 128 : 32; | ||||
|               return Container( | ||||
|                 width: size, | ||||
|                 height: size, | ||||
|                 decoration: BoxDecoration( | ||||
|                   borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                   color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|                 ), | ||||
|                 child: ClipRRect( | ||||
|                   borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|                   child: FutureBuilder<SnSticker?>( | ||||
|                     future: st.lookupSticker(alias), | ||||
|                     builder: (context, snapshot) { | ||||
|                       if (snapshot.hasData) { | ||||
|                         return UniversalImage( | ||||
|                           sn.getAttachmentUrl(snapshot.data!.attachment.rid), | ||||
|                           fit: BoxFit.cover, | ||||
|                           width: size, | ||||
|                           height: size, | ||||
|                           cacheHeight: size, | ||||
|                           cacheWidth: size, | ||||
|                         ); | ||||
|                       } | ||||
|                       return const SizedBox.shrink(); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|               ); | ||||
|             case 'attachments': | ||||
|               final attachment = attachments?.firstWhere( | ||||
|                 (ele) => ele?.rid == segments[1], | ||||
|                     (ele) => ele?.rid == segments[1], | ||||
|                 orElse: () => null, | ||||
|               ); | ||||
|               if (attachment != null) { | ||||
| @@ -168,14 +203,6 @@ class MarkdownTextContent extends StatelessWidget { | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (isSelectable) { | ||||
|       return SelectionArea(child: _buildContent(context)); | ||||
|     } | ||||
|     return _buildContent(context); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _UserNameCardInlineSyntax extends markdown.InlineSyntax { | ||||
| @@ -194,45 +221,24 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _MarkdownTextCodeElement extends MarkdownElementBuilder { | ||||
|   @override | ||||
|   Widget? visitElementAfter( | ||||
|     markdown.Element element, | ||||
|     TextStyle? preferredStyle, | ||||
|   ) { | ||||
|     var language = ''; | ||||
| class _CustomEmoteInlineSyntax extends markdown.InlineSyntax { | ||||
|   final BuildContext context; | ||||
|  | ||||
|     if (element.attributes['class'] != null) { | ||||
|       String lg = element.attributes['class'] as String; | ||||
|       language = lg.substring(9).trim(); | ||||
|   _CustomEmoteInlineSyntax(this.context) : super(r':([-\w]+):'); | ||||
|  | ||||
|   @override | ||||
|   bool onMatch(markdown.InlineParser parser, Match match) { | ||||
|     final SnStickerProvider st = context.read<SnStickerProvider>(); | ||||
|     final alias = match[1]!.toUpperCase(); | ||||
|     if (st.hasNotSticker(alias)) { | ||||
|       parser.advanceBy(1); | ||||
|       return false; | ||||
|     } | ||||
|     return SizedBox( | ||||
|       child: FutureBuilder( | ||||
|         future: (() async { | ||||
|           final docPath = '../../../'; | ||||
|           final highlightingPath = join(docPath, 'assets/highlighting', language); | ||||
|           await Highlighter.initialize([highlightingPath]); | ||||
|           return Highlighter( | ||||
|             language: highlightingPath, | ||||
|             theme: PlatformDispatcher.instance.platformBrightness == Brightness.light | ||||
|                 ? await HighlighterTheme.loadLightTheme() | ||||
|                 : await HighlighterTheme.loadDarkTheme(), | ||||
|           ); | ||||
|         })(), | ||||
|         builder: (context, snapshot) { | ||||
|           if (snapshot.hasData) { | ||||
|             final highlighter = snapshot.data!; | ||||
|             return Text.rich( | ||||
|               highlighter.highlight(element.textContent.trim()), | ||||
|               style: GoogleFonts.robotoMono(), | ||||
|             ); | ||||
|           } | ||||
|           return Text( | ||||
|             element.textContent.trim(), | ||||
|             style: GoogleFonts.robotoMono(), | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ).padding(all: 8); | ||||
|  | ||||
|     final element = markdown.Element.empty('img'); | ||||
|     element.attributes['src'] = 'solink://stickers/$alias'; | ||||
|     parser.addNode(element); | ||||
|  | ||||
|     return true; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,13 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.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/navigation.dart'; | ||||
| import 'package:surface/widgets/version_label.dart'; | ||||
|  | ||||
| @@ -28,8 +32,9 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final nav = context.watch<NavigationProvider>(); | ||||
|     final cfg = context.watch<ConfigProvider>(); | ||||
|  | ||||
|     final backgroundColor = ResponsiveBreakpoints.of(context).largerThan(TABLET) ? Colors.transparent : null; | ||||
|     final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null; | ||||
|  | ||||
|     return ListenableBuilder( | ||||
|       listenable: nav, | ||||
| @@ -44,6 +49,18 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> { | ||||
|           backgroundColor: backgroundColor, | ||||
|           selectedIndex: nav.currentIndex, | ||||
|           children: [ | ||||
|             if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS) && !cfg.drawerIsExpanded) | ||||
|               Container( | ||||
|                 decoration: BoxDecoration( | ||||
|                   border: Border( | ||||
|                     bottom: BorderSide( | ||||
|                       color: Theme.of(context).dividerColor, | ||||
|                       width: 1 / MediaQuery.of(context).devicePixelRatio, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|                 child: WindowTitleBarBox(), | ||||
|               ), | ||||
|             Column( | ||||
|               mainAxisSize: MainAxisSize.min, | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|   | ||||
| @@ -18,9 +18,7 @@ class _AppRailNavigationState extends State<AppRailNavigation> { | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|       context | ||||
|           .read<NavigationProvider>() | ||||
|           .autoDetectIndex(GoRouter.maybeOf(context)); | ||||
|       context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context)); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| @@ -31,36 +29,39 @@ class _AppRailNavigationState extends State<AppRailNavigation> { | ||||
|     return ListenableBuilder( | ||||
|       listenable: nav, | ||||
|       builder: (context, _) { | ||||
|         final destinations = | ||||
|             nav.destinations.where((ele) => ele.isPinned).toList(); | ||||
|         final destinations = nav.destinations.where((ele) => ele.isPinned).toList(); | ||||
|  | ||||
|         return NavigationRail( | ||||
|           selectedIndex: nav.currentIndex, | ||||
|           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); | ||||
|           }, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   | ||||
| @@ -1,51 +1,94 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:google_fonts/google_fonts.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/navigation.dart'; | ||||
| import 'package:surface/widgets/connection_indicator.dart'; | ||||
| import 'package:surface/widgets/dialog.dart'; | ||||
| import 'package:surface/widgets/navigation/app_background.dart'; | ||||
| import 'package:surface/widgets/navigation/app_bottom_navigation.dart'; | ||||
| import 'package:surface/widgets/navigation/app_drawer_navigation.dart'; | ||||
| import 'package:surface/widgets/navigation/app_rail_navigation.dart'; | ||||
| import 'package:surface/widgets/notify_indicator.dart'; | ||||
|  | ||||
| final globalRootScaffoldKey = GlobalKey<ScaffoldState>(); | ||||
|  | ||||
| class AppPageScaffold extends StatelessWidget { | ||||
|   final String? title; | ||||
| class AppScaffold extends StatelessWidget { | ||||
|   final Widget? body; | ||||
|   final bool showAppBar; | ||||
|   final bool showBottomNavigation; | ||||
|   final PreferredSizeWidget? bottomNavigationBar; | ||||
|   final PreferredSizeWidget? bottomSheet; | ||||
|   final Drawer? drawer; | ||||
|   final Widget? endDrawer; | ||||
|   final FloatingActionButtonAnimator? floatingActionButtonAnimator; | ||||
|   final FloatingActionButtonLocation? floatingActionButtonLocation; | ||||
|   final Widget? floatingActionButton; | ||||
|   final AppBar? appBar; | ||||
|   final DrawerCallback? onDrawerChanged; | ||||
|   final DrawerCallback? onEndDrawerChanged; | ||||
|  | ||||
|   const AppPageScaffold({ | ||||
|   const AppScaffold({ | ||||
|     super.key, | ||||
|     this.title, | ||||
|     this.appBar, | ||||
|     this.body, | ||||
|     this.showAppBar = true, | ||||
|     this.showBottomNavigation = false, | ||||
|     this.floatingActionButton, | ||||
|     this.floatingActionButtonLocation, | ||||
|     this.floatingActionButtonAnimator, | ||||
|     this.bottomNavigationBar, | ||||
|     this.bottomSheet, | ||||
|     this.drawer, | ||||
|     this.endDrawer, | ||||
|     this.onDrawerChanged, | ||||
|     this.onEndDrawerChanged, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final state = GoRouter.maybeOf(context); | ||||
|     final routeName = state?.routerDelegate.currentConfiguration.last.route.name; | ||||
|  | ||||
|     final autoTitle = state != null ? 'screen${routeName?.capitalize()}' : 'screen'; | ||||
|     final appBarHeight = appBar?.preferredSize.height ?? 0; | ||||
|     final safeTop = MediaQuery.of(context).padding.top; | ||||
|  | ||||
|     return Scaffold( | ||||
|       appBar: showAppBar | ||||
|           ? AppBar( | ||||
|               title: Text(title ?? autoTitle.tr()), | ||||
|             ) | ||||
|           : null, | ||||
|       body: body, | ||||
|       extendBody: true, | ||||
|       extendBodyBehindAppBar: true, | ||||
|       backgroundColor: Theme.of(context).scaffoldBackgroundColor, | ||||
|       body: SizedBox.expand( | ||||
|         child: AppBackground( | ||||
|           child: Column( | ||||
|             children: [ | ||||
|               IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)), | ||||
|               if (body != null) Expanded(child: body!), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|       appBar: appBar, | ||||
|       bottomNavigationBar: bottomNavigationBar, | ||||
|       bottomSheet: bottomSheet, | ||||
|       drawer: drawer, | ||||
|       endDrawer: endDrawer, | ||||
|       floatingActionButton: floatingActionButton, | ||||
|       floatingActionButtonAnimator: floatingActionButtonAnimator, | ||||
|       floatingActionButtonLocation: floatingActionButtonLocation, | ||||
|       onDrawerChanged: onDrawerChanged, | ||||
|       onEndDrawerChanged: onEndDrawerChanged, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PageBackButton extends StatelessWidget { | ||||
|   const PageBackButton({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return BackButton( | ||||
|       onPressed: () { | ||||
|         GoRouter.of(context).pop(); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -57,10 +100,11 @@ class AppRootScaffold extends StatelessWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final cfg = context.watch<ConfigProvider>(); | ||||
|     final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; | ||||
|  | ||||
|     final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE); | ||||
|     final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET); | ||||
|     final isCollapseDrawer = cfg.drawerIsCollapsed; | ||||
|     final isExpandedDrawer = cfg.drawerIsExpanded; | ||||
|  | ||||
|     final routeName = GoRouter.of(context).routerDelegate.currentConfiguration.last.route.name; | ||||
|     final isShowBottomNavigation = NavigationProvider.kShowBottomNavScreen.contains(routeName) | ||||
| @@ -81,7 +125,7 @@ class AppRootScaffold extends StatelessWidget { | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|                 child: isExpandDrawer ? AppNavigationDrawer(elevation: 0) : AppRailNavigation(), | ||||
|                 child: isExpandedDrawer ? AppNavigationDrawer(elevation: 0) : AppRailNavigation(), | ||||
|               ), | ||||
|               Expanded(child: body), | ||||
|             ], | ||||
| @@ -95,62 +139,68 @@ class AppRootScaffold extends StatelessWidget { | ||||
|       iconMouseDown: Theme.of(context).colorScheme.primary, | ||||
|     ); | ||||
|  | ||||
|     return AppBackground( | ||||
|       isRoot: true, | ||||
|       child: Scaffold( | ||||
|         key: globalRootScaffoldKey, | ||||
|         body: Column( | ||||
|           children: [ | ||||
|             if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) | ||||
|               Container( | ||||
|                 decoration: BoxDecoration( | ||||
|                   border: Border( | ||||
|                     bottom: BorderSide( | ||||
|                       color: Theme.of(context).dividerColor, | ||||
|                       width: 1 / devicePixelRatio, | ||||
|     final safeTop = MediaQuery.of(context).padding.top; | ||||
|     final safeBottom = MediaQuery.of(context).padding.bottom; | ||||
|  | ||||
|     return Scaffold( | ||||
|       key: globalRootScaffoldKey, | ||||
|       backgroundColor: Theme.of(context).colorScheme.surface, | ||||
|       body: Stack( | ||||
|         children: [ | ||||
|           Column( | ||||
|             children: [ | ||||
|               if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) | ||||
|                 WindowTitleBarBox( | ||||
|                   child: Container( | ||||
|                     decoration: BoxDecoration( | ||||
|                       border: Border( | ||||
|                         bottom: BorderSide( | ||||
|                           color: Theme.of(context).dividerColor, | ||||
|                           width: 1 / devicePixelRatio, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                     child: MoveWindow( | ||||
|                       child: Row( | ||||
|                         crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                         mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start, | ||||
|                         children: [ | ||||
|                           Text( | ||||
|                             'Solar Network', | ||||
|                             style: GoogleFonts.spaceGrotesk(), | ||||
|                           ).padding(horizontal: 12, vertical: 5), | ||||
|                           if (!Platform.isMacOS) | ||||
|                             Row( | ||||
|                               mainAxisSize: MainAxisSize.min, | ||||
|                               children: [ | ||||
|                                 Expanded(child: MoveWindow()), | ||||
|                                 Row( | ||||
|                                   children: [ | ||||
|                                     MinimizeWindowButton(colors: windowButtonColor), | ||||
|                                     MaximizeWindowButton(colors: windowButtonColor), | ||||
|                                     CloseWindowButton(colors: windowButtonColor), | ||||
|                                   ], | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|                 child: Row( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start, | ||||
|                   children: [ | ||||
|                     WindowTitleBarBox( | ||||
|                       child: MoveWindow( | ||||
|                         child: Text( | ||||
|                           'Solar Network', | ||||
|                           style: GoogleFonts.spaceGrotesk(), | ||||
|                         ).padding(horizontal: 12, vertical: 5), | ||||
|                       ), | ||||
|                     ), | ||||
|                     if (!Platform.isMacOS) | ||||
|                       Expanded( | ||||
|                         child: WindowTitleBarBox( | ||||
|                           child: Row( | ||||
|                             children: [ | ||||
|                               Expanded(child: MoveWindow()), | ||||
|                               Row( | ||||
|                                 children: [ | ||||
|                                   MinimizeWindowButton(colors: windowButtonColor), | ||||
|                                   MaximizeWindowButton(colors: windowButtonColor), | ||||
|                                   CloseWindowButton(colors: windowButtonColor), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ConnectionIndicator(), | ||||
|             Expanded(child: innerWidget), | ||||
|           ], | ||||
|         ), | ||||
|         drawer: !isExpandDrawer ? AppNavigationDrawer() : null, | ||||
|         drawerEdgeDragWidth: isPopable ? 0 : null, | ||||
|         bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null, | ||||
|               Expanded(child: innerWidget), | ||||
|             ], | ||||
|           ), | ||||
|           Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()), | ||||
|           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, | ||||
|       drawerEdgeDragWidth: isPopable ? 0 : null, | ||||
|       bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										187
									
								
								lib/widgets/notify_indicator.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								lib/widgets/notify_indicator.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,187 @@ | ||||
| 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/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'; | ||||
|  | ||||
| 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 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: 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'; | ||||
| @@ -20,6 +21,7 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/config.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/types/attachment.dart'; | ||||
| import 'package:surface/types/post.dart'; | ||||
| import 'package:surface/types/reaction.dart'; | ||||
| import 'package:surface/widgets/account/account_image.dart'; | ||||
| @@ -33,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; | ||||
| @@ -112,7 +115,7 @@ class PostItem extends StatelessWidget { | ||||
|         sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, | ||||
|       ); | ||||
|     } else { | ||||
|       await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}', file: imageFile); | ||||
|       await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}.png', file: imageFile); | ||||
|     } | ||||
|  | ||||
|     await imageFile.delete(); | ||||
| @@ -198,6 +201,12 @@ class PostItem extends StatelessWidget { | ||||
|       ).center(); | ||||
|     } | ||||
|  | ||||
|     final displayableAttachments = data.preload?.attachments | ||||
|         ?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article') | ||||
|         .toList(); | ||||
|  | ||||
|     final cfg = context.read<ConfigProvider>(); | ||||
|  | ||||
|     return Column( | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: [ | ||||
| @@ -247,17 +256,16 @@ class PostItem extends StatelessWidget { | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|         if ((data.preload?.attachments?.isNotEmpty ?? false) && data.type != 'article') | ||||
|         if (displayableAttachments?.isNotEmpty ?? false) | ||||
|           AttachmentList( | ||||
|             data: data.preload!.attachments!, | ||||
|             data: displayableAttachments!, | ||||
|             bordered: true, | ||||
|             gridded: true, | ||||
|             maxHeight: showFullPost ? null : 480, | ||||
|             minWidth: 640, | ||||
|             maxWidth: MediaQuery.of(context).size.width - 20, | ||||
|             fit: showFullPost ? BoxFit.cover : BoxFit.contain, | ||||
|             padding: const EdgeInsets.symmetric(horizontal: 12), | ||||
|           ), | ||||
|         if (data.body['content'] != null) | ||||
|         if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true)) | ||||
|           LinkPreviewWidget( | ||||
|             text: data.body['content'], | ||||
|           ).padding(horizontal: 4), | ||||
| @@ -339,7 +347,7 @@ class PostShareImageWidget extends StatelessWidget { | ||||
|           if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false)) | ||||
|             StyledWidget(AttachmentList( | ||||
|               data: data.preload!.attachments!, | ||||
|               gridded: true, | ||||
|               columned: true, | ||||
|             )).padding(horizontal: 16, bottom: 8), | ||||
|           Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
| @@ -811,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( | ||||
| @@ -874,12 +898,18 @@ class _PostContentBody extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (data.body['content'] == null) return const SizedBox.shrink(); | ||||
|     return MarkdownTextContent( | ||||
|       isSelectable: isSelectable, | ||||
|     final content = MarkdownTextContent( | ||||
|       isAutoWarp: data.type == 'story', | ||||
|       isEnlargeSticker: true, | ||||
|       textScaler: isEnlarge ? TextScaler.linear(1.1) : null, | ||||
|       content: data.body['content'], | ||||
|       attachments: data.preload?.attachments, | ||||
|     ); | ||||
|  | ||||
|     if (isSelectable) { | ||||
|       return SelectionArea(child: content); | ||||
|     } | ||||
|     return content; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -1169,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), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -25,7 +25,7 @@ class PostMiniEditor extends StatefulWidget { | ||||
| } | ||||
|  | ||||
| class _PostMiniEditorState extends State<PostMiniEditor> { | ||||
|   final PostWriteController _writeController = PostWriteController(); | ||||
|   final PostWriteController _writeController = PostWriteController(doLoadFromTemporary: false); | ||||
|  | ||||
|   bool _isFetching = false; | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| @@ -12,6 +13,7 @@ import 'package:surface/widgets/dialog.dart'; | ||||
| class PostReactionPopup extends StatefulWidget { | ||||
|   final SnPost data; | ||||
|   final Function(Map<String, int> value, int attr, int delta)? onChanged; | ||||
|  | ||||
|   const PostReactionPopup({super.key, required this.data, this.onChanged}); | ||||
|  | ||||
|   @override | ||||
| @@ -59,6 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> { | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|       HapticFeedback.heavyImpact(); | ||||
|     } catch (err) { | ||||
|       // ignore: use_build_context_synchronously | ||||
|       if (context.mounted) context.showErrorDialog(err); | ||||
| @@ -84,9 +87,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> { | ||||
|             children: [ | ||||
|               const Icon(Symbols.mood, size: 24), | ||||
|               const Gap(16), | ||||
|               Text('postReactions') | ||||
|                   .tr() | ||||
|                   .textStyle(Theme.of(context).textTheme.titleLarge!), | ||||
|               Text('postReactions').tr().textStyle(Theme.of(context).textTheme.titleLarge!), | ||||
|             ], | ||||
|           ).padding(horizontal: 20, top: 16, bottom: 12), | ||||
|           Container( | ||||
| @@ -102,9 +103,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> { | ||||
|                 Text('postReactionDownvote').plural(widget.data.totalDownvote), | ||||
|                 const Gap(24), | ||||
|                 Icon( | ||||
|                   widget.data.totalUpvote >= widget.data.totalDownvote | ||||
|                       ? Symbols.trending_up | ||||
|                       : Symbols.trending_down, | ||||
|                   widget.data.totalUpvote >= widget.data.totalDownvote ? Symbols.trending_up : Symbols.trending_down, | ||||
|                   size: 16, | ||||
|                 ), | ||||
|                 const Gap(8), | ||||
|   | ||||
| @@ -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; | ||||
| @@ -55,17 +54,20 @@ class UniversalImage extends StatelessWidget { | ||||
|           ? null | ||||
|           : (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { | ||||
|               if (loadingProgress == null) return child; | ||||
|               return Center( | ||||
|                 child: TweenAnimationBuilder( | ||||
|                   tween: Tween( | ||||
|                     begin: 0, | ||||
|                     end: loadingProgress.expectedTotalBytes != null | ||||
|                         ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! | ||||
|                         : 0, | ||||
|                   ), | ||||
|                   duration: const Duration(milliseconds: 300), | ||||
|                   builder: (context, value, _) => CircularProgressIndicator( | ||||
|                     value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null, | ||||
|               return Container( | ||||
|                 constraints: BoxConstraints(maxHeight: 80), | ||||
|                 child: Center( | ||||
|                   child: TweenAnimationBuilder( | ||||
|                     tween: Tween( | ||||
|                       begin: 0, | ||||
|                       end: loadingProgress.expectedTotalBytes != null | ||||
|                           ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! | ||||
|                           : 0, | ||||
|                     ), | ||||
|                     duration: const Duration(milliseconds: 300), | ||||
|                     builder: (context, value, _) => CircularProgressIndicator( | ||||
|                       value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ); | ||||
|   | ||||
| @@ -8,11 +8,13 @@ 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 | ||||
| import firebase_core | ||||
| import firebase_messaging | ||||
| import flutter_inappwebview_macos | ||||
| import flutter_udid | ||||
| import flutter_webrtc | ||||
| import gal | ||||
| @@ -35,11 +37,13 @@ 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")) | ||||
|   FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) | ||||
|   FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) | ||||
|   InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) | ||||
|   FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) | ||||
|   FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) | ||||
|   GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) | ||||
|   | ||||
| @@ -8,63 +8,65 @@ 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): | ||||
|     - FlutterMacOS | ||||
|   - Firebase/Analytics (11.4.0): | ||||
|   - Firebase/Analytics (11.6.0): | ||||
|     - Firebase/Core | ||||
|   - Firebase/Core (11.4.0): | ||||
|   - Firebase/Core (11.6.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseAnalytics (~> 11.4.0) | ||||
|   - Firebase/CoreOnly (11.4.0): | ||||
|     - FirebaseCore (= 11.4.0) | ||||
|   - Firebase/Messaging (11.4.0): | ||||
|     - FirebaseAnalytics (~> 11.6.0) | ||||
|   - Firebase/CoreOnly (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|   - Firebase/Messaging (11.6.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseMessaging (~> 11.4.0) | ||||
|   - firebase_analytics (11.3.6): | ||||
|     - Firebase/Analytics (= 11.4.0) | ||||
|     - FirebaseMessaging (~> 11.6.0) | ||||
|   - firebase_analytics (11.4.1): | ||||
|     - Firebase/Analytics (= 11.6.0) | ||||
|     - firebase_core | ||||
|     - FlutterMacOS | ||||
|   - firebase_core (3.9.0): | ||||
|     - Firebase/CoreOnly (~> 11.4.0) | ||||
|   - firebase_core (3.10.1): | ||||
|     - Firebase/CoreOnly (~> 11.6.0) | ||||
|     - FlutterMacOS | ||||
|   - firebase_messaging (15.1.6): | ||||
|     - Firebase/CoreOnly (~> 11.4.0) | ||||
|     - Firebase/Messaging (~> 11.4.0) | ||||
|   - firebase_messaging (15.2.1): | ||||
|     - Firebase/CoreOnly (~> 11.6.0) | ||||
|     - Firebase/Messaging (~> 11.6.0) | ||||
|     - firebase_core | ||||
|     - FlutterMacOS | ||||
|   - FirebaseAnalytics (11.4.0): | ||||
|     - FirebaseAnalytics/AdIdSupport (= 11.4.0) | ||||
|     - FirebaseCore (~> 11.0) | ||||
|   - FirebaseAnalytics (11.6.0): | ||||
|     - FirebaseAnalytics/AdIdSupport (= 11.6.0) | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseAnalytics/AdIdSupport (11.4.0): | ||||
|     - FirebaseCore (~> 11.0) | ||||
|   - FirebaseAnalytics/AdIdSupport (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleAppMeasurement (= 11.4.0) | ||||
|     - GoogleAppMeasurement (= 11.6.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseCore (11.4.0): | ||||
|     - FirebaseCoreInternal (~> 11.0) | ||||
|   - FirebaseCore (11.6.0): | ||||
|     - FirebaseCoreInternal (~> 11.6.0) | ||||
|     - GoogleUtilities/Environment (~> 8.0) | ||||
|     - GoogleUtilities/Logger (~> 8.0) | ||||
|   - FirebaseCoreInternal (11.6.0): | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|   - FirebaseInstallations (11.4.0): | ||||
|     - FirebaseCore (~> 11.0) | ||||
|   - FirebaseInstallations (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|     - GoogleUtilities/Environment (~> 8.0) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.0) | ||||
|     - PromisesObjC (~> 2.4) | ||||
|   - FirebaseMessaging (11.4.0): | ||||
|     - FirebaseCore (~> 11.0) | ||||
|   - FirebaseMessaging (11.6.0): | ||||
|     - FirebaseCore (~> 11.6.0) | ||||
|     - FirebaseInstallations (~> 11.0) | ||||
|     - GoogleDataTransport (~> 10.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
| @@ -72,31 +74,34 @@ PODS: | ||||
|     - GoogleUtilities/Reachability (~> 8.0) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.0) | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - flutter_inappwebview_macos (0.0.1): | ||||
|     - FlutterMacOS | ||||
|     - OrderedSet (~> 6.0.3) | ||||
|   - flutter_udid (0.0.1): | ||||
|     - FlutterMacOS | ||||
|     - SAMKeychain | ||||
|   - flutter_webrtc (0.12.2): | ||||
|   - flutter_webrtc (0.12.6): | ||||
|     - FlutterMacOS | ||||
|     - WebRTC-SDK (= 125.6422.06) | ||||
|   - FlutterMacOS (1.0.0) | ||||
|   - gal (1.0.0): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - GoogleAppMeasurement (11.4.0): | ||||
|     - GoogleAppMeasurement/AdIdSupport (= 11.4.0) | ||||
|   - GoogleAppMeasurement (11.6.0): | ||||
|     - GoogleAppMeasurement/AdIdSupport (= 11.6.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/AdIdSupport (11.4.0): | ||||
|     - GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0) | ||||
|   - GoogleAppMeasurement/AdIdSupport (11.6.0): | ||||
|     - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/WithoutAdIdSupport (11.4.0): | ||||
|   - GoogleAppMeasurement/WithoutAdIdSupport (11.6.0): | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.0) | ||||
|     - GoogleUtilities/Network (~> 8.0) | ||||
| @@ -134,7 +139,7 @@ PODS: | ||||
|     - GoogleUtilities/Privacy | ||||
|   - in_app_review (2.0.0): | ||||
|     - FlutterMacOS | ||||
|   - livekit_client (2.3.4): | ||||
|   - livekit_client (2.3.5): | ||||
|     - flutter_webrtc | ||||
|     - FlutterMacOS | ||||
|     - WebRTC-SDK (= 125.6422.06) | ||||
| @@ -149,6 +154,7 @@ PODS: | ||||
|     - nanopb/encode (= 3.30910.0) | ||||
|   - nanopb/decode (3.30910.0) | ||||
|   - nanopb/encode (3.30910.0) | ||||
|   - OrderedSet (6.0.3) | ||||
|   - package_info_plus (0.0.1): | ||||
|     - FlutterMacOS | ||||
|   - pasteboard (0.0.1): | ||||
| @@ -181,11 +187,13 @@ 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`) | ||||
|   - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) | ||||
|   - firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`) | ||||
|   - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) | ||||
|   - flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`) | ||||
|   - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) | ||||
|   - FlutterMacOS (from `Flutter/ephemeral`) | ||||
| @@ -218,6 +226,7 @@ SPEC REPOS: | ||||
|     - GoogleDataTransport | ||||
|     - GoogleUtilities | ||||
|     - nanopb | ||||
|     - OrderedSet | ||||
|     - PromisesObjC | ||||
|     - SAMKeychain | ||||
|     - WebRTC-SDK | ||||
| @@ -231,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: | ||||
| @@ -241,6 +252,8 @@ EXTERNAL SOURCES: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos | ||||
|   firebase_messaging: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos | ||||
|   flutter_inappwebview_macos: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos | ||||
|   flutter_udid: | ||||
|     :path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos | ||||
|   flutter_webrtc: | ||||
| @@ -285,30 +298,33 @@ SPEC CHECKSUMS: | ||||
|   connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 | ||||
|   croppy: 25a638bd7d05411d8c697f481568f261037694fc | ||||
|   device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 | ||||
|   file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af | ||||
|   file_saver: 44e6fbf666677faf097302460e214e977fdd977b | ||||
|   file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d | ||||
|   Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 | ||||
|   firebase_analytics: a80b3d6645f2f12d626fde928b61dae12e5ea2ef | ||||
|   firebase_core: 1dfe1f4d02ad78be0277e320aa3d8384cf46231f | ||||
|   firebase_messaging: 61f678060b69a7ae1013e3a939ec8e1c56ef6fcf | ||||
|   FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49 | ||||
|   FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 | ||||
|   Firebase: 374a441a91ead896215703a674d58cdb3e9d772b | ||||
|   firebase_analytics: 91efc58e8e37964469fdd59ad11ba36bc97e75d6 | ||||
|   firebase_core: 75e003524565fb5bd80c9960bc5892e8475821cd | ||||
|   firebase_messaging: 082a385eb98b5bb843a566cb30404859c4bd6e25 | ||||
|   FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 | ||||
|   FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa | ||||
|   FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 | ||||
|   FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 | ||||
|   FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 | ||||
|   FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c | ||||
|   FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021 | ||||
|   flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b | ||||
|   flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52 | ||||
|   flutter_webrtc: 53c9e1285ab32dfb58afb1e1471416a877e23d7a | ||||
|   flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8 | ||||
|   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 | ||||
|   gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 | ||||
|   GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e | ||||
|   GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3 | ||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||
|   GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d | ||||
|   in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 | ||||
|   livekit_client: b7ab91e79e657d7d40da16cb2f90d517cb72d406 | ||||
|   livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1 | ||||
|   media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 | ||||
|   media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 | ||||
|   media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 | ||||
|   nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 | ||||
|   OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 | ||||
|   package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b | ||||
|   pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 | ||||
|   path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 | ||||
|   | ||||
							
								
								
									
										260
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										260
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -13,10 +13,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: _flutterfire_internals | ||||
|       sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe | ||||
|       sha256: e4f2a7ef31b0ab2c89d2bde35ef3e6e6aff1dce5e66069c6540b0e9cfe33ee6b | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.48" | ||||
|     version: "1.3.50" | ||||
|   _macros: | ||||
|     dependency: transitive | ||||
|     description: dart | ||||
| @@ -266,10 +266,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: connectivity_plus | ||||
|       sha256: e0817759ec6d2d8e57eb234e6e57d2173931367a865850c7acea40d4b4f9c27d | ||||
|       sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.1.1" | ||||
|     version: "6.1.2" | ||||
|   connectivity_plus_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -290,10 +290,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: croppy | ||||
|       sha256: "14bb40fd6c1771b093a907ddbf24df9aa49a4e6e379dd630602eb446e30ec629" | ||||
|       sha256: bf99b00023df0d7d047e04d27d496d87cbefd968f578d0bd30f342ff75570a12 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.1" | ||||
|     version: "1.3.3" | ||||
|   cross_file: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -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: | ||||
| @@ -354,18 +354,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: dbus | ||||
|       sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" | ||||
|       sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.7.10" | ||||
|     version: "0.7.11" | ||||
|   device_info_plus: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: device_info_plus | ||||
|       sha256: "4fa68e53e26ab17b70ca39f072c285562cfc1589df5bb1e9295db90f6645f431" | ||||
|       sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "11.2.0" | ||||
|     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: "366140abb55418ea23060b779893fa997c2d8e1974a4d1cc4d9590933b65c5fd" | ||||
|       sha256: eac382bbcd5ae78c1d1ce5619d13f5a7424429f4bf55df9e3ad5110da34d1060 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "11.3.6" | ||||
|     version: "11.4.1" | ||||
|   firebase_analytics_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_analytics_platform_interface | ||||
|       sha256: "8e987cf977c0c8f4ad02d9950a9b25b1a9606899f37b66a322a43af05be0246b" | ||||
|       sha256: a34db46c367265c4c961626e4b128bfb7b7e50958e7add4c27ba103f5f81b9b0 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.2.8" | ||||
|     version: "4.3.1" | ||||
|   firebase_analytics_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_analytics_web | ||||
|       sha256: "0b64ef9060d394bba3d3b4777f49ee098efeeea7b0afb04663c956de6a3da170" | ||||
|       sha256: b6b4cef08e45e4c7d48476d9fc49fe9577081809a59026fe95b1a1b1eea165fa | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.5.10+5" | ||||
|     version: "0.5.10+7" | ||||
|   firebase_core: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: firebase_core | ||||
|       sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde" | ||||
|       sha256: d851c1ca98fd5a4c07c747f8c65dacc2edd84a4d9ac055d32a5f0342529069f5 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.9.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: "151a3ee68736abf293aab66d1317ade53c88abe1db09c75a0460aebf7767bbdf" | ||||
|       sha256: e20ea2a0ecf9b0971575ab3ab42a6e285a94e50092c555b090c1a588a81b4d54 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "15.1.6" | ||||
|     version: "15.2.1" | ||||
|   firebase_messaging_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_messaging_platform_interface | ||||
|       sha256: f331ee51e40c243f90cc7bc059222dfec4e5df53125b08d31fb28961b00d2a9d | ||||
|       sha256: c57a92b5ae1857ef4fe4ae2e73452b44d32e984e15ab8b53415ea1bb514bdabd | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.5.49" | ||||
|     version: "4.6.1" | ||||
|   firebase_messaging_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: firebase_messaging_web | ||||
|       sha256: efaf3fdc54cd77e0eedb8e75f7f01c808828c64d052ddbf94d3009974e47d30f | ||||
|       sha256: "83694a990d8525d6b01039240b97757298369622ca0253ad0ebcfed221bf8ee0" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.9.5" | ||||
|     version: "3.10.1" | ||||
|   fixnum: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -618,10 +618,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: fl_chart | ||||
|       sha256: c724234b05e378383e958f3e82ca84a3e1e3c06a0898462044dd8a24b1ee9864 | ||||
|       sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.70.0" | ||||
|     version: "0.70.2" | ||||
|   flutter: | ||||
|     dependency: "direct main" | ||||
|     description: flutter | ||||
| @@ -675,14 +675,78 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.0" | ||||
|   flutter_inappwebview: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_inappwebview | ||||
|       sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.1.5" | ||||
|   flutter_inappwebview_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_inappwebview_android | ||||
|       sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.1.3" | ||||
|   flutter_inappwebview_internal_annotations: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_inappwebview_internal_annotations | ||||
|       sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.2.0" | ||||
|   flutter_inappwebview_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_inappwebview_ios | ||||
|       sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.1.2" | ||||
|   flutter_inappwebview_macos: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_inappwebview_macos | ||||
|       sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.1.2" | ||||
|   flutter_inappwebview_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_inappwebview_platform_interface | ||||
|       sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.0+1" | ||||
|   flutter_inappwebview_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_inappwebview_web | ||||
|       sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.1.2" | ||||
|   flutter_inappwebview_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_inappwebview_windows | ||||
|       sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.6.0" | ||||
|   flutter_launcher_icons: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: flutter_launcher_icons | ||||
|       sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5" | ||||
|       sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.14.2" | ||||
|     version: "0.14.3" | ||||
|   flutter_lints: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -700,18 +764,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_markdown | ||||
|       sha256: "255b00afa1a7bad19727da6a7780cf3db6c3c12e68d302d85e0ff1fdf173db9e" | ||||
|       sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.7.4+3" | ||||
|     version: "0.7.5" | ||||
|   flutter_native_splash: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: flutter_native_splash | ||||
|       sha256: "1152ab0067ca5a2ebeb862fe0a762057202cceb22b7e62692dcbabf6483891bb" | ||||
|       sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.3" | ||||
|     version: "2.4.4" | ||||
|   flutter_plugin_android_lifecycle: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -740,10 +804,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_svg | ||||
|       sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123" | ||||
|       sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.16" | ||||
|     version: "2.0.17" | ||||
|   flutter_test: | ||||
|     dependency: "direct dev" | ||||
|     description: flutter | ||||
| @@ -766,10 +830,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_webrtc | ||||
|       sha256: "3efe9828f19a07d29a51a726759ad0c70a840d231548a1c7d0332075a94db1df" | ||||
|       sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.12.5+hotfix.1" | ||||
|     version: "0.12.7" | ||||
|   freezed: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -814,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: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539" | ||||
|       sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "14.6.2" | ||||
|     version: "14.7.2" | ||||
|   google_fonts: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -875,7 +939,7 @@ packages: | ||||
|     source: hosted | ||||
|     version: "0.7.0" | ||||
|   html: | ||||
|     dependency: transitive | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: html | ||||
|       sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" | ||||
| @@ -886,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: | ||||
| @@ -902,10 +966,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: http_parser | ||||
|       sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" | ||||
|       sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.1.1" | ||||
|     version: "4.1.2" | ||||
|   icons_launcher: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -934,10 +998,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: image_picker_android | ||||
|       sha256: aa6f1280b670861ac45220cc95adc59bb6ae130259d36f980ccb62220dc5e59f | ||||
|       sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.8.12+19" | ||||
|     version: "0.8.12+20" | ||||
|   image_picker_for_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -950,10 +1014,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: image_picker_ios | ||||
|       sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b" | ||||
|       sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.8.12+1" | ||||
|     version: "0.8.12+2" | ||||
|   image_picker_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -966,18 +1030,18 @@ 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: | ||||
|       name: image_picker_platform_interface | ||||
|       sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" | ||||
|       sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.10.0" | ||||
|     version: "2.10.1" | ||||
|   image_picker_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1086,10 +1150,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: livekit_client | ||||
|       sha256: "7cdeb3eaeec7fb70a4cf88d9caabccbef9e3bd5f0b23c086320bc5c9acb2770b" | ||||
|       sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.4" | ||||
|     version: "2.3.5" | ||||
|   logging: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1110,10 +1174,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: markdown | ||||
|       sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 | ||||
|       sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "7.2.2" | ||||
|     version: "7.3.0" | ||||
|   marquee: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1142,10 +1206,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: material_symbols_icons | ||||
|       sha256: "64404f47f8e0a9d20478468e5decef867a688660bad7173adcd20418d7f892c9" | ||||
|       sha256: "89aac72d25dd49303f71b3b1e70f8374791846729365b25bebc2a2531e5b86cd" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "4.2801.0" | ||||
|     version: "4.2801.1" | ||||
|   media_kit: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1270,10 +1334,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: package_info_plus | ||||
|       sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d" | ||||
|       sha256: b15fad91c4d3d1f2b48c053dd41cb82da007c27407dc9ab5f9aa59881d0e39d4 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "8.1.2" | ||||
|     version: "8.1.4" | ||||
|   package_info_plus_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1502,10 +1566,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: pubspec_parse | ||||
|       sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" | ||||
|       sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.4.0" | ||||
|     version: "1.5.0" | ||||
|   qr: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1630,10 +1694,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: share_plus | ||||
|       sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400" | ||||
|       sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "10.1.3" | ||||
|     version: "10.1.4" | ||||
|   share_plus_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1646,18 +1710,18 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: shared_preferences | ||||
|       sha256: "3c7e73920c694a436afaf65ab60ce3453d91f84208d761fbd83fc21182134d93" | ||||
|       sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.4" | ||||
|     version: "2.5.1" | ||||
|   shared_preferences_android: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_android | ||||
|       sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d" | ||||
|       sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.0" | ||||
|     version: "2.4.4" | ||||
|   shared_preferences_foundation: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1795,10 +1859,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqflite_darwin | ||||
|       sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474" | ||||
|       sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.1" | ||||
|     version: "2.4.1+1" | ||||
|   sqflite_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1863,14 +1927,6 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.3.0+3" | ||||
|   syntax_highlight: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: syntax_highlight | ||||
|       sha256: ee33b6aa82cc722bb9b40152a792181dee222353b486c0255fde666a3e3a4997 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.4.0" | ||||
|   term_glyph: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1979,18 +2035,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_web | ||||
|       sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" | ||||
|       sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.3" | ||||
|     version: "2.4.0" | ||||
|   url_launcher_windows: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_windows | ||||
|       sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" | ||||
|       sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.1.3" | ||||
|     version: "3.1.4" | ||||
|   uuid: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -2003,18 +2059,18 @@ 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: | ||||
|       name: vector_graphics_codec | ||||
|       sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" | ||||
|       sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.1.12" | ||||
|     version: "1.1.13" | ||||
|   vector_graphics_compiler: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2115,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: | ||||
| @@ -2131,10 +2187,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: win32 | ||||
|       sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" | ||||
|       sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.9.0" | ||||
|     version: "5.10.1" | ||||
|   win32_registry: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2160,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.1+44 | ||||
| version: 2.2.2+60 | ||||
|  | ||||
| environment: | ||||
|   sdk: ^3.5.4 | ||||
| @@ -54,7 +54,6 @@ dependencies: | ||||
|   flutter_markdown: ^0.7.4+1 | ||||
|   url_launcher: ^6.3.1 | ||||
|   flutter_animate: ^4.5.0 | ||||
|   syntax_highlight: ^0.4.0 | ||||
|   google_fonts: ^6.2.1 | ||||
|   path: ^1.9.0 | ||||
|   relative_time: ^5.0.0 | ||||
| @@ -116,6 +115,9 @@ dependencies: | ||||
|   slide_countdown: ^2.0.2 | ||||
|   video_compress: ^3.1.3 | ||||
|   cached_network_image: ^3.4.1 | ||||
|   flutter_inappwebview: ^6.1.5 | ||||
|   html: ^0.15.5 | ||||
|   xml: ^6.5.0 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| id = "solian-next" | ||||
| id = "solian" | ||||
|  | ||||
| [[locations]] | ||||
| id = "solian-next" | ||||
| host = ["sn-next.solsynth.dev"] | ||||
| path = ["/"] | ||||
| id = "solian" | ||||
| hosts = ["sn.solsynth.dev"] | ||||
| paths = ["/"] | ||||
| [[locations.destinations]] | ||||
| id = "solian-next-web" | ||||
| uri = "files:///workdir/solian-next?fallback=index.html&index=index.html" | ||||
| id = "solian-web" | ||||
| uri = "files:///workdir/solian?fallback=index.html&index=index.html" | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user