Compare commits
	
		
			50 Commits
		
	
	
		
			2.2.2+53
			...
			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 | 
@@ -17,11 +17,16 @@
 | 
				
			|||||||
        android:label="Solian"
 | 
					        android:label="Solian"
 | 
				
			||||||
        android:name="${applicationName}"
 | 
					        android:name="${applicationName}"
 | 
				
			||||||
        android:icon="@mipmap/ic_launcher"
 | 
					        android:icon="@mipmap/ic_launcher"
 | 
				
			||||||
 | 
					        android:enableOnBackInvokedCallback="true"
 | 
				
			||||||
        android:requestLegacyExternalStorage="true">
 | 
					        android:requestLegacyExternalStorage="true">
 | 
				
			||||||
 | 
					        <meta-data
 | 
				
			||||||
 | 
					            android:name="flutterEmbedding"
 | 
				
			||||||
 | 
					            android:value="2" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <activity
 | 
					        <activity
 | 
				
			||||||
            android:name=".MainActivity"
 | 
					            android:name=".MainActivity"
 | 
				
			||||||
            android:exported="true"
 | 
					            android:exported="true"
 | 
				
			||||||
            android:launchMode="singleTask"
 | 
					            android:launchMode="singleInstance"
 | 
				
			||||||
            android:taskAffinity=""
 | 
					            android:taskAffinity=""
 | 
				
			||||||
            android:theme="@style/LaunchTheme"
 | 
					            android:theme="@style/LaunchTheme"
 | 
				
			||||||
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
 | 
					            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,11 +15,11 @@ body:json {
 | 
				
			|||||||
    "client_id": "{{third_client_id}}",
 | 
					    "client_id": "{{third_client_id}}",
 | 
				
			||||||
    "client_secret":"{{third_client_tk}}",
 | 
					    "client_secret":"{{third_client_tk}}",
 | 
				
			||||||
    "type": "general",
 | 
					    "type": "general",
 | 
				
			||||||
    "subject": "Merry Christmas!",
 | 
					    "subject": "新年快乐!",
 | 
				
			||||||
    "subtitle": "一条来自 Solar Network 团队的信息",
 | 
					    "subtitle": "一条来自 Solar Network 团队的信息",
 | 
				
			||||||
    "content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
 | 
					    "content": "今天是农历正月初一,小羊祝您新年快乐 🎉",
 | 
				
			||||||
    "metadata": {
 | 
					    "metadata": {
 | 
				
			||||||
      "image": "6EqsYQwmFRCkbmhR"
 | 
					      "image": "D2EDbcrsTugs3xk5"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "priority": 10
 | 
					    "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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -17,6 +17,10 @@
 | 
				
			|||||||
  "screenAccountProfileEdit": "Edit Profile",
 | 
					  "screenAccountProfileEdit": "Edit Profile",
 | 
				
			||||||
  "screenAbuseReport": "Abuse Reports",
 | 
					  "screenAbuseReport": "Abuse Reports",
 | 
				
			||||||
  "screenSettings": "Settings",
 | 
					  "screenSettings": "Settings",
 | 
				
			||||||
 | 
					  "screenAccountSettings": "Account Settings",
 | 
				
			||||||
 | 
					  "screenFactorSettings": "Auth Factors",
 | 
				
			||||||
 | 
					  "screenAccountWallet": "Wallet",
 | 
				
			||||||
 | 
					  "screenNews": "News",
 | 
				
			||||||
  "screenAlbum": "Album",
 | 
					  "screenAlbum": "Album",
 | 
				
			||||||
  "screenChat": "Chat",
 | 
					  "screenChat": "Chat",
 | 
				
			||||||
  "screenChatManage": "Edit Channel",
 | 
					  "screenChatManage": "Edit Channel",
 | 
				
			||||||
@@ -103,8 +107,18 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "loginEnterPassword": "Enter the code",
 | 
					  "loginEnterPassword": "Enter the code",
 | 
				
			||||||
  "loginSuccess": "Logged in as {}",
 | 
					  "loginSuccess": "Logged in as {}",
 | 
				
			||||||
 | 
					  "authFactorDelete": "Delete Auth Factor",
 | 
				
			||||||
 | 
					  "authFactorDeleteDescription": "Are you sure you want delete auth factor {}?",
 | 
				
			||||||
  "authFactorPassword": "Password",
 | 
					  "authFactorPassword": "Password",
 | 
				
			||||||
 | 
					  "authFactorPasswordDescription": "The password you set when you registered.",
 | 
				
			||||||
  "authFactorEmail": "Email verification code",
 | 
					  "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!",
 | 
					  "accountIntroTitle": "Hello there!",
 | 
				
			||||||
  "accountIntroSubtitle": "Pick an option below to get started.",
 | 
					  "accountIntroSubtitle": "Pick an option below to get started.",
 | 
				
			||||||
  "accountLogout": "Logout",
 | 
					  "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.",
 | 
					  "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",
 | 
					  "accountPublishers": "Your publishers",
 | 
				
			||||||
  "accountPublishersSubtitle": "Manage your publish identities.",
 | 
					  "accountPublishersSubtitle": "Manage your publish identities.",
 | 
				
			||||||
 | 
					  "accountSettings": "Account Settings",
 | 
				
			||||||
 | 
					  "accountSettingsSubtitle": "Manage your account and make it yours.",
 | 
				
			||||||
  "accountProfileEdit": "Edit your profile",
 | 
					  "accountProfileEdit": "Edit your profile",
 | 
				
			||||||
  "accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
 | 
					  "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.",
 | 
					  "accountProfileEditApplied": "Profile modification applied.",
 | 
				
			||||||
  "publishersNew": "New Publisher",
 | 
					  "publishersNew": "New Publisher",
 | 
				
			||||||
  "publisherNewSubtitle": "Create a new publisher identity.",
 | 
					  "publisherNewSubtitle": "Create a new publisher identity.",
 | 
				
			||||||
@@ -179,6 +199,9 @@
 | 
				
			|||||||
    "other": "{} comments"
 | 
					    "other": "{} comments"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "settingsAppearance": "Appearance",
 | 
					  "settingsAppearance": "Appearance",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguage": "Display Language",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageDescription": "Set the application language.",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageSystem": "Follow System",
 | 
				
			||||||
  "settingsAppBarTransparent": "Transparent App Bar",
 | 
					  "settingsAppBarTransparent": "Transparent App Bar",
 | 
				
			||||||
  "settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
 | 
					  "settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
 | 
				
			||||||
  "settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
 | 
					  "settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
 | 
				
			||||||
@@ -193,6 +216,13 @@
 | 
				
			|||||||
  "settingsColorSchemeDescription": "Set the application primary color.",
 | 
					  "settingsColorSchemeDescription": "Set the application primary color.",
 | 
				
			||||||
  "settingsColorSeed": "Color Seed",
 | 
					  "settingsColorSeed": "Color Seed",
 | 
				
			||||||
  "settingsColorSeedDescription": "Select one of the present color schemes.",
 | 
					  "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",
 | 
					  "settingsNetwork": "Network",
 | 
				
			||||||
  "settingsNetworkServer": "HyperNet Server",
 | 
					  "settingsNetworkServer": "HyperNet Server",
 | 
				
			||||||
  "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
 | 
					  "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
 | 
				
			||||||
@@ -215,8 +245,9 @@
 | 
				
			|||||||
  "sensitiveContentCollapsed": "Sensitive content has been collapsed.",
 | 
					  "sensitiveContentCollapsed": "Sensitive content has been collapsed.",
 | 
				
			||||||
  "sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
 | 
					  "sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
 | 
				
			||||||
  "sensitiveContentReveal": "Reveal",
 | 
					  "sensitiveContentReveal": "Reveal",
 | 
				
			||||||
  "serverConnecting": "Connecting to server...",
 | 
					  "serverConnecting": "Connecting...",
 | 
				
			||||||
  "serverDisconnected": "Lost connection from server",
 | 
					  "serverDisconnected": "Connection Lost",
 | 
				
			||||||
 | 
					  "serverConnected": "Connected",
 | 
				
			||||||
  "fieldChatAlias": "Channel Alias",
 | 
					  "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.",
 | 
					  "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",
 | 
					  "fieldChatName": "Name",
 | 
				
			||||||
@@ -294,6 +325,7 @@
 | 
				
			|||||||
  "addAttachmentFromCameraPhoto": "Take photo",
 | 
					  "addAttachmentFromCameraPhoto": "Take photo",
 | 
				
			||||||
  "addAttachmentFromCameraVideo": "Take video",
 | 
					  "addAttachmentFromCameraVideo": "Take video",
 | 
				
			||||||
  "addAttachmentFromRandomId": "Link via RID",
 | 
					  "addAttachmentFromRandomId": "Link via RID",
 | 
				
			||||||
 | 
					  "attachmentDetailInfo": "Attachment details",
 | 
				
			||||||
  "attachmentPastedImage": "Pasted Image",
 | 
					  "attachmentPastedImage": "Pasted Image",
 | 
				
			||||||
  "attachmentInsertLink": "Insert Link",
 | 
					  "attachmentInsertLink": "Insert Link",
 | 
				
			||||||
  "attachmentSetAsPostThumbnail": "Set as post thumbnail",
 | 
					  "attachmentSetAsPostThumbnail": "Set as post thumbnail",
 | 
				
			||||||
@@ -522,6 +554,9 @@
 | 
				
			|||||||
  "postImageShareAds": "Explore posts on the Solar Network",
 | 
					  "postImageShareAds": "Explore posts on the Solar Network",
 | 
				
			||||||
  "postShare": "Share",
 | 
					  "postShare": "Share",
 | 
				
			||||||
  "postShareImage": "Share via Image",
 | 
					  "postShareImage": "Share via Image",
 | 
				
			||||||
 | 
					  "postGetInsight": "Get Insight",
 | 
				
			||||||
 | 
					  "postGetInsightTitle": "AI Insight",
 | 
				
			||||||
 | 
					  "postGetInsightDescription": "AI may make mistakes, check important information.",
 | 
				
			||||||
  "appInitializing": "Initializing",
 | 
					  "appInitializing": "Initializing",
 | 
				
			||||||
  "poweredBy": "Powered by {}",
 | 
					  "poweredBy": "Powered by {}",
 | 
				
			||||||
  "shareIntent": "Share",
 | 
					  "shareIntent": "Share",
 | 
				
			||||||
@@ -549,5 +584,25 @@
 | 
				
			|||||||
  "postCategoryKnowledge": "Knowledge",
 | 
					  "postCategoryKnowledge": "Knowledge",
 | 
				
			||||||
  "postCategoryLiterature": "Literature",
 | 
					  "postCategoryLiterature": "Literature",
 | 
				
			||||||
  "postCategoryFunny": "Funny",
 | 
					  "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": "编辑资料",
 | 
					  "screenAccountProfileEdit": "编辑资料",
 | 
				
			||||||
  "screenAbuseReport": "滥用检举",
 | 
					  "screenAbuseReport": "滥用检举",
 | 
				
			||||||
  "screenSettings": "设置",
 | 
					  "screenSettings": "设置",
 | 
				
			||||||
 | 
					  "screenAccountSettings": "账号设置",
 | 
				
			||||||
 | 
					  "screenFactorSettings": "验证因子",
 | 
				
			||||||
 | 
					  "screenAccountWallet": "钱包",
 | 
				
			||||||
 | 
					  "screenNews": "新闻",
 | 
				
			||||||
  "screenAlbum": "相册",
 | 
					  "screenAlbum": "相册",
 | 
				
			||||||
  "screenChat": "聊天",
 | 
					  "screenChat": "聊天",
 | 
				
			||||||
  "screenChatManage": "编辑聊天频道",
 | 
					  "screenChatManage": "编辑聊天频道",
 | 
				
			||||||
@@ -87,8 +91,18 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "loginEnterPassword": "验证代码",
 | 
					  "loginEnterPassword": "验证代码",
 | 
				
			||||||
  "loginSuccess": "登录为 {}",
 | 
					  "loginSuccess": "登录为 {}",
 | 
				
			||||||
 | 
					  "authFactorDelete": "删除验证因子",
 | 
				
			||||||
 | 
					  "authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?",
 | 
				
			||||||
  "authFactorPassword": "密码",
 | 
					  "authFactorPassword": "密码",
 | 
				
			||||||
 | 
					  "authFactorPasswordDescription": "注册时选择设置的密码。",
 | 
				
			||||||
  "authFactorEmail": "电邮一次性验证码",
 | 
					  "authFactorEmail": "电邮一次性验证码",
 | 
				
			||||||
 | 
					  "authFactorEmailDescription": "由我们生成并发送到绑定的的电子邮箱的一次性验证码。",
 | 
				
			||||||
 | 
					  "authFactorTOTP": "时序验证码",
 | 
				
			||||||
 | 
					  "authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等验证器生成的一次性验证码。",
 | 
				
			||||||
 | 
					  "authFactorInAppNotify": "应用内通知验证码",
 | 
				
			||||||
 | 
					  "authFactorInAppNotifyDescription": "通过站内通知推送的一次性验证码。",
 | 
				
			||||||
 | 
					  "authFactorAdd": "添加新验证因子",
 | 
				
			||||||
 | 
					  "authFactorAddSubtitle": "给你的帐户登陆时提供另一个方案。",
 | 
				
			||||||
  "accountIntroTitle": "喜欢您来!",
 | 
					  "accountIntroTitle": "喜欢您来!",
 | 
				
			||||||
  "accountIntroSubtitle": "登陆以探索更广大的世界。",
 | 
					  "accountIntroSubtitle": "登陆以探索更广大的世界。",
 | 
				
			||||||
  "accountLogout": "退出登录",
 | 
					  "accountLogout": "退出登录",
 | 
				
			||||||
@@ -97,8 +111,14 @@
 | 
				
			|||||||
  "accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
 | 
					  "accountLogoutConfirm": "您需要重新输入账号密码,甚至可能需要多步验证来再次登陆。",
 | 
				
			||||||
  "accountPublishers": "你的发布者",
 | 
					  "accountPublishers": "你的发布者",
 | 
				
			||||||
  "accountPublishersSubtitle": "管理你的公共形象。",
 | 
					  "accountPublishersSubtitle": "管理你的公共形象。",
 | 
				
			||||||
 | 
					  "accountSettings": "帐号设置",
 | 
				
			||||||
 | 
					  "accountSettingsSubtitle": "管理你的帐号并让它更好的服务你。",
 | 
				
			||||||
  "accountProfileEdit": "编辑资料",
 | 
					  "accountProfileEdit": "编辑资料",
 | 
				
			||||||
  "accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
 | 
					  "accountProfileEditSubtitle": "使你的 Solarpass 账户更像你。",
 | 
				
			||||||
 | 
					  "accountWallet": "钱包",
 | 
				
			||||||
 | 
					  "accountWalletSubtitle": "查看你的余额和交易记录。",
 | 
				
			||||||
 | 
					  "factorSettings": "验证因子",
 | 
				
			||||||
 | 
					  "factorSettingsSubtitle": "管理你的登陆验证方式。",
 | 
				
			||||||
  "accountProfileEditApplied": "个人资料修改已被应用。",
 | 
					  "accountProfileEditApplied": "个人资料修改已被应用。",
 | 
				
			||||||
  "publishersNew": "新发布者",
 | 
					  "publishersNew": "新发布者",
 | 
				
			||||||
  "publisherNewSubtitle": "创建一个新的公共身份。",
 | 
					  "publisherNewSubtitle": "创建一个新的公共身份。",
 | 
				
			||||||
@@ -177,6 +197,9 @@
 | 
				
			|||||||
    "other": "{} 条评论"
 | 
					    "other": "{} 条评论"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "settingsAppearance": "外观",
 | 
					  "settingsAppearance": "外观",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguage": "显示语言",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageDescription": "设置应用程序使用的语言",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageSystem": "跟随系统",
 | 
				
			||||||
  "settingsBackgroundImage": "背景图片",
 | 
					  "settingsBackgroundImage": "背景图片",
 | 
				
			||||||
  "settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
 | 
					  "settingsBackgroundImageDescription": "设置应用全局生效的的背景图片。",
 | 
				
			||||||
  "settingsBackgroundImageClear": "清除现存背景图",
 | 
					  "settingsBackgroundImageClear": "清除现存背景图",
 | 
				
			||||||
@@ -191,6 +214,13 @@
 | 
				
			|||||||
  "settingsColorSchemeDescription": "设置应用主题色。",
 | 
					  "settingsColorSchemeDescription": "设置应用主题色。",
 | 
				
			||||||
  "settingsColorSeed": "预设色彩主题",
 | 
					  "settingsColorSeed": "预设色彩主题",
 | 
				
			||||||
  "settingsColorSeedDescription": "选择一个预设色彩主题。",
 | 
					  "settingsColorSeedDescription": "选择一个预设色彩主题。",
 | 
				
			||||||
 | 
					  "settingsFeatures": "功能",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHaptic": "新通知时振动",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
 | 
				
			||||||
 | 
					  "settingsExpandPostLink": "展开帖子链接",
 | 
				
			||||||
 | 
					  "settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。",
 | 
				
			||||||
 | 
					  "settingsExpandChatLink": "展开聊天链接",
 | 
				
			||||||
 | 
					  "settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。",
 | 
				
			||||||
  "settingsNetwork": "网络",
 | 
					  "settingsNetwork": "网络",
 | 
				
			||||||
  "settingsNetworkServer": "HyperNet 服务器",
 | 
					  "settingsNetworkServer": "HyperNet 服务器",
 | 
				
			||||||
  "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
 | 
					  "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
 | 
				
			||||||
@@ -213,8 +243,9 @@
 | 
				
			|||||||
  "sensitiveContentCollapsed": "敏感内容已折叠。",
 | 
					  "sensitiveContentCollapsed": "敏感内容已折叠。",
 | 
				
			||||||
  "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
 | 
					  "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
 | 
				
			||||||
  "sensitiveContentReveal": "显示内容",
 | 
					  "sensitiveContentReveal": "显示内容",
 | 
				
			||||||
  "serverConnecting": "正在连接服务器…",
 | 
					  "serverConnecting": "正在连接…",
 | 
				
			||||||
  "serverDisconnected": "已与服务器断开连接",
 | 
					  "serverDisconnected": "已断开连接",
 | 
				
			||||||
 | 
					  "serverConnected": "已连接",
 | 
				
			||||||
  "fieldChatAlias": "频道别名",
 | 
					  "fieldChatAlias": "频道别名",
 | 
				
			||||||
  "fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
 | 
					  "fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
 | 
				
			||||||
  "fieldChatName": "名称",
 | 
					  "fieldChatName": "名称",
 | 
				
			||||||
@@ -292,6 +323,7 @@
 | 
				
			|||||||
  "addAttachmentFromCameraPhoto": "拍摄照片",
 | 
					  "addAttachmentFromCameraPhoto": "拍摄照片",
 | 
				
			||||||
  "addAttachmentFromCameraVideo": "拍摄视频",
 | 
					  "addAttachmentFromCameraVideo": "拍摄视频",
 | 
				
			||||||
  "addAttachmentFromRandomId": "通过访问 ID 链接",
 | 
					  "addAttachmentFromRandomId": "通过访问 ID 链接",
 | 
				
			||||||
 | 
					  "attachmentDetailInfo": "附件详细信息",
 | 
				
			||||||
  "attachmentPastedImage": "粘贴的图片",
 | 
					  "attachmentPastedImage": "粘贴的图片",
 | 
				
			||||||
  "attachmentInsertLink": "插入连接",
 | 
					  "attachmentInsertLink": "插入连接",
 | 
				
			||||||
  "attachmentSetAsPostThumbnail": "设置为帖子缩略图",
 | 
					  "attachmentSetAsPostThumbnail": "设置为帖子缩略图",
 | 
				
			||||||
@@ -520,6 +552,9 @@
 | 
				
			|||||||
  "postImageShareAds": "来 Solar Network 探索更多有趣帖子",
 | 
					  "postImageShareAds": "来 Solar Network 探索更多有趣帖子",
 | 
				
			||||||
  "postShare": "分享",
 | 
					  "postShare": "分享",
 | 
				
			||||||
  "postShareImage": "分享帖图",
 | 
					  "postShareImage": "分享帖图",
 | 
				
			||||||
 | 
					  "postGetInsight": "获取见解",
 | 
				
			||||||
 | 
					  "postGetInsightTitle": "AI 见解",
 | 
				
			||||||
 | 
					  "postGetInsightDescription": "AI 可能会出错,检查信息真实性。",
 | 
				
			||||||
  "appInitializing": "正在初始化",
 | 
					  "appInitializing": "正在初始化",
 | 
				
			||||||
  "poweredBy": "由 {} 提供支持",
 | 
					  "poweredBy": "由 {} 提供支持",
 | 
				
			||||||
  "shareIntent": "分享",
 | 
					  "shareIntent": "分享",
 | 
				
			||||||
@@ -547,5 +582,25 @@
 | 
				
			|||||||
  "postCategoryKnowledge": "知识",
 | 
					  "postCategoryKnowledge": "知识",
 | 
				
			||||||
  "postCategoryLiterature": "文学",
 | 
					  "postCategoryLiterature": "文学",
 | 
				
			||||||
  "postCategoryFunny": "搞笑",
 | 
					  "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": "編輯資料",
 | 
					  "screenAccountProfileEdit": "編輯資料",
 | 
				
			||||||
  "screenAbuseReport": "濫用檢舉",
 | 
					  "screenAbuseReport": "濫用檢舉",
 | 
				
			||||||
  "screenSettings": "設置",
 | 
					  "screenSettings": "設置",
 | 
				
			||||||
 | 
					  "screenAccountSettings": "賬號設置",
 | 
				
			||||||
 | 
					  "screenFactorSettings": "驗證因子",
 | 
				
			||||||
 | 
					  "screenAccountWallet": "錢包",
 | 
				
			||||||
 | 
					  "screenNews": "新聞",
 | 
				
			||||||
  "screenAlbum": "相冊",
 | 
					  "screenAlbum": "相冊",
 | 
				
			||||||
  "screenChat": "聊天",
 | 
					  "screenChat": "聊天",
 | 
				
			||||||
  "screenChatManage": "編輯聊天頻道",
 | 
					  "screenChatManage": "編輯聊天頻道",
 | 
				
			||||||
@@ -87,8 +91,18 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "loginEnterPassword": "驗證代碼",
 | 
					  "loginEnterPassword": "驗證代碼",
 | 
				
			||||||
  "loginSuccess": "登錄為 {}",
 | 
					  "loginSuccess": "登錄為 {}",
 | 
				
			||||||
 | 
					  "authFactorDelete": "刪除驗證因子",
 | 
				
			||||||
 | 
					  "authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
 | 
				
			||||||
  "authFactorPassword": "密碼",
 | 
					  "authFactorPassword": "密碼",
 | 
				
			||||||
 | 
					  "authFactorPasswordDescription": "註冊時選擇設置的密碼。",
 | 
				
			||||||
  "authFactorEmail": "電郵一次性驗證碼",
 | 
					  "authFactorEmail": "電郵一次性驗證碼",
 | 
				
			||||||
 | 
					  "authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
 | 
				
			||||||
 | 
					  "authFactorTOTP": "時序驗證碼",
 | 
				
			||||||
 | 
					  "authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
 | 
				
			||||||
 | 
					  "authFactorInAppNotify": "應用內通知驗證碼",
 | 
				
			||||||
 | 
					  "authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
 | 
				
			||||||
 | 
					  "authFactorAdd": "添加新驗證因子",
 | 
				
			||||||
 | 
					  "authFactorAddSubtitle": "給你的帳户登陸時提供另一個方案。",
 | 
				
			||||||
  "accountIntroTitle": "喜歡您來!",
 | 
					  "accountIntroTitle": "喜歡您來!",
 | 
				
			||||||
  "accountIntroSubtitle": "登陸以探索更廣大的世界。",
 | 
					  "accountIntroSubtitle": "登陸以探索更廣大的世界。",
 | 
				
			||||||
  "accountLogout": "退出登錄",
 | 
					  "accountLogout": "退出登錄",
 | 
				
			||||||
@@ -97,8 +111,14 @@
 | 
				
			|||||||
  "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
 | 
					  "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
 | 
				
			||||||
  "accountPublishers": "你的發佈者",
 | 
					  "accountPublishers": "你的發佈者",
 | 
				
			||||||
  "accountPublishersSubtitle": "管理你的公共形象。",
 | 
					  "accountPublishersSubtitle": "管理你的公共形象。",
 | 
				
			||||||
 | 
					  "accountSettings": "帳號設置",
 | 
				
			||||||
 | 
					  "accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
 | 
				
			||||||
  "accountProfileEdit": "編輯資料",
 | 
					  "accountProfileEdit": "編輯資料",
 | 
				
			||||||
  "accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
 | 
					  "accountProfileEditSubtitle": "使你的 Solarpass 賬户更像你。",
 | 
				
			||||||
 | 
					  "accountWallet": "錢包",
 | 
				
			||||||
 | 
					  "accountWalletSubtitle": "查看你的餘額和交易記錄。",
 | 
				
			||||||
 | 
					  "factorSettings": "驗證因子",
 | 
				
			||||||
 | 
					  "factorSettingsSubtitle": "管理你的登陸驗證方式。",
 | 
				
			||||||
  "accountProfileEditApplied": "個人資料修改已被應用。",
 | 
					  "accountProfileEditApplied": "個人資料修改已被應用。",
 | 
				
			||||||
  "publishersNew": "新發布者",
 | 
					  "publishersNew": "新發布者",
 | 
				
			||||||
  "publisherNewSubtitle": "創建一個新的公共身份。",
 | 
					  "publisherNewSubtitle": "創建一個新的公共身份。",
 | 
				
			||||||
@@ -177,6 +197,9 @@
 | 
				
			|||||||
    "other": "{} 條評論"
 | 
					    "other": "{} 條評論"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "settingsAppearance": "外觀",
 | 
					  "settingsAppearance": "外觀",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguage": "顯示語言",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageDescription": "設置應用程序使用的語言",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageSystem": "跟隨系統",
 | 
				
			||||||
  "settingsBackgroundImage": "背景圖片",
 | 
					  "settingsBackgroundImage": "背景圖片",
 | 
				
			||||||
  "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
 | 
					  "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
 | 
				
			||||||
  "settingsBackgroundImageClear": "清除現存背景圖",
 | 
					  "settingsBackgroundImageClear": "清除現存背景圖",
 | 
				
			||||||
@@ -191,6 +214,13 @@
 | 
				
			|||||||
  "settingsColorSchemeDescription": "設置應用主題色。",
 | 
					  "settingsColorSchemeDescription": "設置應用主題色。",
 | 
				
			||||||
  "settingsColorSeed": "預設色彩主題",
 | 
					  "settingsColorSeed": "預設色彩主題",
 | 
				
			||||||
  "settingsColorSeedDescription": "選擇一個預設色彩主題。",
 | 
					  "settingsColorSeedDescription": "選擇一個預設色彩主題。",
 | 
				
			||||||
 | 
					  "settingsFeatures": "功能",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHaptic": "新通知時振動",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
 | 
				
			||||||
 | 
					  "settingsExpandPostLink": "展開帖子鏈接",
 | 
				
			||||||
 | 
					  "settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
 | 
				
			||||||
 | 
					  "settingsExpandChatLink": "展開聊天鏈接",
 | 
				
			||||||
 | 
					  "settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
 | 
				
			||||||
  "settingsNetwork": "網絡",
 | 
					  "settingsNetwork": "網絡",
 | 
				
			||||||
  "settingsNetworkServer": "HyperNet 服務器",
 | 
					  "settingsNetworkServer": "HyperNet 服務器",
 | 
				
			||||||
  "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
 | 
					  "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
 | 
				
			||||||
@@ -213,8 +243,9 @@
 | 
				
			|||||||
  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
					  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
				
			||||||
  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
 | 
					  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
 | 
				
			||||||
  "sensitiveContentReveal": "顯示內容",
 | 
					  "sensitiveContentReveal": "顯示內容",
 | 
				
			||||||
  "serverConnecting": "正在連接服務器…",
 | 
					  "serverConnecting": "正在連接…",
 | 
				
			||||||
  "serverDisconnected": "已與服務器斷開連接",
 | 
					  "serverDisconnected": "已斷開連接",
 | 
				
			||||||
 | 
					  "serverConnected": "已連接",
 | 
				
			||||||
  "fieldChatAlias": "頻道別名",
 | 
					  "fieldChatAlias": "頻道別名",
 | 
				
			||||||
  "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
 | 
					  "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
 | 
				
			||||||
  "fieldChatName": "名稱",
 | 
					  "fieldChatName": "名稱",
 | 
				
			||||||
@@ -292,6 +323,7 @@
 | 
				
			|||||||
  "addAttachmentFromCameraPhoto": "拍攝照片",
 | 
					  "addAttachmentFromCameraPhoto": "拍攝照片",
 | 
				
			||||||
  "addAttachmentFromCameraVideo": "拍攝視頻",
 | 
					  "addAttachmentFromCameraVideo": "拍攝視頻",
 | 
				
			||||||
  "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
 | 
					  "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
 | 
				
			||||||
 | 
					  "attachmentDetailInfo": "附件詳細信息",
 | 
				
			||||||
  "attachmentPastedImage": "粘貼的圖片",
 | 
					  "attachmentPastedImage": "粘貼的圖片",
 | 
				
			||||||
  "attachmentInsertLink": "插入連接",
 | 
					  "attachmentInsertLink": "插入連接",
 | 
				
			||||||
  "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
 | 
					  "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
 | 
				
			||||||
@@ -520,6 +552,9 @@
 | 
				
			|||||||
  "postImageShareAds": "來 Solar Network 探索更多有趣帖子",
 | 
					  "postImageShareAds": "來 Solar Network 探索更多有趣帖子",
 | 
				
			||||||
  "postShare": "分享",
 | 
					  "postShare": "分享",
 | 
				
			||||||
  "postShareImage": "分享帖圖",
 | 
					  "postShareImage": "分享帖圖",
 | 
				
			||||||
 | 
					  "postGetInsight": "獲取見解",
 | 
				
			||||||
 | 
					  "postGetInsightTitle": "AI 見解",
 | 
				
			||||||
 | 
					  "postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
 | 
				
			||||||
  "appInitializing": "正在初始化",
 | 
					  "appInitializing": "正在初始化",
 | 
				
			||||||
  "poweredBy": "由 {} 提供支持",
 | 
					  "poweredBy": "由 {} 提供支持",
 | 
				
			||||||
  "shareIntent": "分享",
 | 
					  "shareIntent": "分享",
 | 
				
			||||||
@@ -547,5 +582,25 @@
 | 
				
			|||||||
  "postCategoryKnowledge": "知識",
 | 
					  "postCategoryKnowledge": "知識",
 | 
				
			||||||
  "postCategoryLiterature": "文學",
 | 
					  "postCategoryLiterature": "文學",
 | 
				
			||||||
  "postCategoryFunny": "搞笑",
 | 
					  "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": "編輯資料",
 | 
					  "screenAccountProfileEdit": "編輯資料",
 | 
				
			||||||
  "screenAbuseReport": "濫用檢舉",
 | 
					  "screenAbuseReport": "濫用檢舉",
 | 
				
			||||||
  "screenSettings": "設置",
 | 
					  "screenSettings": "設置",
 | 
				
			||||||
 | 
					  "screenAccountSettings": "賬號設置",
 | 
				
			||||||
 | 
					  "screenFactorSettings": "驗證因子",
 | 
				
			||||||
 | 
					  "screenAccountWallet": "錢包",
 | 
				
			||||||
 | 
					  "screenNews": "新聞",
 | 
				
			||||||
  "screenAlbum": "相冊",
 | 
					  "screenAlbum": "相冊",
 | 
				
			||||||
  "screenChat": "聊天",
 | 
					  "screenChat": "聊天",
 | 
				
			||||||
  "screenChatManage": "編輯聊天頻道",
 | 
					  "screenChatManage": "編輯聊天頻道",
 | 
				
			||||||
@@ -87,8 +91,18 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "loginEnterPassword": "驗證代碼",
 | 
					  "loginEnterPassword": "驗證代碼",
 | 
				
			||||||
  "loginSuccess": "登錄為 {}",
 | 
					  "loginSuccess": "登錄為 {}",
 | 
				
			||||||
 | 
					  "authFactorDelete": "刪除驗證因子",
 | 
				
			||||||
 | 
					  "authFactorDeleteDescription": "你確定要刪除 {} 驗證因子嗎?",
 | 
				
			||||||
  "authFactorPassword": "密碼",
 | 
					  "authFactorPassword": "密碼",
 | 
				
			||||||
 | 
					  "authFactorPasswordDescription": "註冊時選擇設置的密碼。",
 | 
				
			||||||
  "authFactorEmail": "電郵一次性驗證碼",
 | 
					  "authFactorEmail": "電郵一次性驗證碼",
 | 
				
			||||||
 | 
					  "authFactorEmailDescription": "由我們生成併發送到綁定的的電子郵箱的一次性驗證碼。",
 | 
				
			||||||
 | 
					  "authFactorTOTP": "時序驗證碼",
 | 
				
			||||||
 | 
					  "authFactorTOTPDescription": "使用 Google Authenticator 或 Authy 等驗證器生成的一次性驗證碼。",
 | 
				
			||||||
 | 
					  "authFactorInAppNotify": "應用內通知驗證碼",
 | 
				
			||||||
 | 
					  "authFactorInAppNotifyDescription": "通過站內通知推送的一次性驗證碼。",
 | 
				
			||||||
 | 
					  "authFactorAdd": "添加新驗證因子",
 | 
				
			||||||
 | 
					  "authFactorAddSubtitle": "給你的帳戶登陸時提供另一個方案。",
 | 
				
			||||||
  "accountIntroTitle": "喜歡您來!",
 | 
					  "accountIntroTitle": "喜歡您來!",
 | 
				
			||||||
  "accountIntroSubtitle": "登陸以探索更廣大的世界。",
 | 
					  "accountIntroSubtitle": "登陸以探索更廣大的世界。",
 | 
				
			||||||
  "accountLogout": "退出登錄",
 | 
					  "accountLogout": "退出登錄",
 | 
				
			||||||
@@ -97,8 +111,14 @@
 | 
				
			|||||||
  "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
 | 
					  "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
 | 
				
			||||||
  "accountPublishers": "你的發佈者",
 | 
					  "accountPublishers": "你的發佈者",
 | 
				
			||||||
  "accountPublishersSubtitle": "管理你的公共形象。",
 | 
					  "accountPublishersSubtitle": "管理你的公共形象。",
 | 
				
			||||||
 | 
					  "accountSettings": "帳號設置",
 | 
				
			||||||
 | 
					  "accountSettingsSubtitle": "管理你的帳號並讓它更好的服務你。",
 | 
				
			||||||
  "accountProfileEdit": "編輯資料",
 | 
					  "accountProfileEdit": "編輯資料",
 | 
				
			||||||
  "accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
 | 
					  "accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
 | 
				
			||||||
 | 
					  "accountWallet": "錢包",
 | 
				
			||||||
 | 
					  "accountWalletSubtitle": "查看你的餘額和交易記錄。",
 | 
				
			||||||
 | 
					  "factorSettings": "驗證因子",
 | 
				
			||||||
 | 
					  "factorSettingsSubtitle": "管理你的登陸驗證方式。",
 | 
				
			||||||
  "accountProfileEditApplied": "個人資料修改已被應用。",
 | 
					  "accountProfileEditApplied": "個人資料修改已被應用。",
 | 
				
			||||||
  "publishersNew": "新發布者",
 | 
					  "publishersNew": "新發布者",
 | 
				
			||||||
  "publisherNewSubtitle": "創建一個新的公共身份。",
 | 
					  "publisherNewSubtitle": "創建一個新的公共身份。",
 | 
				
			||||||
@@ -177,6 +197,9 @@
 | 
				
			|||||||
    "other": "{} 條評論"
 | 
					    "other": "{} 條評論"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "settingsAppearance": "外觀",
 | 
					  "settingsAppearance": "外觀",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguage": "顯示語言",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageDescription": "設置應用程序使用的語言",
 | 
				
			||||||
 | 
					  "settingsDisplayLanguageSystem": "跟隨系統",
 | 
				
			||||||
  "settingsBackgroundImage": "背景圖片",
 | 
					  "settingsBackgroundImage": "背景圖片",
 | 
				
			||||||
  "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
 | 
					  "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
 | 
				
			||||||
  "settingsBackgroundImageClear": "清除現存背景圖",
 | 
					  "settingsBackgroundImageClear": "清除現存背景圖",
 | 
				
			||||||
@@ -191,6 +214,13 @@
 | 
				
			|||||||
  "settingsColorSchemeDescription": "設置應用主題色。",
 | 
					  "settingsColorSchemeDescription": "設置應用主題色。",
 | 
				
			||||||
  "settingsColorSeed": "預設色彩主題",
 | 
					  "settingsColorSeed": "預設色彩主題",
 | 
				
			||||||
  "settingsColorSeedDescription": "選擇一個預設色彩主題。",
 | 
					  "settingsColorSeedDescription": "選擇一個預設色彩主題。",
 | 
				
			||||||
 | 
					  "settingsFeatures": "功能",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHaptic": "新通知時振動",
 | 
				
			||||||
 | 
					  "settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
 | 
				
			||||||
 | 
					  "settingsExpandPostLink": "展開帖子鏈接",
 | 
				
			||||||
 | 
					  "settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
 | 
				
			||||||
 | 
					  "settingsExpandChatLink": "展開聊天鏈接",
 | 
				
			||||||
 | 
					  "settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
 | 
				
			||||||
  "settingsNetwork": "網絡",
 | 
					  "settingsNetwork": "網絡",
 | 
				
			||||||
  "settingsNetworkServer": "HyperNet 服務器",
 | 
					  "settingsNetworkServer": "HyperNet 服務器",
 | 
				
			||||||
  "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
 | 
					  "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
 | 
				
			||||||
@@ -213,8 +243,9 @@
 | 
				
			|||||||
  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
					  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
				
			||||||
  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
 | 
					  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
 | 
				
			||||||
  "sensitiveContentReveal": "顯示內容",
 | 
					  "sensitiveContentReveal": "顯示內容",
 | 
				
			||||||
  "serverConnecting": "正在連接服務器…",
 | 
					  "serverConnecting": "正在連接…",
 | 
				
			||||||
  "serverDisconnected": "已與服務器斷開連接",
 | 
					  "serverDisconnected": "已斷開連接",
 | 
				
			||||||
 | 
					  "serverConnected": "已連接",
 | 
				
			||||||
  "fieldChatAlias": "頻道別名",
 | 
					  "fieldChatAlias": "頻道別名",
 | 
				
			||||||
  "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
 | 
					  "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
 | 
				
			||||||
  "fieldChatName": "名稱",
 | 
					  "fieldChatName": "名稱",
 | 
				
			||||||
@@ -292,6 +323,7 @@
 | 
				
			|||||||
  "addAttachmentFromCameraPhoto": "拍攝照片",
 | 
					  "addAttachmentFromCameraPhoto": "拍攝照片",
 | 
				
			||||||
  "addAttachmentFromCameraVideo": "拍攝視頻",
 | 
					  "addAttachmentFromCameraVideo": "拍攝視頻",
 | 
				
			||||||
  "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
 | 
					  "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
 | 
				
			||||||
 | 
					  "attachmentDetailInfo": "附件詳細信息",
 | 
				
			||||||
  "attachmentPastedImage": "粘貼的圖片",
 | 
					  "attachmentPastedImage": "粘貼的圖片",
 | 
				
			||||||
  "attachmentInsertLink": "插入連接",
 | 
					  "attachmentInsertLink": "插入連接",
 | 
				
			||||||
  "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
 | 
					  "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
 | 
				
			||||||
@@ -520,6 +552,9 @@
 | 
				
			|||||||
  "postImageShareAds": "來 Solar Network 探索更多有趣帖子",
 | 
					  "postImageShareAds": "來 Solar Network 探索更多有趣帖子",
 | 
				
			||||||
  "postShare": "分享",
 | 
					  "postShare": "分享",
 | 
				
			||||||
  "postShareImage": "分享帖圖",
 | 
					  "postShareImage": "分享帖圖",
 | 
				
			||||||
 | 
					  "postGetInsight": "獲取見解",
 | 
				
			||||||
 | 
					  "postGetInsightTitle": "AI 見解",
 | 
				
			||||||
 | 
					  "postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。",
 | 
				
			||||||
  "appInitializing": "正在初始化",
 | 
					  "appInitializing": "正在初始化",
 | 
				
			||||||
  "poweredBy": "由 {} 提供支持",
 | 
					  "poweredBy": "由 {} 提供支持",
 | 
				
			||||||
  "shareIntent": "分享",
 | 
					  "shareIntent": "分享",
 | 
				
			||||||
@@ -547,5 +582,25 @@
 | 
				
			|||||||
  "postCategoryKnowledge": "知識",
 | 
					  "postCategoryKnowledge": "知識",
 | 
				
			||||||
  "postCategoryLiterature": "文學",
 | 
					  "postCategoryLiterature": "文學",
 | 
				
			||||||
  "postCategoryFunny": "搞笑",
 | 
					  "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 思考過程"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										102
									
								
								ios/Podfile.lock
									
									
									
									
									
								
							
							
						
						
									
										102
									
								
								ios/Podfile.lock
									
									
									
									
									
								
							@@ -43,58 +43,58 @@ PODS:
 | 
				
			|||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - file_saver (0.0.1):
 | 
					  - file_saver (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - Firebase/Analytics (11.4.0):
 | 
					  - Firebase/Analytics (11.6.0):
 | 
				
			||||||
    - Firebase/Core
 | 
					    - Firebase/Core
 | 
				
			||||||
  - Firebase/Core (11.4.0):
 | 
					  - Firebase/Core (11.6.0):
 | 
				
			||||||
    - Firebase/CoreOnly
 | 
					    - Firebase/CoreOnly
 | 
				
			||||||
    - FirebaseAnalytics (~> 11.4.0)
 | 
					    - FirebaseAnalytics (~> 11.6.0)
 | 
				
			||||||
  - Firebase/CoreOnly (11.4.0):
 | 
					  - Firebase/CoreOnly (11.6.0):
 | 
				
			||||||
    - FirebaseCore (= 11.4.0)
 | 
					    - FirebaseCore (~> 11.6.0)
 | 
				
			||||||
  - Firebase/Messaging (11.4.0):
 | 
					  - Firebase/Messaging (11.6.0):
 | 
				
			||||||
    - Firebase/CoreOnly
 | 
					    - Firebase/CoreOnly
 | 
				
			||||||
    - FirebaseMessaging (~> 11.4.0)
 | 
					    - FirebaseMessaging (~> 11.6.0)
 | 
				
			||||||
  - firebase_analytics (11.3.6):
 | 
					  - firebase_analytics (11.4.1):
 | 
				
			||||||
    - Firebase/Analytics (= 11.4.0)
 | 
					    - Firebase/Analytics (= 11.6.0)
 | 
				
			||||||
    - firebase_core
 | 
					    - firebase_core
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - firebase_core (3.9.0):
 | 
					  - firebase_core (3.10.1):
 | 
				
			||||||
    - Firebase/CoreOnly (= 11.4.0)
 | 
					    - Firebase/CoreOnly (= 11.6.0)
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - firebase_messaging (15.1.6):
 | 
					  - firebase_messaging (15.2.1):
 | 
				
			||||||
    - Firebase/Messaging (= 11.4.0)
 | 
					    - Firebase/Messaging (= 11.6.0)
 | 
				
			||||||
    - firebase_core
 | 
					    - firebase_core
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - FirebaseAnalytics (11.4.0):
 | 
					  - FirebaseAnalytics (11.6.0):
 | 
				
			||||||
    - FirebaseAnalytics/AdIdSupport (= 11.4.0)
 | 
					    - FirebaseAnalytics/AdIdSupport (= 11.6.0)
 | 
				
			||||||
    - FirebaseCore (~> 11.0)
 | 
					    - FirebaseCore (~> 11.6.0)
 | 
				
			||||||
    - FirebaseInstallations (~> 11.0)
 | 
					    - FirebaseInstallations (~> 11.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
  - FirebaseAnalytics/AdIdSupport (11.4.0):
 | 
					  - FirebaseAnalytics/AdIdSupport (11.6.0):
 | 
				
			||||||
    - FirebaseCore (~> 11.0)
 | 
					    - FirebaseCore (~> 11.6.0)
 | 
				
			||||||
    - FirebaseInstallations (~> 11.0)
 | 
					    - FirebaseInstallations (~> 11.0)
 | 
				
			||||||
    - GoogleAppMeasurement (= 11.4.0)
 | 
					    - GoogleAppMeasurement (= 11.6.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
  - FirebaseCore (11.4.0):
 | 
					  - FirebaseCore (11.6.0):
 | 
				
			||||||
    - FirebaseCoreInternal (~> 11.0)
 | 
					    - FirebaseCoreInternal (~> 11.6.0)
 | 
				
			||||||
    - GoogleUtilities/Environment (~> 8.0)
 | 
					    - GoogleUtilities/Environment (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Logger (~> 8.0)
 | 
					    - GoogleUtilities/Logger (~> 8.0)
 | 
				
			||||||
  - FirebaseCoreInternal (11.6.0):
 | 
					  - FirebaseCoreInternal (11.6.0):
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
  - FirebaseInstallations (11.4.0):
 | 
					  - FirebaseInstallations (11.6.0):
 | 
				
			||||||
    - FirebaseCore (~> 11.0)
 | 
					    - FirebaseCore (~> 11.6.0)
 | 
				
			||||||
    - GoogleUtilities/Environment (~> 8.0)
 | 
					    - GoogleUtilities/Environment (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/UserDefaults (~> 8.0)
 | 
					    - GoogleUtilities/UserDefaults (~> 8.0)
 | 
				
			||||||
    - PromisesObjC (~> 2.4)
 | 
					    - PromisesObjC (~> 2.4)
 | 
				
			||||||
  - FirebaseMessaging (11.4.0):
 | 
					  - FirebaseMessaging (11.6.0):
 | 
				
			||||||
    - FirebaseCore (~> 11.0)
 | 
					    - FirebaseCore (~> 11.6.0)
 | 
				
			||||||
    - FirebaseInstallations (~> 11.0)
 | 
					    - FirebaseInstallations (~> 11.0)
 | 
				
			||||||
    - GoogleDataTransport (~> 10.0)
 | 
					    - GoogleDataTransport (~> 10.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
@@ -105,32 +105,39 @@ PODS:
 | 
				
			|||||||
  - Flutter (1.0.0)
 | 
					  - Flutter (1.0.0)
 | 
				
			||||||
  - flutter_app_update (0.0.1):
 | 
					  - flutter_app_update (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - 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_native_splash (2.4.3):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - flutter_udid (0.0.1):
 | 
					  - flutter_udid (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - SAMKeychain
 | 
					    - SAMKeychain
 | 
				
			||||||
  - flutter_webrtc (0.12.2):
 | 
					  - flutter_webrtc (0.12.6):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - WebRTC-SDK (= 125.6422.06)
 | 
					    - WebRTC-SDK (= 125.6422.06)
 | 
				
			||||||
  - gal (1.0.0):
 | 
					  - gal (1.0.0):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - GoogleAppMeasurement (11.4.0):
 | 
					  - GoogleAppMeasurement (11.6.0):
 | 
				
			||||||
    - GoogleAppMeasurement/AdIdSupport (= 11.4.0)
 | 
					    - GoogleAppMeasurement/AdIdSupport (= 11.6.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
  - GoogleAppMeasurement/AdIdSupport (11.4.0):
 | 
					  - GoogleAppMeasurement/AdIdSupport (11.6.0):
 | 
				
			||||||
    - GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0)
 | 
					    - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
  - GoogleAppMeasurement/WithoutAdIdSupport (11.4.0):
 | 
					  - GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
@@ -173,7 +180,7 @@ PODS:
 | 
				
			|||||||
  - in_app_review (2.0.0):
 | 
					  - in_app_review (2.0.0):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - Kingfisher (8.1.3)
 | 
					  - Kingfisher (8.1.3)
 | 
				
			||||||
  - livekit_client (2.3.4):
 | 
					  - livekit_client (2.3.5):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - flutter_webrtc
 | 
					    - flutter_webrtc
 | 
				
			||||||
    - WebRTC-SDK (= 125.6422.06)
 | 
					    - WebRTC-SDK (= 125.6422.06)
 | 
				
			||||||
@@ -188,6 +195,7 @@ PODS:
 | 
				
			|||||||
    - nanopb/encode (= 3.30910.0)
 | 
					    - nanopb/encode (= 3.30910.0)
 | 
				
			||||||
  - nanopb/decode (3.30910.0)
 | 
					  - nanopb/decode (3.30910.0)
 | 
				
			||||||
  - nanopb/encode (3.30910.0)
 | 
					  - nanopb/encode (3.30910.0)
 | 
				
			||||||
 | 
					  - OrderedSet (6.0.3)
 | 
				
			||||||
  - package_info_plus (0.4.5):
 | 
					  - package_info_plus (0.4.5):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - pasteboard (0.0.1):
 | 
					  - pasteboard (0.0.1):
 | 
				
			||||||
@@ -239,6 +247,7 @@ DEPENDENCIES:
 | 
				
			|||||||
  - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
 | 
					  - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
 | 
				
			||||||
  - Flutter (from `Flutter`)
 | 
					  - Flutter (from `Flutter`)
 | 
				
			||||||
  - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
 | 
					  - 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_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
 | 
				
			||||||
  - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
 | 
					  - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
 | 
				
			||||||
  - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
 | 
					  - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
 | 
				
			||||||
@@ -282,6 +291,7 @@ SPEC REPOS:
 | 
				
			|||||||
    - GoogleUtilities
 | 
					    - GoogleUtilities
 | 
				
			||||||
    - Kingfisher
 | 
					    - Kingfisher
 | 
				
			||||||
    - nanopb
 | 
					    - nanopb
 | 
				
			||||||
 | 
					    - OrderedSet
 | 
				
			||||||
    - PromisesObjC
 | 
					    - PromisesObjC
 | 
				
			||||||
    - SAMKeychain
 | 
					    - SAMKeychain
 | 
				
			||||||
    - SDWebImage
 | 
					    - SDWebImage
 | 
				
			||||||
@@ -309,6 +319,8 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
    :path: Flutter
 | 
					    :path: Flutter
 | 
				
			||||||
  flutter_app_update:
 | 
					  flutter_app_update:
 | 
				
			||||||
    :path: ".symlinks/plugins/flutter_app_update/ios"
 | 
					    :path: ".symlinks/plugins/flutter_app_update/ios"
 | 
				
			||||||
 | 
					  flutter_inappwebview_ios:
 | 
				
			||||||
 | 
					    :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
 | 
				
			||||||
  flutter_native_splash:
 | 
					  flutter_native_splash:
 | 
				
			||||||
    :path: ".symlinks/plugins/flutter_native_splash/ios"
 | 
					    :path: ".symlinks/plugins/flutter_native_splash/ios"
 | 
				
			||||||
  flutter_udid:
 | 
					  flutter_udid:
 | 
				
			||||||
@@ -367,35 +379,37 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
 | 
					  device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
 | 
				
			||||||
  DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
 | 
					  DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
 | 
				
			||||||
  DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
 | 
					  DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
 | 
				
			||||||
  file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
 | 
					  file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
 | 
				
			||||||
  file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
 | 
					  file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
 | 
				
			||||||
  Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
 | 
					  Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
 | 
				
			||||||
  firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75
 | 
					  firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e
 | 
				
			||||||
  firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10
 | 
					  firebase_core: e2aa06dbd854d961f8ce46c2e20933bee1bf2d2b
 | 
				
			||||||
  firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9
 | 
					  firebase_messaging: 96cf6d67121b3f39746b2a4f29a26c0eee4af70e
 | 
				
			||||||
  FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
 | 
					  FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
 | 
				
			||||||
  FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
 | 
					  FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
 | 
				
			||||||
  FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
 | 
					  FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
 | 
				
			||||||
  FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
 | 
					  FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
 | 
				
			||||||
  FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
 | 
					  FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
 | 
				
			||||||
  Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
 | 
					  Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
 | 
				
			||||||
  flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
 | 
					  flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
 | 
				
			||||||
 | 
					  flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
 | 
				
			||||||
  flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
 | 
					  flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
 | 
				
			||||||
  flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
 | 
					  flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
 | 
				
			||||||
  flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563
 | 
					  flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
 | 
				
			||||||
  gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
 | 
					  gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
 | 
				
			||||||
  GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
 | 
					  GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
 | 
				
			||||||
  GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
 | 
					  GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
 | 
				
			||||||
  GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
 | 
					  GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
 | 
				
			||||||
  home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
 | 
					  home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
 | 
				
			||||||
  image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
 | 
					  image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
 | 
				
			||||||
  in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
 | 
					  in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
 | 
				
			||||||
  Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
 | 
					  Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
 | 
				
			||||||
  livekit_client: 4eaa7a2968fc7e7c57888f43d90394547cc8d9e9
 | 
					  livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
 | 
				
			||||||
  media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
 | 
					  media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
 | 
				
			||||||
  media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
 | 
					  media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
 | 
				
			||||||
  media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
 | 
					  media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
 | 
				
			||||||
  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
					  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
				
			||||||
 | 
					  OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
 | 
				
			||||||
  package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
 | 
					  package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
 | 
				
			||||||
  pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
 | 
					  pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
 | 
				
			||||||
  path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
 | 
					  path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -279,6 +279,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
 | 
				
			|||||||
      await ws.tryConnect();
 | 
					      await ws.tryConnect();
 | 
				
			||||||
      if (!mounted) return;
 | 
					      if (!mounted) return;
 | 
				
			||||||
      final notify = context.read<NotificationProvider>();
 | 
					      final notify = context.read<NotificationProvider>();
 | 
				
			||||||
 | 
					      notify.listen();
 | 
				
			||||||
      await notify.registerPushNotifications();
 | 
					      await notify.registerPushNotifications();
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      if (!mounted) return;
 | 
					      if (!mounted) return;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,9 @@ const kAppbarTransparentStoreKey = 'app_bar_transparent';
 | 
				
			|||||||
const kAppBackgroundStoreKey = 'app_has_background';
 | 
					const kAppBackgroundStoreKey = 'app_has_background';
 | 
				
			||||||
const kAppColorSchemeStoreKey = 'app_color_scheme';
 | 
					const kAppColorSchemeStoreKey = 'app_color_scheme';
 | 
				
			||||||
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
 | 
					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 = {
 | 
					const Map<String, FilterQuality> kImageQualityLevel = {
 | 
				
			||||||
  'settingsImageQualityLowest': FilterQuality.none,
 | 
					  'settingsImageQualityLowest': FilterQuality.none,
 | 
				
			||||||
@@ -38,14 +41,22 @@ class ConfigProvider extends ChangeNotifier {
 | 
				
			|||||||
  bool drawerIsCollapsed = false;
 | 
					  bool drawerIsCollapsed = false;
 | 
				
			||||||
  bool drawerIsExpanded = false;
 | 
					  bool drawerIsExpanded = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void calcDrawerSize(BuildContext context) {
 | 
					  void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
 | 
				
			||||||
    final rpb = ResponsiveBreakpoints.of(context);
 | 
					    bool newDrawerIsCollapsed = false;
 | 
				
			||||||
    final newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
 | 
					    bool newDrawerIsExpanded = false;
 | 
				
			||||||
    final newDrawerIsExpanded = rpb.largerThan(TABLET)
 | 
					    if (withMediaQuery) {
 | 
				
			||||||
        ? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
 | 
					      newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450;
 | 
				
			||||||
            ? false
 | 
					      newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451;
 | 
				
			||||||
            : true
 | 
					    } else {
 | 
				
			||||||
        : false;
 | 
					      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) {
 | 
					    if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) {
 | 
				
			||||||
      drawerIsExpanded = newDrawerIsExpanded;
 | 
					      drawerIsExpanded = newDrawerIsExpanded;
 | 
				
			||||||
      drawerIsCollapsed = newDrawerIsCollapsed;
 | 
					      drawerIsCollapsed = newDrawerIsCollapsed;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -58,6 +58,11 @@ class NavigationProvider extends ChangeNotifier {
 | 
				
			|||||||
      screen: 'realm',
 | 
					      screen: 'realm',
 | 
				
			||||||
      label: 'screenRealm',
 | 
					      label: 'screenRealm',
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    AppNavDestination(
 | 
				
			||||||
 | 
					      icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
 | 
				
			||||||
 | 
					      screen: 'news',
 | 
				
			||||||
 | 
					      label: 'screenNews',
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
    AppNavDestination(
 | 
					    AppNavDestination(
 | 
				
			||||||
      icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
 | 
					      icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
 | 
				
			||||||
      screen: 'album',
 | 
					      screen: 'album',
 | 
				
			||||||
@@ -83,8 +88,7 @@ class NavigationProvider extends ChangeNotifier {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  List<AppNavDestination> destinations = [];
 | 
					  List<AppNavDestination> destinations = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  int get pinnedDestinationCount =>
 | 
					  int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
 | 
				
			||||||
      destinations.where((ele) => ele.isPinned).length;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  NavigationProvider() {
 | 
					  NavigationProvider() {
 | 
				
			||||||
    buildDestinations(kDefaultPinnedDestination);
 | 
					    buildDestinations(kDefaultPinnedDestination);
 | 
				
			||||||
@@ -113,17 +117,13 @@ class NavigationProvider extends ChangeNotifier {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool isIndexInRange(int min, int max) {
 | 
					  bool isIndexInRange(int min, int max) {
 | 
				
			||||||
    return _currentIndex != null &&
 | 
					    return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
 | 
				
			||||||
        _currentIndex! >= min &&
 | 
					 | 
				
			||||||
        _currentIndex! < max;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void autoDetectIndex(GoRouter? state) {
 | 
					  void autoDetectIndex(GoRouter? state) {
 | 
				
			||||||
    if (state == null) return;
 | 
					    if (state == null) return;
 | 
				
			||||||
    final idx = destinations.indexWhere(
 | 
					    final idx = destinations.indexWhere(
 | 
				
			||||||
      (ele) =>
 | 
					      (ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name,
 | 
				
			||||||
          ele.screen ==
 | 
					 | 
				
			||||||
          state.routerDelegate.currentConfiguration.last.route.name,
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    _currentIndex = idx == -1 ? null : idx;
 | 
					    _currentIndex = idx == -1 ? null : idx;
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,18 +4,26 @@ import 'dart:io';
 | 
				
			|||||||
import 'package:firebase_messaging/firebase_messaging.dart';
 | 
					import 'package:firebase_messaging/firebase_messaging.dart';
 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
import 'package:flutter_udid/flutter_udid.dart';
 | 
					import 'package:flutter_udid/flutter_udid.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/config.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
import 'package:surface/providers/userinfo.dart';
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/websocket.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/notification.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NotificationProvider extends ChangeNotifier {
 | 
					class NotificationProvider extends ChangeNotifier {
 | 
				
			||||||
  late final SnNetworkProvider _sn;
 | 
					  late final SnNetworkProvider _sn;
 | 
				
			||||||
  late final UserProvider _ua;
 | 
					  late final UserProvider _ua;
 | 
				
			||||||
 | 
					  late final WebSocketProvider _ws;
 | 
				
			||||||
 | 
					  late final ConfigProvider _cfg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  NotificationProvider(BuildContext context) {
 | 
					  NotificationProvider(BuildContext context) {
 | 
				
			||||||
    _sn = context.read<SnNetworkProvider>();
 | 
					    _sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
    _ua = context.read<UserProvider>();
 | 
					    _ua = context.read<UserProvider>();
 | 
				
			||||||
 | 
					    _ws = context.read<WebSocketProvider>();
 | 
				
			||||||
 | 
					    _cfg = context.read<ConfigProvider>();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> registerPushNotifications() async {
 | 
					  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();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										422
									
								
								lib/router.dart
									
									
									
									
									
								
							
							
						
						
									
										422
									
								
								lib/router.dart
									
									
									
									
									
								
							@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
 | 
				
			|||||||
import 'package:go_router/go_router.dart';
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
import 'package:surface/screens/abuse_report.dart';
 | 
					import 'package:surface/screens/abuse_report.dart';
 | 
				
			||||||
import 'package:surface/screens/account.dart';
 | 
					import 'package:surface/screens/account.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/account/account_settings.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/account/factor_settings.dart';
 | 
				
			||||||
import 'package:surface/screens/account/profile_page.dart';
 | 
					import 'package:surface/screens/account/profile_page.dart';
 | 
				
			||||||
import 'package:surface/screens/account/profile_edit.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_edit.dart';
 | 
				
			||||||
@@ -19,6 +21,8 @@ import 'package:surface/screens/chat/room.dart';
 | 
				
			|||||||
import 'package:surface/screens/explore.dart';
 | 
					import 'package:surface/screens/explore.dart';
 | 
				
			||||||
import 'package:surface/screens/friend.dart';
 | 
					import 'package:surface/screens/friend.dart';
 | 
				
			||||||
import 'package:surface/screens/home.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/notification.dart';
 | 
				
			||||||
import 'package:surface/screens/post/post_detail.dart';
 | 
					import 'package:surface/screens/post/post_detail.dart';
 | 
				
			||||||
import 'package:surface/screens/post/post_editor.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/realm/realm_detail.dart';
 | 
				
			||||||
import 'package:surface/screens/settings.dart';
 | 
					import 'package:surface/screens/settings.dart';
 | 
				
			||||||
import 'package:surface/screens/sharing.dart';
 | 
					import 'package:surface/screens/sharing.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/wallet.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/about.dart';
 | 
					import 'package:surface/widgets/about.dart';
 | 
				
			||||||
import 'package:surface/widgets/navigation/app_background.dart';
 | 
					 | 
				
			||||||
import 'package:surface/widgets/navigation/app_scaffold.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 = [
 | 
					final _appRoutes = [
 | 
				
			||||||
  ShellRoute(
 | 
					  GoRoute(
 | 
				
			||||||
    builder: (context, state, child) => AppPageScaffold(
 | 
					    path: '/',
 | 
				
			||||||
      body: child,
 | 
					    name: 'home',
 | 
				
			||||||
      showAppBar: false,
 | 
					    builder: (context, state) => const HomeScreen(),
 | 
				
			||||||
    ),
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/posts',
 | 
				
			||||||
 | 
					    name: 'explore',
 | 
				
			||||||
 | 
					    builder: (context, state) => const ExploreScreen(),
 | 
				
			||||||
    routes: [
 | 
					    routes: [
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/',
 | 
					        path: '/write/:mode',
 | 
				
			||||||
        name: 'home',
 | 
					        name: 'postEditor',
 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					        builder: (context, state) => PostEditorScreen(
 | 
				
			||||||
          child: const HomeScreen(),
 | 
					          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(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/posts',
 | 
					        path: '/search',
 | 
				
			||||||
        name: 'explore',
 | 
					        name: 'postSearch',
 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					        builder: (context, state) => PostSearchScreen(
 | 
				
			||||||
          child: const ExploreScreen(),
 | 
					          initialTags: state.uri.queryParameters['tags']?.split(','),
 | 
				
			||||||
        ),
 | 
					          initialCategories: state.uri.queryParameters['categories']?.split(','),
 | 
				
			||||||
        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(),
 | 
					 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/friend',
 | 
					        path: '/publishers/:name',
 | 
				
			||||||
        name: 'friend',
 | 
					        name: 'postPublisher',
 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					        builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
 | 
				
			||||||
          child: const FriendScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/notification',
 | 
					        path: '/:slug',
 | 
				
			||||||
        name: 'notification',
 | 
					        name: 'postDetail',
 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					        builder: (context, state) => PostDetailScreen(
 | 
				
			||||||
          child: const NotificationScreen(),
 | 
					          slug: state.pathParameters['slug']!,
 | 
				
			||||||
 | 
					          preload: state.extra as SnPost?,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  ShellRoute(
 | 
					  GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [
 | 
				
			||||||
    builder: (context, state, child) => AppPageScaffold(body: child),
 | 
					    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: [
 | 
					    routes: [
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/auth/login',
 | 
					        path: '/:scope/:alias',
 | 
				
			||||||
        name: 'authLogin',
 | 
					        name: 'chatRoom',
 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					        builder: (context, state) => ChatRoomScreen(
 | 
				
			||||||
          child: LoginScreen(),
 | 
					          scope: state.pathParameters['scope']!,
 | 
				
			||||||
 | 
					          alias: state.pathParameters['alias']!,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/auth/register',
 | 
					        path: '/:scope/:alias/call',
 | 
				
			||||||
        name: 'authRegister',
 | 
					        name: 'chatCallRoom',
 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					        builder: (context, state) => CallRoomScreen(
 | 
				
			||||||
          child: RegisterScreen(),
 | 
					          scope: state.pathParameters['scope']!,
 | 
				
			||||||
 | 
					          alias: state.pathParameters['alias']!,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/reports',
 | 
					        path: '/:scope/:alias/detail',
 | 
				
			||||||
        name: 'abuseReport',
 | 
					        name: 'channelDetail',
 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					        builder: (context, state) => ChannelDetailScreen(
 | 
				
			||||||
          child: AbuseReportScreen(),
 | 
					          scope: state.pathParameters['scope']!,
 | 
				
			||||||
 | 
					          alias: state.pathParameters['alias']!,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/account/profile/edit',
 | 
					        path: '/manage',
 | 
				
			||||||
        name: 'accountProfileEdit',
 | 
					        name: 'chatManage',
 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					        builder: (context, state) => ChatManageScreen(
 | 
				
			||||||
          child: ProfileEditScreen(),
 | 
					          editingChannelAlias: state.uri.queryParameters['editing'],
 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      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']!,
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  GoRoute(
 | 
					  GoRoute(
 | 
				
			||||||
    path: '/account/:name',
 | 
					    path: '/realm',
 | 
				
			||||||
    name: 'accountProfilePage',
 | 
					    name: 'realm',
 | 
				
			||||||
    pageBuilder: (context, state) => NoTransitionPage(
 | 
					    pageBuilder: (context, state) => CustomTransitionPage(
 | 
				
			||||||
      child: UserScreen(name: state.pathParameters['name']!),
 | 
					      transitionsBuilder: _fadeThroughTransition,
 | 
				
			||||||
 | 
					      child: const RealmScreen(),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
  ),
 | 
					 | 
				
			||||||
  ShellRoute(
 | 
					 | 
				
			||||||
    builder: (context, state, child) => AppPageScaffold(body: child),
 | 
					 | 
				
			||||||
    routes: [
 | 
					    routes: [
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/settings',
 | 
					        path: '/:alias',
 | 
				
			||||||
        name: 'settings',
 | 
					        name: 'realmDetail',
 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					        builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
 | 
				
			||||||
          child: SettingsScreen(),
 | 
					      ),
 | 
				
			||||||
 | 
					      GoRoute(
 | 
				
			||||||
 | 
					        path: '/manage',
 | 
				
			||||||
 | 
					        name: 'realmManage',
 | 
				
			||||||
 | 
					        builder: (context, state) => RealmManageScreen(
 | 
				
			||||||
 | 
					          editingRealmAlias: state.uri.queryParameters['editing'],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  ShellRoute(
 | 
					  GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
 | 
				
			||||||
    builder: (context, state, child) => AppPageScaffold(body: child),
 | 
					    GoRoute(
 | 
				
			||||||
    routes: [
 | 
					      path: '/:hash',
 | 
				
			||||||
      GoRoute(
 | 
					      name: 'newsDetail',
 | 
				
			||||||
        path: '/about',
 | 
					      builder: (context, state) => NewsDetailScreen(
 | 
				
			||||||
        name: 'about',
 | 
					        hash: state.pathParameters['hash']!,
 | 
				
			||||||
        builder: (context, state) => const AppBackground(
 | 
					 | 
				
			||||||
          child: AboutScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    ],
 | 
					    ),
 | 
				
			||||||
 | 
					  ]),
 | 
				
			||||||
 | 
					  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:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '../types/account.dart';
 | 
					import '../types/account.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
 | 
					        title: Text('screenAbuseReport').tr(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      body: Column(
 | 
					      body: Column(
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          ListTile(
 | 
					          ListTile(
 | 
				
			||||||
@@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
 | 
				
			|||||||
          else
 | 
					          else
 | 
				
			||||||
            Expanded(
 | 
					            Expanded(
 | 
				
			||||||
              child: ListView.builder(
 | 
					              child: ListView.builder(
 | 
				
			||||||
 | 
					                padding: EdgeInsets.only(top: 8),
 | 
				
			||||||
                itemCount: _reports.length,
 | 
					                itemCount: _reports.length,
 | 
				
			||||||
                itemBuilder: (context, idx) {
 | 
					                itemBuilder: (context, idx) {
 | 
				
			||||||
                  return ListTile(
 | 
					                  return ListTile(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					import 'dart:ui';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:gap/gap.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/account/account_image.dart';
 | 
				
			||||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
					import 'package:surface/widgets/app_bar_leading.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.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 {
 | 
					class AccountScreen extends StatelessWidget {
 | 
				
			||||||
  const AccountScreen({super.key});
 | 
					  const AccountScreen({super.key});
 | 
				
			||||||
@@ -19,11 +23,51 @@ class AccountScreen extends StatelessWidget {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final ua = context.watch<UserProvider>();
 | 
					    final ua = context.watch<UserProvider>();
 | 
				
			||||||
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        leading: AutoAppBarLeading(),
 | 
					        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: [
 | 
					        actions: [
 | 
				
			||||||
          IconButton(
 | 
					          IconButton(
 | 
				
			||||||
            icon: const Icon(Symbols.settings, fill: 1),
 | 
					            icon: const Icon(Symbols.settings, fill: 1),
 | 
				
			||||||
@@ -82,16 +126,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
				
			|||||||
            );
 | 
					            );
 | 
				
			||||||
          }).padding(all: 20),
 | 
					          }).padding(all: 20),
 | 
				
			||||||
        ).padding(horizontal: 8, top: 16, bottom: 4),
 | 
					        ).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(
 | 
					        ListTile(
 | 
				
			||||||
          title: Text('accountPublishers').tr(),
 | 
					          title: Text('accountPublishers').tr(),
 | 
				
			||||||
          subtitle: Text('accountPublishersSubtitle').tr(),
 | 
					          subtitle: Text('accountPublishersSubtitle').tr(),
 | 
				
			||||||
@@ -112,6 +146,36 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
				
			|||||||
            GoRouter.of(context).pushNamed('abuseReport');
 | 
					            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(
 | 
					        ListTile(
 | 
				
			||||||
          title: Text('accountLogout').tr(),
 | 
					          title: Text('accountLogout').tr(),
 | 
				
			||||||
          subtitle: Text('accountLogoutSubtitle').tr(),
 | 
					          subtitle: Text('accountLogoutSubtitle').tr(),
 | 
				
			||||||
@@ -133,33 +197,6 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
				
			|||||||
            await Hive.initFlutter();
 | 
					            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/account/account_image.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.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:surface/widgets/universal_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProfileEditScreen extends StatefulWidget {
 | 
					class ProfileEditScreen extends StatefulWidget {
 | 
				
			||||||
@@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
				
			|||||||
            onDateTimeChanged: (DateTime newDate) {
 | 
					            onDateTimeChanged: (DateTime newDate) {
 | 
				
			||||||
              setState(() {
 | 
					              setState(() {
 | 
				
			||||||
                _birthday = newDate;
 | 
					                _birthday = newDate;
 | 
				
			||||||
                _birthdayController.text =
 | 
					                _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
 | 
				
			||||||
                    DateFormat(_kDateFormat).format(_birthday!);
 | 
					 | 
				
			||||||
              });
 | 
					              });
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
@@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
				
			|||||||
    if (image == null) return;
 | 
					    if (image == null) return;
 | 
				
			||||||
    if (!mounted) return;
 | 
					    if (!mounted) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final ImageProvider imageProvider =
 | 
					    final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
 | 
				
			||||||
        kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
 | 
					    final aspectRatios =
 | 
				
			||||||
    final aspectRatios = place == 'banner'
 | 
					        place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
 | 
				
			||||||
        ? [CropAspectRatio(width: 16, height: 7)]
 | 
					 | 
				
			||||||
        : [CropAspectRatio(width: 1, height: 1)];
 | 
					 | 
				
			||||||
    final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
 | 
					    final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
 | 
				
			||||||
        ? await showCupertinoImageCropper(
 | 
					        ? await showCupertinoImageCropper(
 | 
				
			||||||
            // ignore: use_build_context_synchronously
 | 
					            // ignore: use_build_context_synchronously
 | 
				
			||||||
@@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final rawBytes =
 | 
					    final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
 | 
				
			||||||
        (await result.uiImage.toByteData(format: ImageByteFormat.png))!
 | 
					 | 
				
			||||||
            .buffer
 | 
					 | 
				
			||||||
            .asUint8List();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final attachment = await attach.directUploadOne(
 | 
					      final attachment = await attach.directUploadOne(
 | 
				
			||||||
@@ -212,136 +207,141 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final sn = context.read<SnNetworkProvider>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return SingleChildScrollView(
 | 
					    return AppScaffold(
 | 
				
			||||||
      child: Column(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
        children: [
 | 
					        title: Text('screenAccountProfileEdit').tr(),
 | 
				
			||||||
          LoadingIndicator(isActive: _isBusy),
 | 
					      ),
 | 
				
			||||||
          const Gap(24),
 | 
					      body: SingleChildScrollView(
 | 
				
			||||||
          Stack(
 | 
					        child: Column(
 | 
				
			||||||
            clipBehavior: Clip.none,
 | 
					          crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
            children: [
 | 
					          children: [
 | 
				
			||||||
              Material(
 | 
					            LoadingIndicator(isActive: _isBusy),
 | 
				
			||||||
                elevation: 0,
 | 
					            const Gap(24),
 | 
				
			||||||
                child: InkWell(
 | 
					            Stack(
 | 
				
			||||||
                  child: ClipRRect(
 | 
					              clipBehavior: Clip.none,
 | 
				
			||||||
                    borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					              children: [
 | 
				
			||||||
                    child: AspectRatio(
 | 
					                Material(
 | 
				
			||||||
                      aspectRatio: 16 / 9,
 | 
					                  elevation: 0,
 | 
				
			||||||
                      child: Container(
 | 
					                  child: InkWell(
 | 
				
			||||||
                        color:
 | 
					                    child: ClipRRect(
 | 
				
			||||||
                            Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
					                      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
                        child: _banner != null
 | 
					                      child: AspectRatio(
 | 
				
			||||||
                            ? AutoResizeUniversalImage(
 | 
					                        aspectRatio: 16 / 9,
 | 
				
			||||||
                                sn.getAttachmentUrl(_banner!),
 | 
					                        child: Container(
 | 
				
			||||||
                                fit: BoxFit.cover,
 | 
					                          color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
				
			||||||
                              )
 | 
					                          child: _banner != null
 | 
				
			||||||
                            : const SizedBox.shrink(),
 | 
					                              ? 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: () {
 | 
					                    onTap: () {
 | 
				
			||||||
                      _updateImage('avatar');
 | 
					                      _updateImage('banner');
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					                Positioned(
 | 
				
			||||||
            ],
 | 
					                  bottom: -28,
 | 
				
			||||||
          ).padding(horizontal: padding),
 | 
					                  left: 16,
 | 
				
			||||||
          const Gap(8 + 28),
 | 
					                  child: Material(
 | 
				
			||||||
          Column(
 | 
					                    elevation: 2,
 | 
				
			||||||
            children: [
 | 
					                    borderRadius: const BorderRadius.all(Radius.circular(40)),
 | 
				
			||||||
              TextField(
 | 
					                    child: InkWell(
 | 
				
			||||||
                readOnly: true,
 | 
					                      child: AccountImage(content: _avatar, radius: 40),
 | 
				
			||||||
                controller: _usernameController,
 | 
					                      onTap: () {
 | 
				
			||||||
                decoration: InputDecoration(
 | 
					                        _updateImage('avatar');
 | 
				
			||||||
                  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,
 | 
					            ).padding(horizontal: padding),
 | 
				
			||||||
                    child: TextField(
 | 
					            const Gap(8 + 28),
 | 
				
			||||||
                      controller: _lastNameController,
 | 
					            Column(
 | 
				
			||||||
                      decoration: InputDecoration(
 | 
					              children: [
 | 
				
			||||||
                        border: const UnderlineInputBorder(),
 | 
					                TextField(
 | 
				
			||||||
                        labelText: 'fieldLastName'.tr(),
 | 
					                  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),
 | 
				
			||||||
              const Gap(4),
 | 
					                TextField(
 | 
				
			||||||
              TextField(
 | 
					                  controller: _birthdayController,
 | 
				
			||||||
                controller: _birthdayController,
 | 
					                  readOnly: true,
 | 
				
			||||||
                readOnly: true,
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
                decoration: InputDecoration(
 | 
					                    border: const UnderlineInputBorder(),
 | 
				
			||||||
                  border: const UnderlineInputBorder(),
 | 
					                    labelText: 'fieldBirthday'.tr(),
 | 
				
			||||||
                  labelText: 'fieldBirthday'.tr(),
 | 
					                  ),
 | 
				
			||||||
 | 
					                  onTap: () => _selectBirthday(),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                onTap: () => _selectBirthday(),
 | 
					              ],
 | 
				
			||||||
              ),
 | 
					            ).padding(horizontal: padding + 8),
 | 
				
			||||||
            ],
 | 
					            const Gap(12),
 | 
				
			||||||
          ).padding(horizontal: padding + 8),
 | 
					            Row(
 | 
				
			||||||
          const Gap(12),
 | 
					              mainAxisAlignment: MainAxisAlignment.end,
 | 
				
			||||||
          Row(
 | 
					              children: [
 | 
				
			||||||
            mainAxisAlignment: MainAxisAlignment.end,
 | 
					                ElevatedButton.icon(
 | 
				
			||||||
            children: [
 | 
					                  onPressed: _isBusy ? null : _updateUserInfo,
 | 
				
			||||||
              ElevatedButton.icon(
 | 
					                  icon: const Icon(Symbols.save),
 | 
				
			||||||
                onPressed: _isBusy ? null : _updateUserInfo,
 | 
					                  label: Text('apply').tr(),
 | 
				
			||||||
                icon: const Icon(Symbols.save),
 | 
					                ),
 | 
				
			||||||
                label: Text('apply').tr(),
 | 
					              ],
 | 
				
			||||||
              ),
 | 
					            ).padding(horizontal: padding),
 | 
				
			||||||
            ],
 | 
					          ],
 | 
				
			||||||
          ).padding(horizontal: padding),
 | 
					        ),
 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -241,6 +241,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
 | 
				
			|||||||
    final sn = context.read<SnNetworkProvider>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
 | 
					      backgroundColor: Colors.transparent,
 | 
				
			||||||
      body: CustomScrollView(
 | 
					      body: CustomScrollView(
 | 
				
			||||||
        controller: _scrollController,
 | 
					        controller: _scrollController,
 | 
				
			||||||
        slivers: [
 | 
					        slivers: [
 | 
				
			||||||
@@ -594,7 +595,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
 | 
				
			|||||||
                subtitle: Text('@${ele.name}'),
 | 
					                subtitle: Text('@${ele.name}'),
 | 
				
			||||||
                trailing: const Icon(Symbols.chevron_right),
 | 
					                trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
                onTap: () {
 | 
					                onTap: () {
 | 
				
			||||||
                  GoRouter.of(context).pushNamed(
 | 
					                  GoRouter.of(context).goNamed(
 | 
				
			||||||
                    'postPublisher',
 | 
					                    'postPublisher',
 | 
				
			||||||
                    pathParameters: {'name': ele.name},
 | 
					                    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/account/account_image.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.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:surface/widgets/universal_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountPublisherEditScreen extends StatefulWidget {
 | 
					class AccountPublisherEditScreen extends StatefulWidget {
 | 
				
			||||||
@@ -176,7 +177,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final sn = context.read<SnNetworkProvider>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      body: SingleChildScrollView(
 | 
					      body: SingleChildScrollView(
 | 
				
			||||||
        child: Column(
 | 
					        child: Column(
 | 
				
			||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
 | 
				
			|||||||
import 'package:surface/types/realm.dart';
 | 
					import 'package:surface/types/realm.dart';
 | 
				
			||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountPublisherNewScreen extends StatefulWidget {
 | 
					class AccountPublisherNewScreen extends StatefulWidget {
 | 
				
			||||||
  const AccountPublisherNewScreen({super.key});
 | 
					  const AccountPublisherNewScreen({super.key});
 | 
				
			||||||
@@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return  AppScaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
 | 
					        title: Text('screenAccountPublisherNew').tr(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      body: SingleChildScrollView(
 | 
					      body: SingleChildScrollView(
 | 
				
			||||||
        child: Column(
 | 
					        child: Column(
 | 
				
			||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ import 'package:surface/types/post.dart';
 | 
				
			|||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PublisherScreen extends StatefulWidget {
 | 
					class PublisherScreen extends StatefulWidget {
 | 
				
			||||||
  const PublisherScreen({super.key});
 | 
					  const PublisherScreen({super.key});
 | 
				
			||||||
@@ -32,8 +33,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final resp = await sn.client.get('/cgi/co/publishers/me');
 | 
					      final resp = await sn.client.get('/cgi/co/publishers/me');
 | 
				
			||||||
      final List<SnPublisher> out = List<SnPublisher>.from(
 | 
					      final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
 | 
				
			||||||
          resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!mounted) return;
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
 | 
					        title: Text('screenAccountPublishers').tr(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      body: Column(
 | 
					      body: Column(
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          ListTile(
 | 
					          ListTile(
 | 
				
			||||||
@@ -62,9 +66,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
				
			|||||||
            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
					            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
				
			||||||
            leading: const Icon(Symbols.add_circle),
 | 
					            leading: const Icon(Symbols.add_circle),
 | 
				
			||||||
            onTap: () {
 | 
					            onTap: () {
 | 
				
			||||||
              GoRouter.of(context)
 | 
					              GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
 | 
				
			||||||
                  .pushNamed('accountPublisherNew')
 | 
					 | 
				
			||||||
                  .then((value) {
 | 
					 | 
				
			||||||
                if (value == true) {
 | 
					                if (value == true) {
 | 
				
			||||||
                  _publishers.clear();
 | 
					                  _publishers.clear();
 | 
				
			||||||
                  _fetchPublishers();
 | 
					                  _fetchPublishers();
 | 
				
			||||||
@@ -75,48 +77,52 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
				
			|||||||
          const Divider(height: 1),
 | 
					          const Divider(height: 1),
 | 
				
			||||||
          LoadingIndicator(isActive: _isBusy),
 | 
					          LoadingIndicator(isActive: _isBusy),
 | 
				
			||||||
          Expanded(
 | 
					          Expanded(
 | 
				
			||||||
            child: RefreshIndicator(
 | 
					            child: MediaQuery.removePadding(
 | 
				
			||||||
              onRefresh: () {
 | 
					              context: context,
 | 
				
			||||||
                _publishers.clear();
 | 
					              removeTop: true,
 | 
				
			||||||
                return _fetchPublishers();
 | 
					              child: RefreshIndicator(
 | 
				
			||||||
              },
 | 
					                onRefresh: () {
 | 
				
			||||||
              child: ListView.builder(
 | 
					                  _publishers.clear();
 | 
				
			||||||
                itemCount: _publishers.length,
 | 
					                  return _fetchPublishers();
 | 
				
			||||||
                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: 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_zoom.dart';
 | 
				
			||||||
import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
					import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:uuid/uuid.dart';
 | 
					import 'package:uuid/uuid.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AlbumScreen extends StatefulWidget {
 | 
					class AlbumScreen extends StatefulWidget {
 | 
				
			||||||
@@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      body: CustomScrollView(
 | 
					      body: CustomScrollView(
 | 
				
			||||||
        controller: _scrollController,
 | 
					        controller: _scrollController,
 | 
				
			||||||
        slivers: [
 | 
					        slivers: [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,17 +7,14 @@ import 'package:provider/provider.dart';
 | 
				
			|||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
import 'package:surface/providers/userinfo.dart';
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/account/factor_settings.dart';
 | 
				
			||||||
import 'package:surface/types/auth.dart';
 | 
					import 'package:surface/types/auth.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '../../providers/websocket.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 {
 | 
					class LoginScreen extends StatefulWidget {
 | 
				
			||||||
  const LoginScreen({super.key});
 | 
					  const LoginScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -35,67 +32,73 @@ class _LoginScreenState extends State<LoginScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Theme(
 | 
					    return AppScaffold(
 | 
				
			||||||
      data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
 | 
					      appBar: AppBar(
 | 
				
			||||||
      child: SingleChildScrollView(
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
        child: PageTransitionSwitcher(
 | 
					        title: Text('screenAuthLogin').tr(),
 | 
				
			||||||
          transitionBuilder: (
 | 
					      ),
 | 
				
			||||||
            Widget child,
 | 
					      body: Theme(
 | 
				
			||||||
            Animation<double> primaryAnimation,
 | 
					        data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
 | 
				
			||||||
            Animation<double> secondaryAnimation,
 | 
					        child: SingleChildScrollView(
 | 
				
			||||||
          ) {
 | 
					          child: PageTransitionSwitcher(
 | 
				
			||||||
            return SharedAxisTransition(
 | 
					            transitionBuilder: (
 | 
				
			||||||
              animation: primaryAnimation,
 | 
					              Widget child,
 | 
				
			||||||
              secondaryAnimation: secondaryAnimation,
 | 
					              Animation<double> primaryAnimation,
 | 
				
			||||||
              transitionType: SharedAxisTransitionType.horizontal,
 | 
					              Animation<double> secondaryAnimation,
 | 
				
			||||||
              child: Container(
 | 
					            ) {
 | 
				
			||||||
                constraints: BoxConstraints(maxWidth: 380),
 | 
					              return SharedAxisTransition(
 | 
				
			||||||
                child: child,
 | 
					                animation: primaryAnimation,
 | 
				
			||||||
              ),
 | 
					                secondaryAnimation: secondaryAnimation,
 | 
				
			||||||
            );
 | 
					                transitionType: SharedAxisTransitionType.horizontal,
 | 
				
			||||||
          },
 | 
					                child: Container(
 | 
				
			||||||
          child: switch (_period % 3) {
 | 
					                  constraints: BoxConstraints(maxWidth: 380),
 | 
				
			||||||
            1 => _LoginPickerScreen(
 | 
					                  child: child,
 | 
				
			||||||
                key: const ValueKey(1),
 | 
					                ),
 | 
				
			||||||
                ticket: _currentTicket,
 | 
					              );
 | 
				
			||||||
                factors: _factors,
 | 
					            },
 | 
				
			||||||
                onTicket: (p0) => setState(() {
 | 
					            child: switch (_period % 3) {
 | 
				
			||||||
                  _currentTicket = p0;
 | 
					              1 => _LoginPickerScreen(
 | 
				
			||||||
                }),
 | 
					                  key: const ValueKey(1),
 | 
				
			||||||
                onPickFactor: (p0) => setState(() {
 | 
					                  ticket: _currentTicket,
 | 
				
			||||||
                  _factorPicked = p0;
 | 
					                  factors: _factors,
 | 
				
			||||||
                }),
 | 
					                  onTicket: (p0) => setState(() {
 | 
				
			||||||
                onNext: () => setState(() {
 | 
					                    _currentTicket = p0;
 | 
				
			||||||
                  _period++;
 | 
					                  }),
 | 
				
			||||||
                }),
 | 
					                  onPickFactor: (p0) => setState(() {
 | 
				
			||||||
              ),
 | 
					                    _factorPicked = p0;
 | 
				
			||||||
            2 => _LoginCheckScreen(
 | 
					                  }),
 | 
				
			||||||
                key: const ValueKey(2),
 | 
					                  onNext: () => setState(() {
 | 
				
			||||||
                ticket: _currentTicket,
 | 
					                    _period++;
 | 
				
			||||||
                factor: _factorPicked,
 | 
					                  }),
 | 
				
			||||||
                onTicket: (p0) => setState(() {
 | 
					                ),
 | 
				
			||||||
                  _currentTicket = p0;
 | 
					              2 => _LoginCheckScreen(
 | 
				
			||||||
                }),
 | 
					                  key: const ValueKey(2),
 | 
				
			||||||
                onNext: () => setState(() {
 | 
					                  ticket: _currentTicket,
 | 
				
			||||||
                  _period = 1;
 | 
					                  factor: _factorPicked,
 | 
				
			||||||
                }),
 | 
					                  onTicket: (p0) => setState(() {
 | 
				
			||||||
              ),
 | 
					                    _currentTicket = p0;
 | 
				
			||||||
            _ => _LoginLookupScreen(
 | 
					                  }),
 | 
				
			||||||
                key: const ValueKey(0),
 | 
					                  onNext: () => setState(() {
 | 
				
			||||||
                ticket: _currentTicket,
 | 
					                    _period = 1;
 | 
				
			||||||
                onTicket: (p0) => setState(() {
 | 
					                  }),
 | 
				
			||||||
                  _currentTicket = p0;
 | 
					                ),
 | 
				
			||||||
                }),
 | 
					              _ => _LoginLookupScreen(
 | 
				
			||||||
                onFactor: (p0) => setState(() {
 | 
					                  key: const ValueKey(0),
 | 
				
			||||||
                  _factors = p0;
 | 
					                  ticket: _currentTicket,
 | 
				
			||||||
                }),
 | 
					                  onTicket: (p0) => setState(() {
 | 
				
			||||||
                onNext: () => setState(() {
 | 
					                    _currentTicket = p0;
 | 
				
			||||||
                  _period++;
 | 
					                  }),
 | 
				
			||||||
                }),
 | 
					                  onFactor: (p0) => setState(() {
 | 
				
			||||||
              ),
 | 
					                    _factors = p0;
 | 
				
			||||||
          },
 | 
					                  }),
 | 
				
			||||||
        ).padding(all: 24),
 | 
					                  onNext: () => setState(() {
 | 
				
			||||||
      ).center(),
 | 
					                    _period++;
 | 
				
			||||||
 | 
					                  }),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ).padding(all: 24),
 | 
				
			||||||
 | 
					        ).center(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -205,7 +208,9 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
 | 
				
			|||||||
          controller: _passwordController,
 | 
					          controller: _passwordController,
 | 
				
			||||||
          obscureText: true,
 | 
					          obscureText: true,
 | 
				
			||||||
          autofillHints: [
 | 
					          autofillHints: [
 | 
				
			||||||
            (_factorLabelMap[widget.factor!.type]?.$3 ?? true) ? AutofillHints.password : AutofillHints.oneTimeCode
 | 
					            widget.factor!.type == 0
 | 
				
			||||||
 | 
					                ? AutofillHints.password
 | 
				
			||||||
 | 
					                : AutofillHints.oneTimeCode
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
          decoration: InputDecoration(
 | 
					          decoration: InputDecoration(
 | 
				
			||||||
            isDense: true,
 | 
					            isDense: true,
 | 
				
			||||||
@@ -260,7 +265,8 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
 | 
				
			|||||||
  bool _isBusy = false;
 | 
					  bool _isBusy = false;
 | 
				
			||||||
  int? _factorPicked;
 | 
					  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 {
 | 
					  void _performGetFactorCode() async {
 | 
				
			||||||
    if (_factorPicked == null) return;
 | 
					    if (_factorPicked == null) return;
 | 
				
			||||||
@@ -321,11 +327,11 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
 | 
				
			|||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        secondary: Icon(
 | 
					                        secondary: Icon(
 | 
				
			||||||
                          _factorLabelMap[x.type]?.$2 ?? Symbols.question_mark,
 | 
					                          kFactorTypes[x.type]?.$3 ?? Symbols.question_mark,
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        title: Text(
 | 
					                        title: Text(
 | 
				
			||||||
                          _factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(),
 | 
					                          kFactorTypes[x.type]?.$1 ?? 'unknown',
 | 
				
			||||||
                        ),
 | 
					                        ).tr(),
 | 
				
			||||||
                        enabled: !widget.ticket!.factorTrail.contains(x.id),
 | 
					                        enabled: !widget.ticket!.factorTrail.contains(x.id),
 | 
				
			||||||
                        value: _factorPicked == x.id,
 | 
					                        value: _factorPicked == x.id,
 | 
				
			||||||
                        onChanged: (value) {
 | 
					                        onChanged: (value) {
 | 
				
			||||||
@@ -401,11 +407,14 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final sn = context.read<SnNetworkProvider>();
 | 
					      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: {
 | 
					      await sn.client.post('/cgi/id/users/me/password-reset', data: {
 | 
				
			||||||
        'user_id': lookupResp.data['id'],
 | 
					        'user_id': lookupResp.data['id'],
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      if (mounted) context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
 | 
					      if (mounted) {
 | 
				
			||||||
 | 
					        context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      if (mounted) context.showErrorDialog(err);
 | 
					      if (mounted) context.showErrorDialog(err);
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
@@ -430,7 +439,8 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
 | 
				
			|||||||
      widget.onTicket(result.ticket);
 | 
					      widget.onTicket(result.ticket);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Pull factors
 | 
					      // 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(),
 | 
					        'ticketId': result.ticket!.id.toString(),
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      widget.onFactor(
 | 
					      widget.onFactor(
 | 
				
			||||||
@@ -441,7 +451,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      widget.onNext();
 | 
					      widget.onNext();
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      if(mounted) context.showErrorDialog(err);
 | 
					      if (mounted) context.showErrorDialog(err);
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      setState(() => _isBusy = false);
 | 
					      setState(() => _isBusy = false);
 | 
				
			||||||
@@ -524,7 +534,10 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
 | 
				
			|||||||
                    'termAcceptNextWithAgree'.tr(),
 | 
					                    'termAcceptNextWithAgree'.tr(),
 | 
				
			||||||
                    textAlign: TextAlign.end,
 | 
					                    textAlign: TextAlign.end,
 | 
				
			||||||
                    style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
					                    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(
 | 
					                  Material(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
 | 
				
			|||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RegisterScreen extends StatefulWidget {
 | 
					class RegisterScreen extends StatefulWidget {
 | 
				
			||||||
@@ -54,175 +55,178 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return StyledWidget(Container(
 | 
					    return AppScaffold(
 | 
				
			||||||
      constraints: const BoxConstraints(maxWidth: 380),
 | 
					      appBar: AppBar(
 | 
				
			||||||
      child: SingleChildScrollView(
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
        child: Column(
 | 
					        title: Text('screenAuthRegister').tr(),
 | 
				
			||||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
					      ),
 | 
				
			||||||
          children: [
 | 
					      body: StyledWidget(Container(
 | 
				
			||||||
            Align(
 | 
					        constraints: const BoxConstraints(maxWidth: 380),
 | 
				
			||||||
              alignment: Alignment.centerLeft,
 | 
					        child: SingleChildScrollView(
 | 
				
			||||||
              child: CircleAvatar(
 | 
					          child: Column(
 | 
				
			||||||
                radius: 26,
 | 
					            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                child: const Icon(
 | 
					            children: [
 | 
				
			||||||
                  Symbols.person_add,
 | 
					              Align(
 | 
				
			||||||
                  size: 28,
 | 
					                alignment: Alignment.centerLeft,
 | 
				
			||||||
                ),
 | 
					                child: CircleAvatar(
 | 
				
			||||||
              ).padding(bottom: 8),
 | 
					                  radius: 26,
 | 
				
			||||||
            ),
 | 
					                  child: const Icon(
 | 
				
			||||||
            Text(
 | 
					                    Symbols.person_add,
 | 
				
			||||||
              'screenAuthRegister',
 | 
					                    size: 28,
 | 
				
			||||||
              style: const TextStyle(
 | 
					                  ),
 | 
				
			||||||
                fontSize: 28,
 | 
					                ).padding(bottom: 8),
 | 
				
			||||||
                fontWeight: FontWeight.w900,
 | 
					 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ).tr().padding(left: 4, bottom: 16),
 | 
					              Text(
 | 
				
			||||||
            Form(
 | 
					                'screenAuthRegister',
 | 
				
			||||||
              key: _formKey,
 | 
					                style: const TextStyle(
 | 
				
			||||||
              autovalidateMode: AutovalidateMode.onUserInteraction,
 | 
					                  fontSize: 28,
 | 
				
			||||||
              child: Column(
 | 
					                  fontWeight: FontWeight.w900,
 | 
				
			||||||
                children: [
 | 
					                ),
 | 
				
			||||||
                  TextFormField(
 | 
					              ).tr().padding(left: 4, bottom: 16),
 | 
				
			||||||
                    validator: (value) {
 | 
					              Form(
 | 
				
			||||||
                      if (value == null || value.length < 4 || value.length > 32) {
 | 
					                key: _formKey,
 | 
				
			||||||
                        return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
 | 
					                autovalidateMode: AutovalidateMode.onUserInteraction,
 | 
				
			||||||
                      }
 | 
					                child: Column(
 | 
				
			||||||
                      if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
 | 
					                  children: [
 | 
				
			||||||
                        return 'fieldUsernameAlphanumOnly'.tr();
 | 
					                    TextFormField(
 | 
				
			||||||
                      }
 | 
					                      validator: (value) {
 | 
				
			||||||
                      return null;
 | 
					                        if (value == null || value.length < 4 || value.length > 32) {
 | 
				
			||||||
                    },
 | 
					                          return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
 | 
				
			||||||
                    autocorrect: false,
 | 
					                        }
 | 
				
			||||||
                    enableSuggestions: false,
 | 
					                        if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
 | 
				
			||||||
                    controller: _usernameController,
 | 
					                          return 'fieldUsernameAlphanumOnly'.tr();
 | 
				
			||||||
                    autofillHints: const [AutofillHints.username],
 | 
					                        }
 | 
				
			||||||
                    decoration: InputDecoration(
 | 
					                        return null;
 | 
				
			||||||
                      isDense: true,
 | 
					                      },
 | 
				
			||||||
                      border: const UnderlineInputBorder(),
 | 
					                      autocorrect: false,
 | 
				
			||||||
                      labelText: 'fieldUsername'.tr(),
 | 
					                      enableSuggestions: false,
 | 
				
			||||||
                    ),
 | 
					                      controller: _usernameController,
 | 
				
			||||||
                    onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					                      autofillHints: const [AutofillHints.username],
 | 
				
			||||||
                  ),
 | 
					                      decoration: InputDecoration(
 | 
				
			||||||
                  const Gap(12),
 | 
					                        isDense: true,
 | 
				
			||||||
                  TextFormField(
 | 
					                        border: const UnderlineInputBorder(),
 | 
				
			||||||
                    validator: (value) {
 | 
					                        labelText: 'fieldUsername'.tr(),
 | 
				
			||||||
                      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(
 | 
					                      onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                        color: Colors.transparent,
 | 
					                    ),
 | 
				
			||||||
                        child: InkWell(
 | 
					                    const Gap(12),
 | 
				
			||||||
                          child: Row(
 | 
					                    TextFormField(
 | 
				
			||||||
                            mainAxisSize: MainAxisSize.min,
 | 
					                      validator: (value) {
 | 
				
			||||||
                            children: [
 | 
					                        if (value == null || value.length < 4 || value.length > 32) {
 | 
				
			||||||
                              Text('termAcceptLink'.tr()),
 | 
					                          return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
 | 
				
			||||||
                              const Gap(4),
 | 
					                        }
 | 
				
			||||||
                              const Icon(Symbols.launch, size: 14),
 | 
					                        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/app_bar_leading.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.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/unauthorized_hint.dart';
 | 
				
			||||||
import 'package:uuid/uuid.dart';
 | 
					import 'package:uuid/uuid.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -120,7 +121,7 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
    final ua = context.read<UserProvider>();
 | 
					    final ua = context.read<UserProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!ua.isAuthorized) {
 | 
					    if (!ua.isAuthorized) {
 | 
				
			||||||
      return Scaffold(
 | 
					      return AppScaffold(
 | 
				
			||||||
        appBar: AppBar(
 | 
					        appBar: AppBar(
 | 
				
			||||||
          leading: AutoAppBarLeading(),
 | 
					          leading: AutoAppBarLeading(),
 | 
				
			||||||
          title: Text('screenChat').tr(),
 | 
					          title: Text('screenChat').tr(),
 | 
				
			||||||
@@ -131,7 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        leading: AutoAppBarLeading(),
 | 
					        leading: AutoAppBarLeading(),
 | 
				
			||||||
        title: Text('screenChat').tr(),
 | 
					        title: Text('screenChat').tr(),
 | 
				
			||||||
@@ -195,22 +196,58 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          LoadingIndicator(isActive: _isBusy),
 | 
					          LoadingIndicator(isActive: _isBusy),
 | 
				
			||||||
          Expanded(
 | 
					          Expanded(
 | 
				
			||||||
            child: RefreshIndicator(
 | 
					            child: MediaQuery.removePadding(
 | 
				
			||||||
              onRefresh: () => Future.sync(() => _refreshChannels()),
 | 
					              context: context,
 | 
				
			||||||
              child: ListView.builder(
 | 
					              removeTop: true,
 | 
				
			||||||
                itemCount: _channels?.length ?? 0,
 | 
					              child: RefreshIndicator(
 | 
				
			||||||
                itemBuilder: (context, idx) {
 | 
					                onRefresh: () => Future.sync(() => _refreshChannels()),
 | 
				
			||||||
                  final channel = _channels![idx];
 | 
					                child: ListView.builder(
 | 
				
			||||||
                  final lastMessage = _lastMessages?[channel.id];
 | 
					                  itemCount: _channels?.length ?? 0,
 | 
				
			||||||
 | 
					                  itemBuilder: (context, idx) {
 | 
				
			||||||
 | 
					                    final channel = _channels![idx];
 | 
				
			||||||
 | 
					                    final lastMessage = _lastMessages?[channel.id];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  if (channel.type == 1) {
 | 
					                    if (channel.type == 1) {
 | 
				
			||||||
                    final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
 | 
					                      final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
 | 
				
			||||||
                          (ele) => ele?.accountId != ua.user?.id,
 | 
					                            (ele) => ele?.accountId != ua.user?.id,
 | 
				
			||||||
                          orElse: () => null,
 | 
					                            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(
 | 
					                    return ListTile(
 | 
				
			||||||
                      title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
 | 
					                      title: Text(channel.name),
 | 
				
			||||||
                      subtitle: lastMessage != null
 | 
					                      subtitle: lastMessage != null
 | 
				
			||||||
                          ? Text(
 | 
					                          ? Text(
 | 
				
			||||||
                              '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
 | 
					                              '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
 | 
				
			||||||
@@ -218,15 +255,14 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
                              overflow: TextOverflow.ellipsis,
 | 
					                              overflow: TextOverflow.ellipsis,
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
                          : Text(
 | 
					                          : Text(
 | 
				
			||||||
                              'channelDirectMessageDescription'.tr(args: [
 | 
					                              channel.description,
 | 
				
			||||||
                                '@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
 | 
					 | 
				
			||||||
                              ]),
 | 
					 | 
				
			||||||
                              maxLines: 1,
 | 
					                              maxLines: 1,
 | 
				
			||||||
                              overflow: TextOverflow.ellipsis,
 | 
					                              overflow: TextOverflow.ellipsis,
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                      contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
					                      contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
				
			||||||
                      leading: AccountImage(
 | 
					                      leading: AccountImage(
 | 
				
			||||||
                        content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
 | 
					                        content: null,
 | 
				
			||||||
 | 
					                        fallbackWidget: const Icon(Symbols.chat, size: 20),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                      onTap: () {
 | 
					                      onTap: () {
 | 
				
			||||||
                        GoRouter.of(context).pushNamed(
 | 
					                        GoRouter.of(context).pushNamed(
 | 
				
			||||||
@@ -236,43 +272,12 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
				
			|||||||
                            'alias': channel.alias,
 | 
					                            'alias': channel.alias,
 | 
				
			||||||
                          },
 | 
					                          },
 | 
				
			||||||
                        ).then((value) {
 | 
					                        ).then((value) {
 | 
				
			||||||
                          if (mounted) _refreshChannels();
 | 
					                          if (value == true) _refreshChannels();
 | 
				
			||||||
                        });
 | 
					                        });
 | 
				
			||||||
                      },
 | 
					                      },
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
                  }
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
                  return ListTile(
 | 
					 | 
				
			||||||
                    title: Text(channel.name),
 | 
					 | 
				
			||||||
                    subtitle: lastMessage != null
 | 
					 | 
				
			||||||
                        ? Text(
 | 
					 | 
				
			||||||
                            '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
 | 
					 | 
				
			||||||
                            maxLines: 1,
 | 
					 | 
				
			||||||
                            overflow: TextOverflow.ellipsis,
 | 
					 | 
				
			||||||
                          )
 | 
					 | 
				
			||||||
                        : Text(
 | 
					 | 
				
			||||||
                            channel.description,
 | 
					 | 
				
			||||||
                            maxLines: 1,
 | 
					 | 
				
			||||||
                            overflow: TextOverflow.ellipsis,
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                    contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
					 | 
				
			||||||
                    leading: AccountImage(
 | 
					 | 
				
			||||||
                      content: null,
 | 
					 | 
				
			||||||
                      fallbackWidget: const Icon(Symbols.chat, size: 20),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                    onTap: () {
 | 
					 | 
				
			||||||
                      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/providers/chat_call.dart';
 | 
				
			||||||
import 'package:surface/widgets/chat/call/call_controls.dart';
 | 
					import 'package:surface/widgets/chat/call/call_controls.dart';
 | 
				
			||||||
import 'package:surface/widgets/chat/call/call_participant.dart';
 | 
					import 'package:surface/widgets/chat/call/call_participant.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CallRoomScreen extends StatefulWidget {
 | 
					class CallRoomScreen extends StatefulWidget {
 | 
				
			||||||
  final String scope;
 | 
					  final String scope;
 | 
				
			||||||
  final String alias;
 | 
					  final String alias;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const CallRoomScreen({super.key, required this.scope, required this.alias});
 | 
					  const CallRoomScreen({super.key, required this.scope, required this.alias});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -35,8 +37,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
    return Stack(
 | 
					    return Stack(
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
        Container(
 | 
					        Container(
 | 
				
			||||||
          color:
 | 
					          color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
 | 
				
			||||||
              Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
 | 
					 | 
				
			||||||
          child: call.focusTrack != null
 | 
					          child: call.focusTrack != null
 | 
				
			||||||
              ? InteractiveParticipantWidget(
 | 
					              ? InteractiveParticipantWidget(
 | 
				
			||||||
                  isFixedAvatar: false,
 | 
					                  isFixedAvatar: false,
 | 
				
			||||||
@@ -71,8 +72,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
                      color: Theme.of(context).cardColor,
 | 
					                      color: Theme.of(context).cardColor,
 | 
				
			||||||
                      participant: track,
 | 
					                      participant: track,
 | 
				
			||||||
                      onTap: () {
 | 
					                      onTap: () {
 | 
				
			||||||
                        if (track.participant.sid !=
 | 
					                        if (track.participant.sid != call.focusTrack?.participant.sid) {
 | 
				
			||||||
                            call.focusTrack?.participant.sid) {
 | 
					 | 
				
			||||||
                          call.setFocusTrack(track);
 | 
					                          call.setFocusTrack(track);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                      },
 | 
					                      },
 | 
				
			||||||
@@ -114,14 +114,10 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
            child: ClipRRect(
 | 
					            child: ClipRRect(
 | 
				
			||||||
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
              child: InteractiveParticipantWidget(
 | 
					              child: InteractiveParticipantWidget(
 | 
				
			||||||
                color: Theme.of(context)
 | 
					                color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
 | 
				
			||||||
                    .colorScheme
 | 
					 | 
				
			||||||
                    .surfaceContainerHigh
 | 
					 | 
				
			||||||
                    .withOpacity(0.75),
 | 
					 | 
				
			||||||
                participant: track,
 | 
					                participant: track,
 | 
				
			||||||
                onTap: () {
 | 
					                onTap: () {
 | 
				
			||||||
                  if (track.participant.sid !=
 | 
					                  if (track.participant.sid != call.focusTrack?.participant.sid) {
 | 
				
			||||||
                      call.focusTrack?.participant.sid) {
 | 
					 | 
				
			||||||
                    call.setFocusTrack(track);
 | 
					                    call.setFocusTrack(track);
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
@@ -152,157 +148,134 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
    return ListenableBuilder(
 | 
					    return ListenableBuilder(
 | 
				
			||||||
        listenable: call,
 | 
					        listenable: call,
 | 
				
			||||||
        builder: (context, _) {
 | 
					        builder: (context, _) {
 | 
				
			||||||
          return Scaffold(
 | 
					          return AppScaffold(
 | 
				
			||||||
            appBar: AppBar(
 | 
					            appBar: AppBar(
 | 
				
			||||||
              title: RichText(
 | 
					              title: RichText(
 | 
				
			||||||
                textAlign: TextAlign.center,
 | 
					                textAlign: TextAlign.center,
 | 
				
			||||||
                text: TextSpan(children: [
 | 
					                text: TextSpan(children: [
 | 
				
			||||||
                  TextSpan(
 | 
					                  TextSpan(
 | 
				
			||||||
                    text: 'call'.tr(),
 | 
					                    text: 'call'.tr(),
 | 
				
			||||||
                    style: Theme.of(context)
 | 
					                    style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
 | 
				
			||||||
                        .textTheme
 | 
					 | 
				
			||||||
                        .titleLarge!
 | 
					 | 
				
			||||||
                        .copyWith(color: Colors.white),
 | 
					 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  const TextSpan(text: '\n'),
 | 
					                  const TextSpan(text: '\n'),
 | 
				
			||||||
                  TextSpan(
 | 
					                  TextSpan(
 | 
				
			||||||
                    text: call.lastDuration.toString(),
 | 
					                    text: call.lastDuration.toString(),
 | 
				
			||||||
                    style: Theme.of(context)
 | 
					                    style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
 | 
				
			||||||
                        .textTheme
 | 
					 | 
				
			||||||
                        .bodySmall!
 | 
					 | 
				
			||||||
                        .copyWith(color: Colors.white),
 | 
					 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ]),
 | 
					                ]),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            body: SafeArea(
 | 
					            body: GestureDetector(
 | 
				
			||||||
              child: GestureDetector(
 | 
					              behavior: HitTestBehavior.translucent,
 | 
				
			||||||
                behavior: HitTestBehavior.translucent,
 | 
					              child: Column(
 | 
				
			||||||
                child: Column(
 | 
					                children: [
 | 
				
			||||||
                  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(
 | 
					                    SizedBox(
 | 
				
			||||||
                      width: MediaQuery.of(context).size.width,
 | 
					                      width: MediaQuery.of(context).size.width,
 | 
				
			||||||
                      height: 64,
 | 
					                      child: ControlsWidget(
 | 
				
			||||||
                      child: Row(
 | 
					                        call.room,
 | 
				
			||||||
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
					                        call.room.localParticipant!,
 | 
				
			||||||
                        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,
 | 
					 | 
				
			||||||
                        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/account/account_image.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.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 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ChannelDetailScreen extends StatefulWidget {
 | 
					class ChannelDetailScreen extends StatefulWidget {
 | 
				
			||||||
@@ -189,7 +190,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
 | 
					    final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
 | 
					        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/account/account_image.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:uuid/uuid.dart';
 | 
					import 'package:uuid/uuid.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ChatManageScreen extends StatefulWidget {
 | 
					class ChatManageScreen extends StatefulWidget {
 | 
				
			||||||
@@ -121,7 +122,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: widget.editingChannelAlias != null
 | 
					        title: widget.editingChannelAlias != null
 | 
				
			||||||
            ? Text('screenChatManage').tr()
 | 
					            ? 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/chat/chat_typing_indicator.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.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 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '../../providers/user_directory.dart';
 | 
					import '../../providers/user_directory.dart';
 | 
				
			||||||
@@ -211,7 +212,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
				
			|||||||
    final call = context.watch<ChatCallProvider>();
 | 
					    final call = context.watch<ChatCallProvider>();
 | 
				
			||||||
    final ud = context.read<UserDirectoryProvider>();
 | 
					    final ud = context.read<UserDirectoryProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: Text(
 | 
					        title: Text(
 | 
				
			||||||
          _channel?.type == 1
 | 
					          _channel?.type == 1
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import 'package:animations/animations.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.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:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/config.dart';
 | 
				
			||||||
import 'package:surface/providers/post.dart';
 | 
					import 'package:surface/providers/post.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.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/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
					import 'package:surface/widgets/app_bar_leading.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.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_item.dart';
 | 
				
			||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
					import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -93,7 +97,9 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    final cfg = context.read<ConfigProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return AppScaffold(
 | 
				
			||||||
      floatingActionButtonLocation: ExpandableFab.location,
 | 
					      floatingActionButtonLocation: ExpandableFab.location,
 | 
				
			||||||
      floatingActionButton: ExpandableFab(
 | 
					      floatingActionButton: ExpandableFab(
 | 
				
			||||||
        key: _fabKey,
 | 
					        key: _fabKey,
 | 
				
			||||||
@@ -210,6 +216,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					            const SliverGap(12),
 | 
				
			||||||
            SliverInfiniteList(
 | 
					            SliverInfiniteList(
 | 
				
			||||||
              itemCount: _posts.length,
 | 
					              itemCount: _posts.length,
 | 
				
			||||||
              isLoading: _isBusy,
 | 
					              isLoading: _isBusy,
 | 
				
			||||||
@@ -217,27 +224,39 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
              hasReachedMax: _postCount != null && _posts.length >= _postCount!,
 | 
					              hasReachedMax: _postCount != null && _posts.length >= _postCount!,
 | 
				
			||||||
              onFetchData: _fetchPosts,
 | 
					              onFetchData: _fetchPosts,
 | 
				
			||||||
              itemBuilder: (context, idx) {
 | 
					              itemBuilder: (context, idx) {
 | 
				
			||||||
                return GestureDetector(
 | 
					                return Center(
 | 
				
			||||||
                  child: PostItem(
 | 
					                  child: OpenContainer(
 | 
				
			||||||
                    data: _posts[idx],
 | 
					                    closedBuilder: (_, __) => Container(
 | 
				
			||||||
                    maxWidth: 640,
 | 
					                      constraints: const BoxConstraints(maxWidth: 640),
 | 
				
			||||||
                    onChanged: (data) {
 | 
					                      child: PostItem(
 | 
				
			||||||
                      setState(() => _posts[idx] = data);
 | 
					                        data: _posts[idx],
 | 
				
			||||||
                    },
 | 
					                        maxWidth: 640,
 | 
				
			||||||
                    onDeleted: () {
 | 
					                        onChanged: (data) {
 | 
				
			||||||
                      _refreshPosts();
 | 
					                          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/app_bar_leading.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '../providers/userinfo.dart';
 | 
					import '../providers/userinfo.dart';
 | 
				
			||||||
import '../widgets/unauthorized_hint.dart';
 | 
					import '../widgets/unauthorized_hint.dart';
 | 
				
			||||||
@@ -180,7 +181,7 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
				
			|||||||
    final ua = context.read<UserProvider>();
 | 
					    final ua = context.read<UserProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!ua.isAuthorized) {
 | 
					    if (!ua.isAuthorized) {
 | 
				
			||||||
      return Scaffold(
 | 
					      return AppScaffold(
 | 
				
			||||||
        appBar: AppBar(
 | 
					        appBar: AppBar(
 | 
				
			||||||
          leading: AutoAppBarLeading(),
 | 
					          leading: AutoAppBarLeading(),
 | 
				
			||||||
          title: Text('screenFriend').tr(),
 | 
					          title: Text('screenFriend').tr(),
 | 
				
			||||||
@@ -191,7 +192,7 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        leading: AutoAppBarLeading(),
 | 
					        leading: AutoAppBarLeading(),
 | 
				
			||||||
        title: Text('screenFriend').tr(),
 | 
					        title: Text('screenFriend').tr(),
 | 
				
			||||||
@@ -233,52 +234,56 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
				
			|||||||
          if (_requests.isNotEmpty || _blocks.isNotEmpty)
 | 
					          if (_requests.isNotEmpty || _blocks.isNotEmpty)
 | 
				
			||||||
            const Divider(height: 1),
 | 
					            const Divider(height: 1),
 | 
				
			||||||
          Expanded(
 | 
					          Expanded(
 | 
				
			||||||
            child: RefreshIndicator(
 | 
					            child: MediaQuery.removePadding(
 | 
				
			||||||
              onRefresh: () => Future.wait([
 | 
					              context: context,
 | 
				
			||||||
                _fetchRelations(),
 | 
					              removeTop: true,
 | 
				
			||||||
                _fetchRequests(),
 | 
					              child: RefreshIndicator(
 | 
				
			||||||
              ]),
 | 
					                onRefresh: () => Future.wait([
 | 
				
			||||||
              child: ListView.builder(
 | 
					                  _fetchRelations(),
 | 
				
			||||||
                itemCount: _relations.length,
 | 
					                  _fetchRequests(),
 | 
				
			||||||
                itemBuilder: (context, index) {
 | 
					                ]),
 | 
				
			||||||
                  final relation = _relations[index];
 | 
					                child: ListView.builder(
 | 
				
			||||||
                  final other = relation.related;
 | 
					                  itemCount: _relations.length,
 | 
				
			||||||
                  return ListTile(
 | 
					                  itemBuilder: (context, index) {
 | 
				
			||||||
                    contentPadding: const EdgeInsets.only(right: 24, left: 16),
 | 
					                    final relation = _relations[index];
 | 
				
			||||||
                    leading: AccountImage(content: other?.avatar),
 | 
					                    final other = relation.related;
 | 
				
			||||||
                    title: Text(other?.nick ?? 'unknown'),
 | 
					                    return ListTile(
 | 
				
			||||||
                    subtitle: Text(other?.nick ?? 'unknown'),
 | 
					                      contentPadding: const EdgeInsets.only(right: 24, left: 16),
 | 
				
			||||||
                    trailing: SizedBox(
 | 
					                      leading: AccountImage(content: other?.avatar),
 | 
				
			||||||
                      height: 48,
 | 
					                      title: Text(other?.nick ?? 'unknown'),
 | 
				
			||||||
                      width: 120,
 | 
					                      subtitle: Text(other?.nick ?? 'unknown'),
 | 
				
			||||||
                      child: Column(
 | 
					                      trailing: SizedBox(
 | 
				
			||||||
                        mainAxisSize: MainAxisSize.min,
 | 
					                        height: 48,
 | 
				
			||||||
                        mainAxisAlignment: MainAxisAlignment.center,
 | 
					                        width: 120,
 | 
				
			||||||
                        crossAxisAlignment: CrossAxisAlignment.end,
 | 
					                        child: Column(
 | 
				
			||||||
                        children: [
 | 
					                          mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
                          Row(
 | 
					                          mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
                            mainAxisAlignment: MainAxisAlignment.end,
 | 
					                          crossAxisAlignment: CrossAxisAlignment.end,
 | 
				
			||||||
                            children: [
 | 
					                          children: [
 | 
				
			||||||
                              InkWell(
 | 
					                            Row(
 | 
				
			||||||
                                onTap: _isUpdating
 | 
					                              mainAxisAlignment: MainAxisAlignment.end,
 | 
				
			||||||
                                    ? null
 | 
					                              children: [
 | 
				
			||||||
                                    : () => _changeRelation(relation, 2),
 | 
					                                InkWell(
 | 
				
			||||||
                                child: Text('friendBlock').tr(),
 | 
					                                  onTap: _isUpdating
 | 
				
			||||||
                              ),
 | 
					                                      ? null
 | 
				
			||||||
                              const Gap(8),
 | 
					                                      : () => _changeRelation(relation, 2),
 | 
				
			||||||
                              InkWell(
 | 
					                                  child: Text('friendBlock').tr(),
 | 
				
			||||||
                                onTap: _isUpdating
 | 
					                                ),
 | 
				
			||||||
                                    ? null
 | 
					                                const Gap(8),
 | 
				
			||||||
                                    : () => _deleteRelation(relation),
 | 
					                                InkWell(
 | 
				
			||||||
                                child: Text('friendDeleteAction').tr(),
 | 
					                                  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:gap/gap.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
import 'package:google_fonts/google_fonts.dart';
 | 
					import 'package:google_fonts/google_fonts.dart';
 | 
				
			||||||
 | 
					import 'package:html/parser.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:relative_time/relative_time.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/userinfo.dart';
 | 
				
			||||||
import 'package:surface/providers/widget.dart';
 | 
					import 'package:surface/providers/widget.dart';
 | 
				
			||||||
import 'package:surface/types/check_in.dart';
 | 
					import 'package:surface/types/check_in.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/news.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
					import 'package:surface/widgets/app_bar_leading.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.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_item.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class HomeScreenDashEntry {
 | 
					class HomeScreenDashEntry {
 | 
				
			||||||
@@ -48,12 +51,12 @@ class HomeScreen extends StatefulWidget {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _HomeScreenState extends State<HomeScreen> {
 | 
					class _HomeScreenState extends State<HomeScreen> {
 | 
				
			||||||
  static const List<HomeScreenDashEntry> kCards = [
 | 
					  late final List<HomeScreenDashEntry> kCards = [
 | 
				
			||||||
    HomeScreenDashEntry(
 | 
					    HomeScreenDashEntry(
 | 
				
			||||||
      name: 'dashEntryRecommendation',
 | 
					      name: 'dashEntryRecommendation',
 | 
				
			||||||
      cols: 2,
 | 
					 | 
				
			||||||
      rows: 2,
 | 
					 | 
				
			||||||
      child: _HomeDashRecommendationPostWidget(),
 | 
					      child: _HomeDashRecommendationPostWidget(),
 | 
				
			||||||
 | 
					      rows: 2,
 | 
				
			||||||
 | 
					      cols: 2,
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    HomeScreenDashEntry(
 | 
					    HomeScreenDashEntry(
 | 
				
			||||||
      name: 'dashEntryCheckIn',
 | 
					      name: 'dashEntryCheckIn',
 | 
				
			||||||
@@ -63,11 +66,16 @@ class _HomeScreenState extends State<HomeScreen> {
 | 
				
			|||||||
      name: 'dashEntryNotification',
 | 
					      name: 'dashEntryNotification',
 | 
				
			||||||
      child: _HomeDashNotificationWidget(),
 | 
					      child: _HomeDashNotificationWidget(),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    HomeScreenDashEntry(
 | 
				
			||||||
 | 
					      name: 'dashEntryTodayNews',
 | 
				
			||||||
 | 
					      child: _HomeDashTodayNews(),
 | 
				
			||||||
 | 
					      cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        leading: AutoAppBarLeading(),
 | 
					        leading: AutoAppBarLeading(),
 | 
				
			||||||
        title: Text("screenHome").tr(),
 | 
					        title: Text("screenHome").tr(),
 | 
				
			||||||
@@ -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 {
 | 
					class _HomeDashCheckInWidget extends StatefulWidget {
 | 
				
			||||||
  const _HomeDashCheckInWidget();
 | 
					  const _HomeDashCheckInWidget();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -387,6 +495,8 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
 | 
				
			|||||||
                        Text(
 | 
					                        Text(
 | 
				
			||||||
                          'dailyCheckInNone',
 | 
					                          'dailyCheckInNone',
 | 
				
			||||||
                          style: Theme.of(context).textTheme.bodyLarge,
 | 
					                          style: Theme.of(context).textTheme.bodyLarge,
 | 
				
			||||||
 | 
					                          maxLines: 2,
 | 
				
			||||||
 | 
					                          overflow: TextOverflow.ellipsis,
 | 
				
			||||||
                        ).tr(),
 | 
					                        ).tr(),
 | 
				
			||||||
                      ],
 | 
					                      ],
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
@@ -404,6 +514,11 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
 | 
				
			|||||||
                          '+${_todayRecord!.resultExperience} EXP',
 | 
					                          '+${_todayRecord!.resultExperience} EXP',
 | 
				
			||||||
                          style: Theme.of(context).textTheme.bodyLarge,
 | 
					                          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/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
import 'package:surface/widgets/markdown_content.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:surface/widgets/post/post_item.dart';
 | 
				
			||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
					import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '../providers/userinfo.dart';
 | 
					import '../providers/userinfo.dart';
 | 
				
			||||||
import '../widgets/unauthorized_hint.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 {
 | 
					class NotificationScreen extends StatefulWidget {
 | 
				
			||||||
  const NotificationScreen({super.key});
 | 
					  const NotificationScreen({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -35,13 +46,6 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
				
			|||||||
  final List<SnNotification> _notifications = List.empty(growable: true);
 | 
					  final List<SnNotification> _notifications = List.empty(growable: true);
 | 
				
			||||||
  int? _totalCount;
 | 
					  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 {
 | 
					  Future<void> _fetchNotifications() async {
 | 
				
			||||||
    final ua = context.read<UserProvider>();
 | 
					    final ua = context.read<UserProvider>();
 | 
				
			||||||
    if (!ua.isAuthorized) return;
 | 
					    if (!ua.isAuthorized) return;
 | 
				
			||||||
@@ -137,7 +141,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
				
			|||||||
    final ua = context.read<UserProvider>();
 | 
					    final ua = context.read<UserProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!ua.isAuthorized) {
 | 
					    if (!ua.isAuthorized) {
 | 
				
			||||||
      return Scaffold(
 | 
					      return AppScaffold(
 | 
				
			||||||
        appBar: AppBar(
 | 
					        appBar: AppBar(
 | 
				
			||||||
          leading: AutoAppBarLeading(),
 | 
					          leading: AutoAppBarLeading(),
 | 
				
			||||||
          title: Text('screenNotification').tr(),
 | 
					          title: Text('screenNotification').tr(),
 | 
				
			||||||
@@ -148,7 +152,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        leading: AutoAppBarLeading(),
 | 
					        leading: AutoAppBarLeading(),
 | 
				
			||||||
        title: Text('screenNotification').tr(),
 | 
					        title: Text('screenNotification').tr(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,6 +13,8 @@ import 'package:surface/providers/userinfo.dart';
 | 
				
			|||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.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_comment_list.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/post_item.dart';
 | 
					import 'package:surface/widgets/post/post_item.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/post_mini_editor.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 {
 | 
					class PostDetailScreen extends StatefulWidget {
 | 
				
			||||||
  final String slug;
 | 
					  final String slug;
 | 
				
			||||||
  final SnPost? preload;
 | 
					  final SnPost? preload;
 | 
				
			||||||
 | 
					  final Function? onBack;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const PostDetailScreen({
 | 
					  const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack});
 | 
				
			||||||
    super.key,
 | 
					 | 
				
			||||||
    required this.slug,
 | 
					 | 
				
			||||||
    this.preload,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<PostDetailScreen> createState() => _PostDetailScreenState();
 | 
					  State<PostDetailScreen> createState() => _PostDetailScreenState();
 | 
				
			||||||
@@ -67,123 +66,129 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
 | 
				
			|||||||
    final ua = context.watch<UserProvider>();
 | 
					    final ua = context.watch<UserProvider>();
 | 
				
			||||||
    final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
 | 
					    final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppBackground(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      isRoot: widget.onBack != null,
 | 
				
			||||||
        leading: BackButton(
 | 
					      child: AppScaffold(
 | 
				
			||||||
          onPressed: () {
 | 
					        appBar: AppBar(
 | 
				
			||||||
            if (GoRouter.of(context).canPop()) {
 | 
					          leading: BackButton(
 | 
				
			||||||
              GoRouter.of(context).pop(context);
 | 
					            onPressed: () {
 | 
				
			||||||
              return;
 | 
					              if (widget.onBack != null) {
 | 
				
			||||||
            }
 | 
					                widget.onBack!.call();
 | 
				
			||||||
            GoRouter.of(context).replaceNamed('explore');
 | 
					              }
 | 
				
			||||||
          },
 | 
					              if (GoRouter.of(context).canPop()) {
 | 
				
			||||||
        ),
 | 
					                GoRouter.of(context).pop(context);
 | 
				
			||||||
        title: _data?.body['title'] != null
 | 
					                return;
 | 
				
			||||||
            ? RichText(
 | 
					              }
 | 
				
			||||||
                textAlign: TextAlign.center,
 | 
					              GoRouter.of(context).replaceNamed('explore');
 | 
				
			||||||
                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!,
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ]),
 | 
					 | 
				
			||||||
                maxLines: 2,
 | 
					 | 
				
			||||||
                overflow: TextOverflow.ellipsis,
 | 
					 | 
				
			||||||
              )
 | 
					 | 
				
			||||||
            : Text('postDetail').tr(),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      body: CustomScrollView(
 | 
					 | 
				
			||||||
        slivers: [
 | 
					 | 
				
			||||||
          SliverToBoxAdapter(
 | 
					 | 
				
			||||||
            child: LoadingIndicator(isActive: _isBusy),
 | 
					 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          if (_data != null)
 | 
					          title: _data?.body['title'] != null
 | 
				
			||||||
            SliverToBoxAdapter(
 | 
					              ? RichText(
 | 
				
			||||||
              child: PostItem(
 | 
					                  textAlign: TextAlign.center,
 | 
				
			||||||
                data: _data!,
 | 
					                  text: TextSpan(children: [
 | 
				
			||||||
                maxWidth: 640,
 | 
					                    TextSpan(
 | 
				
			||||||
                showComments: false,
 | 
					                      text: _data?.body['title'] ?? 'postNoun'.tr(),
 | 
				
			||||||
                showFullPost: true,
 | 
					                      style: Theme.of(context).textTheme.titleLarge!.copyWith(
 | 
				
			||||||
                onChanged: (data) {
 | 
					                            color: Theme.of(context).appBarTheme.foregroundColor!,
 | 
				
			||||||
                  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,
 | 
					 | 
				
			||||||
                          ),
 | 
					                          ),
 | 
				
			||||||
                        ),
 | 
					                    ),
 | 
				
			||||||
                ),
 | 
					                    const TextSpan(text: '\n'),
 | 
				
			||||||
                child: PostMiniEditor(
 | 
					                    TextSpan(
 | 
				
			||||||
                  postReplyId: _data!.id,
 | 
					                      text: 'postDetail'.tr(),
 | 
				
			||||||
                  onPost: () {
 | 
					                      style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
				
			||||||
                    setState(() {
 | 
					                            color: Theme.of(context).appBarTheme.foregroundColor!,
 | 
				
			||||||
                      _data = _data!.copyWith(
 | 
					                          ),
 | 
				
			||||||
                        metric: _data!.metric.copyWith(
 | 
					                    ),
 | 
				
			||||||
                          replyCount: _data!.metric.replyCount + 1,
 | 
					                  ]),
 | 
				
			||||||
                        ),
 | 
					                  maxLines: 2,
 | 
				
			||||||
                      );
 | 
					                  overflow: TextOverflow.ellipsis,
 | 
				
			||||||
                    });
 | 
					                )
 | 
				
			||||||
                    _childListKey.currentState!.refresh();
 | 
					              : 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(),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            const SliverToBoxAdapter(child: Divider(height: 1)),
 | 
				
			||||||
          if (_data != null)
 | 
					            if (_data != null)
 | 
				
			||||||
            PostCommentSliverList(
 | 
					              SliverToBoxAdapter(
 | 
				
			||||||
              key: _childListKey,
 | 
					                child: Container(
 | 
				
			||||||
              parentPostId: _data!.id,
 | 
					                  constraints: const BoxConstraints(maxWidth: 640),
 | 
				
			||||||
              maxWidth: 640,
 | 
					                  child: Row(
 | 
				
			||||||
            ),
 | 
					                    crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
          SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
 | 
					                    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/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.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_item.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/post_media_pending_list.dart';
 | 
					import 'package:surface/widgets/post/post_media_pending_list.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/post_meta_editor.dart';
 | 
					import 'package:surface/widgets/post/post_meta_editor.dart';
 | 
				
			||||||
@@ -128,7 +129,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
				
			|||||||
    return ListenableBuilder(
 | 
					    return ListenableBuilder(
 | 
				
			||||||
      listenable: _writeController,
 | 
					      listenable: _writeController,
 | 
				
			||||||
      builder: (context, _) {
 | 
					      builder: (context, _) {
 | 
				
			||||||
        return Scaffold(
 | 
					        return AppScaffold(
 | 
				
			||||||
          appBar: AppBar(
 | 
					          appBar: AppBar(
 | 
				
			||||||
            leading: BackButton(
 | 
					            leading: BackButton(
 | 
				
			||||||
              onPressed: () {
 | 
					              onPressed: () {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ import 'package:styled_widget/styled_widget.dart';
 | 
				
			|||||||
import 'package:surface/providers/post.dart';
 | 
					import 'package:surface/providers/post.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.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_item.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/post_tags_field.dart';
 | 
					import 'package:surface/widgets/post/post_tags_field.dart';
 | 
				
			||||||
import 'package:very_good_infinite_list/very_good_infinite_list.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(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: Text('screenPostSearch').tr(),
 | 
					        title: Text('screenPostSearch').tr(),
 | 
				
			||||||
        actions: [
 | 
					        actions: [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,7 @@ import 'package:surface/types/post.dart';
 | 
				
			|||||||
import 'package:surface/types/realm.dart';
 | 
					import 'package:surface/types/realm.dart';
 | 
				
			||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.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_item.dart';
 | 
				
			||||||
import 'package:surface/widgets/universal_image.dart';
 | 
					import 'package:surface/widgets/universal_image.dart';
 | 
				
			||||||
import 'package:very_good_infinite_list/very_good_infinite_list.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>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      body: NestedScrollView(
 | 
					      body: NestedScrollView(
 | 
				
			||||||
        controller: _scrollController,
 | 
					        controller: _scrollController,
 | 
				
			||||||
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
 | 
					        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/app_bar_leading.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.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/unauthorized_hint.dart';
 | 
				
			||||||
import 'package:surface/widgets/universal_image.dart';
 | 
					import 'package:surface/widgets/universal_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -83,7 +84,7 @@ class _RealmScreenState extends State<RealmScreen> {
 | 
				
			|||||||
    final ua = context.read<UserProvider>();
 | 
					    final ua = context.read<UserProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!ua.isAuthorized) {
 | 
					    if (!ua.isAuthorized) {
 | 
				
			||||||
      return Scaffold(
 | 
					      return AppScaffold(
 | 
				
			||||||
        appBar: AppBar(
 | 
					        appBar: AppBar(
 | 
				
			||||||
          leading: AutoAppBarLeading(),
 | 
					          leading: AutoAppBarLeading(),
 | 
				
			||||||
          title: Text('screenRealm').tr(),
 | 
					          title: Text('screenRealm').tr(),
 | 
				
			||||||
@@ -94,7 +95,7 @@ class _RealmScreenState extends State<RealmScreen> {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        leading: AutoAppBarLeading(),
 | 
					        leading: AutoAppBarLeading(),
 | 
				
			||||||
        title: Text('screenRealm').tr(),
 | 
					        title: Text('screenRealm').tr(),
 | 
				
			||||||
@@ -118,113 +119,61 @@ class _RealmScreenState extends State<RealmScreen> {
 | 
				
			|||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          LoadingIndicator(isActive: _isBusy),
 | 
					          LoadingIndicator(isActive: _isBusy),
 | 
				
			||||||
          Expanded(
 | 
					          Expanded(
 | 
				
			||||||
            child: RefreshIndicator(
 | 
					            child: MediaQuery.removePadding(
 | 
				
			||||||
              onRefresh: _fetchRealms,
 | 
					              context: context,
 | 
				
			||||||
              child: ListView.builder(
 | 
					              removeTop: true,
 | 
				
			||||||
                itemCount: _realms?.length ?? 0,
 | 
					              child: RefreshIndicator(
 | 
				
			||||||
                itemBuilder: (context, idx) {
 | 
					                onRefresh: _fetchRealms,
 | 
				
			||||||
                  final realm = _realms![idx];
 | 
					                child: ListView.builder(
 | 
				
			||||||
                  if (_isCompactView) {
 | 
					                  itemCount: _realms?.length ?? 0,
 | 
				
			||||||
                    return ListTile(
 | 
					                  itemBuilder: (context, idx) {
 | 
				
			||||||
                      contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
					                    final realm = _realms![idx];
 | 
				
			||||||
                      leading: AccountImage(
 | 
					                    if (_isCompactView) {
 | 
				
			||||||
                        content: realm.avatar,
 | 
					                      return ListTile(
 | 
				
			||||||
                        fallbackWidget: const Icon(Symbols.group, size: 20),
 | 
					                        contentPadding: const EdgeInsets.symmetric(horizontal: 16),
 | 
				
			||||||
                      ),
 | 
					                        leading: AccountImage(
 | 
				
			||||||
                      title: Text(realm.name),
 | 
					                          content: realm.avatar,
 | 
				
			||||||
                      subtitle: Text(
 | 
					                          fallbackWidget: const Icon(Symbols.group, size: 20),
 | 
				
			||||||
                        realm.description,
 | 
					                        ),
 | 
				
			||||||
                        maxLines: 1,
 | 
					                        title: Text(realm.name),
 | 
				
			||||||
                        overflow: TextOverflow.ellipsis,
 | 
					                        subtitle: Text(
 | 
				
			||||||
                      ),
 | 
					                          realm.description,
 | 
				
			||||||
                      trailing: PopupMenuButton(
 | 
					                          maxLines: 1,
 | 
				
			||||||
                        itemBuilder: (BuildContext context) => [
 | 
					                          overflow: TextOverflow.ellipsis,
 | 
				
			||||||
                          PopupMenuItem(
 | 
					                        ),
 | 
				
			||||||
                            child: Row(
 | 
					                        trailing: PopupMenuButton(
 | 
				
			||||||
                              children: [
 | 
					                          itemBuilder: (BuildContext context) => [
 | 
				
			||||||
                                const Icon(Symbols.edit),
 | 
					                            PopupMenuItem(
 | 
				
			||||||
                                const Gap(16),
 | 
					                              child: Row(
 | 
				
			||||||
                                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,
 | 
					 | 
				
			||||||
                                children: [
 | 
					                                children: [
 | 
				
			||||||
                                  Container(
 | 
					                                  const Icon(Symbols.edit),
 | 
				
			||||||
                                    color: Theme.of(context).colorScheme.surfaceContainer,
 | 
					                                  const Gap(16),
 | 
				
			||||||
                                    child: (realm.banner?.isEmpty ?? true)
 | 
					                                  Text('edit').tr(),
 | 
				
			||||||
                                        ? 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),
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                  ),
 | 
					 | 
				
			||||||
                                ],
 | 
					                                ],
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
 | 
					                              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: () {
 | 
					                        onTap: () {
 | 
				
			||||||
@@ -233,10 +182,69 @@ class _RealmScreenState extends State<RealmScreen> {
 | 
				
			|||||||
                            pathParameters: {'alias': realm.alias},
 | 
					                            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/account/account_image.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.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:surface/widgets/universal_image.dart';
 | 
				
			||||||
import 'package:uuid/uuid.dart';
 | 
					import 'package:uuid/uuid.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -179,7 +180,7 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final sn = context.read<SnNetworkProvider>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: widget.editingRealmAlias != null
 | 
					        title: widget.editingRealmAlias != null
 | 
				
			||||||
            ? Text('screenRealmManage').tr()
 | 
					            ? 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/sn_network.dart';
 | 
				
			||||||
import 'package:surface/providers/user_directory.dart';
 | 
					import 'package:surface/providers/user_directory.dart';
 | 
				
			||||||
import 'package:surface/providers/userinfo.dart';
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/types/realm.dart';
 | 
					import 'package:surface/types/realm.dart';
 | 
				
			||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.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 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '../../types/post.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class RealmDetailScreen extends StatefulWidget {
 | 
					class RealmDetailScreen extends StatefulWidget {
 | 
				
			||||||
  final String alias;
 | 
					  final String alias;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -70,19 +70,11 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return DefaultTabController(
 | 
					    return DefaultTabController(
 | 
				
			||||||
      length: 3,
 | 
					      length: 3,
 | 
				
			||||||
      child: Scaffold(
 | 
					      child: AppScaffold(
 | 
				
			||||||
        body: NestedScrollView(
 | 
					        body: NestedScrollView(
 | 
				
			||||||
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
 | 
					          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
 | 
				
			||||||
            // These are the slivers that show up in the "outer" scroll view.
 | 
					 | 
				
			||||||
            return <Widget>[
 | 
					            return <Widget>[
 | 
				
			||||||
              SliverOverlapAbsorber(
 | 
					              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),
 | 
					                handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
 | 
				
			||||||
                sliver: SliverAppBar(
 | 
					                sliver: SliverAppBar(
 | 
				
			||||||
                  title: Text(_realm?.name ?? 'loading'.tr()),
 | 
					                  title: Text(_realm?.name ?? 'loading'.tr()),
 | 
				
			||||||
@@ -428,7 +420,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return Column(
 | 
					    return Column(
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
        const Gap(16),
 | 
					        const Gap(8),
 | 
				
			||||||
        ListTile(
 | 
					        ListTile(
 | 
				
			||||||
          leading: const Icon(Symbols.edit),
 | 
					          leading: const Icon(Symbols.edit),
 | 
				
			||||||
          trailing: const Icon(Symbols.chevron_right),
 | 
					          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/providers/theme.dart';
 | 
				
			||||||
import 'package:surface/theme.dart';
 | 
					import 'package:surface/theme.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Map<String, Color> kColorSchemes = {
 | 
					const Map<String, Color> kColorSchemes = {
 | 
				
			||||||
  'colorSchemeIndigo': Colors.indigo,
 | 
					  'colorSchemeIndigo': Colors.indigo,
 | 
				
			||||||
@@ -67,7 +68,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final sn = context.read<SnNetworkProvider>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
 | 
					        title: Text('screenSettings').tr(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
      body: SingleChildScrollView(
 | 
					      body: SingleChildScrollView(
 | 
				
			||||||
        child: Column(
 | 
					        child: Column(
 | 
				
			||||||
          spacing: 16,
 | 
					          spacing: 16,
 | 
				
			||||||
@@ -77,6 +82,48 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
				
			|||||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
					              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
                Text('settingsAppearance').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
 | 
					                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)
 | 
					                if (!kIsWeb)
 | 
				
			||||||
                  ListTile(
 | 
					                  ListTile(
 | 
				
			||||||
                    title: Text('settingsBackgroundImage').tr(),
 | 
					                    title: Text('settingsBackgroundImage').tr(),
 | 
				
			||||||
@@ -120,7 +167,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
				
			|||||||
                  subtitle: Text('settingsThemeMaterial3Description').tr(),
 | 
					                  subtitle: Text('settingsThemeMaterial3Description').tr(),
 | 
				
			||||||
                  contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
					                  contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
				
			||||||
                  secondary: const Icon(Symbols.new_releases),
 | 
					                  secondary: const Icon(Symbols.new_releases),
 | 
				
			||||||
                  value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false,
 | 
					                  value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
 | 
				
			||||||
                  onChanged: (value) {
 | 
					                  onChanged: (value) {
 | 
				
			||||||
                    setState(() {
 | 
					                    setState(() {
 | 
				
			||||||
                      _prefs.setBool(
 | 
					                      _prefs.setBool(
 | 
				
			||||||
@@ -142,30 +189,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
				
			|||||||
                    Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value);
 | 
					                    Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value);
 | 
				
			||||||
                    final color = await showDialog<Color?>(
 | 
					                    final color = await showDialog<Color?>(
 | 
				
			||||||
                      context: context,
 | 
					                      context: context,
 | 
				
			||||||
                      builder: (context) => AlertDialog(
 | 
					                      builder: (context) =>
 | 
				
			||||||
                        content: SingleChildScrollView(
 | 
					                          AlertDialog(
 | 
				
			||||||
                          child: ColorPicker(
 | 
					                            content: SingleChildScrollView(
 | 
				
			||||||
                            pickerColor: pickerColor,
 | 
					                              child: ColorPicker(
 | 
				
			||||||
                            onColorChanged: (color) => pickerColor = color,
 | 
					                                pickerColor: pickerColor,
 | 
				
			||||||
                            enableAlpha: false,
 | 
					                                onColorChanged: (color) => pickerColor = color,
 | 
				
			||||||
                            hexInputBar: true,
 | 
					                                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;
 | 
					                    if (color == null || !context.mounted) return;
 | 
				
			||||||
@@ -201,11 +249,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
				
			|||||||
                      value: _prefs.getInt(kAppColorSchemeStoreKey) == null
 | 
					                      value: _prefs.getInt(kAppColorSchemeStoreKey) == null
 | 
				
			||||||
                          ? 1
 | 
					                          ? 1
 | 
				
			||||||
                          : kColorSchemes.values
 | 
					                          : kColorSchemes.values
 | 
				
			||||||
                              .toList()
 | 
					                          .toList()
 | 
				
			||||||
                              .indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
 | 
					                          .indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
 | 
				
			||||||
                      onChanged: (int? value) {
 | 
					                      onChanged: (int? value) {
 | 
				
			||||||
                        if (value != null && value != -1) {
 | 
					                        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>();
 | 
					                          final th = context.read<ThemeProvider>();
 | 
				
			||||||
                          th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value));
 | 
					                          th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value));
 | 
				
			||||||
                          setState(() {});
 | 
					                          setState(() {});
 | 
				
			||||||
@@ -255,6 +305,48 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
				
			|||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
					            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(
 | 
					            Column(
 | 
				
			||||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
					              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
              children: [
 | 
					              children: [
 | 
				
			||||||
@@ -295,7 +387,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
				
			|||||||
                          ('Custom', _serverUrlController.text),
 | 
					                          ('Custom', _serverUrlController.text),
 | 
				
			||||||
                      ]
 | 
					                      ]
 | 
				
			||||||
                          .map(
 | 
					                          .map(
 | 
				
			||||||
                            (item) => DropdownMenuItem<String>(
 | 
					                            (item) =>
 | 
				
			||||||
 | 
					                            DropdownMenuItem<String>(
 | 
				
			||||||
                              value: item.$2,
 | 
					                              value: item.$2,
 | 
				
			||||||
                              child: Column(
 | 
					                              child: Column(
 | 
				
			||||||
                                mainAxisSize: MainAxisSize.max,
 | 
					                                mainAxisSize: MainAxisSize.max,
 | 
				
			||||||
@@ -307,7 +400,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
				
			|||||||
                                ],
 | 
					                                ],
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                          )
 | 
					                      )
 | 
				
			||||||
                          .toList(),
 | 
					                          .toList(),
 | 
				
			||||||
                      value: _serverUrlController.text,
 | 
					                      value: _serverUrlController.text,
 | 
				
			||||||
                      onChanged: (String? value) {
 | 
					                      onChanged: (String? value) {
 | 
				
			||||||
@@ -362,11 +455,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
				
			|||||||
                      isExpanded: true,
 | 
					                      isExpanded: true,
 | 
				
			||||||
                      items: kImageQualityLevel.entries
 | 
					                      items: kImageQualityLevel.entries
 | 
				
			||||||
                          .map(
 | 
					                          .map(
 | 
				
			||||||
                            (item) => DropdownMenuItem<FilterQuality>(
 | 
					                            (item) =>
 | 
				
			||||||
 | 
					                            DropdownMenuItem<FilterQuality>(
 | 
				
			||||||
                              value: item.value,
 | 
					                              value: item.value,
 | 
				
			||||||
                              child: Text(item.key).tr().fontSize(14),
 | 
					                              child: Text(item.key).tr().fontSize(14),
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                          )
 | 
					                      )
 | 
				
			||||||
                          .toList(),
 | 
					                          .toList(),
 | 
				
			||||||
                      onChanged: (FilterQuality? value) {
 | 
					                      onChanged: (FilterQuality? value) {
 | 
				
			||||||
                        if (value == null) return;
 | 
					                        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(
 | 
					Future<ThemeData> createAppTheme(
 | 
				
			||||||
  Brightness brightness, {
 | 
					  Brightness brightness, {
 | 
				
			||||||
    Color? seedColorOverride,
 | 
					  Color? seedColorOverride,
 | 
				
			||||||
  bool? useMaterial3,
 | 
					  bool? useMaterial3,
 | 
				
			||||||
}) async {
 | 
					}) async {
 | 
				
			||||||
  final prefs = await SharedPreferences.getInstance();
 | 
					  final prefs = await SharedPreferences.getInstance();
 | 
				
			||||||
@@ -34,9 +34,10 @@ Future<ThemeData> createAppTheme(
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
 | 
					  final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
 | 
				
			||||||
 | 
					  final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return ThemeData(
 | 
					  return ThemeData(
 | 
				
			||||||
    useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false),
 | 
					    useMaterial3: useM3,
 | 
				
			||||||
    colorScheme: colorScheme,
 | 
					    colorScheme: colorScheme,
 | 
				
			||||||
    brightness: brightness,
 | 
					    brightness: brightness,
 | 
				
			||||||
    iconTheme: IconThemeData(
 | 
					    iconTheme: IconThemeData(
 | 
				
			||||||
@@ -45,12 +46,24 @@ Future<ThemeData> createAppTheme(
 | 
				
			|||||||
      opticalSize: 20,
 | 
					      opticalSize: 20,
 | 
				
			||||||
      color: colorScheme.onSurface,
 | 
					      color: colorScheme.onSurface,
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    snackBarTheme: SnackBarThemeData(
 | 
				
			||||||
 | 
					      behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
    appBarTheme: AppBarTheme(
 | 
					    appBarTheme: AppBarTheme(
 | 
				
			||||||
      centerTitle: true,
 | 
					      centerTitle: true,
 | 
				
			||||||
      elevation: hasAppBarBlurry ? 0 : null,
 | 
					      elevation: hasAppBarBlurry ? 0 : null,
 | 
				
			||||||
      backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
 | 
					      backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
 | 
				
			||||||
      foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
 | 
					      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? deletedAt,
 | 
				
			||||||
    required DateTime? confirmedAt,
 | 
					    required DateTime? confirmedAt,
 | 
				
			||||||
    required List<SnAccountContact>? contacts,
 | 
					    required List<SnAccountContact>? contacts,
 | 
				
			||||||
    required String avatar,
 | 
					    @Default("") String avatar,
 | 
				
			||||||
    required String banner,
 | 
					    @Default("") String banner,
 | 
				
			||||||
    required String description,
 | 
					    required String description,
 | 
				
			||||||
    required String name,
 | 
					    required String name,
 | 
				
			||||||
    required String nick,
 | 
					    required String nick,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -367,8 +367,8 @@ class _$SnAccountImpl extends _SnAccount {
 | 
				
			|||||||
      required this.deletedAt,
 | 
					      required this.deletedAt,
 | 
				
			||||||
      required this.confirmedAt,
 | 
					      required this.confirmedAt,
 | 
				
			||||||
      required final List<SnAccountContact>? contacts,
 | 
					      required final List<SnAccountContact>? contacts,
 | 
				
			||||||
      required this.avatar,
 | 
					      this.avatar = "",
 | 
				
			||||||
      required this.banner,
 | 
					      this.banner = "",
 | 
				
			||||||
      required this.description,
 | 
					      required this.description,
 | 
				
			||||||
      required this.name,
 | 
					      required this.name,
 | 
				
			||||||
      required this.nick,
 | 
					      required this.nick,
 | 
				
			||||||
@@ -410,8 +410,10 @@ class _$SnAccountImpl extends _SnAccount {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 | 
					  @JsonKey()
 | 
				
			||||||
  final String avatar;
 | 
					  final String avatar;
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 | 
					  @JsonKey()
 | 
				
			||||||
  final String banner;
 | 
					  final String banner;
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  final String description;
 | 
					  final String description;
 | 
				
			||||||
@@ -540,8 +542,8 @@ abstract class _SnAccount extends SnAccount {
 | 
				
			|||||||
      required final DateTime? deletedAt,
 | 
					      required final DateTime? deletedAt,
 | 
				
			||||||
      required final DateTime? confirmedAt,
 | 
					      required final DateTime? confirmedAt,
 | 
				
			||||||
      required final List<SnAccountContact>? contacts,
 | 
					      required final List<SnAccountContact>? contacts,
 | 
				
			||||||
      required final String avatar,
 | 
					      final String avatar,
 | 
				
			||||||
      required final String banner,
 | 
					      final String banner,
 | 
				
			||||||
      required final String description,
 | 
					      required final String description,
 | 
				
			||||||
      required final String name,
 | 
					      required final String name,
 | 
				
			||||||
      required final String nick,
 | 
					      required final String nick,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,8 +20,8 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
 | 
				
			|||||||
      contacts: (json['contacts'] as List<dynamic>?)
 | 
					      contacts: (json['contacts'] as List<dynamic>?)
 | 
				
			||||||
          ?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
 | 
					          ?.map((e) => SnAccountContact.fromJson(e as Map<String, dynamic>))
 | 
				
			||||||
          .toList(),
 | 
					          .toList(),
 | 
				
			||||||
      avatar: json['avatar'] as String,
 | 
					      avatar: json['avatar'] as String? ?? "",
 | 
				
			||||||
      banner: json['banner'] as String,
 | 
					      banner: json['banner'] as String? ?? "",
 | 
				
			||||||
      description: json['description'] as String,
 | 
					      description: json['description'] as String,
 | 
				
			||||||
      name: json['name'] as String,
 | 
					      name: json['name'] as String,
 | 
				
			||||||
      nick: json['nick'] as String,
 | 
					      nick: json['nick'] as String,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,7 @@ class SnCheckInRecord with _$SnCheckInRecord {
 | 
				
			|||||||
    required DateTime? deletedAt,
 | 
					    required DateTime? deletedAt,
 | 
				
			||||||
    required int resultTier,
 | 
					    required int resultTier,
 | 
				
			||||||
    required int resultExperience,
 | 
					    required int resultExperience,
 | 
				
			||||||
 | 
					    required double resultCoin,
 | 
				
			||||||
    required List<int> resultModifiers,
 | 
					    required List<int> resultModifiers,
 | 
				
			||||||
    required int accountId,
 | 
					    required int accountId,
 | 
				
			||||||
  }) = _SnCheckInRecord;
 | 
					  }) = _SnCheckInRecord;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,6 +26,7 @@ mixin _$SnCheckInRecord {
 | 
				
			|||||||
  DateTime? get deletedAt => throw _privateConstructorUsedError;
 | 
					  DateTime? get deletedAt => throw _privateConstructorUsedError;
 | 
				
			||||||
  int get resultTier => throw _privateConstructorUsedError;
 | 
					  int get resultTier => throw _privateConstructorUsedError;
 | 
				
			||||||
  int get resultExperience => throw _privateConstructorUsedError;
 | 
					  int get resultExperience => throw _privateConstructorUsedError;
 | 
				
			||||||
 | 
					  double get resultCoin => throw _privateConstructorUsedError;
 | 
				
			||||||
  List<int> get resultModifiers => throw _privateConstructorUsedError;
 | 
					  List<int> get resultModifiers => throw _privateConstructorUsedError;
 | 
				
			||||||
  int get accountId => throw _privateConstructorUsedError;
 | 
					  int get accountId => throw _privateConstructorUsedError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -52,6 +53,7 @@ abstract class $SnCheckInRecordCopyWith<$Res> {
 | 
				
			|||||||
      DateTime? deletedAt,
 | 
					      DateTime? deletedAt,
 | 
				
			||||||
      int resultTier,
 | 
					      int resultTier,
 | 
				
			||||||
      int resultExperience,
 | 
					      int resultExperience,
 | 
				
			||||||
 | 
					      double resultCoin,
 | 
				
			||||||
      List<int> resultModifiers,
 | 
					      List<int> resultModifiers,
 | 
				
			||||||
      int accountId});
 | 
					      int accountId});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -77,6 +79,7 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
 | 
				
			|||||||
    Object? deletedAt = freezed,
 | 
					    Object? deletedAt = freezed,
 | 
				
			||||||
    Object? resultTier = null,
 | 
					    Object? resultTier = null,
 | 
				
			||||||
    Object? resultExperience = null,
 | 
					    Object? resultExperience = null,
 | 
				
			||||||
 | 
					    Object? resultCoin = null,
 | 
				
			||||||
    Object? resultModifiers = null,
 | 
					    Object? resultModifiers = null,
 | 
				
			||||||
    Object? accountId = null,
 | 
					    Object? accountId = null,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
@@ -105,6 +108,10 @@ class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
 | 
				
			|||||||
          ? _value.resultExperience
 | 
					          ? _value.resultExperience
 | 
				
			||||||
          : resultExperience // ignore: cast_nullable_to_non_nullable
 | 
					          : resultExperience // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
              as int,
 | 
					              as int,
 | 
				
			||||||
 | 
					      resultCoin: null == resultCoin
 | 
				
			||||||
 | 
					          ? _value.resultCoin
 | 
				
			||||||
 | 
					          : resultCoin // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					              as double,
 | 
				
			||||||
      resultModifiers: null == resultModifiers
 | 
					      resultModifiers: null == resultModifiers
 | 
				
			||||||
          ? _value.resultModifiers
 | 
					          ? _value.resultModifiers
 | 
				
			||||||
          : resultModifiers // ignore: cast_nullable_to_non_nullable
 | 
					          : resultModifiers // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
@@ -132,6 +139,7 @@ abstract class _$$SnCheckInRecordImplCopyWith<$Res>
 | 
				
			|||||||
      DateTime? deletedAt,
 | 
					      DateTime? deletedAt,
 | 
				
			||||||
      int resultTier,
 | 
					      int resultTier,
 | 
				
			||||||
      int resultExperience,
 | 
					      int resultExperience,
 | 
				
			||||||
 | 
					      double resultCoin,
 | 
				
			||||||
      List<int> resultModifiers,
 | 
					      List<int> resultModifiers,
 | 
				
			||||||
      int accountId});
 | 
					      int accountId});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -155,6 +163,7 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
 | 
				
			|||||||
    Object? deletedAt = freezed,
 | 
					    Object? deletedAt = freezed,
 | 
				
			||||||
    Object? resultTier = null,
 | 
					    Object? resultTier = null,
 | 
				
			||||||
    Object? resultExperience = null,
 | 
					    Object? resultExperience = null,
 | 
				
			||||||
 | 
					    Object? resultCoin = null,
 | 
				
			||||||
    Object? resultModifiers = null,
 | 
					    Object? resultModifiers = null,
 | 
				
			||||||
    Object? accountId = null,
 | 
					    Object? accountId = null,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
@@ -183,6 +192,10 @@ class __$$SnCheckInRecordImplCopyWithImpl<$Res>
 | 
				
			|||||||
          ? _value.resultExperience
 | 
					          ? _value.resultExperience
 | 
				
			||||||
          : resultExperience // ignore: cast_nullable_to_non_nullable
 | 
					          : resultExperience // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
              as int,
 | 
					              as int,
 | 
				
			||||||
 | 
					      resultCoin: null == resultCoin
 | 
				
			||||||
 | 
					          ? _value.resultCoin
 | 
				
			||||||
 | 
					          : resultCoin // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					              as double,
 | 
				
			||||||
      resultModifiers: null == resultModifiers
 | 
					      resultModifiers: null == resultModifiers
 | 
				
			||||||
          ? _value._resultModifiers
 | 
					          ? _value._resultModifiers
 | 
				
			||||||
          : resultModifiers // ignore: cast_nullable_to_non_nullable
 | 
					          : resultModifiers // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
@@ -205,6 +218,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
 | 
				
			|||||||
      required this.deletedAt,
 | 
					      required this.deletedAt,
 | 
				
			||||||
      required this.resultTier,
 | 
					      required this.resultTier,
 | 
				
			||||||
      required this.resultExperience,
 | 
					      required this.resultExperience,
 | 
				
			||||||
 | 
					      required this.resultCoin,
 | 
				
			||||||
      required final List<int> resultModifiers,
 | 
					      required final List<int> resultModifiers,
 | 
				
			||||||
      required this.accountId})
 | 
					      required this.accountId})
 | 
				
			||||||
      : _resultModifiers = resultModifiers,
 | 
					      : _resultModifiers = resultModifiers,
 | 
				
			||||||
@@ -225,6 +239,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
 | 
				
			|||||||
  final int resultTier;
 | 
					  final int resultTier;
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  final int resultExperience;
 | 
					  final int resultExperience;
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  final double resultCoin;
 | 
				
			||||||
  final List<int> _resultModifiers;
 | 
					  final List<int> _resultModifiers;
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  List<int> get resultModifiers {
 | 
					  List<int> get resultModifiers {
 | 
				
			||||||
@@ -238,7 +254,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  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
 | 
					  @override
 | 
				
			||||||
@@ -257,6 +273,8 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
 | 
				
			|||||||
                other.resultTier == resultTier) &&
 | 
					                other.resultTier == resultTier) &&
 | 
				
			||||||
            (identical(other.resultExperience, resultExperience) ||
 | 
					            (identical(other.resultExperience, resultExperience) ||
 | 
				
			||||||
                other.resultExperience == resultExperience) &&
 | 
					                other.resultExperience == resultExperience) &&
 | 
				
			||||||
 | 
					            (identical(other.resultCoin, resultCoin) ||
 | 
				
			||||||
 | 
					                other.resultCoin == resultCoin) &&
 | 
				
			||||||
            const DeepCollectionEquality()
 | 
					            const DeepCollectionEquality()
 | 
				
			||||||
                .equals(other._resultModifiers, _resultModifiers) &&
 | 
					                .equals(other._resultModifiers, _resultModifiers) &&
 | 
				
			||||||
            (identical(other.accountId, accountId) ||
 | 
					            (identical(other.accountId, accountId) ||
 | 
				
			||||||
@@ -273,6 +291,7 @@ class _$SnCheckInRecordImpl extends _SnCheckInRecord {
 | 
				
			|||||||
      deletedAt,
 | 
					      deletedAt,
 | 
				
			||||||
      resultTier,
 | 
					      resultTier,
 | 
				
			||||||
      resultExperience,
 | 
					      resultExperience,
 | 
				
			||||||
 | 
					      resultCoin,
 | 
				
			||||||
      const DeepCollectionEquality().hash(_resultModifiers),
 | 
					      const DeepCollectionEquality().hash(_resultModifiers),
 | 
				
			||||||
      accountId);
 | 
					      accountId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -301,6 +320,7 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
 | 
				
			|||||||
      required final DateTime? deletedAt,
 | 
					      required final DateTime? deletedAt,
 | 
				
			||||||
      required final int resultTier,
 | 
					      required final int resultTier,
 | 
				
			||||||
      required final int resultExperience,
 | 
					      required final int resultExperience,
 | 
				
			||||||
 | 
					      required final double resultCoin,
 | 
				
			||||||
      required final List<int> resultModifiers,
 | 
					      required final List<int> resultModifiers,
 | 
				
			||||||
      required final int accountId}) = _$SnCheckInRecordImpl;
 | 
					      required final int accountId}) = _$SnCheckInRecordImpl;
 | 
				
			||||||
  const _SnCheckInRecord._() : super._();
 | 
					  const _SnCheckInRecord._() : super._();
 | 
				
			||||||
@@ -321,6 +341,8 @@ abstract class _SnCheckInRecord extends SnCheckInRecord {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get resultExperience;
 | 
					  int get resultExperience;
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 | 
					  double get resultCoin;
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
  List<int> get resultModifiers;
 | 
					  List<int> get resultModifiers;
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get accountId;
 | 
					  int get accountId;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,7 @@ _$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson(
 | 
				
			|||||||
          : DateTime.parse(json['deleted_at'] as String),
 | 
					          : DateTime.parse(json['deleted_at'] as String),
 | 
				
			||||||
      resultTier: (json['result_tier'] as num).toInt(),
 | 
					      resultTier: (json['result_tier'] as num).toInt(),
 | 
				
			||||||
      resultExperience: (json['result_experience'] as num).toInt(),
 | 
					      resultExperience: (json['result_experience'] as num).toInt(),
 | 
				
			||||||
 | 
					      resultCoin: (json['result_coin'] as num).toDouble(),
 | 
				
			||||||
      resultModifiers: (json['result_modifiers'] as List<dynamic>)
 | 
					      resultModifiers: (json['result_modifiers'] as List<dynamic>)
 | 
				
			||||||
          .map((e) => (e as num).toInt())
 | 
					          .map((e) => (e as num).toInt())
 | 
				
			||||||
          .toList(),
 | 
					          .toList(),
 | 
				
			||||||
@@ -32,6 +33,7 @@ Map<String, dynamic> _$$SnCheckInRecordImplToJson(
 | 
				
			|||||||
      'deleted_at': instance.deletedAt?.toIso8601String(),
 | 
					      'deleted_at': instance.deletedAt?.toIso8601String(),
 | 
				
			||||||
      'result_tier': instance.resultTier,
 | 
					      'result_tier': instance.resultTier,
 | 
				
			||||||
      'result_experience': instance.resultExperience,
 | 
					      'result_experience': instance.resultExperience,
 | 
				
			||||||
 | 
					      'result_coin': instance.resultCoin,
 | 
				
			||||||
      'result_modifiers': instance.resultModifiers,
 | 
					      'result_modifiers': instance.resultModifiers,
 | 
				
			||||||
      'account_id': instance.accountId,
 | 
					      '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:gap/gap.dart';
 | 
				
			||||||
import 'package:package_info_plus/package_info_plus.dart';
 | 
					import 'package:package_info_plus/package_info_plus.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AboutScreen extends StatelessWidget {
 | 
					class AboutScreen extends StatelessWidget {
 | 
				
			||||||
@@ -12,97 +13,103 @@ class AboutScreen extends StatelessWidget {
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4));
 | 
					    const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return SizedBox(
 | 
					    return AppScaffold(
 | 
				
			||||||
      width: double.infinity,
 | 
					      appBar: AppBar(
 | 
				
			||||||
      child: Column(
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
        mainAxisAlignment: MainAxisAlignment.center,
 | 
					        title: Text('screenAbout').tr(),
 | 
				
			||||||
        children: [
 | 
					      ),
 | 
				
			||||||
          ClipRRect(
 | 
					      body: SizedBox(
 | 
				
			||||||
            borderRadius: const BorderRadius.all(Radius.circular(16)),
 | 
					        width: double.infinity,
 | 
				
			||||||
            child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120),
 | 
					        child: Column(
 | 
				
			||||||
          ),
 | 
					          mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
          const Gap(8),
 | 
					          children: [
 | 
				
			||||||
          Text(
 | 
					            ClipRRect(
 | 
				
			||||||
            'Solian',
 | 
					              borderRadius: const BorderRadius.all(Radius.circular(16)),
 | 
				
			||||||
            style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36),
 | 
					              child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120),
 | 
				
			||||||
          ),
 | 
					            ),
 | 
				
			||||||
          const Text(
 | 
					            const Gap(8),
 | 
				
			||||||
            'The Solar Network',
 | 
					            Text(
 | 
				
			||||||
            style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
 | 
					              'Solian',
 | 
				
			||||||
          ),
 | 
					              style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36),
 | 
				
			||||||
          const Gap(8),
 | 
					            ),
 | 
				
			||||||
          FutureBuilder(
 | 
					            const Text(
 | 
				
			||||||
            future: PackageInfo.fromPlatform(),
 | 
					              'The Solar Network',
 | 
				
			||||||
            builder: (context, snapshot) {
 | 
					              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
 | 
				
			||||||
              if (!snapshot.hasData) {
 | 
					            ),
 | 
				
			||||||
                return const SizedBox.shrink();
 | 
					            const Gap(8),
 | 
				
			||||||
              }
 | 
					            FutureBuilder(
 | 
				
			||||||
 | 
					              future: PackageInfo.fromPlatform(),
 | 
				
			||||||
 | 
					              builder: (context, snapshot) {
 | 
				
			||||||
 | 
					                if (!snapshot.hasData) {
 | 
				
			||||||
 | 
					                  return const SizedBox.shrink();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
              return Text(
 | 
					                return Text(
 | 
				
			||||||
                'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
 | 
					                  'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
 | 
				
			||||||
                style: const TextStyle(fontFamily: 'monospace'),
 | 
					                  style: const TextStyle(fontFamily: 'monospace'),
 | 
				
			||||||
              );
 | 
					                );
 | 
				
			||||||
            },
 | 
					              },
 | 
				
			||||||
          ),
 | 
					            ),
 | 
				
			||||||
          Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
 | 
					            Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
 | 
				
			||||||
          const Gap(16),
 | 
					            const Gap(16),
 | 
				
			||||||
          Container(
 | 
					            Container(
 | 
				
			||||||
            constraints: const BoxConstraints(maxWidth: 280),
 | 
					              constraints: const BoxConstraints(maxWidth: 280),
 | 
				
			||||||
            child: Wrap(
 | 
					              child: Wrap(
 | 
				
			||||||
              spacing: 4,
 | 
					                spacing: 4,
 | 
				
			||||||
              runSpacing: 4,
 | 
					                runSpacing: 4,
 | 
				
			||||||
              alignment: WrapAlignment.center,
 | 
					                alignment: WrapAlignment.center,
 | 
				
			||||||
              children: [
 | 
					                children: [
 | 
				
			||||||
                TextButton(
 | 
					                  TextButton(
 | 
				
			||||||
                  style: denseButtonStyle,
 | 
					                    style: denseButtonStyle,
 | 
				
			||||||
                  child: Text('appDetails').tr(),
 | 
					                    child: Text('appDetails').tr(),
 | 
				
			||||||
                  onPressed: () async {
 | 
					                    onPressed: () async {
 | 
				
			||||||
                    final info = await PackageInfo.fromPlatform();
 | 
					                      final info = await PackageInfo.fromPlatform();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (!context.mounted) return;
 | 
					                      if (!context.mounted) return;
 | 
				
			||||||
                    showAboutDialog(
 | 
					                      showAboutDialog(
 | 
				
			||||||
                      context: context,
 | 
					                        context: context,
 | 
				
			||||||
                      applicationName: 'Solian',
 | 
					                        applicationName: 'Solian',
 | 
				
			||||||
                      applicationVersion: '${info.version}+${info.buildNumber}',
 | 
					                        applicationVersion: '${info.version}+${info.buildNumber}',
 | 
				
			||||||
                      applicationLegalese:
 | 
					                        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.',
 | 
					                            '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(
 | 
					                        applicationIcon: ClipRRect(
 | 
				
			||||||
                        borderRadius: const BorderRadius.all(Radius.circular(16)),
 | 
					                          borderRadius: const BorderRadius.all(Radius.circular(16)),
 | 
				
			||||||
                        child: Image.asset(
 | 
					                          child: Image.asset(
 | 
				
			||||||
                          'assets/icon/icon-light-radius.png',
 | 
					                            'assets/icon/icon-light-radius.png',
 | 
				
			||||||
                          width: 60,
 | 
					                            width: 60,
 | 
				
			||||||
                          height: 60,
 | 
					                            height: 60,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      );
 | 
				
			||||||
                    );
 | 
					                    },
 | 
				
			||||||
                  },
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                  TextButton(
 | 
				
			||||||
                TextButton(
 | 
					                    style: denseButtonStyle,
 | 
				
			||||||
                  style: denseButtonStyle,
 | 
					                    child: Text('termRelated').tr(),
 | 
				
			||||||
                  child: Text('termRelated').tr(),
 | 
					                    onPressed: () {
 | 
				
			||||||
                  onPressed: () {
 | 
					                      launchUrlString('https://solsynth.dev/terms');
 | 
				
			||||||
                    launchUrlString('https://solsynth.dev/terms');
 | 
					                    },
 | 
				
			||||||
                  },
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                  TextButton(
 | 
				
			||||||
                TextButton(
 | 
					                    style: denseButtonStyle,
 | 
				
			||||||
                  style: denseButtonStyle,
 | 
					                    child: Text('serviceStatus').tr(),
 | 
				
			||||||
                  child: Text('serviceStatus').tr(),
 | 
					                    onPressed: () {
 | 
				
			||||||
                  onPressed: () {
 | 
					                      launchUrlString('https://status.solsynth.dev');
 | 
				
			||||||
                    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,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,6 @@ import 'package:surface/providers/experience.dart';
 | 
				
			|||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
import 'package:surface/screens/account/profile_page.dart';
 | 
					import 'package:surface/screens/account/profile_page.dart';
 | 
				
			||||||
import 'package:surface/types/account.dart';
 | 
					import 'package:surface/types/account.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					 | 
				
			||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
import 'package:surface/widgets/universal_image.dart';
 | 
					import 'package:surface/widgets/universal_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@ class AttachmentList extends StatefulWidget {
 | 
				
			|||||||
  final List<SnAttachment?> data;
 | 
					  final List<SnAttachment?> data;
 | 
				
			||||||
  final bool bordered;
 | 
					  final bool bordered;
 | 
				
			||||||
  final bool gridded;
 | 
					  final bool gridded;
 | 
				
			||||||
 | 
					  final bool columned;
 | 
				
			||||||
  final BoxFit fit;
 | 
					  final BoxFit fit;
 | 
				
			||||||
  final double? maxHeight;
 | 
					  final double? maxHeight;
 | 
				
			||||||
  final double? minWidth;
 | 
					  final double? minWidth;
 | 
				
			||||||
@@ -26,6 +27,7 @@ class AttachmentList extends StatefulWidget {
 | 
				
			|||||||
    required this.data,
 | 
					    required this.data,
 | 
				
			||||||
    this.bordered = false,
 | 
					    this.bordered = false,
 | 
				
			||||||
    this.gridded = false,
 | 
					    this.gridded = false,
 | 
				
			||||||
 | 
					    this.columned = false,
 | 
				
			||||||
    this.fit = BoxFit.cover,
 | 
					    this.fit = BoxFit.cover,
 | 
				
			||||||
    this.maxHeight,
 | 
					    this.maxHeight,
 | 
				
			||||||
    this.minWidth,
 | 
					    this.minWidth,
 | 
				
			||||||
@@ -105,45 +107,10 @@ class _AttachmentListState extends State<AttachmentList> {
 | 
				
			|||||||
          );
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (widget.gridded) {
 | 
					        final fullOfImage =
 | 
				
			||||||
          final fullOfImage =
 | 
					            widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
 | 
				
			||||||
              widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
 | 
					
 | 
				
			||||||
          if(!fullOfImage) {
 | 
					        if (widget.gridded && fullOfImage) {
 | 
				
			||||||
            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(
 | 
					 | 
				
			||||||
                  spacing: 4,
 | 
					 | 
				
			||||||
                  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,
 | 
					 | 
				
			||||||
                              ),
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      )
 | 
					 | 
				
			||||||
                      .toList(),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return Container(
 | 
					          return Container(
 | 
				
			||||||
            margin: widget.padding ?? EdgeInsets.zero,
 | 
					            margin: widget.padding ?? EdgeInsets.zero,
 | 
				
			||||||
            decoration: BoxDecoration(
 | 
					            decoration: BoxDecoration(
 | 
				
			||||||
@@ -191,68 +158,109 @@ class _AttachmentListState extends State<AttachmentList> {
 | 
				
			|||||||
          );
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return Container(
 | 
					        if ((!fullOfImage && widget.gridded) || widget.columned) {
 | 
				
			||||||
          constraints: BoxConstraints(maxHeight: constraints.maxHeight),
 | 
					          return Container(
 | 
				
			||||||
          child: ScrollConfiguration(
 | 
					            margin: widget.padding ?? EdgeInsets.zero,
 | 
				
			||||||
            behavior: _AttachmentListScrollBehavior(),
 | 
					            decoration: BoxDecoration(
 | 
				
			||||||
            child: ListView.separated(
 | 
					              color: backgroundColor,
 | 
				
			||||||
              padding: widget.padding,
 | 
					              border: Border(
 | 
				
			||||||
              shrinkWrap: true,
 | 
					                top: borderSide,
 | 
				
			||||||
              itemCount: widget.data.length,
 | 
					                bottom: borderSide,
 | 
				
			||||||
              itemBuilder: (context, idx) {
 | 
					              ),
 | 
				
			||||||
                return Container(
 | 
					              borderRadius: AttachmentList.kDefaultRadius,
 | 
				
			||||||
                  constraints: constraints.copyWith(maxWidth: widget.maxWidth),
 | 
					            ),
 | 
				
			||||||
                  child: AspectRatio(
 | 
					            child: ClipRRect(
 | 
				
			||||||
                    aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
 | 
					              borderRadius: AttachmentList.kDefaultRadius,
 | 
				
			||||||
                    child: GestureDetector(
 | 
					              child: Column(
 | 
				
			||||||
                      onTap: () {
 | 
					                children: widget.data
 | 
				
			||||||
                        if (widget.data[idx]?.mediaType != SnMediaType.image) return;
 | 
					                    .mapIndexed(
 | 
				
			||||||
                        context.pushTransparentRoute(
 | 
					                      (idx, ele) => GestureDetector(
 | 
				
			||||||
                          AttachmentZoomView(
 | 
					                        child: AspectRatio(
 | 
				
			||||||
                            data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
 | 
					                          aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
 | 
				
			||||||
                            initialIndex: idx,
 | 
					                          child: Container(
 | 
				
			||||||
                            heroTags: heroTags,
 | 
					                            constraints: constraints,
 | 
				
			||||||
                          ),
 | 
					                            child: AttachmentItem(
 | 
				
			||||||
                          backgroundColor: Colors.black.withOpacity(0.7),
 | 
					                              data: ele,
 | 
				
			||||||
                          rootNavigator: true,
 | 
					                              heroTag: heroTags[idx],
 | 
				
			||||||
                        );
 | 
					                              fit: BoxFit.cover,
 | 
				
			||||||
                      },
 | 
					 | 
				
			||||||
                      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],
 | 
					                    .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(
 | 
				
			||||||
                          Positioned(
 | 
					                              right: 8,
 | 
				
			||||||
                            right: 8,
 | 
					                              bottom: 8,
 | 
				
			||||||
                            bottom: 8,
 | 
					                              child: Chip(
 | 
				
			||||||
                            child: Chip(
 | 
					                                label: Text('${idx + 1}/${widget.data.length}'),
 | 
				
			||||||
                              label: Text('${idx + 1}/${widget.data.length}'),
 | 
					                              ),
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                          ),
 | 
					                          ],
 | 
				
			||||||
                        ],
 | 
					                        ),
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  );
 | 
				
			||||||
                );
 | 
					                },
 | 
				
			||||||
              },
 | 
					                separatorBuilder: (context, index) => const Gap(8),
 | 
				
			||||||
              separatorBuilder: (context, index) => const Gap(8),
 | 
					                physics: const BouncingScrollPhysics(),
 | 
				
			||||||
              physics: const BouncingScrollPhysics(),
 | 
					                scrollDirection: Axis.horizontal,
 | 
				
			||||||
              scrollDirection: Axis.horizontal,
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -129,6 +129,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
 | 
					  Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool _showDetail = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final sn = context.read<SnNetworkProvider>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
@@ -144,218 +146,350 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
 | 
				
			|||||||
      onDismissed: () {
 | 
					      onDismissed: () {
 | 
				
			||||||
        Navigator.of(context).pop();
 | 
					        Navigator.of(context).pop();
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      direction: DismissiblePageDismissDirection.down,
 | 
					      direction: DismissiblePageDismissDirection.none,
 | 
				
			||||||
      backgroundColor: Colors.transparent,
 | 
					      backgroundColor: Colors.transparent,
 | 
				
			||||||
      isFullScreen: true,
 | 
					      isFullScreen: true,
 | 
				
			||||||
      child: Scaffold(
 | 
					      child: GestureDetector(
 | 
				
			||||||
        body: Stack(
 | 
					        behavior: HitTestBehavior.translucent,
 | 
				
			||||||
          children: [
 | 
					        child: Scaffold(
 | 
				
			||||||
            Builder(builder: (context) {
 | 
					          body: Stack(
 | 
				
			||||||
              if (widget.data.length == 1) {
 | 
					            children: [
 | 
				
			||||||
                final heroTag = widget.heroTags?.first ?? uuid.v4();
 | 
					              Builder(builder: (context) {
 | 
				
			||||||
                return Hero(
 | 
					                if (widget.data.length == 1) {
 | 
				
			||||||
                  tag: 'attachment-${widget.data.first.rid}-$heroTag',
 | 
					                  final heroTag = widget.heroTags?.first ?? uuid.v4();
 | 
				
			||||||
                  child: PhotoView(
 | 
					                  return Hero(
 | 
				
			||||||
                    key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
 | 
					                    tag: 'attachment-${widget.data.first.rid}-$heroTag',
 | 
				
			||||||
                    backgroundDecoration: BoxDecoration(color: Colors.transparent),
 | 
					                    child: PhotoView(
 | 
				
			||||||
                    imageProvider: UniversalImage.provider(
 | 
					                      key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
 | 
				
			||||||
                      sn.getAttachmentUrl(widget.data.first.rid),
 | 
					                      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',
 | 
					 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  );
 | 
					                  );
 | 
				
			||||||
                },
 | 
					                }
 | 
				
			||||||
                itemCount: widget.data.length,
 | 
					
 | 
				
			||||||
                loadingBuilder: (context, event) => Center(
 | 
					                return PhotoViewGallery.builder(
 | 
				
			||||||
                  child: SizedBox(
 | 
					                  pageController: _pageController,
 | 
				
			||||||
                    width: 20.0,
 | 
					                  scrollPhysics: const BouncingScrollPhysics(),
 | 
				
			||||||
                    height: 20.0,
 | 
					                  builder: (context, idx) {
 | 
				
			||||||
                    child: CircularProgressIndicator(
 | 
					                    final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
 | 
				
			||||||
                      value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
 | 
					                    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),
 | 
				
			||||||
                backgroundDecoration: BoxDecoration(color: Colors.transparent),
 | 
					                );
 | 
				
			||||||
              );
 | 
					              }),
 | 
				
			||||||
            }),
 | 
					              Align(
 | 
				
			||||||
            Align(
 | 
					                alignment: Alignment.bottomCenter,
 | 
				
			||||||
              alignment: Alignment.bottomCenter,
 | 
					                child: IgnorePointer(
 | 
				
			||||||
              child: IgnorePointer(
 | 
					                  child: Container(
 | 
				
			||||||
                child: Container(
 | 
					                    height: 300,
 | 
				
			||||||
                  height: 300,
 | 
					                    decoration: BoxDecoration(
 | 
				
			||||||
                  decoration: BoxDecoration(
 | 
					                      gradient: LinearGradient(
 | 
				
			||||||
                    gradient: LinearGradient(
 | 
					                        begin: Alignment.bottomCenter,
 | 
				
			||||||
                      begin: Alignment.bottomCenter,
 | 
					                        end: Alignment.topCenter,
 | 
				
			||||||
                      end: Alignment.topCenter,
 | 
					                        colors: [
 | 
				
			||||||
                      colors: [
 | 
					                          Theme.of(context).colorScheme.surface,
 | 
				
			||||||
                        Theme.of(context).colorScheme.surface,
 | 
					                          Colors.transparent,
 | 
				
			||||||
                        Colors.transparent,
 | 
					                        ],
 | 
				
			||||||
                      ],
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					              Positioned(
 | 
				
			||||||
            Positioned(
 | 
					                left: 16,
 | 
				
			||||||
              left: 16,
 | 
					                right: 16,
 | 
				
			||||||
              right: 16,
 | 
					                bottom: 16 + MediaQuery.of(context).padding.bottom,
 | 
				
			||||||
              bottom: 16 + MediaQuery.of(context).padding.bottom,
 | 
					                child: Material(
 | 
				
			||||||
              child: Material(
 | 
					                  color: Colors.transparent,
 | 
				
			||||||
                color: Colors.transparent,
 | 
					                  child: Builder(builder: (context) {
 | 
				
			||||||
                child: Builder(builder: (context) {
 | 
					                    final ud = context.read<UserDirectoryProvider>();
 | 
				
			||||||
                  final ud = context.read<UserDirectoryProvider>();
 | 
					                    final item = widget.data.elementAt(
 | 
				
			||||||
                  final item = widget.data.elementAt(
 | 
					                      widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
 | 
				
			||||||
                    widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
 | 
					                    );
 | 
				
			||||||
                  );
 | 
					                    final account = ud.getAccountFromCache(item.accountId);
 | 
				
			||||||
                  final account = ud.getAccountFromCache(item.accountId);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  return Column(
 | 
					                    return Column(
 | 
				
			||||||
                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
					                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                    children: [
 | 
					                      children: [
 | 
				
			||||||
                      if (item.accountId > 0)
 | 
					                        if (item.accountId > 0)
 | 
				
			||||||
                        Row(
 | 
					                          Row(
 | 
				
			||||||
                          children: [
 | 
					                            children: [
 | 
				
			||||||
                            IgnorePointer(
 | 
					                              IgnorePointer(
 | 
				
			||||||
                              child: AccountImage(
 | 
					                                child: AccountImage(
 | 
				
			||||||
                                content: account?.avatar,
 | 
					                                  content: account?.avatar,
 | 
				
			||||||
                                radius: 19,
 | 
					                                  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,
 | 
					 | 
				
			||||||
                                    ),
 | 
					 | 
				
			||||||
                                  ],
 | 
					 | 
				
			||||||
                                ),
 | 
					                                ),
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
                            ),
 | 
					                              const Gap(8),
 | 
				
			||||||
                            if (widget.data.length > 1)
 | 
					                              Expanded(
 | 
				
			||||||
                              IgnorePointer(
 | 
					                                child: IgnorePointer(
 | 
				
			||||||
                                child: Text(
 | 
					                                  child: Column(
 | 
				
			||||||
                                  '${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}',
 | 
					                                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                                  style: GoogleFonts.robotoMono(fontSize: 13),
 | 
					                                    children: [
 | 
				
			||||||
                                ).padding(right: 8),
 | 
					                                      Text(
 | 
				
			||||||
                              ),
 | 
					                                        'attachmentUploadBy'.tr(),
 | 
				
			||||||
                            InkWell(
 | 
					                                        style: Theme.of(context).textTheme.bodySmall,
 | 
				
			||||||
                              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,
 | 
					 | 
				
			||||||
                                        ),
 | 
					 | 
				
			||||||
                                      ),
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                      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),
 | 
				
			||||||
                      const Gap(2),
 | 
					                        IgnorePointer(
 | 
				
			||||||
                      IgnorePointer(
 | 
					                          child: Wrap(
 | 
				
			||||||
                        child: Wrap(
 | 
					                            spacing: 6,
 | 
				
			||||||
                          spacing: 6,
 | 
					                            children: [
 | 
				
			||||||
                          children: [
 | 
					                              if (item.metadata['exif'] == null)
 | 
				
			||||||
                            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(
 | 
					                              Text(
 | 
				
			||||||
                                '#${item.rid}',
 | 
					                                item.mimetype,
 | 
				
			||||||
                                style: metaTextStyle,
 | 
					                                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.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),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ import 'package:material_symbols_icons/symbols.dart';
 | 
				
			|||||||
import 'package:popover/popover.dart';
 | 
					import 'package:popover/popover.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.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/user_directory.dart';
 | 
				
			||||||
import 'package:surface/providers/userinfo.dart';
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
import 'package:surface/types/chat.dart';
 | 
					import 'package:surface/types/chat.dart';
 | 
				
			||||||
@@ -53,6 +54,8 @@ class ChatMessage extends StatelessWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final dateFormatter = DateFormat('MM/dd HH:mm');
 | 
					    final dateFormatter = DateFormat('MM/dd HH:mm');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final cfg = context.read<ConfigProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return SwipeTo(
 | 
					    return SwipeTo(
 | 
				
			||||||
      key: Key('chat-message-${data.id}'),
 | 
					      key: Key('chat-message-${data.id}'),
 | 
				
			||||||
      iconOnLeftSwipe: Symbols.reply,
 | 
					      iconOnLeftSwipe: Symbols.reply,
 | 
				
			||||||
@@ -192,7 +195,10 @@ class ChatMessage extends StatelessWidget {
 | 
				
			|||||||
                ],
 | 
					                ],
 | 
				
			||||||
              ).opacity(isPending ? 0.5 : 1),
 | 
					              ).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']!),
 | 
					              LinkPreviewWidget(text: data.body['text']!),
 | 
				
			||||||
            if (data.preload?.attachments?.isNotEmpty ?? false)
 | 
					            if (data.preload?.attachments?.isNotEmpty ?? false)
 | 
				
			||||||
              AttachmentList(
 | 
					              AttachmentList(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,10 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.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:provider/provider.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/config.dart';
 | 
				
			||||||
import 'package:surface/providers/userinfo.dart';
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
import 'package:surface/providers/websocket.dart';
 | 
					import 'package:surface/providers/websocket.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -11,50 +14,62 @@ class ConnectionIndicator extends StatelessWidget {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final ws = context.watch<WebSocketProvider>();
 | 
					    final ws = context.watch<WebSocketProvider>();
 | 
				
			||||||
 | 
					    final cfg = context.watch<ConfigProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return ListenableBuilder(
 | 
					    return ListenableBuilder(
 | 
				
			||||||
      listenable: ws,
 | 
					      listenable: ws,
 | 
				
			||||||
      builder: (context, _) {
 | 
					      builder: (context, _) {
 | 
				
			||||||
        final ua = context.read<UserProvider>();
 | 
					        final ua = context.read<UserProvider>();
 | 
				
			||||||
 | 
					        final show = (ws.isBusy || !ws.isConnected) && ua.isAuthorized;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return GestureDetector(
 | 
					        return IgnorePointer(
 | 
				
			||||||
          child: Container(
 | 
					          ignoring: !show,
 | 
				
			||||||
            padding: EdgeInsets.only(
 | 
					          child: Center(
 | 
				
			||||||
              bottom: 8,
 | 
					            child: GestureDetector(
 | 
				
			||||||
              top: MediaQuery.of(context).padding.top + 8,
 | 
					              child: Material(
 | 
				
			||||||
              left: 24,
 | 
					                elevation: 2,
 | 
				
			||||||
              right: 24,
 | 
					                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,
 | 
					          ).padding(left: marginLeft),
 | 
				
			||||||
            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();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,7 +28,7 @@ class ContextMenuArea extends StatelessWidget {
 | 
				
			|||||||
          // Leave padding for side navigation
 | 
					          // Leave padding for side navigation
 | 
				
			||||||
          mousePosition = cfg.drawerIsExpanded
 | 
					          mousePosition = cfg.drawerIsExpanded
 | 
				
			||||||
              ? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
 | 
					              ? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
 | 
				
			||||||
              : mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
 | 
					              : mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      child: GestureDetector(
 | 
					      child: GestureDetector(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,9 @@ import 'dart:math' as math;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import 'package:dio/dio.dart';
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/gestures.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
extension AppPromptExtension on BuildContext {
 | 
					extension AppPromptExtension on BuildContext {
 | 
				
			||||||
  void showSnackbar(String content, {SnackBarAction? action}) {
 | 
					  void showSnackbar(String content, {SnackBarAction? action}) {
 | 
				
			||||||
@@ -111,7 +113,34 @@ extension AppPromptExtension on BuildContext {
 | 
				
			|||||||
      context: this,
 | 
					      context: this,
 | 
				
			||||||
      builder: (ctx) => AlertDialog(
 | 
					      builder: (ctx) => AlertDialog(
 | 
				
			||||||
        title: Text('dialogError').tr(),
 | 
					        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: [
 | 
					        actions: [
 | 
				
			||||||
          TextButton(
 | 
					          TextButton(
 | 
				
			||||||
            onPressed: () => Navigator.pop(ctx),
 | 
					            onPressed: () => Navigator.pop(ctx),
 | 
				
			||||||
@@ -128,17 +157,7 @@ extension ByteFormatter on int {
 | 
				
			|||||||
    if (this == 0) return '0 Bytes';
 | 
					    if (this == 0) return '0 Bytes';
 | 
				
			||||||
    const k = 1024;
 | 
					    const k = 1024;
 | 
				
			||||||
    final dm = decimals < 0 ? 0 : decimals;
 | 
					    final dm = decimals < 0 ? 0 : decimals;
 | 
				
			||||||
    final sizes = [
 | 
					    final sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
 | 
				
			||||||
      'Bytes',
 | 
					 | 
				
			||||||
      'KiB',
 | 
					 | 
				
			||||||
      'MiB',
 | 
					 | 
				
			||||||
      'GiB',
 | 
					 | 
				
			||||||
      'TiB',
 | 
					 | 
				
			||||||
      'PiB',
 | 
					 | 
				
			||||||
      'EiB',
 | 
					 | 
				
			||||||
      'ZiB',
 | 
					 | 
				
			||||||
      'YiB'
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
    final i = (math.log(this) / math.log(k)).floor().toInt();
 | 
					    final i = (math.log(this) / math.log(k)).floor().toInt();
 | 
				
			||||||
    return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
 | 
					    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:provider/provider.dart';
 | 
				
			||||||
import 'package:responsive_framework/responsive_framework.dart';
 | 
					import 'package:responsive_framework/responsive_framework.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/link_preview.dart';
 | 
				
			||||||
import 'package:surface/types/link.dart';
 | 
					import 'package:surface/types/link.dart';
 | 
				
			||||||
import 'package:surface/widgets/universal_image.dart';
 | 
					import 'package:surface/widgets/universal_image.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '../providers/link_preview.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class LinkPreviewWidget extends StatefulWidget {
 | 
					class LinkPreviewWidget extends StatefulWidget {
 | 
				
			||||||
  final String text;
 | 
					  final String text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -81,8 +80,9 @@ class _LinkPreviewEntry extends StatelessWidget {
 | 
				
			|||||||
                  child: AspectRatio(
 | 
					                  child: AspectRatio(
 | 
				
			||||||
                    aspectRatio: 16 / 9,
 | 
					                    aspectRatio: 16 / 9,
 | 
				
			||||||
                    child: ClipRRect(
 | 
					                    child: ClipRRect(
 | 
				
			||||||
 | 
					                      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
                      child: AutoResizeUniversalImage(
 | 
					                      child: AutoResizeUniversalImage(
 | 
				
			||||||
                        meta.image!,
 | 
					                        meta.image!.startsWith('//') ? 'https:${meta.image}' : meta.image!,
 | 
				
			||||||
                        fit: BoxFit.contain,
 | 
					                        fit: BoxFit.contain,
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
@@ -94,11 +94,14 @@ class _LinkPreviewEntry extends StatelessWidget {
 | 
				
			|||||||
                  crossAxisAlignment: CrossAxisAlignment.center,
 | 
					                  crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
                  children: [
 | 
					                  children: [
 | 
				
			||||||
                    if (meta.icon?.isNotEmpty ?? false)
 | 
					                    if (meta.icon?.isNotEmpty ?? false)
 | 
				
			||||||
                      StyledWidget(
 | 
					                      SizedBox(
 | 
				
			||||||
                        meta.icon!.endsWith('.svg')
 | 
					                        width: 36,
 | 
				
			||||||
                            ? SvgPicture.network(meta.icon!)
 | 
					                        height: 36,
 | 
				
			||||||
 | 
					                        child: meta.icon!.endsWith('.svg')
 | 
				
			||||||
 | 
					                            ? SvgPicture.network(meta.icon!, width: 36, height: 36)
 | 
				
			||||||
                            : UniversalImage(
 | 
					                            : UniversalImage(
 | 
				
			||||||
                                meta.icon!,
 | 
					                                meta.icon!,
 | 
				
			||||||
 | 
					                                noErrorWidget: true,
 | 
				
			||||||
                                width: 36,
 | 
					                                width: 36,
 | 
				
			||||||
                                height: 36,
 | 
					                                height: 36,
 | 
				
			||||||
                                cacheHeight: 36,
 | 
					                                cacheHeight: 36,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,7 @@ class MarkdownTextContent extends StatelessWidget {
 | 
				
			|||||||
  final bool isAutoWarp;
 | 
					  final bool isAutoWarp;
 | 
				
			||||||
  final bool isEnlargeSticker;
 | 
					  final bool isEnlargeSticker;
 | 
				
			||||||
  final TextScaler? textScaler;
 | 
					  final TextScaler? textScaler;
 | 
				
			||||||
 | 
					  final Color? textColor;
 | 
				
			||||||
  final List<SnAttachment?>? attachments;
 | 
					  final List<SnAttachment?>? attachments;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const MarkdownTextContent({
 | 
					  const MarkdownTextContent({
 | 
				
			||||||
@@ -28,6 +29,7 @@ class MarkdownTextContent extends StatelessWidget {
 | 
				
			|||||||
    this.isAutoWarp = false,
 | 
					    this.isAutoWarp = false,
 | 
				
			||||||
    this.isEnlargeSticker = false,
 | 
					    this.isEnlargeSticker = false,
 | 
				
			||||||
    this.textScaler,
 | 
					    this.textScaler,
 | 
				
			||||||
 | 
					    this.textColor,
 | 
				
			||||||
    this.attachments,
 | 
					    this.attachments,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -42,6 +44,7 @@ class MarkdownTextContent extends StatelessWidget {
 | 
				
			|||||||
        Theme.of(context),
 | 
					        Theme.of(context),
 | 
				
			||||||
      ).copyWith(
 | 
					      ).copyWith(
 | 
				
			||||||
        textScaler: textScaler,
 | 
					        textScaler: textScaler,
 | 
				
			||||||
 | 
					        p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null,
 | 
				
			||||||
        blockquote: TextStyle(
 | 
					        blockquote: TextStyle(
 | 
				
			||||||
          color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
					          color: Theme.of(context).colorScheme.onSurfaceVariant,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,9 +18,7 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
 | 
				
			|||||||
  void initState() {
 | 
					  void initState() {
 | 
				
			||||||
    super.initState();
 | 
					    super.initState();
 | 
				
			||||||
    WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
					    WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
				
			||||||
      context
 | 
					      context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context));
 | 
				
			||||||
          .read<NavigationProvider>()
 | 
					 | 
				
			||||||
          .autoDetectIndex(GoRouter.maybeOf(context));
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -31,36 +29,39 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
 | 
				
			|||||||
    return ListenableBuilder(
 | 
					    return ListenableBuilder(
 | 
				
			||||||
      listenable: nav,
 | 
					      listenable: nav,
 | 
				
			||||||
      builder: (context, _) {
 | 
					      builder: (context, _) {
 | 
				
			||||||
        final destinations =
 | 
					        final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
 | 
				
			||||||
            nav.destinations.where((ele) => ele.isPinned).toList();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return NavigationRail(
 | 
					        return SizedBox(
 | 
				
			||||||
          selectedIndex: nav.currentIndex,
 | 
					          width: 80,
 | 
				
			||||||
          destinations: [
 | 
					          child: NavigationRail(
 | 
				
			||||||
            ...destinations.where((ele) => ele.isPinned).map((ele) {
 | 
					            selectedIndex:
 | 
				
			||||||
              return NavigationRailDestination(
 | 
					                nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
 | 
				
			||||||
                icon: ele.icon,
 | 
					            destinations: [
 | 
				
			||||||
                label: Text(ele.label).tr(),
 | 
					              ...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(
 | 
					            trailing: Expanded(
 | 
				
			||||||
                IconButton(
 | 
					              child: Align(
 | 
				
			||||||
                  icon: const Icon(Symbols.menu),
 | 
					                alignment: Alignment.bottomCenter,
 | 
				
			||||||
                  onPressed: () {
 | 
					                child: StyledWidget(
 | 
				
			||||||
                    Scaffold.of(context).openDrawer();
 | 
					                  IconButton(
 | 
				
			||||||
                  },
 | 
					                    icon: const Icon(Symbols.menu),
 | 
				
			||||||
                ),
 | 
					                    onPressed: () {
 | 
				
			||||||
              ).padding(bottom: 16),
 | 
					                      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,7 +1,6 @@
 | 
				
			|||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
 | 
					import 'package:bitsdojo_window/bitsdojo_window.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
@@ -12,42 +11,84 @@ import 'package:styled_widget/styled_widget.dart';
 | 
				
			|||||||
import 'package:surface/providers/config.dart';
 | 
					import 'package:surface/providers/config.dart';
 | 
				
			||||||
import 'package:surface/providers/navigation.dart';
 | 
					import 'package:surface/providers/navigation.dart';
 | 
				
			||||||
import 'package:surface/widgets/connection_indicator.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_background.dart';
 | 
				
			||||||
import 'package:surface/widgets/navigation/app_bottom_navigation.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_drawer_navigation.dart';
 | 
				
			||||||
import 'package:surface/widgets/navigation/app_rail_navigation.dart';
 | 
					import 'package:surface/widgets/navigation/app_rail_navigation.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/notify_indicator.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final globalRootScaffoldKey = GlobalKey<ScaffoldState>();
 | 
					final globalRootScaffoldKey = GlobalKey<ScaffoldState>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AppPageScaffold extends StatelessWidget {
 | 
					class AppScaffold extends StatelessWidget {
 | 
				
			||||||
  final String? title;
 | 
					 | 
				
			||||||
  final Widget? body;
 | 
					  final Widget? body;
 | 
				
			||||||
  final bool showAppBar;
 | 
					  final PreferredSizeWidget? bottomNavigationBar;
 | 
				
			||||||
  final bool showBottomNavigation;
 | 
					  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,
 | 
					    super.key,
 | 
				
			||||||
    this.title,
 | 
					    this.appBar,
 | 
				
			||||||
    this.body,
 | 
					    this.body,
 | 
				
			||||||
    this.showAppBar = true,
 | 
					    this.floatingActionButton,
 | 
				
			||||||
    this.showBottomNavigation = false,
 | 
					    this.floatingActionButtonLocation,
 | 
				
			||||||
 | 
					    this.floatingActionButtonAnimator,
 | 
				
			||||||
 | 
					    this.bottomNavigationBar,
 | 
				
			||||||
 | 
					    this.bottomSheet,
 | 
				
			||||||
 | 
					    this.drawer,
 | 
				
			||||||
 | 
					    this.endDrawer,
 | 
				
			||||||
 | 
					    this.onDrawerChanged,
 | 
				
			||||||
 | 
					    this.onEndDrawerChanged,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final state = GoRouter.maybeOf(context);
 | 
					    final appBarHeight = appBar?.preferredSize.height ?? 0;
 | 
				
			||||||
    final routeName = state?.routerDelegate.currentConfiguration.last.route.name;
 | 
					    final safeTop = MediaQuery.of(context).padding.top;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    final autoTitle = state != null ? 'screen${routeName?.capitalize()}' : 'screen';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      appBar: showAppBar
 | 
					      extendBody: true,
 | 
				
			||||||
          ? AppBar(
 | 
					      extendBodyBehindAppBar: true,
 | 
				
			||||||
              title: Text(title ?? autoTitle.tr()),
 | 
					      backgroundColor: Theme.of(context).scaffoldBackgroundColor,
 | 
				
			||||||
            )
 | 
					      body: SizedBox.expand(
 | 
				
			||||||
          : null,
 | 
					        child: AppBackground(
 | 
				
			||||||
      body: body,
 | 
					          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();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -98,62 +139,68 @@ class AppRootScaffold extends StatelessWidget {
 | 
				
			|||||||
      iconMouseDown: Theme.of(context).colorScheme.primary,
 | 
					      iconMouseDown: Theme.of(context).colorScheme.primary,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppBackground(
 | 
					    final safeTop = MediaQuery.of(context).padding.top;
 | 
				
			||||||
      isRoot: true,
 | 
					    final safeBottom = MediaQuery.of(context).padding.bottom;
 | 
				
			||||||
      child: Scaffold(
 | 
					
 | 
				
			||||||
        key: globalRootScaffoldKey,
 | 
					    return Scaffold(
 | 
				
			||||||
        body: Column(
 | 
					      key: globalRootScaffoldKey,
 | 
				
			||||||
          children: [
 | 
					      backgroundColor: Theme.of(context).colorScheme.surface,
 | 
				
			||||||
            if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
 | 
					      body: Stack(
 | 
				
			||||||
              Container(
 | 
					        children: [
 | 
				
			||||||
                decoration: BoxDecoration(
 | 
					          Column(
 | 
				
			||||||
                  border: Border(
 | 
					            children: [
 | 
				
			||||||
                    bottom: BorderSide(
 | 
					              if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
 | 
				
			||||||
                      color: Theme.of(context).dividerColor,
 | 
					                WindowTitleBarBox(
 | 
				
			||||||
                      width: 1 / devicePixelRatio,
 | 
					                  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(
 | 
					              Expanded(child: innerWidget),
 | 
				
			||||||
                  crossAxisAlignment: CrossAxisAlignment.center,
 | 
					            ],
 | 
				
			||||||
                  mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
 | 
					          ),
 | 
				
			||||||
                  children: [
 | 
					          Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
 | 
				
			||||||
                    WindowTitleBarBox(
 | 
					          if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE))
 | 
				
			||||||
                      child: MoveWindow(
 | 
					            Positioned(bottom: safeBottom > 0 ? safeBottom : 16, left: 0, right: 0, child: ConnectionIndicator())
 | 
				
			||||||
                        child: Text(
 | 
					          else
 | 
				
			||||||
                          'Solar Network',
 | 
					            Positioned(top: safeTop > 0 ? safeTop : 16, left: 0, right: 0, child: ConnectionIndicator()),
 | 
				
			||||||
                          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: !isExpandedDrawer ? AppNavigationDrawer() : null,
 | 
					 | 
				
			||||||
        drawerEdgeDragWidth: isPopable ? 0 : null,
 | 
					 | 
				
			||||||
        bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
 | 
					      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:io';
 | 
				
			||||||
import 'dart:math' as math;
 | 
					import 'dart:math' as math;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:file_saver/file_saver.dart';
 | 
					import 'package:file_saver/file_saver.dart';
 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
@@ -34,6 +35,7 @@ import 'package:surface/widgets/post/post_meta_editor.dart';
 | 
				
			|||||||
import 'package:surface/widgets/post/post_reaction.dart';
 | 
					import 'package:surface/widgets/post/post_reaction.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/publisher_popover.dart';
 | 
					import 'package:surface/widgets/post/publisher_popover.dart';
 | 
				
			||||||
import 'package:surface/widgets/universal_image.dart';
 | 
					import 'package:surface/widgets/universal_image.dart';
 | 
				
			||||||
 | 
					import 'package:xml/xml.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostItem extends StatelessWidget {
 | 
					class PostItem extends StatelessWidget {
 | 
				
			||||||
  final SnPost data;
 | 
					  final SnPost data;
 | 
				
			||||||
@@ -203,6 +205,8 @@ class PostItem extends StatelessWidget {
 | 
				
			|||||||
        ?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
 | 
					        ?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
 | 
				
			||||||
        .toList();
 | 
					        .toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final cfg = context.read<ConfigProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Column(
 | 
					    return Column(
 | 
				
			||||||
      crossAxisAlignment: CrossAxisAlignment.center,
 | 
					      crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
@@ -256,13 +260,12 @@ class PostItem extends StatelessWidget {
 | 
				
			|||||||
          AttachmentList(
 | 
					          AttachmentList(
 | 
				
			||||||
            data: displayableAttachments!,
 | 
					            data: displayableAttachments!,
 | 
				
			||||||
            bordered: true,
 | 
					            bordered: true,
 | 
				
			||||||
            gridded: true,
 | 
					 | 
				
			||||||
            maxHeight: showFullPost ? null : 480,
 | 
					            maxHeight: showFullPost ? null : 480,
 | 
				
			||||||
            minWidth: 640,
 | 
					            maxWidth: MediaQuery.of(context).size.width - 20,
 | 
				
			||||||
            fit: showFullPost ? BoxFit.cover : BoxFit.contain,
 | 
					            fit: showFullPost ? BoxFit.cover : BoxFit.contain,
 | 
				
			||||||
            padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
					            padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        if (data.body['content'] != null)
 | 
					        if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
 | 
				
			||||||
          LinkPreviewWidget(
 | 
					          LinkPreviewWidget(
 | 
				
			||||||
            text: data.body['content'],
 | 
					            text: data.body['content'],
 | 
				
			||||||
          ).padding(horizontal: 4),
 | 
					          ).padding(horizontal: 4),
 | 
				
			||||||
@@ -344,7 +347,7 @@ class PostShareImageWidget extends StatelessWidget {
 | 
				
			|||||||
          if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
 | 
					          if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
 | 
				
			||||||
            StyledWidget(AttachmentList(
 | 
					            StyledWidget(AttachmentList(
 | 
				
			||||||
              data: data.preload!.attachments!,
 | 
					              data: data.preload!.attachments!,
 | 
				
			||||||
              gridded: true,
 | 
					              columned: true,
 | 
				
			||||||
            )).padding(horizontal: 16, bottom: 8),
 | 
					            )).padding(horizontal: 16, bottom: 8),
 | 
				
			||||||
          Column(
 | 
					          Column(
 | 
				
			||||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
					            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
@@ -816,6 +819,22 @@ class _PostContentHeader extends StatelessWidget {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              const PopupMenuDivider(),
 | 
					              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(
 | 
					              PopupMenuItem(
 | 
				
			||||||
                onTap: onShare,
 | 
					                onTap: onShare,
 | 
				
			||||||
                child: Row(
 | 
					                child: Row(
 | 
				
			||||||
@@ -1180,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),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
import 'package:gap/gap.dart';
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
@@ -12,6 +13,7 @@ import 'package:surface/widgets/dialog.dart';
 | 
				
			|||||||
class PostReactionPopup extends StatefulWidget {
 | 
					class PostReactionPopup extends StatefulWidget {
 | 
				
			||||||
  final SnPost data;
 | 
					  final SnPost data;
 | 
				
			||||||
  final Function(Map<String, int> value, int attr, int delta)? onChanged;
 | 
					  final Function(Map<String, int> value, int attr, int delta)? onChanged;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const PostReactionPopup({super.key, required this.data, this.onChanged});
 | 
					  const PostReactionPopup({super.key, required this.data, this.onChanged});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -59,6 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
 | 
				
			|||||||
          );
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      HapticFeedback.heavyImpact();
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      // ignore: use_build_context_synchronously
 | 
					      // ignore: use_build_context_synchronously
 | 
				
			||||||
      if (context.mounted) context.showErrorDialog(err);
 | 
					      if (context.mounted) context.showErrorDialog(err);
 | 
				
			||||||
@@ -84,9 +87,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
 | 
				
			|||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
              const Icon(Symbols.mood, size: 24),
 | 
					              const Icon(Symbols.mood, size: 24),
 | 
				
			||||||
              const Gap(16),
 | 
					              const Gap(16),
 | 
				
			||||||
              Text('postReactions')
 | 
					              Text('postReactions').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
				
			||||||
                  .tr()
 | 
					 | 
				
			||||||
                  .textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
					 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
					          ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
				
			||||||
          Container(
 | 
					          Container(
 | 
				
			||||||
@@ -102,9 +103,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
 | 
				
			|||||||
                Text('postReactionDownvote').plural(widget.data.totalDownvote),
 | 
					                Text('postReactionDownvote').plural(widget.data.totalDownvote),
 | 
				
			||||||
                const Gap(24),
 | 
					                const Gap(24),
 | 
				
			||||||
                Icon(
 | 
					                Icon(
 | 
				
			||||||
                  widget.data.totalUpvote >= widget.data.totalDownvote
 | 
					                  widget.data.totalUpvote >= widget.data.totalDownvote ? Symbols.trending_up : Symbols.trending_down,
 | 
				
			||||||
                      ? Symbols.trending_up
 | 
					 | 
				
			||||||
                      : Symbols.trending_down,
 | 
					 | 
				
			||||||
                  size: 16,
 | 
					                  size: 16,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const Gap(8),
 | 
					                const Gap(8),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,10 +5,9 @@ import 'package:material_symbols_icons/symbols.dart';
 | 
				
			|||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:flutter_animate/flutter_animate.dart';
 | 
					import 'package:flutter_animate/flutter_animate.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/config.dart';
 | 
				
			||||||
// Keep this import to make the web image render work
 | 
					// 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:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
 | 
				
			||||||
import 'package:surface/providers/config.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UniversalImage extends StatelessWidget {
 | 
					class UniversalImage extends StatelessWidget {
 | 
				
			||||||
  final String url;
 | 
					  final String url;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,11 +8,13 @@ import Foundation
 | 
				
			|||||||
import bitsdojo_window_macos
 | 
					import bitsdojo_window_macos
 | 
				
			||||||
import connectivity_plus
 | 
					import connectivity_plus
 | 
				
			||||||
import device_info_plus
 | 
					import device_info_plus
 | 
				
			||||||
 | 
					import file_picker
 | 
				
			||||||
import file_saver
 | 
					import file_saver
 | 
				
			||||||
import file_selector_macos
 | 
					import file_selector_macos
 | 
				
			||||||
import firebase_analytics
 | 
					import firebase_analytics
 | 
				
			||||||
import firebase_core
 | 
					import firebase_core
 | 
				
			||||||
import firebase_messaging
 | 
					import firebase_messaging
 | 
				
			||||||
 | 
					import flutter_inappwebview_macos
 | 
				
			||||||
import flutter_udid
 | 
					import flutter_udid
 | 
				
			||||||
import flutter_webrtc
 | 
					import flutter_webrtc
 | 
				
			||||||
import gal
 | 
					import gal
 | 
				
			||||||
@@ -35,11 +37,13 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
				
			|||||||
  BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
 | 
					  BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
 | 
				
			||||||
  ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
 | 
					  ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
 | 
				
			||||||
  DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
 | 
					  DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
 | 
				
			||||||
 | 
					  FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
 | 
				
			||||||
  FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
 | 
					  FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
 | 
				
			||||||
  FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
 | 
					  FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
 | 
				
			||||||
  FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
 | 
					  FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
 | 
				
			||||||
  FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
 | 
					  FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
 | 
				
			||||||
  FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
 | 
					  FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
 | 
				
			||||||
 | 
					  InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
 | 
				
			||||||
  FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
 | 
					  FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
 | 
				
			||||||
  FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
 | 
					  FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
 | 
				
			||||||
  GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
 | 
					  GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,63 +8,65 @@ PODS:
 | 
				
			|||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - device_info_plus (0.0.1):
 | 
					  - device_info_plus (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
 | 
					  - file_picker (0.0.1):
 | 
				
			||||||
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - file_saver (0.0.1):
 | 
					  - file_saver (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - file_selector_macos (0.0.1):
 | 
					  - file_selector_macos (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - Firebase/Analytics (11.4.0):
 | 
					  - Firebase/Analytics (11.6.0):
 | 
				
			||||||
    - Firebase/Core
 | 
					    - Firebase/Core
 | 
				
			||||||
  - Firebase/Core (11.4.0):
 | 
					  - Firebase/Core (11.6.0):
 | 
				
			||||||
    - Firebase/CoreOnly
 | 
					    - Firebase/CoreOnly
 | 
				
			||||||
    - FirebaseAnalytics (~> 11.4.0)
 | 
					    - FirebaseAnalytics (~> 11.6.0)
 | 
				
			||||||
  - Firebase/CoreOnly (11.4.0):
 | 
					  - Firebase/CoreOnly (11.6.0):
 | 
				
			||||||
    - FirebaseCore (= 11.4.0)
 | 
					    - FirebaseCore (~> 11.6.0)
 | 
				
			||||||
  - Firebase/Messaging (11.4.0):
 | 
					  - Firebase/Messaging (11.6.0):
 | 
				
			||||||
    - Firebase/CoreOnly
 | 
					    - Firebase/CoreOnly
 | 
				
			||||||
    - FirebaseMessaging (~> 11.4.0)
 | 
					    - FirebaseMessaging (~> 11.6.0)
 | 
				
			||||||
  - firebase_analytics (11.3.6):
 | 
					  - firebase_analytics (11.4.1):
 | 
				
			||||||
    - Firebase/Analytics (= 11.4.0)
 | 
					    - Firebase/Analytics (= 11.6.0)
 | 
				
			||||||
    - firebase_core
 | 
					    - firebase_core
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - firebase_core (3.9.0):
 | 
					  - firebase_core (3.10.1):
 | 
				
			||||||
    - Firebase/CoreOnly (~> 11.4.0)
 | 
					    - Firebase/CoreOnly (~> 11.6.0)
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - firebase_messaging (15.1.6):
 | 
					  - firebase_messaging (15.2.1):
 | 
				
			||||||
    - Firebase/CoreOnly (~> 11.4.0)
 | 
					    - Firebase/CoreOnly (~> 11.6.0)
 | 
				
			||||||
    - Firebase/Messaging (~> 11.4.0)
 | 
					    - Firebase/Messaging (~> 11.6.0)
 | 
				
			||||||
    - firebase_core
 | 
					    - firebase_core
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - FirebaseAnalytics (11.4.0):
 | 
					  - FirebaseAnalytics (11.6.0):
 | 
				
			||||||
    - FirebaseAnalytics/AdIdSupport (= 11.4.0)
 | 
					    - FirebaseAnalytics/AdIdSupport (= 11.6.0)
 | 
				
			||||||
    - FirebaseCore (~> 11.0)
 | 
					    - FirebaseCore (~> 11.6.0)
 | 
				
			||||||
    - FirebaseInstallations (~> 11.0)
 | 
					    - FirebaseInstallations (~> 11.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
  - FirebaseAnalytics/AdIdSupport (11.4.0):
 | 
					  - FirebaseAnalytics/AdIdSupport (11.6.0):
 | 
				
			||||||
    - FirebaseCore (~> 11.0)
 | 
					    - FirebaseCore (~> 11.6.0)
 | 
				
			||||||
    - FirebaseInstallations (~> 11.0)
 | 
					    - FirebaseInstallations (~> 11.0)
 | 
				
			||||||
    - GoogleAppMeasurement (= 11.4.0)
 | 
					    - GoogleAppMeasurement (= 11.6.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
  - FirebaseCore (11.4.0):
 | 
					  - FirebaseCore (11.6.0):
 | 
				
			||||||
    - FirebaseCoreInternal (~> 11.0)
 | 
					    - FirebaseCoreInternal (~> 11.6.0)
 | 
				
			||||||
    - GoogleUtilities/Environment (~> 8.0)
 | 
					    - GoogleUtilities/Environment (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Logger (~> 8.0)
 | 
					    - GoogleUtilities/Logger (~> 8.0)
 | 
				
			||||||
  - FirebaseCoreInternal (11.6.0):
 | 
					  - FirebaseCoreInternal (11.6.0):
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
  - FirebaseInstallations (11.4.0):
 | 
					  - FirebaseInstallations (11.6.0):
 | 
				
			||||||
    - FirebaseCore (~> 11.0)
 | 
					    - FirebaseCore (~> 11.6.0)
 | 
				
			||||||
    - GoogleUtilities/Environment (~> 8.0)
 | 
					    - GoogleUtilities/Environment (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/UserDefaults (~> 8.0)
 | 
					    - GoogleUtilities/UserDefaults (~> 8.0)
 | 
				
			||||||
    - PromisesObjC (~> 2.4)
 | 
					    - PromisesObjC (~> 2.4)
 | 
				
			||||||
  - FirebaseMessaging (11.4.0):
 | 
					  - FirebaseMessaging (11.6.0):
 | 
				
			||||||
    - FirebaseCore (~> 11.0)
 | 
					    - FirebaseCore (~> 11.6.0)
 | 
				
			||||||
    - FirebaseInstallations (~> 11.0)
 | 
					    - FirebaseInstallations (~> 11.0)
 | 
				
			||||||
    - GoogleDataTransport (~> 10.0)
 | 
					    - GoogleDataTransport (~> 10.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
@@ -72,31 +74,34 @@ PODS:
 | 
				
			|||||||
    - GoogleUtilities/Reachability (~> 8.0)
 | 
					    - GoogleUtilities/Reachability (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/UserDefaults (~> 8.0)
 | 
					    - GoogleUtilities/UserDefaults (~> 8.0)
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
 | 
					  - flutter_inappwebview_macos (0.0.1):
 | 
				
			||||||
 | 
					    - FlutterMacOS
 | 
				
			||||||
 | 
					    - OrderedSet (~> 6.0.3)
 | 
				
			||||||
  - flutter_udid (0.0.1):
 | 
					  - flutter_udid (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
    - SAMKeychain
 | 
					    - SAMKeychain
 | 
				
			||||||
  - flutter_webrtc (0.12.2):
 | 
					  - flutter_webrtc (0.12.6):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
    - WebRTC-SDK (= 125.6422.06)
 | 
					    - WebRTC-SDK (= 125.6422.06)
 | 
				
			||||||
  - FlutterMacOS (1.0.0)
 | 
					  - FlutterMacOS (1.0.0)
 | 
				
			||||||
  - gal (1.0.0):
 | 
					  - gal (1.0.0):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - GoogleAppMeasurement (11.4.0):
 | 
					  - GoogleAppMeasurement (11.6.0):
 | 
				
			||||||
    - GoogleAppMeasurement/AdIdSupport (= 11.4.0)
 | 
					    - GoogleAppMeasurement/AdIdSupport (= 11.6.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
  - GoogleAppMeasurement/AdIdSupport (11.4.0):
 | 
					  - GoogleAppMeasurement/AdIdSupport (11.6.0):
 | 
				
			||||||
    - GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0)
 | 
					    - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
					    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
  - GoogleAppMeasurement/WithoutAdIdSupport (11.4.0):
 | 
					  - GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
 | 
				
			||||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
					    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/Network (~> 8.0)
 | 
					    - GoogleUtilities/Network (~> 8.0)
 | 
				
			||||||
@@ -134,7 +139,7 @@ PODS:
 | 
				
			|||||||
    - GoogleUtilities/Privacy
 | 
					    - GoogleUtilities/Privacy
 | 
				
			||||||
  - in_app_review (2.0.0):
 | 
					  - in_app_review (2.0.0):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - livekit_client (2.3.4):
 | 
					  - livekit_client (2.3.5):
 | 
				
			||||||
    - flutter_webrtc
 | 
					    - flutter_webrtc
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
    - WebRTC-SDK (= 125.6422.06)
 | 
					    - WebRTC-SDK (= 125.6422.06)
 | 
				
			||||||
@@ -149,6 +154,7 @@ PODS:
 | 
				
			|||||||
    - nanopb/encode (= 3.30910.0)
 | 
					    - nanopb/encode (= 3.30910.0)
 | 
				
			||||||
  - nanopb/decode (3.30910.0)
 | 
					  - nanopb/decode (3.30910.0)
 | 
				
			||||||
  - nanopb/encode (3.30910.0)
 | 
					  - nanopb/encode (3.30910.0)
 | 
				
			||||||
 | 
					  - OrderedSet (6.0.3)
 | 
				
			||||||
  - package_info_plus (0.0.1):
 | 
					  - package_info_plus (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - pasteboard (0.0.1):
 | 
					  - pasteboard (0.0.1):
 | 
				
			||||||
@@ -181,11 +187,13 @@ DEPENDENCIES:
 | 
				
			|||||||
  - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
 | 
					  - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
 | 
				
			||||||
  - croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
 | 
					  - croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
 | 
				
			||||||
  - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/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_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`)
 | 
				
			||||||
  - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
 | 
					  - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
 | 
				
			||||||
  - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
 | 
					  - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
 | 
				
			||||||
  - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
 | 
					  - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
 | 
				
			||||||
  - firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/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_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
 | 
				
			||||||
  - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
 | 
					  - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
 | 
				
			||||||
  - FlutterMacOS (from `Flutter/ephemeral`)
 | 
					  - FlutterMacOS (from `Flutter/ephemeral`)
 | 
				
			||||||
@@ -218,6 +226,7 @@ SPEC REPOS:
 | 
				
			|||||||
    - GoogleDataTransport
 | 
					    - GoogleDataTransport
 | 
				
			||||||
    - GoogleUtilities
 | 
					    - GoogleUtilities
 | 
				
			||||||
    - nanopb
 | 
					    - nanopb
 | 
				
			||||||
 | 
					    - OrderedSet
 | 
				
			||||||
    - PromisesObjC
 | 
					    - PromisesObjC
 | 
				
			||||||
    - SAMKeychain
 | 
					    - SAMKeychain
 | 
				
			||||||
    - WebRTC-SDK
 | 
					    - WebRTC-SDK
 | 
				
			||||||
@@ -231,6 +240,8 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
    :path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
 | 
				
			||||||
  device_info_plus:
 | 
					  device_info_plus:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
 | 
				
			||||||
 | 
					  file_picker:
 | 
				
			||||||
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
 | 
				
			||||||
  file_saver:
 | 
					  file_saver:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos
 | 
				
			||||||
  file_selector_macos:
 | 
					  file_selector_macos:
 | 
				
			||||||
@@ -241,6 +252,8 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
    :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
 | 
				
			||||||
  firebase_messaging:
 | 
					  firebase_messaging:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
 | 
				
			||||||
 | 
					  flutter_inappwebview_macos:
 | 
				
			||||||
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
 | 
				
			||||||
  flutter_udid:
 | 
					  flutter_udid:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
 | 
				
			||||||
  flutter_webrtc:
 | 
					  flutter_webrtc:
 | 
				
			||||||
@@ -285,30 +298,33 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
 | 
					  connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
 | 
				
			||||||
  croppy: 25a638bd7d05411d8c697f481568f261037694fc
 | 
					  croppy: 25a638bd7d05411d8c697f481568f261037694fc
 | 
				
			||||||
  device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
 | 
					  device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
 | 
				
			||||||
 | 
					  file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
 | 
				
			||||||
  file_saver: 44e6fbf666677faf097302460e214e977fdd977b
 | 
					  file_saver: 44e6fbf666677faf097302460e214e977fdd977b
 | 
				
			||||||
  file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
 | 
					  file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
 | 
				
			||||||
  Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
 | 
					  Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
 | 
				
			||||||
  firebase_analytics: a80b3d6645f2f12d626fde928b61dae12e5ea2ef
 | 
					  firebase_analytics: 91efc58e8e37964469fdd59ad11ba36bc97e75d6
 | 
				
			||||||
  firebase_core: 1dfe1f4d02ad78be0277e320aa3d8384cf46231f
 | 
					  firebase_core: 75e003524565fb5bd80c9960bc5892e8475821cd
 | 
				
			||||||
  firebase_messaging: 61f678060b69a7ae1013e3a939ec8e1c56ef6fcf
 | 
					  firebase_messaging: 082a385eb98b5bb843a566cb30404859c4bd6e25
 | 
				
			||||||
  FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
 | 
					  FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
 | 
				
			||||||
  FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
 | 
					  FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
 | 
				
			||||||
  FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
 | 
					  FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
 | 
				
			||||||
  FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
 | 
					  FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
 | 
				
			||||||
  FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
 | 
					  FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
 | 
				
			||||||
 | 
					  flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
 | 
				
			||||||
  flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
 | 
					  flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
 | 
				
			||||||
  flutter_webrtc: 53c9e1285ab32dfb58afb1e1471416a877e23d7a
 | 
					  flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
 | 
				
			||||||
  FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
 | 
					  FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
 | 
				
			||||||
  gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
 | 
					  gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
 | 
				
			||||||
  GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
 | 
					  GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
 | 
				
			||||||
  GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
 | 
					  GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
 | 
				
			||||||
  GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
 | 
					  GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
 | 
				
			||||||
  in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
 | 
					  in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
 | 
				
			||||||
  livekit_client: b7ab91e79e657d7d40da16cb2f90d517cb72d406
 | 
					  livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1
 | 
				
			||||||
  media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
 | 
					  media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
 | 
				
			||||||
  media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
 | 
					  media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
 | 
				
			||||||
  media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
 | 
					  media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
 | 
				
			||||||
  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
					  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
				
			||||||
 | 
					  OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
 | 
				
			||||||
  package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
 | 
					  package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
 | 
				
			||||||
  pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
 | 
					  pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
 | 
				
			||||||
  path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
 | 
					  path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										230
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										230
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -13,10 +13,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: _flutterfire_internals
 | 
					      name: _flutterfire_internals
 | 
				
			||||||
      sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe
 | 
					      sha256: e4f2a7ef31b0ab2c89d2bde35ef3e6e6aff1dce5e66069c6540b0e9cfe33ee6b
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.3.48"
 | 
					    version: "1.3.50"
 | 
				
			||||||
  _macros:
 | 
					  _macros:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description: dart
 | 
					    description: dart
 | 
				
			||||||
@@ -266,10 +266,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: connectivity_plus
 | 
					      name: connectivity_plus
 | 
				
			||||||
      sha256: e0817759ec6d2d8e57eb234e6e57d2173931367a865850c7acea40d4b4f9c27d
 | 
					      sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.1.1"
 | 
					    version: "6.1.2"
 | 
				
			||||||
  connectivity_plus_platform_interface:
 | 
					  connectivity_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -290,10 +290,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: croppy
 | 
					      name: croppy
 | 
				
			||||||
      sha256: "14bb40fd6c1771b093a907ddbf24df9aa49a4e6e379dd630602eb446e30ec629"
 | 
					      sha256: bf99b00023df0d7d047e04d27d496d87cbefd968f578d0bd30f342ff75570a12
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.3.1"
 | 
					    version: "1.3.3"
 | 
				
			||||||
  cross_file:
 | 
					  cross_file:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -338,10 +338,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: dart_style
 | 
					      name: dart_style
 | 
				
			||||||
      sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
 | 
					      sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.3.7"
 | 
					    version: "2.3.8"
 | 
				
			||||||
  dart_webrtc:
 | 
					  dart_webrtc:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -354,18 +354,18 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: dbus
 | 
					      name: dbus
 | 
				
			||||||
      sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
 | 
					      sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.7.10"
 | 
					    version: "0.7.11"
 | 
				
			||||||
  device_info_plus:
 | 
					  device_info_plus:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: device_info_plus
 | 
					      name: device_info_plus
 | 
				
			||||||
      sha256: "4fa68e53e26ab17b70ca39f072c285562cfc1589df5bb1e9295db90f6645f431"
 | 
					      sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "11.2.0"
 | 
					    version: "11.2.2"
 | 
				
			||||||
  device_info_plus_platform_interface:
 | 
					  device_info_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -378,10 +378,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: dio
 | 
					      name: dio
 | 
				
			||||||
      sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
 | 
					      sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "5.7.0"
 | 
					    version: "5.8.0+1"
 | 
				
			||||||
  dio_smart_retry:
 | 
					  dio_smart_retry:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -394,10 +394,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: dio_web_adapter
 | 
					      name: dio_web_adapter
 | 
				
			||||||
      sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
 | 
					      sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.0.0"
 | 
					    version: "2.1.0"
 | 
				
			||||||
  dismissible_page:
 | 
					  dismissible_page:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -418,10 +418,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: easy_localization
 | 
					      name: easy_localization
 | 
				
			||||||
      sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201
 | 
					      sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.0.7"
 | 
					    version: "3.0.7+1"
 | 
				
			||||||
  easy_localization_loader:
 | 
					  easy_localization_loader:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -490,10 +490,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: file_picker
 | 
					      name: file_picker
 | 
				
			||||||
      sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204
 | 
					      sha256: c9943dd7d702ab4199d199bc151a2d79c86db031a02ad84566dab58c494d2adc
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "8.1.7"
 | 
					    version: "8.3.1"
 | 
				
			||||||
  file_saver:
 | 
					  file_saver:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -538,34 +538,34 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: firebase_analytics
 | 
					      name: firebase_analytics
 | 
				
			||||||
      sha256: "366140abb55418ea23060b779893fa997c2d8e1974a4d1cc4d9590933b65c5fd"
 | 
					      sha256: eac382bbcd5ae78c1d1ce5619d13f5a7424429f4bf55df9e3ad5110da34d1060
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "11.3.6"
 | 
					    version: "11.4.1"
 | 
				
			||||||
  firebase_analytics_platform_interface:
 | 
					  firebase_analytics_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: firebase_analytics_platform_interface
 | 
					      name: firebase_analytics_platform_interface
 | 
				
			||||||
      sha256: "8e987cf977c0c8f4ad02d9950a9b25b1a9606899f37b66a322a43af05be0246b"
 | 
					      sha256: a34db46c367265c4c961626e4b128bfb7b7e50958e7add4c27ba103f5f81b9b0
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.2.8"
 | 
					    version: "4.3.1"
 | 
				
			||||||
  firebase_analytics_web:
 | 
					  firebase_analytics_web:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: firebase_analytics_web
 | 
					      name: firebase_analytics_web
 | 
				
			||||||
      sha256: "0b64ef9060d394bba3d3b4777f49ee098efeeea7b0afb04663c956de6a3da170"
 | 
					      sha256: b6b4cef08e45e4c7d48476d9fc49fe9577081809a59026fe95b1a1b1eea165fa
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.5.10+5"
 | 
					    version: "0.5.10+7"
 | 
				
			||||||
  firebase_core:
 | 
					  firebase_core:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: firebase_core
 | 
					      name: firebase_core
 | 
				
			||||||
      sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde"
 | 
					      sha256: d851c1ca98fd5a4c07c747f8c65dacc2edd84a4d9ac055d32a5f0342529069f5
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.9.0"
 | 
					    version: "3.10.1"
 | 
				
			||||||
  firebase_core_platform_interface:
 | 
					  firebase_core_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -586,26 +586,26 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: firebase_messaging
 | 
					      name: firebase_messaging
 | 
				
			||||||
      sha256: "151a3ee68736abf293aab66d1317ade53c88abe1db09c75a0460aebf7767bbdf"
 | 
					      sha256: e20ea2a0ecf9b0971575ab3ab42a6e285a94e50092c555b090c1a588a81b4d54
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "15.1.6"
 | 
					    version: "15.2.1"
 | 
				
			||||||
  firebase_messaging_platform_interface:
 | 
					  firebase_messaging_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: firebase_messaging_platform_interface
 | 
					      name: firebase_messaging_platform_interface
 | 
				
			||||||
      sha256: f331ee51e40c243f90cc7bc059222dfec4e5df53125b08d31fb28961b00d2a9d
 | 
					      sha256: c57a92b5ae1857ef4fe4ae2e73452b44d32e984e15ab8b53415ea1bb514bdabd
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.5.49"
 | 
					    version: "4.6.1"
 | 
				
			||||||
  firebase_messaging_web:
 | 
					  firebase_messaging_web:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: firebase_messaging_web
 | 
					      name: firebase_messaging_web
 | 
				
			||||||
      sha256: efaf3fdc54cd77e0eedb8e75f7f01c808828c64d052ddbf94d3009974e47d30f
 | 
					      sha256: "83694a990d8525d6b01039240b97757298369622ca0253ad0ebcfed221bf8ee0"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.9.5"
 | 
					    version: "3.10.1"
 | 
				
			||||||
  fixnum:
 | 
					  fixnum:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -618,10 +618,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: fl_chart
 | 
					      name: fl_chart
 | 
				
			||||||
      sha256: "10ddaf334fe84d59333a12d153043e366f243e0bdfff2df0313e1e249f5bf926"
 | 
					      sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.70.1"
 | 
					    version: "0.70.2"
 | 
				
			||||||
  flutter:
 | 
					  flutter:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description: flutter
 | 
					    description: flutter
 | 
				
			||||||
@@ -675,14 +675,78 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.3.0"
 | 
					    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:
 | 
					  flutter_launcher_icons:
 | 
				
			||||||
    dependency: "direct dev"
 | 
					    dependency: "direct dev"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_launcher_icons
 | 
					      name: flutter_launcher_icons
 | 
				
			||||||
      sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5"
 | 
					      sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.14.2"
 | 
					    version: "0.14.3"
 | 
				
			||||||
  flutter_lints:
 | 
					  flutter_lints:
 | 
				
			||||||
    dependency: "direct dev"
 | 
					    dependency: "direct dev"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -740,10 +804,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_svg
 | 
					      name: flutter_svg
 | 
				
			||||||
      sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123"
 | 
					      sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.0.16"
 | 
					    version: "2.0.17"
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
    dependency: "direct dev"
 | 
					    dependency: "direct dev"
 | 
				
			||||||
    description: flutter
 | 
					    description: flutter
 | 
				
			||||||
@@ -766,10 +830,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_webrtc
 | 
					      name: flutter_webrtc
 | 
				
			||||||
      sha256: e82ffd0d0b79621c5554eed73509d7f5bd286d57fef29a573846785c65237fb1
 | 
					      sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.12.5+hotfix.2"
 | 
					    version: "0.12.7"
 | 
				
			||||||
  freezed:
 | 
					  freezed:
 | 
				
			||||||
    dependency: "direct dev"
 | 
					    dependency: "direct dev"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -814,18 +878,18 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: glob
 | 
					      name: glob
 | 
				
			||||||
      sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
 | 
					      sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.1.2"
 | 
					    version: "2.1.3"
 | 
				
			||||||
  go_router:
 | 
					  go_router:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: go_router
 | 
					      name: go_router
 | 
				
			||||||
      sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d"
 | 
					      sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "14.6.3"
 | 
					    version: "14.7.2"
 | 
				
			||||||
  google_fonts:
 | 
					  google_fonts:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -875,7 +939,7 @@ packages:
 | 
				
			|||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.7.0"
 | 
					    version: "0.7.0"
 | 
				
			||||||
  html:
 | 
					  html:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: html
 | 
					      name: html
 | 
				
			||||||
      sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
 | 
					      sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
 | 
				
			||||||
@@ -886,10 +950,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: http
 | 
					      name: http
 | 
				
			||||||
      sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
 | 
					      sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.2.2"
 | 
					    version: "1.3.0"
 | 
				
			||||||
  http_multi_server:
 | 
					  http_multi_server:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -934,10 +998,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: image_picker_android
 | 
					      name: image_picker_android
 | 
				
			||||||
      sha256: aa6f1280b670861ac45220cc95adc59bb6ae130259d36f980ccb62220dc5e59f
 | 
					      sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.8.12+19"
 | 
					    version: "0.8.12+20"
 | 
				
			||||||
  image_picker_for_web:
 | 
					  image_picker_for_web:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -966,18 +1030,18 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: image_picker_macos
 | 
					      name: image_picker_macos
 | 
				
			||||||
      sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
 | 
					      sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.2.1+1"
 | 
					    version: "0.2.1+2"
 | 
				
			||||||
  image_picker_platform_interface:
 | 
					  image_picker_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: image_picker_platform_interface
 | 
					      name: image_picker_platform_interface
 | 
				
			||||||
      sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80"
 | 
					      sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.10.0"
 | 
					    version: "2.10.1"
 | 
				
			||||||
  image_picker_windows:
 | 
					  image_picker_windows:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1086,10 +1150,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: livekit_client
 | 
					      name: livekit_client
 | 
				
			||||||
      sha256: a19bcf8640b45e0730b1e3e3e78be7882dad680c6ebe8ae75294fd8d4612450d
 | 
					      sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.3.4+hotfix.2"
 | 
					    version: "2.3.5"
 | 
				
			||||||
  logging:
 | 
					  logging:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1110,10 +1174,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: markdown
 | 
					      name: markdown
 | 
				
			||||||
      sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
 | 
					      sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "7.2.2"
 | 
					    version: "7.3.0"
 | 
				
			||||||
  marquee:
 | 
					  marquee:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1270,10 +1334,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: package_info_plus
 | 
					      name: package_info_plus
 | 
				
			||||||
      sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d"
 | 
					      sha256: b15fad91c4d3d1f2b48c053dd41cb82da007c27407dc9ab5f9aa59881d0e39d4
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "8.1.2"
 | 
					    version: "8.1.4"
 | 
				
			||||||
  package_info_plus_platform_interface:
 | 
					  package_info_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1502,10 +1566,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: pubspec_parse
 | 
					      name: pubspec_parse
 | 
				
			||||||
      sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
 | 
					      sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.4.0"
 | 
					    version: "1.5.0"
 | 
				
			||||||
  qr:
 | 
					  qr:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1630,10 +1694,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: share_plus
 | 
					      name: share_plus
 | 
				
			||||||
      sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400"
 | 
					      sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "10.1.3"
 | 
					    version: "10.1.4"
 | 
				
			||||||
  share_plus_platform_interface:
 | 
					  share_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1646,18 +1710,18 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: shared_preferences
 | 
					      name: shared_preferences
 | 
				
			||||||
      sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a
 | 
					      sha256: "688ee90fbfb6989c980254a56cb26ebe9bb30a3a2dff439a78894211f73de67a"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.3.5"
 | 
					    version: "2.5.1"
 | 
				
			||||||
  shared_preferences_android:
 | 
					  shared_preferences_android:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: shared_preferences_android
 | 
					      name: shared_preferences_android
 | 
				
			||||||
      sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d"
 | 
					      sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.4.0"
 | 
					    version: "2.4.4"
 | 
				
			||||||
  shared_preferences_foundation:
 | 
					  shared_preferences_foundation:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1971,18 +2035,18 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: url_launcher_web
 | 
					      name: url_launcher_web
 | 
				
			||||||
      sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
 | 
					      sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.3.3"
 | 
					    version: "2.4.0"
 | 
				
			||||||
  url_launcher_windows:
 | 
					  url_launcher_windows:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: url_launcher_windows
 | 
					      name: url_launcher_windows
 | 
				
			||||||
      sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
 | 
					      sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.1.3"
 | 
					    version: "3.1.4"
 | 
				
			||||||
  uuid:
 | 
					  uuid:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1995,18 +2059,18 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: vector_graphics
 | 
					      name: vector_graphics
 | 
				
			||||||
      sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
 | 
					      sha256: "7ed22c21d7fdcc88dd6ba7860384af438cd220b251ad65dfc142ab722fabef61"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.1.15"
 | 
					    version: "1.1.16"
 | 
				
			||||||
  vector_graphics_codec:
 | 
					  vector_graphics_codec:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: vector_graphics_codec
 | 
					      name: vector_graphics_codec
 | 
				
			||||||
      sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb"
 | 
					      sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.1.12"
 | 
					    version: "1.1.13"
 | 
				
			||||||
  vector_graphics_compiler:
 | 
					  vector_graphics_compiler:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -2107,10 +2171,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: web_socket_channel
 | 
					      name: web_socket_channel
 | 
				
			||||||
      sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
 | 
					      sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.0.1"
 | 
					    version: "3.0.2"
 | 
				
			||||||
  webrtc_interface:
 | 
					  webrtc_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -2123,10 +2187,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: win32
 | 
					      name: win32
 | 
				
			||||||
      sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29"
 | 
					      sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "5.10.0"
 | 
					    version: "5.10.1"
 | 
				
			||||||
  win32_registry:
 | 
					  win32_registry:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -2152,7 +2216,7 @@ packages:
 | 
				
			|||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.1.0"
 | 
					    version: "1.1.0"
 | 
				
			||||||
  xml:
 | 
					  xml:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: xml
 | 
					      name: xml
 | 
				
			||||||
      sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
 | 
					      sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
 | 
				
			||||||
@@ -2169,4 +2233,4 @@ packages:
 | 
				
			|||||||
    version: "3.1.3"
 | 
					    version: "3.1.3"
 | 
				
			||||||
sdks:
 | 
					sdks:
 | 
				
			||||||
  dart: ">=3.6.0 <4.0.0"
 | 
					  dart: ">=3.6.0 <4.0.0"
 | 
				
			||||||
  flutter: ">=3.24.0"
 | 
					  flutter: ">=3.27.0"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
					# 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
 | 
					# 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.
 | 
					# of the product and file versions while build-number is used as the build suffix.
 | 
				
			||||||
version: 2.2.2+53
 | 
					version: 2.2.2+60
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ^3.5.4
 | 
					  sdk: ^3.5.4
 | 
				
			||||||
@@ -115,6 +115,9 @@ dependencies:
 | 
				
			|||||||
  slide_countdown: ^2.0.2
 | 
					  slide_countdown: ^2.0.2
 | 
				
			||||||
  video_compress: ^3.1.3
 | 
					  video_compress: ^3.1.3
 | 
				
			||||||
  cached_network_image: ^3.4.1
 | 
					  cached_network_image: ^3.4.1
 | 
				
			||||||
 | 
					  flutter_inappwebview: ^6.1.5
 | 
				
			||||||
 | 
					  html: ^0.15.5
 | 
				
			||||||
 | 
					  xml: ^6.5.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,8 @@ id = "solian"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[locations]]
 | 
					[[locations]]
 | 
				
			||||||
id = "solian"
 | 
					id = "solian"
 | 
				
			||||||
host = ["sn.solsynth.dev"]
 | 
					hosts = ["sn.solsynth.dev"]
 | 
				
			||||||
path = ["/"]
 | 
					paths = ["/"]
 | 
				
			||||||
[[locations.destinations]]
 | 
					[[locations.destinations]]
 | 
				
			||||||
id = "solian-web"
 | 
					id = "solian-web"
 | 
				
			||||||
uri = "files:///workdir/solian?fallback=index.html&index=index.html"
 | 
					uri = "files:///workdir/solian?fallback=index.html&index=index.html"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,8 @@
 | 
				
			|||||||
    -->
 | 
					    -->
 | 
				
			||||||
    <base href="$FLUTTER_BASE_HREF">
 | 
					    <base href="$FLUTTER_BASE_HREF">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script type="application/javascript" src="/assets/packages/flutter_inappwebview_web/assets/web/web_support.js" defer></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <meta charset="UTF-8">
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
    <meta content="IE=Edge" http-equiv="X-UA-Compatible">
 | 
					    <meta content="IE=Edge" http-equiv="X-UA-Compatible">
 | 
				
			||||||
    <meta name="description" content="A new Flutter project.">
 | 
					    <meta name="description" content="A new Flutter project.">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@
 | 
				
			|||||||
#include <file_saver/file_saver_plugin.h>
 | 
					#include <file_saver/file_saver_plugin.h>
 | 
				
			||||||
#include <file_selector_windows/file_selector_windows.h>
 | 
					#include <file_selector_windows/file_selector_windows.h>
 | 
				
			||||||
#include <firebase_core/firebase_core_plugin_c_api.h>
 | 
					#include <firebase_core/firebase_core_plugin_c_api.h>
 | 
				
			||||||
 | 
					#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
 | 
				
			||||||
#include <flutter_udid/flutter_udid_plugin_c_api.h>
 | 
					#include <flutter_udid/flutter_udid_plugin_c_api.h>
 | 
				
			||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
 | 
					#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
 | 
				
			||||||
#include <gal/gal_plugin_c_api.h>
 | 
					#include <gal/gal_plugin_c_api.h>
 | 
				
			||||||
@@ -34,6 +35,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
 | 
				
			|||||||
      registry->GetRegistrarForPlugin("FileSelectorWindows"));
 | 
					      registry->GetRegistrarForPlugin("FileSelectorWindows"));
 | 
				
			||||||
  FirebaseCorePluginCApiRegisterWithRegistrar(
 | 
					  FirebaseCorePluginCApiRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
 | 
					      registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
 | 
				
			||||||
 | 
					  FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
 | 
				
			||||||
 | 
					      registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
 | 
				
			||||||
  FlutterUdidPluginCApiRegisterWithRegistrar(
 | 
					  FlutterUdidPluginCApiRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
 | 
					      registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
 | 
				
			||||||
  FlutterWebRTCPluginRegisterWithRegistrar(
 | 
					  FlutterWebRTCPluginRegisterWithRegistrar(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
 | 
				
			|||||||
  file_saver
 | 
					  file_saver
 | 
				
			||||||
  file_selector_windows
 | 
					  file_selector_windows
 | 
				
			||||||
  firebase_core
 | 
					  firebase_core
 | 
				
			||||||
 | 
					  flutter_inappwebview_windows
 | 
				
			||||||
  flutter_udid
 | 
					  flutter_udid
 | 
				
			||||||
  flutter_webrtc
 | 
					  flutter_webrtc
 | 
				
			||||||
  gal
 | 
					  gal
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user