Compare commits
	
		
			41 Commits
		
	
	
		
			2.2.2+56
			...
			3818328afe
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3818328afe | |||
| 11627e2455 | |||
| 3f82c06ff8 | |||
| 2350f59131 | |||
| 9fe7c9530a | |||
| 52f1826e91 | |||
| 28a4c86dbf | |||
| 85e48ce03b | |||
| efef61a8ea | |||
| 10ead95af9 | |||
| 838ee4d55d | |||
| 13e42429a9 | |||
| c6ce3fe2b7 | |||
| ae9a7eb0fd | |||
| 5d6fb2442f | |||
| 5a85985534 | |||
| c80499db03 | |||
| b8dcdb2315 | |||
| b7b921f1f4 | |||
| 319d5c7d7f | |||
| 4b5b001739 | |||
| db8871a455 | |||
| 38dcaa6066 | |||
| 03275b46ca | |||
| cf3b482fef | |||
| aa4c04d4ef | |||
| 73b82f65e4 | |||
| 9471fe40fe | |||
| 0d1e18735e | |||
| 8bb62b5992 | |||
| 1e8a6dea5b | |||
| 5c2804cc4d | |||
| 0dbb8f132a | |||
| 3395f3dbd0 | |||
| d258ba776e | |||
| 0dcfcaad56 | |||
| 687e720956 | |||
| 180876949e | |||
| 9718965809 | |||
| 5377161fb0 | |||
| 963e538ae5 | 
| @@ -19,10 +19,14 @@ | |||||||
|         android:icon="@mipmap/ic_launcher" |         android:icon="@mipmap/ic_launcher" | ||||||
|         android:enableOnBackInvokedCallback="true" |         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" | ||||||
|   | |||||||
| @@ -12,9 +12,9 @@ post { | |||||||
|  |  | ||||||
| body:json { | body:json { | ||||||
|   { |   { | ||||||
|     "alias": "AteChip", |     "alias": "Meltdown", | ||||||
|     "name": "Cat ate chips", |     "name": "Meltdown", | ||||||
|     "attachment_id": "d0b692cc64054463", |     "attachment_id": "IpDPHEbWDDCbBofX", | ||||||
|     "pack_id": 2 |     "pack_id": 4 | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								api/Paperclip/Stickers/Get Sticker Packs.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/Paperclip/Stickers/Get Sticker Packs.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | meta { | ||||||
|  |   name: Get Sticker Packs | ||||||
|  |   type: http | ||||||
|  |   seq: 3 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | get { | ||||||
|  |   url: {{endpoint}}/cgi/uc/stickers/packs | ||||||
|  |   body: none | ||||||
|  |   auth: none | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								api/Paperclip/Stickers/Get Stickers.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								api/Paperclip/Stickers/Get Stickers.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | meta { | ||||||
|  |   name: Get Stickers | ||||||
|  |   type: http | ||||||
|  |   seq: 4 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | get { | ||||||
|  |   url: {{endpoint}}/cgi/uc/stickers?take=10 | ||||||
|  |   body: none | ||||||
|  |   auth: none | ||||||
|  | } | ||||||
|  |  | ||||||
|  | params:query { | ||||||
|  |   take: 10 | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								api/Wallet/Create Order.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/Wallet/Create Order.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | meta { | ||||||
|  |   name: Create Order | ||||||
|  |   type: http | ||||||
|  |   seq: 1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | post { | ||||||
|  |   url: {{endpoint}}/cgi/wa/orders | ||||||
|  |   body: json | ||||||
|  |   auth: none | ||||||
|  | } | ||||||
|  |  | ||||||
|  | body:json { | ||||||
|  |   { | ||||||
|  |     "client_id": "highland-mc", | ||||||
|  |     "client_secret": "(3^DLAvo3v", | ||||||
|  |     "remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。", | ||||||
|  |     "amount": 500 | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								api/Wallet/Create Transaction.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								api/Wallet/Create Transaction.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | meta { | ||||||
|  |   name: Create Transaction | ||||||
|  |   type: http | ||||||
|  |   seq: 3 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | post { | ||||||
|  |   url: {{endpoint}}/cgi/wa/transactions | ||||||
|  |   body: json | ||||||
|  |   auth: none | ||||||
|  | } | ||||||
|  |  | ||||||
|  | body:json { | ||||||
|  |   { | ||||||
|  |     "client_id": "alphabot", | ||||||
|  |     "client_secret": "_uR0sVnHTh", | ||||||
|  |     "remark": "新年红包", | ||||||
|  |     "amount": 9705, | ||||||
|  |     "payee_id": 2 | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								api/Wallet/Get Order.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/Wallet/Get Order.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | meta { | ||||||
|  |   name: Get Order | ||||||
|  |   type: http | ||||||
|  |   seq: 2 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | get { | ||||||
|  |   url: {{endpoint}}/cgi/wa/orders/4 | ||||||
|  |   body: none | ||||||
|  |   auth: none | ||||||
|  | } | ||||||
|  |  | ||||||
|  | body:json { | ||||||
|  |   { | ||||||
|  |     "client_id": "highland-mc", | ||||||
|  |     "client_secret": "(3^DLAvo3v", | ||||||
|  |     "remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。", | ||||||
|  |     "amount": 500 | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								api/Wallet/Get Transaction.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/Wallet/Get Transaction.bru
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | meta { | ||||||
|  |   name: Get Transaction | ||||||
|  |   type: http | ||||||
|  |   seq: 4 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | get { | ||||||
|  |   url: {{endpoint}}/cgi/wa/transactions/67 | ||||||
|  |   body: none | ||||||
|  |   auth: inherit | ||||||
|  | } | ||||||
|  |  | ||||||
|  | body:json { | ||||||
|  |   { | ||||||
|  |     "client_id": "highland-mc", | ||||||
|  |     "client_secret": "(3^DLAvo3v", | ||||||
|  |     "remark": "我是秦始皇,现在被困香港,现在 SN 转我 500 源点,帮助我回到咸阳,到时候封你为太监一职。", | ||||||
|  |     "amount": 500 | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										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 | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								assets/icon/tray-icon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/icon/tray-icon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 16 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icon/tray-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/icon/tray-icon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 228 KiB | 
| @@ -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", | ||||||
| @@ -218,6 +241,8 @@ | |||||||
|   "settingsMisc": "Misc", |   "settingsMisc": "Misc", | ||||||
|   "settingsMiscAbout": "About", |   "settingsMiscAbout": "About", | ||||||
|   "settingsMiscAboutDescription": "View the version information of Solian.", |   "settingsMiscAboutDescription": "View the version information of Solian.", | ||||||
|  |   "settingsAccountLanguage": "Account Language", | ||||||
|  |   "settingsAccountLanguageDescription": "Set the language for email, notification, and other account-related content.", | ||||||
|   "sensitiveContent": "Sensitive Content", |   "sensitiveContent": "Sensitive Content", | ||||||
|   "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.", | ||||||
| @@ -531,11 +556,15 @@ | |||||||
|   "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", | ||||||
|   "shareIntentDescription": "What do you want to do with the content you are sharing?", |   "shareIntentDescription": "What do you want to do with the content you are sharing?", | ||||||
|   "shareIntentPostStory": "Post a Story", |   "shareIntentPostStory": "Post a Story", | ||||||
|  |   "shareIntentSendChannel": "Share to Channel", | ||||||
|   "updateAvailable": "Update Available", |   "updateAvailable": "Update Available", | ||||||
|   "updateOngoing": "Updating, please wait...", |   "updateOngoing": "Updating, please wait...", | ||||||
|   "custom": "Custom", |   "custom": "Custom", | ||||||
| @@ -548,6 +577,7 @@ | |||||||
|   "colorSchemeWhite": "White", |   "colorSchemeWhite": "White", | ||||||
|   "colorSchemeBlack": "Black", |   "colorSchemeBlack": "Black", | ||||||
|   "colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.", |   "colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.", | ||||||
|  |   "postFeaturedComment": "Featured Comment", | ||||||
|   "postCategoryTechnology": "Technology", |   "postCategoryTechnology": "Technology", | ||||||
|   "postCategoryGaming": "Gaming", |   "postCategoryGaming": "Gaming", | ||||||
|   "postCategoryLife": "Life", |   "postCategoryLife": "Life", | ||||||
| @@ -558,5 +588,27 @@ | |||||||
|   "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", | ||||||
|  |   "accountSettingsApplied": "Account settings have been applied.", | ||||||
|  |   "trayMenuExit": "Exit" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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": "清除现存背景图", | ||||||
| @@ -216,6 +239,8 @@ | |||||||
|   "settingsMisc": "杂项", |   "settingsMisc": "杂项", | ||||||
|   "settingsMiscAbout": "关于", |   "settingsMiscAbout": "关于", | ||||||
|   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", |   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", | ||||||
|  |   "settingsAccountLanguage": "帐号偏好语言", | ||||||
|  |   "settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。", | ||||||
|   "sensitiveContent": "敏感内容", |   "sensitiveContent": "敏感内容", | ||||||
|   "sensitiveContentCollapsed": "敏感内容已折叠。", |   "sensitiveContentCollapsed": "敏感内容已折叠。", | ||||||
|   "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。", |   "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。", | ||||||
| @@ -529,11 +554,15 @@ | |||||||
|   "postImageShareAds": "来 Solar Network 探索更多有趣帖子", |   "postImageShareAds": "来 Solar Network 探索更多有趣帖子", | ||||||
|   "postShare": "分享", |   "postShare": "分享", | ||||||
|   "postShareImage": "分享帖图", |   "postShareImage": "分享帖图", | ||||||
|  |   "postGetInsight": "获取见解", | ||||||
|  |   "postGetInsightTitle": "AI 见解", | ||||||
|  |   "postGetInsightDescription": "AI 可能会出错,检查信息真实性。", | ||||||
|   "appInitializing": "正在初始化", |   "appInitializing": "正在初始化", | ||||||
|   "poweredBy": "由 {} 提供支持", |   "poweredBy": "由 {} 提供支持", | ||||||
|   "shareIntent": "分享", |   "shareIntent": "分享", | ||||||
|   "shareIntentDescription": "您想对您分享的内容做些什么?", |   "shareIntentDescription": "您想对您分享的内容做些什么?", | ||||||
|   "shareIntentPostStory": "发布动态", |   "shareIntentPostStory": "发布动态", | ||||||
|  |   "shareIntentSendChannel": "分享到聊天频道", | ||||||
|   "updateAvailable": "检测到更新可用", |   "updateAvailable": "检测到更新可用", | ||||||
|   "updateOngoing": "正在更新,请稍后……", |   "updateOngoing": "正在更新,请稍后……", | ||||||
|   "custom": "自定义", |   "custom": "自定义", | ||||||
| @@ -546,6 +575,7 @@ | |||||||
|   "colorSchemeWhite": "白色", |   "colorSchemeWhite": "白色", | ||||||
|   "colorSchemeBlack": "黑色", |   "colorSchemeBlack": "黑色", | ||||||
|   "colorSchemeApplied": "主题色已应用,可能需要重启来生效。", |   "colorSchemeApplied": "主题色已应用,可能需要重启来生效。", | ||||||
|  |   "postFeaturedComment": "精选评论", | ||||||
|   "postCategoryTechnology": "技术", |   "postCategoryTechnology": "技术", | ||||||
|   "postCategoryGaming": "游戏", |   "postCategoryGaming": "游戏", | ||||||
|   "postCategoryLife": "生活", |   "postCategoryLife": "生活", | ||||||
| @@ -556,5 +586,27 @@ | |||||||
|   "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 思考过程", | ||||||
|  |   "accountSettingsApplied": "帐号设置已应用。", | ||||||
|  |   "trayMenuExit": "退出" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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": "清除現存背景圖", | ||||||
| @@ -194,6 +217,10 @@ | |||||||
|   "settingsFeatures": "功能", |   "settingsFeatures": "功能", | ||||||
|   "settingsNotifyWithHaptic": "新通知時振動", |   "settingsNotifyWithHaptic": "新通知時振動", | ||||||
|   "settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。", |   "settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。", | ||||||
|  |   "settingsExpandPostLink": "展開帖子鏈接", | ||||||
|  |   "settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。", | ||||||
|  |   "settingsExpandChatLink": "展開聊天鏈接", | ||||||
|  |   "settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。", | ||||||
|   "settingsNetwork": "網絡", |   "settingsNetwork": "網絡", | ||||||
|   "settingsNetworkServer": "HyperNet 服務器", |   "settingsNetworkServer": "HyperNet 服務器", | ||||||
|   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", |   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", | ||||||
| @@ -212,6 +239,8 @@ | |||||||
|   "settingsMisc": "雜項", |   "settingsMisc": "雜項", | ||||||
|   "settingsMiscAbout": "關於", |   "settingsMiscAbout": "關於", | ||||||
|   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", |   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", | ||||||
|  |   "settingsAccountLanguage": "帳號偏好語言", | ||||||
|  |   "settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。", | ||||||
|   "sensitiveContent": "敏感內容", |   "sensitiveContent": "敏感內容", | ||||||
|   "sensitiveContentCollapsed": "敏感內容已摺疊。", |   "sensitiveContentCollapsed": "敏感內容已摺疊。", | ||||||
|   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", |   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", | ||||||
| @@ -525,11 +554,15 @@ | |||||||
|   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", |   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", | ||||||
|   "postShare": "分享", |   "postShare": "分享", | ||||||
|   "postShareImage": "分享帖圖", |   "postShareImage": "分享帖圖", | ||||||
|  |   "postGetInsight": "獲取見解", | ||||||
|  |   "postGetInsightTitle": "AI 見解", | ||||||
|  |   "postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。", | ||||||
|   "appInitializing": "正在初始化", |   "appInitializing": "正在初始化", | ||||||
|   "poweredBy": "由 {} 提供支持", |   "poweredBy": "由 {} 提供支持", | ||||||
|   "shareIntent": "分享", |   "shareIntent": "分享", | ||||||
|   "shareIntentDescription": "您想對您分享的內容做些什麼?", |   "shareIntentDescription": "您想對您分享的內容做些什麼?", | ||||||
|   "shareIntentPostStory": "發佈動態", |   "shareIntentPostStory": "發佈動態", | ||||||
|  |   "shareIntentSendChannel": "分享到聊天頻道", | ||||||
|   "updateAvailable": "檢測到更新可用", |   "updateAvailable": "檢測到更新可用", | ||||||
|   "updateOngoing": "正在更新,請稍後……", |   "updateOngoing": "正在更新,請稍後……", | ||||||
|   "custom": "自定義", |   "custom": "自定義", | ||||||
| @@ -542,6 +575,7 @@ | |||||||
|   "colorSchemeWhite": "白色", |   "colorSchemeWhite": "白色", | ||||||
|   "colorSchemeBlack": "黑色", |   "colorSchemeBlack": "黑色", | ||||||
|   "colorSchemeApplied": "主題色已應用,可能需要重啓來生效。", |   "colorSchemeApplied": "主題色已應用,可能需要重啓來生效。", | ||||||
|  |   "postFeaturedComment": "精選評論", | ||||||
|   "postCategoryTechnology": "技術", |   "postCategoryTechnology": "技術", | ||||||
|   "postCategoryGaming": "遊戲", |   "postCategoryGaming": "遊戲", | ||||||
|   "postCategoryLife": "生活", |   "postCategoryLife": "生活", | ||||||
| @@ -552,5 +586,26 @@ | |||||||
|   "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 思考過程", | ||||||
|  |   "accountSettingsApplied": "帳號設置已應用。" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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": "清除現存背景圖", | ||||||
| @@ -194,6 +217,10 @@ | |||||||
|   "settingsFeatures": "功能", |   "settingsFeatures": "功能", | ||||||
|   "settingsNotifyWithHaptic": "新通知時振動", |   "settingsNotifyWithHaptic": "新通知時振動", | ||||||
|   "settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。", |   "settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。", | ||||||
|  |   "settingsExpandPostLink": "展開帖子鏈接", | ||||||
|  |   "settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。", | ||||||
|  |   "settingsExpandChatLink": "展開聊天鏈接", | ||||||
|  |   "settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。", | ||||||
|   "settingsNetwork": "網絡", |   "settingsNetwork": "網絡", | ||||||
|   "settingsNetworkServer": "HyperNet 服務器", |   "settingsNetworkServer": "HyperNet 服務器", | ||||||
|   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", |   "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", | ||||||
| @@ -212,6 +239,8 @@ | |||||||
|   "settingsMisc": "雜項", |   "settingsMisc": "雜項", | ||||||
|   "settingsMiscAbout": "關於", |   "settingsMiscAbout": "關於", | ||||||
|   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", |   "settingsMiscAboutDescription": "查看 Solian 的版本信息。", | ||||||
|  |   "settingsAccountLanguage": "帳號偏好語言", | ||||||
|  |   "settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。", | ||||||
|   "sensitiveContent": "敏感內容", |   "sensitiveContent": "敏感內容", | ||||||
|   "sensitiveContentCollapsed": "敏感內容已摺疊。", |   "sensitiveContentCollapsed": "敏感內容已摺疊。", | ||||||
|   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", |   "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", | ||||||
| @@ -525,11 +554,15 @@ | |||||||
|   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", |   "postImageShareAds": "來 Solar Network 探索更多有趣帖子", | ||||||
|   "postShare": "分享", |   "postShare": "分享", | ||||||
|   "postShareImage": "分享帖圖", |   "postShareImage": "分享帖圖", | ||||||
|  |   "postGetInsight": "獲取見解", | ||||||
|  |   "postGetInsightTitle": "AI 見解", | ||||||
|  |   "postGetInsightDescription": "AI 可能會出錯,檢查信息真實性。", | ||||||
|   "appInitializing": "正在初始化", |   "appInitializing": "正在初始化", | ||||||
|   "poweredBy": "由 {} 提供支持", |   "poweredBy": "由 {} 提供支持", | ||||||
|   "shareIntent": "分享", |   "shareIntent": "分享", | ||||||
|   "shareIntentDescription": "您想對您分享的內容做些什麼?", |   "shareIntentDescription": "您想對您分享的內容做些什麼?", | ||||||
|   "shareIntentPostStory": "發佈動態", |   "shareIntentPostStory": "發佈動態", | ||||||
|  |   "shareIntentSendChannel": "分享到聊天頻道", | ||||||
|   "updateAvailable": "檢測到更新可用", |   "updateAvailable": "檢測到更新可用", | ||||||
|   "updateOngoing": "正在更新,請稍後……", |   "updateOngoing": "正在更新,請稍後……", | ||||||
|   "custom": "自定義", |   "custom": "自定義", | ||||||
| @@ -542,6 +575,7 @@ | |||||||
|   "colorSchemeWhite": "白色", |   "colorSchemeWhite": "白色", | ||||||
|   "colorSchemeBlack": "黑色", |   "colorSchemeBlack": "黑色", | ||||||
|   "colorSchemeApplied": "主題色已應用,可能需要重啟來生效。", |   "colorSchemeApplied": "主題色已應用,可能需要重啟來生效。", | ||||||
|  |   "postFeaturedComment": "精選評論", | ||||||
|   "postCategoryTechnology": "技術", |   "postCategoryTechnology": "技術", | ||||||
|   "postCategoryGaming": "遊戲", |   "postCategoryGaming": "遊戲", | ||||||
|   "postCategoryLife": "生活", |   "postCategoryLife": "生活", | ||||||
| @@ -552,5 +586,26 @@ | |||||||
|   "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 思考過程", | ||||||
|  |   "accountSettingsApplied": "帳號設置已應用。" | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										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.6.0): |   - Firebase/Analytics (11.7.0): | ||||||
|     - Firebase/Core |     - Firebase/Core | ||||||
|   - Firebase/Core (11.6.0): |   - Firebase/Core (11.7.0): | ||||||
|     - Firebase/CoreOnly |     - Firebase/CoreOnly | ||||||
|     - FirebaseAnalytics (~> 11.6.0) |     - FirebaseAnalytics (~> 11.7.0) | ||||||
|   - Firebase/CoreOnly (11.6.0): |   - Firebase/CoreOnly (11.7.0): | ||||||
|     - FirebaseCore (~> 11.6.0) |     - FirebaseCore (~> 11.7.0) | ||||||
|   - Firebase/Messaging (11.6.0): |   - Firebase/Messaging (11.7.0): | ||||||
|     - Firebase/CoreOnly |     - Firebase/CoreOnly | ||||||
|     - FirebaseMessaging (~> 11.6.0) |     - FirebaseMessaging (~> 11.7.0) | ||||||
|   - firebase_analytics (11.4.0): |   - firebase_analytics (11.4.2): | ||||||
|     - Firebase/Analytics (= 11.6.0) |     - Firebase/Analytics (= 11.7.0) | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - Flutter |     - Flutter | ||||||
|   - firebase_core (3.10.0): |   - firebase_core (3.11.0): | ||||||
|     - Firebase/CoreOnly (= 11.6.0) |     - Firebase/CoreOnly (= 11.7.0) | ||||||
|     - Flutter |     - Flutter | ||||||
|   - firebase_messaging (15.2.0): |   - firebase_messaging (15.2.2): | ||||||
|     - Firebase/Messaging (= 11.6.0) |     - Firebase/Messaging (= 11.7.0) | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - Flutter |     - Flutter | ||||||
|   - FirebaseAnalytics (11.6.0): |   - FirebaseAnalytics (11.7.0): | ||||||
|     - FirebaseAnalytics/AdIdSupport (= 11.6.0) |     - FirebaseAnalytics/AdIdSupport (= 11.7.0) | ||||||
|     - FirebaseCore (~> 11.6.0) |     - FirebaseCore (~> 11.7.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.6.0): |   - FirebaseAnalytics/AdIdSupport (11.7.0): | ||||||
|     - FirebaseCore (~> 11.6.0) |     - FirebaseCore (~> 11.7.0) | ||||||
|     - FirebaseInstallations (~> 11.0) |     - FirebaseInstallations (~> 11.0) | ||||||
|     - GoogleAppMeasurement (= 11.6.0) |     - GoogleAppMeasurement (= 11.7.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.6.0): |   - FirebaseCore (11.7.0): | ||||||
|     - FirebaseCoreInternal (~> 11.6.0) |     - FirebaseCoreInternal (~> 11.7.0) | ||||||
|     - GoogleUtilities/Environment (~> 8.0) |     - GoogleUtilities/Environment (~> 8.0) | ||||||
|     - GoogleUtilities/Logger (~> 8.0) |     - GoogleUtilities/Logger (~> 8.0) | ||||||
|   - FirebaseCoreInternal (11.6.0): |   - FirebaseCoreInternal (11.7.0): | ||||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" |     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||||
|   - FirebaseInstallations (11.6.0): |   - FirebaseInstallations (11.7.0): | ||||||
|     - FirebaseCore (~> 11.6.0) |     - FirebaseCore (~> 11.7.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.6.0): |   - FirebaseMessaging (11.7.0): | ||||||
|     - FirebaseCore (~> 11.6.0) |     - FirebaseCore (~> 11.7.0) | ||||||
|     - FirebaseInstallations (~> 11.0) |     - FirebaseInstallations (~> 11.0) | ||||||
|     - GoogleDataTransport (~> 10.0) |     - GoogleDataTransport (~> 10.0) | ||||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) |     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||||
| @@ -105,6 +105,13 @@ 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): | ||||||
| @@ -116,21 +123,21 @@ PODS: | |||||||
|   - gal (1.0.0): |   - gal (1.0.0): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - GoogleAppMeasurement (11.6.0): |   - GoogleAppMeasurement (11.7.0): | ||||||
|     - GoogleAppMeasurement/AdIdSupport (= 11.6.0) |     - GoogleAppMeasurement/AdIdSupport (= 11.7.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.6.0): |   - GoogleAppMeasurement/AdIdSupport (11.7.0): | ||||||
|     - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0) |     - GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.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.6.0): |   - GoogleAppMeasurement/WithoutAdIdSupport (11.7.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) | ||||||
| @@ -172,7 +179,7 @@ PODS: | |||||||
|     - Flutter |     - Flutter | ||||||
|   - in_app_review (2.0.0): |   - in_app_review (2.0.0): | ||||||
|     - Flutter |     - Flutter | ||||||
|   - Kingfisher (8.1.3) |   - Kingfisher (8.2.0) | ||||||
|   - livekit_client (2.3.5): |   - livekit_client (2.3.5): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - flutter_webrtc |     - flutter_webrtc | ||||||
| @@ -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: 374a441a91ead896215703a674d58cdb3e9d772b |   Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4 | ||||||
|   firebase_analytics: 07bd7cfbac54bfcdccf2bb2530f9a65486f7ef3f |   firebase_analytics: 7236e6115c1b4e62c2270faa29c052a317e31107 | ||||||
|   firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10 |   firebase_core: aa979ae726f00b3ef4ccf59dfb96170af84efbd4 | ||||||
|   firebase_messaging: e2f0ba891b1509668c07f5099761518a5af8fe3c |   firebase_messaging: 3af84b6a90aeac4d7a67fbf4c43a91e7083bea1f | ||||||
|   FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 |   FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec | ||||||
|   FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa |   FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4 | ||||||
|   FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 |   FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881 | ||||||
|   FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c |   FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9 | ||||||
|   FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021 |   FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c | ||||||
|   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: 90260f83024b1b96d239a575ea4e3708e79344d1 |   flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1 | ||||||
|   gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 |   gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 | ||||||
|   GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3 |   GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967 | ||||||
|   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: 323e5c4ec7983aaace12af655a7b51a7f88a599d | ||||||
|   livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976 |   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 | ||||||
|   | |||||||
							
								
								
									
										108
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										108
									
								
								lib/main.dart
									
									
									
									
									
								
							| @@ -1,6 +1,7 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:developer'; | import 'dart:developer'; | ||||||
| import 'dart:io'; | import 'dart:io'; | ||||||
|  | import 'dart:ui'; | ||||||
|  |  | ||||||
| import 'package:bitsdojo_window/bitsdojo_window.dart'; | import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||||
| import 'package:croppy/croppy.dart'; | import 'package:croppy/croppy.dart'; | ||||||
| @@ -10,8 +11,10 @@ import 'package:easy_localization_loader/easy_localization_loader.dart'; | |||||||
| import 'package:firebase_core/firebase_core.dart'; | import 'package:firebase_core/firebase_core.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:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:hive_flutter/hive_flutter.dart'; | import 'package:hive_flutter/hive_flutter.dart'; | ||||||
|  | import 'package:hotkey_manager/hotkey_manager.dart'; | ||||||
| import 'package:package_info_plus/package_info_plus.dart'; | import 'package:package_info_plus/package_info_plus.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'; | ||||||
| @@ -40,6 +43,7 @@ import 'package:surface/types/chat.dart'; | |||||||
| import 'package:surface/types/realm.dart'; | import 'package:surface/types/realm.dart'; | ||||||
| import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; | import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
|  | import 'package:tray_manager/tray_manager.dart'; | ||||||
| import 'package:version/version.dart'; | import 'package:version/version.dart'; | ||||||
| import 'package:workmanager/workmanager.dart'; | import 'package:workmanager/workmanager.dart'; | ||||||
| import 'package:in_app_review/in_app_review.dart'; | import 'package:in_app_review/in_app_review.dart'; | ||||||
| @@ -206,7 +210,7 @@ class _AppSplashScreen extends StatefulWidget { | |||||||
|   State<_AppSplashScreen> createState() => _AppSplashScreenState(); |   State<_AppSplashScreen> createState() => _AppSplashScreenState(); | ||||||
| } | } | ||||||
|  |  | ||||||
| class _AppSplashScreenState extends State<_AppSplashScreen> { | class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { | ||||||
|   void _tryRequestRating() async { |   void _tryRequestRating() async { | ||||||
|     final prefs = await SharedPreferences.getInstance(); |     final prefs = await SharedPreferences.getInstance(); | ||||||
|     if (prefs.containsKey('first_boot_time')) { |     if (prefs.containsKey('first_boot_time')) { | ||||||
| @@ -260,7 +264,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | |||||||
|     try { |     try { | ||||||
|       final cfg = context.read<ConfigProvider>(); |       final cfg = context.read<ConfigProvider>(); | ||||||
|       WidgetsBinding.instance.addPostFrameCallback((_) { |       WidgetsBinding.instance.addPostFrameCallback((_) { | ||||||
|         cfg.calcDrawerSize(context, withMediaQuery: true); |         cfg.calcDrawerSize(context); | ||||||
|       }); |       }); | ||||||
|       final home = context.read<HomeWidgetProvider>(); |       final home = context.read<HomeWidgetProvider>(); | ||||||
|       await home.initialize(); |       await home.initialize(); | ||||||
| @@ -281,6 +285,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | |||||||
|       final notify = context.read<NotificationProvider>(); |       final notify = context.read<NotificationProvider>(); | ||||||
|       notify.listen(); |       notify.listen(); | ||||||
|       await notify.registerPushNotifications(); |       await notify.registerPushNotifications(); | ||||||
|  |       if (!mounted) return; | ||||||
|  |       final sticker = context.read<SnStickerProvider>(); | ||||||
|  |       await sticker.listStickerEagerly(); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       if (!mounted) return; |       if (!mounted) return; | ||||||
|       await context.showErrorDialog(err); |       await context.showErrorDialog(err); | ||||||
| @@ -291,9 +298,62 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | |||||||
|     await widgetUpdateRandomPost(); |     await widgetUpdateRandomPost(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Future<void> _hotkeyInitialization() async { | ||||||
|  |     if (kIsWeb) return; | ||||||
|  |  | ||||||
|  |     if (Platform.isMacOS) { | ||||||
|  |       HotKey quitHotKey = HotKey( | ||||||
|  |         key: PhysicalKeyboardKey.keyQ, | ||||||
|  |         modifiers: [HotKeyModifier.meta], | ||||||
|  |         scope: HotKeyScope.inapp, | ||||||
|  |       ); | ||||||
|  |       await hotKeyManager.register(quitHotKey, keyUpHandler: (_) { | ||||||
|  |         _appLifecycleListener?.dispose(); | ||||||
|  |         SystemChannels.platform.invokeMethod('SystemNavigator.pop'); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<void> _trayInitialization() async { | ||||||
|  |     if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; | ||||||
|  |  | ||||||
|  |     final icon = Platform.isWindows ? 'assets/icon/tray-icon.ico' : 'assets/icon/tray-icon.png'; | ||||||
|  |     final appVersion = await PackageInfo.fromPlatform(); | ||||||
|  |  | ||||||
|  |     trayManager.addListener(this); | ||||||
|  |     await trayManager.setIcon(icon); | ||||||
|  |  | ||||||
|  |     Menu menu = Menu( | ||||||
|  |       items: [ | ||||||
|  |         MenuItem( | ||||||
|  |           key: 'version_label', | ||||||
|  |           label: 'Solian ${appVersion.version}+${appVersion.buildNumber}', | ||||||
|  |           disabled: true, | ||||||
|  |         ), | ||||||
|  |         MenuItem.separator(), | ||||||
|  |         MenuItem( | ||||||
|  |           key: 'exit', | ||||||
|  |           label: 'trayMenuExit'.tr(), | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |     await trayManager.setContextMenu(menu); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   AppLifecycleListener? _appLifecycleListener; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|  |  | ||||||
|  |     if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) { | ||||||
|  |       _appLifecycleListener = AppLifecycleListener( | ||||||
|  |         onExitRequested: _onExitRequested, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     _trayInitialization(); | ||||||
|  |     _hotkeyInitialization(); | ||||||
|     _initialize().then((_) { |     _initialize().then((_) { | ||||||
|       _postInitialization(); |       _postInitialization(); | ||||||
|       _tryRequestRating(); |       _tryRequestRating(); | ||||||
| @@ -301,6 +361,50 @@ class _AppSplashScreenState extends State<_AppSplashScreen> { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Future<AppExitResponse> _onExitRequested() async { | ||||||
|  |     appWindow.hide(); | ||||||
|  |     return AppExitResponse.cancel; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onTrayIconMouseDown() { | ||||||
|  |     if (Platform.isWindows) { | ||||||
|  |       context.read<NotificationProvider>().clearTray(); | ||||||
|  |       appWindow.show(); | ||||||
|  |     } else { | ||||||
|  |       trayManager.popUpContextMenu(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onTrayIconRightMouseDown() { | ||||||
|  |     if (Platform.isWindows) { | ||||||
|  |       trayManager.popUpContextMenu(); | ||||||
|  |     } else { | ||||||
|  |       context.read<NotificationProvider>().clearTray(); | ||||||
|  |       appWindow.show(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void onTrayMenuItemClick(MenuItem menuItem) { | ||||||
|  |     switch (menuItem.key) { | ||||||
|  |       case 'exit': | ||||||
|  |         _appLifecycleListener?.dispose(); | ||||||
|  |         SystemChannels.platform.invokeMethod('SystemNavigator.pop'); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void dispose() { | ||||||
|  |     if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) { | ||||||
|  |       trayManager.removeListener(this); | ||||||
|  |       hotKeyManager.unregisterAll(); | ||||||
|  |     } | ||||||
|  |     super.dispose(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     final cfg = context.read<ConfigProvider>(); |     final cfg = context.read<ConfigProvider>(); | ||||||
|   | |||||||
| @@ -50,7 +50,7 @@ class ConfigProvider extends ChangeNotifier { | |||||||
|     } else { |     } else { | ||||||
|       final rpb = ResponsiveBreakpoints.of(context); |       final rpb = ResponsiveBreakpoints.of(context); | ||||||
|       newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE); |       newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE); | ||||||
|       newDrawerIsCollapsed = rpb.largerThan(TABLET) |       newDrawerIsExpanded = rpb.largerThan(TABLET) | ||||||
|           ? (prefs.getBool(kAppDrawerPreferCollapse) ?? false) |           ? (prefs.getBool(kAppDrawerPreferCollapse) ?? false) | ||||||
|               ? false |               ? false | ||||||
|               : true |               : true | ||||||
|   | |||||||
| @@ -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(); | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ 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/providers/websocket.dart'; | ||||||
| import 'package:surface/types/notification.dart'; | import 'package:surface/types/notification.dart'; | ||||||
|  | import 'package:tray_manager/tray_manager.dart'; | ||||||
|  |  | ||||||
| class NotificationProvider extends ChangeNotifier { | class NotificationProvider extends ChangeNotifier { | ||||||
|   late final SnNetworkProvider _sn; |   late final SnNetworkProvider _sn; | ||||||
| @@ -71,22 +72,48 @@ class NotificationProvider extends ChangeNotifier { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   int showingCount = 0; | ||||||
|  |   int showingTrayCount = 0; | ||||||
|   List<SnNotification> notifications = List.empty(growable: true); |   List<SnNotification> notifications = List.empty(growable: true); | ||||||
|  |  | ||||||
|   void listen() { |   void listen() { | ||||||
|     _ws.stream.stream.listen((event) { |     _ws.stream.stream.listen((event) { | ||||||
|       if (event.method == 'notifications.new') { |       if (event.method == 'notifications.new') { | ||||||
|         final notification = SnNotification.fromJson(event.payload!); |         final notification = SnNotification.fromJson(event.payload!); | ||||||
|  |         if (showingCount < 0) showingCount = 0; | ||||||
|  |         showingCount++; | ||||||
|  |         showingTrayCount++; | ||||||
|         notifications.add(notification); |         notifications.add(notification); | ||||||
|  |         Future.delayed(const Duration(seconds: 3), () { | ||||||
|  |           if (showingCount >= 0) showingCount--; | ||||||
|  |           notifyListeners(); | ||||||
|  |         }); | ||||||
|         notifyListeners(); |         notifyListeners(); | ||||||
|  |         updateTray(); | ||||||
|         final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true; |         final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true; | ||||||
|         if (doHaptic) HapticFeedback.lightImpact(); |         if (doHaptic) HapticFeedback.mediumImpact(); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void clearTray() { | ||||||
|  |     showingTrayCount = 0; | ||||||
|  |     updateTray(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void updateTray() { | ||||||
|  |     if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; | ||||||
|  |     if (notifications.isEmpty) { | ||||||
|  |       trayManager.setTitle(''); | ||||||
|  |     } else { | ||||||
|  |       trayManager.setTitle(' ${notifications.length.toString()}'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   void clear() { |   void clear() { | ||||||
|  |     showingCount = 0; | ||||||
|     notifications.clear(); |     notifications.clear(); | ||||||
|  |     updateTray(); | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,6 +9,10 @@ class SnStickerProvider { | |||||||
|   late final SnNetworkProvider _sn; |   late final SnNetworkProvider _sn; | ||||||
|   final Map<String, SnSticker?> _cache = {}; |   final Map<String, SnSticker?> _cache = {}; | ||||||
|  |  | ||||||
|  |   final Map<int, List<SnSticker>> stickersByPack = {}; | ||||||
|  |  | ||||||
|  |   List<SnSticker> get stickers => _cache.values.where((ele) => ele != null).cast<SnSticker>().toList(); | ||||||
|  |  | ||||||
|   SnStickerProvider(BuildContext context) { |   SnStickerProvider(BuildContext context) { | ||||||
|     _sn = context.read<SnNetworkProvider>(); |     _sn = context.read<SnNetworkProvider>(); | ||||||
|   } |   } | ||||||
| @@ -17,6 +21,12 @@ class SnStickerProvider { | |||||||
|     return _cache.containsKey(alias) && _cache[alias] == null; |     return _cache.containsKey(alias) && _cache[alias] == null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void _cacheSticker(SnSticker sticker) { | ||||||
|  |     _cache['${sticker.pack.prefix}:${sticker.alias}'] = sticker; | ||||||
|  |     if (stickersByPack[sticker.pack.id] == null) stickersByPack[sticker.pack.id] = List.empty(growable: true); | ||||||
|  |     if (!stickersByPack[sticker.pack.id]!.contains(sticker)) stickersByPack[sticker.pack.id]!.add(sticker); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   Future<SnSticker?> lookupSticker(String alias) async { |   Future<SnSticker?> lookupSticker(String alias) async { | ||||||
|     if (_cache.containsKey(alias)) { |     if (_cache.containsKey(alias)) { | ||||||
|       return _cache[alias]; |       return _cache[alias]; | ||||||
| @@ -25,7 +35,7 @@ class SnStickerProvider { | |||||||
|     try { |     try { | ||||||
|       final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias'); |       final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias'); | ||||||
|       final sticker = SnSticker.fromJson(resp.data); |       final sticker = SnSticker.fromJson(resp.data); | ||||||
|       _cache[alias] = sticker; |       _cacheSticker(sticker); | ||||||
|  |  | ||||||
|       return sticker; |       return sticker; | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
| @@ -35,4 +45,30 @@ class SnStickerProvider { | |||||||
|  |  | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Future<void> listStickerEagerly() async { | ||||||
|  |     var count = await listSticker(); | ||||||
|  |     for (var page = 1; count > 0; count -= 10) { | ||||||
|  |       await listSticker(page: page); | ||||||
|  |       page++; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<int> listSticker({int page = 0}) async { | ||||||
|  |     try { | ||||||
|  |       final resp = await _sn.client.get('/cgi/uc/stickers', queryParameters: { | ||||||
|  |         'take': 10, | ||||||
|  |         'offset': page * 10, | ||||||
|  |       }); | ||||||
|  |       final data = resp.data; | ||||||
|  |       final stickers = List.from(data['data']).map((ele) => SnSticker.fromJson(ele)); | ||||||
|  |       for (final sticker in stickers) { | ||||||
|  |         _cacheSticker(sticker); | ||||||
|  |       } | ||||||
|  |       return data['count'] as int; | ||||||
|  |     } catch (err) { | ||||||
|  |       log('[Sticker] Failed to list stickers: $err'); | ||||||
|  |       rethrow; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -53,4 +53,11 @@ class UserProvider extends ChangeNotifier { | |||||||
|     user = null; |     user = null; | ||||||
|     notifyListeners(); |     notifyListeners(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void setLanguage(String? value) { | ||||||
|  |     if (value == null) return; | ||||||
|  |     if (user == null) return; | ||||||
|  |     user = user!.copyWith(language: value); | ||||||
|  |     notifyListeners(); | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -33,7 +33,16 @@ class WebSocketProvider extends ChangeNotifier { | |||||||
|     await connect(); |     await connect(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Completer<void>? _connectCompleter; | ||||||
|  |  | ||||||
|   Future<void> connect({noRetry = false}) async { |   Future<void> connect({noRetry = false}) async { | ||||||
|  |     if(_connectCompleter != null) { | ||||||
|  |       await _connectCompleter!.future; | ||||||
|  |       _connectCompleter = null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     _connectCompleter = Completer<void>(); | ||||||
|  |  | ||||||
|     if (!_ua.isAuthorized) return; |     if (!_ua.isAuthorized) return; | ||||||
|     if (isConnected || conn != null) { |     if (isConnected || conn != null) { | ||||||
|       disconnect(); |       disconnect(); | ||||||
| @@ -64,12 +73,13 @@ class WebSocketProvider extends ChangeNotifier { | |||||||
|         log('Retry connecting to websocket in 3 seconds...'); |         log('Retry connecting to websocket in 3 seconds...'); | ||||||
|         return Future.delayed( |         return Future.delayed( | ||||||
|           const Duration(seconds: 3), |           const Duration(seconds: 3), | ||||||
|           () => connect(noRetry: true), |               () => connect(noRetry: true), | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|     } finally { |     } finally { | ||||||
|       isBusy = false; |       isBusy = false; | ||||||
|       notifyListeners(); |       notifyListeners(); | ||||||
|  |       _connectCompleter!.complete(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -47,6 +47,7 @@ class HomeWidgetProvider { | |||||||
| } | } | ||||||
|  |  | ||||||
| Future<void> widgetUpdateRandomPost() async { | Future<void> widgetUpdateRandomPost() async { | ||||||
|  |   if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) return; | ||||||
|   final snc = await SnNetworkProvider.createOffContextClient(); |   final snc = await SnNetworkProvider.createOffContextClient(); | ||||||
|   final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1'); |   final resp = await snc.get('/cgi/co/recommendations/shuffle?take=1'); | ||||||
|   final post = SnPost.fromJson(resp.data['data'][0]); |   final post = SnPost.fromJson(resp.data['data'][0]); | ||||||
|   | |||||||
							
								
								
									
										172
									
								
								lib/router.dart
									
									
									
									
									
								
							
							
						
						
									
										172
									
								
								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,9 +33,9 @@ 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( | Widget _fadeThroughTransition( | ||||||
| @@ -48,18 +52,12 @@ final _appRoutes = [ | |||||||
|   GoRoute( |   GoRoute( | ||||||
|     path: '/', |     path: '/', | ||||||
|     name: 'home', |     name: 'home', | ||||||
|     pageBuilder: (context, state) => CustomTransitionPage( |     builder: (context, state) => const HomeScreen(), | ||||||
|       transitionsBuilder: _fadeThroughTransition, |  | ||||||
|       child: const HomeScreen(), |  | ||||||
|     ), |  | ||||||
|   ), |   ), | ||||||
|   GoRoute( |   GoRoute( | ||||||
|     path: '/posts', |     path: '/posts', | ||||||
|     name: 'explore', |     name: 'explore', | ||||||
|     pageBuilder: (context, state) => CustomTransitionPage( |     builder: (context, state) => const ExploreScreen(), | ||||||
|       transitionsBuilder: _fadeThroughTransition, |  | ||||||
|       child: const ExploreScreen(), |  | ||||||
|     ), |  | ||||||
|     routes: [ |     routes: [ | ||||||
|       GoRoute( |       GoRoute( | ||||||
|         path: '/write/:mode', |         path: '/write/:mode', | ||||||
| @@ -75,7 +73,7 @@ final _appRoutes = [ | |||||||
|           postRepostId: int.tryParse( |           postRepostId: int.tryParse( | ||||||
|             state.uri.queryParameters['reposting'] ?? '', |             state.uri.queryParameters['reposting'] ?? '', | ||||||
|           ), |           ), | ||||||
|           extraProps: state.extra as PostEditorExtraProps?, |           extraProps: state.extra as PostEditorExtra?, | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|       GoRoute( |       GoRoute( | ||||||
| @@ -101,67 +99,87 @@ final _appRoutes = [ | |||||||
|       ), |       ), | ||||||
|     ], |     ], | ||||||
|   ), |   ), | ||||||
|   GoRoute( |   GoRoute(path: '/account', name: 'account', builder: (context, state) => const AccountScreen(), routes: [ | ||||||
|     path: '/account', |     GoRoute( | ||||||
|     name: 'account', |       path: '/wallet', | ||||||
|     pageBuilder: (context, state) => CustomTransitionPage( |       name: 'accountWallet', | ||||||
|       transitionsBuilder: _fadeThroughTransition, |       builder: (context, state) => const WalletScreen(), | ||||||
|       child: const AccountScreen(), |  | ||||||
|     ), |     ), | ||||||
|   ), |     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( |   GoRoute( | ||||||
|     path: '/chat', |     path: '/chat', | ||||||
|     name: 'chat', |     name: 'chat', | ||||||
|     pageBuilder: (context, state) => CustomTransitionPage( |     builder: (context, state) => const ChatScreen(), | ||||||
|       transitionsBuilder: _fadeThroughTransition, |  | ||||||
|       child: const ChatScreen(), |  | ||||||
|     ), |  | ||||||
|     routes: [ |     routes: [ | ||||||
|       GoRoute( |       GoRoute( | ||||||
|         path: '/:scope/:alias', |         path: '/:scope/:alias', | ||||||
|         name: 'chatRoom', |         name: 'chatRoom', | ||||||
|         builder: (context, state) => AppBackground( |         builder: (context, state) => ChatRoomScreen( | ||||||
|           child: ChatRoomScreen( |           scope: state.pathParameters['scope']!, | ||||||
|             scope: state.pathParameters['scope']!, |           alias: state.pathParameters['alias']!, | ||||||
|             alias: state.pathParameters['alias']!, |           extra: state.extra as ChatRoomScreenExtra?, | ||||||
|           ), |  | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|       GoRoute( |       GoRoute( | ||||||
|         path: '/:scope/:alias/call', |         path: '/:scope/:alias/call', | ||||||
|         name: 'chatCallRoom', |         name: 'chatCallRoom', | ||||||
|         builder: (context, state) => AppBackground( |         builder: (context, state) => CallRoomScreen( | ||||||
|           child: CallRoomScreen( |           scope: state.pathParameters['scope']!, | ||||||
|             scope: state.pathParameters['scope']!, |           alias: state.pathParameters['alias']!, | ||||||
|             alias: state.pathParameters['alias']!, |  | ||||||
|           ), |  | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|       GoRoute( |       GoRoute( | ||||||
|         path: '/:scope/:alias/detail', |         path: '/:scope/:alias/detail', | ||||||
|         name: 'channelDetail', |         name: 'channelDetail', | ||||||
|         builder: (context, state) => AppBackground( |         builder: (context, state) => ChannelDetailScreen( | ||||||
|           child: ChannelDetailScreen( |           scope: state.pathParameters['scope']!, | ||||||
|             scope: state.pathParameters['scope']!, |           alias: state.pathParameters['alias']!, | ||||||
|             alias: state.pathParameters['alias']!, |  | ||||||
|           ), |  | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|       GoRoute( |       GoRoute( | ||||||
|         path: '/manage', |         path: '/manage', | ||||||
|         name: 'chatManage', |         name: 'chatManage', | ||||||
|         pageBuilder: (context, state) => CustomTransitionPage( |         builder: (context, state) => ChatManageScreen( | ||||||
|           child: ChatManageScreen( |           editingChannelAlias: state.uri.queryParameters['editing'], | ||||||
|             editingChannelAlias: state.uri.queryParameters['editing'], |  | ||||||
|           ), |  | ||||||
|           transitionsBuilder: (context, animation, secondaryAnimation, child) { |  | ||||||
|             return FadeThroughTransition( |  | ||||||
|               animation: animation, |  | ||||||
|               secondaryAnimation: secondaryAnimation, |  | ||||||
|               fillColor: Colors.transparent, |  | ||||||
|               child: child, |  | ||||||
|             ); |  | ||||||
|           }, |  | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|     ], |     ], | ||||||
| @@ -182,36 +200,35 @@ final _appRoutes = [ | |||||||
|       GoRoute( |       GoRoute( | ||||||
|         path: '/manage', |         path: '/manage', | ||||||
|         name: 'realmManage', |         name: 'realmManage', | ||||||
|         pageBuilder: (context, state) => CustomTransitionPage( |         builder: (context, state) => RealmManageScreen( | ||||||
|           transitionsBuilder: _fadeThroughTransition, |           editingRealmAlias: state.uri.queryParameters['editing'], | ||||||
|           child: RealmManageScreen( |  | ||||||
|             editingRealmAlias: state.uri.queryParameters['editing'], |  | ||||||
|           ), |  | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|     ], |     ], | ||||||
|   ), |   ), | ||||||
|  |   GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [ | ||||||
|  |     GoRoute( | ||||||
|  |       path: '/:hash', | ||||||
|  |       name: 'newsDetail', | ||||||
|  |       builder: (context, state) => NewsDetailScreen( | ||||||
|  |         hash: state.pathParameters['hash']!, | ||||||
|  |       ), | ||||||
|  |     ), | ||||||
|  |   ]), | ||||||
|   GoRoute( |   GoRoute( | ||||||
|     path: '/album', |     path: '/album', | ||||||
|     name: 'album', |     name: 'album', | ||||||
|     pageBuilder: (context, state) => CustomTransitionPage( |     builder: (context, state) => const AlbumScreen(), | ||||||
|       transitionsBuilder: _fadeThroughTransition, |  | ||||||
|       child: const AlbumScreen(), |  | ||||||
|     ), |  | ||||||
|   ), |   ), | ||||||
|   GoRoute( |   GoRoute( | ||||||
|     path: '/friend', |     path: '/friend', | ||||||
|     name: 'friend', |     name: 'friend', | ||||||
|     pageBuilder: (context, state) => NoTransitionPage( |     builder: (context, state) => const FriendScreen(), | ||||||
|       child: const FriendScreen(), |  | ||||||
|     ), |  | ||||||
|   ), |   ), | ||||||
|   GoRoute( |   GoRoute( | ||||||
|     path: '/notification', |     path: '/notification', | ||||||
|     name: 'notification', |     name: 'notification', | ||||||
|     pageBuilder: (context, state) => NoTransitionPage( |     builder: (context, state) => const NotificationScreen(), | ||||||
|       child: const NotificationScreen(), |  | ||||||
|     ), |  | ||||||
|   ), |   ), | ||||||
|   GoRoute( |   GoRoute( | ||||||
|     path: '/auth/login', |     path: '/auth/login', | ||||||
| @@ -228,35 +245,6 @@ final _appRoutes = [ | |||||||
|     name: 'abuseReport', |     name: 'abuseReport', | ||||||
|     builder: (context, state) => AbuseReportScreen(), |     builder: (context, state) => AbuseReportScreen(), | ||||||
|   ), |   ), | ||||||
|   GoRoute( |  | ||||||
|     path: '/account/profile/edit', |  | ||||||
|     name: 'accountProfileEdit', |  | ||||||
|     builder: (context, state) => ProfileEditScreen(), |  | ||||||
|   ), |  | ||||||
|   GoRoute( |  | ||||||
|     path: '/account/publishers', |  | ||||||
|     name: 'accountPublishers', |  | ||||||
|     builder: (context, state) => PublisherScreen(), |  | ||||||
|   ), |  | ||||||
|   GoRoute( |  | ||||||
|     path: '/account/publishers/new', |  | ||||||
|     name: 'accountPublisherNew', |  | ||||||
|     builder: (context, state) => AccountPublisherNewScreen(), |  | ||||||
|   ), |  | ||||||
|   GoRoute( |  | ||||||
|     path: '/account/publishers/edit/:name', |  | ||||||
|     name: 'accountPublisherEdit', |  | ||||||
|     builder: (context, state) => AccountPublisherEditScreen( |  | ||||||
|       name: state.pathParameters['name']!, |  | ||||||
|     ), |  | ||||||
|   ), |  | ||||||
|   GoRoute( |  | ||||||
|     path: '/account/:name', |  | ||||||
|     name: 'accountProfilePage', |  | ||||||
|     pageBuilder: (context, state) => NoTransitionPage( |  | ||||||
|       child: UserScreen(name: state.pathParameters['name']!), |  | ||||||
|     ), |  | ||||||
|   ), |  | ||||||
|   GoRoute( |   GoRoute( | ||||||
|     path: '/settings', |     path: '/settings', | ||||||
|     name: 'settings', |     name: 'settings', | ||||||
|   | |||||||
| @@ -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'; | ||||||
| @@ -13,6 +15,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/navigation/app_scaffold.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}); | ||||||
| @@ -20,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 AppScaffold( |     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), | ||||||
| @@ -83,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(), | ||||||
| @@ -113,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(), | ||||||
| @@ -134,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); |  | ||||||
|                 } |  | ||||||
|               }); |  | ||||||
|             }); |  | ||||||
|           }, |  | ||||||
|         ), |  | ||||||
|       ], |       ], | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										126
									
								
								lib/screens/account/account_settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								lib/screens/account/account_settings.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | |||||||
|  | import 'package:collection/collection.dart'; | ||||||
|  | import 'package:dropdown_button2/dropdown_button2.dart'; | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:go_router/go_router.dart'; | ||||||
|  | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|  | import 'package:provider/provider.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  | import 'package:surface/providers/sn_network.dart'; | ||||||
|  | import 'package:surface/providers/userinfo.dart'; | ||||||
|  | import 'package:surface/widgets/dialog.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
|  | import 'package:intl/locale.dart'; | ||||||
|  |  | ||||||
|  | class AccountSettingsScreen extends StatelessWidget { | ||||||
|  |   const AccountSettingsScreen({super.key}); | ||||||
|  |  | ||||||
|  |   Future<void> _setAccountLanguage(BuildContext context, Locale? value) async { | ||||||
|  |     if (value == null) return; | ||||||
|  |     try { | ||||||
|  |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       final ua = context.read<UserProvider>(); | ||||||
|  |       await sn.client.put('/cgi/id/users/me/language', data: { | ||||||
|  |         'language': value.toString(), | ||||||
|  |       }); | ||||||
|  |       if (!context.mounted) return; | ||||||
|  |       context.showSnackbar('accountSettingsApplied'.tr()); | ||||||
|  |       await ua.refreshUser(); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (!context.mounted) return; | ||||||
|  |       context.showErrorDialog(err); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final ua = context.watch<UserProvider>(); | ||||||
|  |  | ||||||
|  |     return AppScaffold( | ||||||
|  |       appBar: AppBar( | ||||||
|  |         leading: PageBackButton(), | ||||||
|  |         title: Text('screenAccountSettings').tr(), | ||||||
|  |       ), | ||||||
|  |       body: SingleChildScrollView( | ||||||
|  |         child: Column( | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |           children: [ | ||||||
|  |             ListTile( | ||||||
|  |               title: Text('settingsAccountLanguage').tr(), | ||||||
|  |               subtitle: Text('settingsAccountLanguageDescription').tr(), | ||||||
|  |               contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||||
|  |               leading: const Icon(Symbols.translate), | ||||||
|  |               trailing: DropdownButtonHideUnderline( | ||||||
|  |                 child: DropdownButton2<Locale?>( | ||||||
|  |                   isExpanded: true, | ||||||
|  |                   items: [ | ||||||
|  |                     ...EasyLocalization.of(context)!.supportedLocales.mapIndexed((idx, ele) { | ||||||
|  |                       return DropdownMenuItem<Locale?>( | ||||||
|  |                         value: Locale.parse(ele.toString()), | ||||||
|  |                         child: Text('${ele.languageCode}-${ele.countryCode}').fontSize(14), | ||||||
|  |                       ); | ||||||
|  |                     }), | ||||||
|  |                   ], | ||||||
|  |                   value: ua.user?.language != null ? Locale.parse(ua.user!.language) : Locale.parse('en-US'), | ||||||
|  |                   onChanged: (Locale? value) { | ||||||
|  |                     if (value == null) return; | ||||||
|  |                     _setAccountLanguage(context, value); | ||||||
|  |                     ua.setLanguage(value.toString()); | ||||||
|  |                   }, | ||||||
|  |                   buttonStyleData: const ButtonStyleData( | ||||||
|  |                     padding: EdgeInsets.symmetric( | ||||||
|  |                       horizontal: 16, | ||||||
|  |                       vertical: 5, | ||||||
|  |                     ), | ||||||
|  |                     height: 40, | ||||||
|  |                     width: 160, | ||||||
|  |                   ), | ||||||
|  |                   menuItemStyleData: const MenuItemStyleData( | ||||||
|  |                     height: 40, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |             ListTile( | ||||||
|  |               title: Text('accountProfileEdit').tr(), | ||||||
|  |               subtitle: Text('accountProfileEditSubtitle').tr(), | ||||||
|  |               contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||||
|  |               leading: const Icon(Symbols.contact_page), | ||||||
|  |               trailing: const Icon(Symbols.chevron_right), | ||||||
|  |               onTap: () { | ||||||
|  |                 GoRouter.of(context).pushNamed('accountProfileEdit'); | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |             ListTile( | ||||||
|  |               title: Text('accountDeletion'.tr()), | ||||||
|  |               subtitle: Text('accountDeletionActionDescription'.tr()), | ||||||
|  |               contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||||
|  |               leading: const Icon(Symbols.person_cancel), | ||||||
|  |               trailing: const Icon(Symbols.chevron_right), | ||||||
|  |               onTap: () { | ||||||
|  |                 context | ||||||
|  |                     .showConfirmDialog( | ||||||
|  |                   'accountDeletion'.tr(), | ||||||
|  |                   'accountDeletionDescription'.tr(), | ||||||
|  |                 ) | ||||||
|  |                     .then((value) { | ||||||
|  |                   if (!value || !context.mounted) return; | ||||||
|  |                   final sn = context.read<SnNetworkProvider>(); | ||||||
|  |                   sn.client.post('/cgi/id/users/me/deletion').then((value) { | ||||||
|  |                     if (context.mounted) { | ||||||
|  |                       context.showSnackbar('accountDeletionSubmitted'.tr()); | ||||||
|  |                     } | ||||||
|  |                   }).catchError((err) { | ||||||
|  |                     if (context.mounted) { | ||||||
|  |                       context.showErrorDialog(err); | ||||||
|  |                     } | ||||||
|  |                   }); | ||||||
|  |                 }); | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										294
									
								
								lib/screens/account/factor_settings.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								lib/screens/account/factor_settings.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,294 @@ | |||||||
|  | import 'package:dropdown_button2/dropdown_button2.dart'; | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:gap/gap.dart'; | ||||||
|  | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|  | import 'package:provider/provider.dart'; | ||||||
|  | import 'package:qr_flutter/qr_flutter.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  | import 'package:surface/providers/sn_network.dart'; | ||||||
|  | import 'package:surface/types/auth.dart'; | ||||||
|  | import 'package:surface/widgets/dialog.dart'; | ||||||
|  | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
|  | import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||||
|  |  | ||||||
|  | final Map<int, (String, String, IconData)> kFactorTypes = { | ||||||
|  |   0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password), | ||||||
|  |   1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email), | ||||||
|  |   2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer), | ||||||
|  |   3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active), | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class FactorSettingsScreen extends StatefulWidget { | ||||||
|  |   const FactorSettingsScreen({super.key}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<FactorSettingsScreen> createState() => _FactorSettingsScreenState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _FactorSettingsScreenState extends State<FactorSettingsScreen> { | ||||||
|  |   bool _isBusy = false; | ||||||
|  |   List<SnAuthFactor>? _factors; | ||||||
|  |  | ||||||
|  |   Future<void> _fetchFactors() async { | ||||||
|  |     try { | ||||||
|  |       setState(() => _isBusy = true); | ||||||
|  |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       final resp = await sn.client.get('/cgi/id/users/me/factors'); | ||||||
|  |       _factors = List<SnAuthFactor>.from( | ||||||
|  |         resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [], | ||||||
|  |       ); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (!mounted) return; | ||||||
|  |       context.showErrorDialog(err); | ||||||
|  |     } finally { | ||||||
|  |       setState(() => _isBusy = false); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |     _fetchFactors(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return AppScaffold( | ||||||
|  |       appBar: AppBar( | ||||||
|  |         leading: PageBackButton(), | ||||||
|  |         title: Text('screenFactorSettings').tr(), | ||||||
|  |       ), | ||||||
|  |       body: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: [ | ||||||
|  |           LoadingIndicator( | ||||||
|  |             isActive: _isBusy, | ||||||
|  |           ), | ||||||
|  |           ListTile( | ||||||
|  |             title: Text('authFactorAdd').tr(), | ||||||
|  |             subtitle: Text('authFactorAddSubtitle').tr(), | ||||||
|  |             contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||||
|  |             leading: const Icon(Symbols.add), | ||||||
|  |             trailing: const Icon(Symbols.chevron_right), | ||||||
|  |             onTap: () { | ||||||
|  |               showDialog( | ||||||
|  |                 context: context, | ||||||
|  |                 builder: (context) => _FactorNewDialog( | ||||||
|  |                   currentlyHave: _factors!, | ||||||
|  |                 ), | ||||||
|  |               ).then((val) { | ||||||
|  |                 if (val == true) _fetchFactors(); | ||||||
|  |               }); | ||||||
|  |             }, | ||||||
|  |           ), | ||||||
|  |           const Divider(height: 1), | ||||||
|  |           Expanded( | ||||||
|  |             child: MediaQuery.removePadding( | ||||||
|  |               context: context, | ||||||
|  |               removeTop: true, | ||||||
|  |               child: RefreshIndicator( | ||||||
|  |                 onRefresh: _fetchFactors, | ||||||
|  |                 child: ListView.builder( | ||||||
|  |                   itemCount: _factors?.length ?? 0, | ||||||
|  |                   itemBuilder: (context, idx) { | ||||||
|  |                     final ele = _factors![idx]; | ||||||
|  |                     return ListTile( | ||||||
|  |                       title: Text(kFactorTypes[ele.type]!.$1).tr(), | ||||||
|  |                       subtitle: Text(kFactorTypes[ele.type]!.$2).tr(), | ||||||
|  |                       contentPadding: const EdgeInsets.only(left: 24, right: 12), | ||||||
|  |                       leading: Icon(kFactorTypes[ele.type]!.$3), | ||||||
|  |                       trailing: IconButton( | ||||||
|  |                         icon: const Icon(Symbols.close), | ||||||
|  |                         onPressed: ele.type > 0 | ||||||
|  |                             ? () { | ||||||
|  |                                 context | ||||||
|  |                                     .showConfirmDialog( | ||||||
|  |                                   'authFactorDelete'.tr(), | ||||||
|  |                                   'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]), | ||||||
|  |                                 ) | ||||||
|  |                                     .then((val) async { | ||||||
|  |                                   if (!val) return; | ||||||
|  |                                   try { | ||||||
|  |                                     if (!context.mounted) return; | ||||||
|  |                                     final sn = context.read<SnNetworkProvider>(); | ||||||
|  |                                     await sn.client.delete('/cgi/id/users/me/factors/${ele.id}'); | ||||||
|  |                                     _fetchFactors(); | ||||||
|  |                                   } catch (err) { | ||||||
|  |                                     if (!context.mounted) return; | ||||||
|  |                                     context.showErrorDialog(err); | ||||||
|  |                                   } | ||||||
|  |                                 }); | ||||||
|  |                               } | ||||||
|  |                             : null, | ||||||
|  |                       ), | ||||||
|  |                     ); | ||||||
|  |                   }, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _FactorNewDialog extends StatefulWidget { | ||||||
|  |   final List<SnAuthFactor> currentlyHave; | ||||||
|  |  | ||||||
|  |   const _FactorNewDialog({required this.currentlyHave}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<_FactorNewDialog> createState() => _FactorNewDialogState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _FactorNewDialogState extends State<_FactorNewDialog> { | ||||||
|  |   int? _factorType; | ||||||
|  |   bool _isBusy = false; | ||||||
|  |  | ||||||
|  |   Future<void> _submit() async { | ||||||
|  |     try { | ||||||
|  |       setState(() => _isBusy = true); | ||||||
|  |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       final resp = await sn.client.post('/cgi/id/users/me/factors', data: { | ||||||
|  |         'type': _factorType, | ||||||
|  |       }); | ||||||
|  |       final factor = SnAuthFactor.fromJson(resp.data); | ||||||
|  |       if (!mounted) return; | ||||||
|  |       if (factor.type == 2) { | ||||||
|  |         await showModalBottomSheet( | ||||||
|  |           context: context, | ||||||
|  |           builder: (context) => _FactorTotpFactorDialog(factor: factor), | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       if (!mounted) return; | ||||||
|  |       Navigator.of(context).pop(true); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (!mounted) return; | ||||||
|  |       context.showErrorDialog(err); | ||||||
|  |     } finally { | ||||||
|  |       setState(() => _isBusy = false); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return AlertDialog( | ||||||
|  |       title: Text('authFactorAdd').tr(), | ||||||
|  |       content: Column( | ||||||
|  |         mainAxisSize: MainAxisSize.min, | ||||||
|  |         children: [ | ||||||
|  |           DropdownButtonHideUnderline( | ||||||
|  |             child: DropdownButton2<int>( | ||||||
|  |               hint: Text( | ||||||
|  |                 'Select Item', | ||||||
|  |                 style: TextStyle( | ||||||
|  |                   fontSize: 14, | ||||||
|  |                 ), | ||||||
|  |                 overflow: TextOverflow.ellipsis, | ||||||
|  |               ), | ||||||
|  |               value: _factorType, | ||||||
|  |               items: kFactorTypes.entries.map( | ||||||
|  |                 (ele) { | ||||||
|  |                   final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key); | ||||||
|  |                   return DropdownMenuItem<int>( | ||||||
|  |                     enabled: !contains, | ||||||
|  |                     value: ele.key, | ||||||
|  |                     child: Text( | ||||||
|  |                       ele.value.$1.tr(), | ||||||
|  |                       style: const TextStyle( | ||||||
|  |                         fontSize: 14, | ||||||
|  |                       ), | ||||||
|  |                     ).opacity(contains ? 0.75 : 1), | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |               ).toList(), | ||||||
|  |               onChanged: (val) => setState(() { | ||||||
|  |                 _factorType = val; | ||||||
|  |               }), | ||||||
|  |               buttonStyleData: ButtonStyleData( | ||||||
|  |                 height: 50, | ||||||
|  |                 padding: const EdgeInsets.only(left: 14, right: 14), | ||||||
|  |                 decoration: BoxDecoration( | ||||||
|  |                   borderRadius: BorderRadius.circular(14), | ||||||
|  |                   border: Border.all( | ||||||
|  |                     color: Theme.of(context).dividerColor, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |       actions: [ | ||||||
|  |         TextButton( | ||||||
|  |           onPressed: _isBusy ? null : () => Navigator.of(context).pop(), | ||||||
|  |           child: Text('dialogCancel').tr(), | ||||||
|  |         ), | ||||||
|  |         TextButton( | ||||||
|  |           onPressed: _isBusy ? null : () => _submit(), | ||||||
|  |           child: Text('dialogConfirm').tr(), | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _FactorTotpFactorDialog extends StatelessWidget { | ||||||
|  |   final SnAuthFactor factor; | ||||||
|  |  | ||||||
|  |   const _FactorTotpFactorDialog({required this.factor}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Center( | ||||||
|  |       child: Column( | ||||||
|  |         mainAxisSize: MainAxisSize.min, | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |         children: [ | ||||||
|  |           Center( | ||||||
|  |             child: Text( | ||||||
|  |               'totpPostSetup', | ||||||
|  |               textAlign: TextAlign.center, | ||||||
|  |               style: Theme.of(context).textTheme.titleLarge, | ||||||
|  |             ).tr().width(280), | ||||||
|  |           ), | ||||||
|  |           const Gap(4), | ||||||
|  |           Center( | ||||||
|  |             child: Text( | ||||||
|  |               'totpPostSetupDescription', | ||||||
|  |               textAlign: TextAlign.center, | ||||||
|  |               style: Theme.of(context).textTheme.bodySmall, | ||||||
|  |             ).tr().width(280), | ||||||
|  |           ), | ||||||
|  |           const Gap(16), | ||||||
|  |           QrImageView( | ||||||
|  |             padding: EdgeInsets.zero, | ||||||
|  |             data: factor.config!['url'], | ||||||
|  |             errorCorrectionLevel: QrErrorCorrectLevel.H, | ||||||
|  |             version: QrVersions.auto, | ||||||
|  |             size: 160, | ||||||
|  |             gapless: true, | ||||||
|  |             eyeStyle: QrEyeStyle( | ||||||
|  |               eyeShape: QrEyeShape.circle, | ||||||
|  |               color: Theme.of(context).colorScheme.onSurface, | ||||||
|  |             ), | ||||||
|  |             dataModuleStyle: QrDataModuleStyle( | ||||||
|  |               dataModuleShape: QrDataModuleShape.square, | ||||||
|  |               color: Theme.of(context).colorScheme.onSurface, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           const Gap(16), | ||||||
|  |           Center( | ||||||
|  |             child: Text( | ||||||
|  |               'totpNeverShare', | ||||||
|  |               textAlign: TextAlign.center, | ||||||
|  |               style: Theme.of(context).textTheme.bodyMedium, | ||||||
|  |             ).tr().bold().width(280), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; | |||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package: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:surface/widgets/navigation/app_scaffold.dart'; | ||||||
| @@ -14,11 +15,6 @@ 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}); | ||||||
|  |  | ||||||
| @@ -212,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, | ||||||
| @@ -267,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; | ||||||
| @@ -328,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) { | ||||||
| @@ -408,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 { | ||||||
| @@ -437,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( | ||||||
| @@ -531,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( | ||||||
|   | |||||||
| @@ -44,6 +44,7 @@ class _RegisterScreenState extends State<RegisterScreen> { | |||||||
|         'nick': nickname, |         'nick': nickname, | ||||||
|         'email': email, |         'email': email, | ||||||
|         'password': password, |         'password': password, | ||||||
|  |         'language': EasyLocalization.of(context)!.currentLocale.toString(), | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       if (!context.mounted) return; |       if (!context.mounted) return; | ||||||
|   | |||||||
| @@ -10,8 +10,10 @@ import 'package:surface/providers/channel.dart'; | |||||||
| import 'package:surface/providers/sn_network.dart'; | import 'package:surface/providers/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/account.dart'; | ||||||
| import 'package:surface/types/chat.dart'; | 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/account/account_select.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/navigation/app_scaffold.dart'; | ||||||
| @@ -20,6 +22,7 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | |||||||
| class ChannelDetailScreen extends StatefulWidget { | class ChannelDetailScreen extends StatefulWidget { | ||||||
|   final String scope; |   final String scope; | ||||||
|   final String alias; |   final String alias; | ||||||
|  |  | ||||||
|   const ChannelDetailScreen({ |   const ChannelDetailScreen({ | ||||||
|     super.key, |     super.key, | ||||||
|     required this.scope, |     required this.scope, | ||||||
| @@ -55,8 +58,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | |||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       final sn = context.read<SnNetworkProvider>(); |       final sn = context.read<SnNetworkProvider>(); | ||||||
|       final resp = await sn.client |       final resp = await sn.client.get('/cgi/im/channels/${_channel!.keyPath}/members/me'); | ||||||
|           .get('/cgi/im/channels/${_channel!.keyPath}/members/me'); |  | ||||||
|       _profile = SnChannelMember.fromJson(resp.data); |       _profile = SnChannelMember.fromJson(resp.data); | ||||||
|       _notifyLevel = _profile!.notify; |       _notifyLevel = _profile!.notify; | ||||||
|       if (!mounted) return; |       if (!mounted) return; | ||||||
| @@ -143,6 +145,25 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Future<void> _addMember(SnAccount related) async { | ||||||
|  |     setState(() => _isBusy = true); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       await sn.client.post( | ||||||
|  |         '/cgi/im/channels/${_channel!.keyPath}/members', | ||||||
|  |         data: {'related': related.name}, | ||||||
|  |       ); | ||||||
|  |       if (!mounted) return; | ||||||
|  |       context.showSnackbar('channelMemberAdded'.tr()); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (!mounted) return; | ||||||
|  |       context.showErrorDialog(err); | ||||||
|  |     } finally { | ||||||
|  |       setState(() => _isBusy = false); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   void _showChannelProfileDetail() { |   void _showChannelProfileDetail() { | ||||||
|     showDialog( |     showDialog( | ||||||
|       context: context, |       context: context, | ||||||
| @@ -166,13 +187,16 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _showMemberAdd() { |   void _showMemberAdd() async { | ||||||
|     showModalBottomSheet( |     final user = await showModalBottomSheet<SnAccount?>( | ||||||
|       context: context, |       context: context, | ||||||
|       builder: (context) => _NewChannelMemberWidget( |       builder: (context) => AccountSelect( | ||||||
|         channel: _channel!, |         title: 'channelMemberAdd'.tr(), | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|  |     if (!mounted) return; | ||||||
|  |     if (user == null) return; | ||||||
|  |     _addMember(user); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -221,11 +245,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | |||||||
|               Column( |               Column( | ||||||
|                 crossAxisAlignment: CrossAxisAlignment.start, |                 crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                 children: [ |                 children: [ | ||||||
|                   Text('channelDetailPersonalRegion') |                   Text('channelDetailPersonalRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), | ||||||
|                       .bold() |  | ||||||
|                       .fontSize(17) |  | ||||||
|                       .tr() |  | ||||||
|                       .padding(horizontal: 20, bottom: 4), |  | ||||||
|                   ListTile( |                   ListTile( | ||||||
|                     leading: const Icon(Symbols.notifications), |                     leading: const Icon(Symbols.notifications), | ||||||
|                     trailing: DropdownButtonHideUnderline( |                     trailing: DropdownButtonHideUnderline( | ||||||
| @@ -264,8 +284,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | |||||||
|                   ), |                   ), | ||||||
|                   ListTile( |                   ListTile( | ||||||
|                     leading: AccountImage( |                     leading: AccountImage( | ||||||
|                       content: |                       content: ud.getAccountFromCache(_profile!.accountId)?.avatar, | ||||||
|                           ud.getAccountFromCache(_profile!.accountId)?.avatar, |  | ||||||
|                       radius: 18, |                       radius: 18, | ||||||
|                     ), |                     ), | ||||||
|                     trailing: const Icon(Symbols.chevron_right), |                     trailing: const Icon(Symbols.chevron_right), | ||||||
| @@ -284,8 +303,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | |||||||
|                       trailing: const Icon(Symbols.chevron_right), |                       trailing: const Icon(Symbols.chevron_right), | ||||||
|                       title: Text('channelActionLeave').tr(), |                       title: Text('channelActionLeave').tr(), | ||||||
|                       subtitle: Text('channelActionLeaveDescription').tr(), |                       subtitle: Text('channelActionLeaveDescription').tr(), | ||||||
|                       contentPadding: |                       contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||||
|                           const EdgeInsets.symmetric(horizontal: 24), |  | ||||||
|                       onTap: _leaveChannel, |                       onTap: _leaveChannel, | ||||||
|                     ), |                     ), | ||||||
|                 ], |                 ], | ||||||
| @@ -293,11 +311,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | |||||||
|             Column( |             Column( | ||||||
|               crossAxisAlignment: CrossAxisAlignment.start, |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|               children: [ |               children: [ | ||||||
|                 Text('channelDetailMemberRegion') |                 Text('channelDetailMemberRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), | ||||||
|                     .bold() |  | ||||||
|                     .fontSize(17) |  | ||||||
|                     .tr() |  | ||||||
|                     .padding(horizontal: 20, bottom: 4), |  | ||||||
|                 ListTile( |                 ListTile( | ||||||
|                   leading: const Icon(Symbols.group), |                   leading: const Icon(Symbols.group), | ||||||
|                   trailing: const Icon(Symbols.chevron_right), |                   trailing: const Icon(Symbols.chevron_right), | ||||||
| @@ -319,11 +333,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | |||||||
|             Column( |             Column( | ||||||
|               crossAxisAlignment: CrossAxisAlignment.start, |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|               children: [ |               children: [ | ||||||
|                 Text('channelDetailAdminRegion') |                 Text('channelDetailAdminRegion').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4), | ||||||
|                     .bold() |  | ||||||
|                     .fontSize(17) |  | ||||||
|                     .tr() |  | ||||||
|                     .padding(horizontal: 20, bottom: 4), |  | ||||||
|                 ListTile( |                 ListTile( | ||||||
|                   leading: const Icon(Symbols.edit), |                   leading: const Icon(Symbols.edit), | ||||||
|                   trailing: const Icon(Symbols.chevron_right), |                   trailing: const Icon(Symbols.chevron_right), | ||||||
| @@ -362,18 +372,17 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> { | |||||||
| class _ChannelProfileDetailDialog extends StatefulWidget { | class _ChannelProfileDetailDialog extends StatefulWidget { | ||||||
|   final SnChannel channel; |   final SnChannel channel; | ||||||
|   final SnChannelMember current; |   final SnChannelMember current; | ||||||
|  |  | ||||||
|   const _ChannelProfileDetailDialog({ |   const _ChannelProfileDetailDialog({ | ||||||
|     required this.channel, |     required this.channel, | ||||||
|     required this.current, |     required this.current, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<_ChannelProfileDetailDialog> createState() => |   State<_ChannelProfileDetailDialog> createState() => _ChannelProfileDetailDialogState(); | ||||||
|       _ChannelProfileDetailDialogState(); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| class _ChannelProfileDetailDialogState | class _ChannelProfileDetailDialogState extends State<_ChannelProfileDetailDialog> { | ||||||
|     extends State<_ChannelProfileDetailDialog> { |  | ||||||
|   bool _isBusy = false; |   bool _isBusy = false; | ||||||
|  |  | ||||||
|   final TextEditingController _nickController = TextEditingController(); |   final TextEditingController _nickController = TextEditingController(); | ||||||
| @@ -444,11 +453,11 @@ class _ChannelProfileDetailDialogState | |||||||
|  |  | ||||||
| class _ChannelMemberListWidget extends StatefulWidget { | class _ChannelMemberListWidget extends StatefulWidget { | ||||||
|   final SnChannel channel; |   final SnChannel channel; | ||||||
|  |  | ||||||
|   const _ChannelMemberListWidget({required this.channel}); |   const _ChannelMemberListWidget({required this.channel}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<_ChannelMemberListWidget> createState() => |   State<_ChannelMemberListWidget> createState() => _ChannelMemberListWidgetState(); | ||||||
|       _ChannelMemberListWidgetState(); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | ||||||
| @@ -463,12 +472,10 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | |||||||
|     try { |     try { | ||||||
|       final ud = context.read<UserDirectoryProvider>(); |       final ud = context.read<UserDirectoryProvider>(); | ||||||
|       final sn = context.read<SnNetworkProvider>(); |       final sn = context.read<SnNetworkProvider>(); | ||||||
|       final resp = await sn.client.get( |       final resp = await sn.client.get('/cgi/im/channels/${widget.channel.keyPath}/members', queryParameters: { | ||||||
|           '/cgi/im/channels/${widget.channel.keyPath}/members', |         'take': 10, | ||||||
|           queryParameters: { |         'offset': 0, | ||||||
|             'take': 10, |       }); | ||||||
|             'offset': 0, |  | ||||||
|           }); |  | ||||||
|       final out = List<SnChannelMember>.from( |       final out = List<SnChannelMember>.from( | ||||||
|         resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [], |         resp.data['data']?.map((e) => SnChannelMember.fromJson(e)) ?? [], | ||||||
|       ); |       ); | ||||||
| @@ -526,9 +533,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | |||||||
|           children: [ |           children: [ | ||||||
|             const Icon(Symbols.group, size: 24), |             const Icon(Symbols.group, size: 24), | ||||||
|             const Gap(16), |             const Gap(16), | ||||||
|             Text('channelMemberManage') |             Text('channelMemberManage').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), | ||||||
|         Expanded( |         Expanded( | ||||||
| @@ -539,8 +544,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | |||||||
|             }, |             }, | ||||||
|             child: InfiniteList( |             child: InfiniteList( | ||||||
|               itemCount: _members.length, |               itemCount: _members.length, | ||||||
|               hasReachedMax: |               hasReachedMax: _totalCount != null && _members.length >= _totalCount!, | ||||||
|                   _totalCount != null && _members.length >= _totalCount!, |  | ||||||
|               isLoading: _isBusy, |               isLoading: _isBusy, | ||||||
|               onFetchData: _fetchMembers, |               onFetchData: _fetchMembers, | ||||||
|               itemBuilder: (context, index) { |               itemBuilder: (context, index) { | ||||||
| @@ -551,8 +555,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | |||||||
|                     content: ud.getAccountFromCache(member.accountId)?.avatar, |                     content: ud.getAccountFromCache(member.accountId)?.avatar, | ||||||
|                   ), |                   ), | ||||||
|                   title: Text( |                   title: Text( | ||||||
|                     ud.getAccountFromCache(member.accountId)?.name ?? |                     ud.getAccountFromCache(member.accountId)?.name ?? 'unknown'.tr(), | ||||||
|                         'unknown'.tr(), |  | ||||||
|                   ), |                   ), | ||||||
|                   subtitle: Text(member.nick ?? 'unknown'.tr()), |                   subtitle: Text(member.nick ?? 'unknown'.tr()), | ||||||
|                   trailing: SizedBox( |                   trailing: SizedBox( | ||||||
| @@ -562,8 +565,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | |||||||
|                       mainAxisAlignment: MainAxisAlignment.end, |                       mainAxisAlignment: MainAxisAlignment.end, | ||||||
|                       children: [ |                       children: [ | ||||||
|                         IconButton( |                         IconButton( | ||||||
|                           onPressed: |                           onPressed: _isUpdating ? null : () => _deleteMember(member), | ||||||
|                               _isUpdating ? null : () => _deleteMember(member), |  | ||||||
|                           icon: const Icon(Symbols.person_remove), |                           icon: const Icon(Symbols.person_remove), | ||||||
|                         ), |                         ), | ||||||
|                       ], |                       ], | ||||||
| @@ -578,83 +580,3 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class _NewChannelMemberWidget extends StatefulWidget { |  | ||||||
|   final SnChannel channel; |  | ||||||
|   const _NewChannelMemberWidget({required this.channel}); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   State<_NewChannelMemberWidget> createState() => |  | ||||||
|       _NewChannelMemberWidgetState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _NewChannelMemberWidgetState extends State<_NewChannelMemberWidget> { |  | ||||||
|   bool _isBusy = false; |  | ||||||
|  |  | ||||||
|   final TextEditingController _relatedController = TextEditingController(); |  | ||||||
|  |  | ||||||
|   Future<void> _performAction() async { |  | ||||||
|     if (_relatedController.text.isEmpty) return; |  | ||||||
|  |  | ||||||
|     setState(() => _isBusy = true); |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       final sn = context.read<SnNetworkProvider>(); |  | ||||||
|       await sn.client.post( |  | ||||||
|         '/cgi/im/channels/${widget.channel.keyPath}/members', |  | ||||||
|         data: { |  | ||||||
|           'related': _relatedController.text, |  | ||||||
|         }, |  | ||||||
|       ); |  | ||||||
|       if (!mounted) return; |  | ||||||
|       Navigator.pop(context, true); |  | ||||||
|       context.showSnackbar('channelMemberAdded'.tr()); |  | ||||||
|     } catch (err) { |  | ||||||
|       if (!mounted) return; |  | ||||||
|       context.showErrorDialog(err); |  | ||||||
|     } finally { |  | ||||||
|       setState(() => _isBusy = false); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   void dispose() { |  | ||||||
|     super.dispose(); |  | ||||||
|     _relatedController.dispose(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     return StyledWidget(Column( |  | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|       children: [ |  | ||||||
|         Text( |  | ||||||
|           'channelMemberAdd', |  | ||||||
|           style: Theme.of(context).textTheme.titleLarge, |  | ||||||
|         ).tr(), |  | ||||||
|         const Gap(12), |  | ||||||
|         TextField( |  | ||||||
|           controller: _relatedController, |  | ||||||
|           readOnly: _isBusy, |  | ||||||
|           autocorrect: false, |  | ||||||
|           autofocus: true, |  | ||||||
|           textCapitalization: TextCapitalization.none, |  | ||||||
|           decoration: InputDecoration( |  | ||||||
|             labelText: 'fieldMemberRelatedName'.tr(), |  | ||||||
|             suffix: SizedBox( |  | ||||||
|               height: 24, |  | ||||||
|               child: IconButton( |  | ||||||
|                 onPressed: _isBusy ? null : () => _performAction(), |  | ||||||
|                 icon: Icon(Symbols.send), |  | ||||||
|                 visualDensity: |  | ||||||
|                     const VisualDensity(horizontal: -4, vertical: -4), |  | ||||||
|                 padding: EdgeInsets.zero, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |  | ||||||
|         ) |  | ||||||
|       ], |  | ||||||
|     )).padding(all: 24); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
|  | import 'dart:developer'; | ||||||
|  |  | ||||||
| import 'package:dio/dio.dart'; | import 'package:dio/dio.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| @@ -9,9 +10,12 @@ import 'package:material_symbols_icons/symbols.dart'; | |||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:surface/controllers/chat_message_controller.dart'; | import 'package:surface/controllers/chat_message_controller.dart'; | ||||||
|  | import 'package:surface/controllers/post_write_controller.dart'; | ||||||
| import 'package:surface/providers/channel.dart'; | import 'package:surface/providers/channel.dart'; | ||||||
| import 'package:surface/providers/chat_call.dart'; | import 'package:surface/providers/chat_call.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/userinfo.dart'; | ||||||
| import 'package:surface/providers/websocket.dart'; | import 'package:surface/providers/websocket.dart'; | ||||||
| import 'package:surface/types/chat.dart'; | import 'package:surface/types/chat.dart'; | ||||||
| import 'package:surface/widgets/chat/call/call_prejoin.dart'; | import 'package:surface/widgets/chat/call/call_prejoin.dart'; | ||||||
| @@ -23,14 +27,19 @@ import 'package:surface/widgets/loading_indicator.dart'; | |||||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | import 'package: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'; | class ChatRoomScreenExtra { | ||||||
| import '../../providers/userinfo.dart'; |   final String? initialText; | ||||||
|  |   final List<PostWriteMedia>? initialAttachments; | ||||||
|  |  | ||||||
|  |   ChatRoomScreenExtra({this.initialText, this.initialAttachments}); | ||||||
|  | } | ||||||
|  |  | ||||||
| class ChatRoomScreen extends StatefulWidget { | class ChatRoomScreen extends StatefulWidget { | ||||||
|   final String scope; |   final String scope; | ||||||
|   final String alias; |   final String alias; | ||||||
|  |   final ChatRoomScreenExtra? extra; | ||||||
|  |  | ||||||
|   const ChatRoomScreen({super.key, required this.scope, required this.alias}); |   const ChatRoomScreen({super.key, required this.scope, required this.alias, this.extra}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   State<ChatRoomScreen> createState() => _ChatRoomScreenState(); |   State<ChatRoomScreen> createState() => _ChatRoomScreenState(); | ||||||
| @@ -177,8 +186,23 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | |||||||
|     _messageController = ChatMessageController(context); |     _messageController = ChatMessageController(context); | ||||||
|     _fetchChannel().then((_) async { |     _fetchChannel().then((_) async { | ||||||
|       await _messageController.initialize(_channel!); |       await _messageController.initialize(_channel!); | ||||||
|       await _messageController.checkUpdate(); |  | ||||||
|       await _fetchOngoingCall(); |       if (widget.extra != null) { | ||||||
|  |         WidgetsBinding.instance.addPostFrameCallback((_) { | ||||||
|  |           log('[ChatInput] Setting initial text and attachments...'); | ||||||
|  |           if (widget.extra!.initialText != null) { | ||||||
|  |             _inputGlobalKey.currentState?.setInitialText(widget.extra!.initialText!); | ||||||
|  |           } | ||||||
|  |           if (widget.extra!.initialAttachments != null) { | ||||||
|  |             _inputGlobalKey.currentState?.setInitialAttachments(widget.extra!.initialAttachments!); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       await Future.wait([ | ||||||
|  |         _messageController.checkUpdate(), | ||||||
|  |         _fetchOngoingCall(), | ||||||
|  |       ]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     final ws = context.read<WebSocketProvider>(); |     final ws = context.read<WebSocketProvider>(); | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ 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/screens/post/post_detail.dart'; | ||||||
| @@ -96,6 +97,8 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|  |     final cfg = context.read<ConfigProvider>(); | ||||||
|  |  | ||||||
|     return AppScaffold( |     return AppScaffold( | ||||||
|       floatingActionButtonLocation: ExpandableFab.location, |       floatingActionButtonLocation: ExpandableFab.location, | ||||||
|       floatingActionButton: ExpandableFab( |       floatingActionButton: ExpandableFab( | ||||||
| @@ -243,8 +246,10 @@ class _ExploreScreenState extends State<ExploreScreen> { | |||||||
|                     ), |                     ), | ||||||
|                     openColor: Colors.transparent, |                     openColor: Colors.transparent, | ||||||
|                     openElevation: 0, |                     openElevation: 0, | ||||||
|                     closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75), |  | ||||||
|                     transitionType: ContainerTransitionType.fade, |                     transitionType: ContainerTransitionType.fade, | ||||||
|  |                     closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity( | ||||||
|  |                           cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1, | ||||||
|  |                         ), | ||||||
|                     closedShape: const RoundedRectangleBorder( |                     closedShape: const RoundedRectangleBorder( | ||||||
|                       borderRadius: BorderRadius.all(Radius.circular(16)), |                       borderRadius: BorderRadius.all(Radius.circular(16)), | ||||||
|                     ), |                     ), | ||||||
|   | |||||||
| @@ -6,15 +6,15 @@ import 'package:provider/provider.dart'; | |||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:surface/providers/relationship.dart'; | import 'package:surface/providers/relationship.dart'; | ||||||
| import 'package:surface/providers/sn_network.dart'; | import 'package:surface/providers/sn_network.dart'; | ||||||
|  | import 'package:surface/providers/userinfo.dart'; | ||||||
| import 'package:surface/types/account.dart'; | import 'package:surface/types/account.dart'; | ||||||
| import 'package:surface/widgets/account/account_image.dart'; | import 'package:surface/widgets/account/account_image.dart'; | ||||||
|  | import 'package:surface/widgets/account/account_select.dart'; | ||||||
| import 'package:surface/widgets/app_bar_leading.dart'; | import 'package:surface/widgets/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/navigation/app_scaffold.dart'; | ||||||
|  | import 'package:surface/widgets/unauthorized_hint.dart'; | ||||||
| import '../providers/userinfo.dart'; |  | ||||||
| import '../widgets/unauthorized_hint.dart'; |  | ||||||
|  |  | ||||||
| const kFriendStatus = { | const kFriendStatus = { | ||||||
|   0: 'friendStatusPending', |   0: 'friendStatusPending', | ||||||
| @@ -168,6 +168,24 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Future<void> _sendRequest(SnAccount user) async { | ||||||
|  |     setState(() => _isBusy = true); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       await sn.client.post('/cgi/id/users/me/relations', data: { | ||||||
|  |         'related': user.name, | ||||||
|  |       }); | ||||||
|  |       if (!mounted) return; | ||||||
|  |       context.showSnackbar('friendRequestSent'.tr()); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (!mounted) return; | ||||||
|  |       context.showErrorDialog(err); | ||||||
|  |     } finally { | ||||||
|  |       setState(() => _isBusy = false); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
| @@ -199,11 +217,16 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|       ), |       ), | ||||||
|       floatingActionButton: FloatingActionButton( |       floatingActionButton: FloatingActionButton( | ||||||
|         child: const Icon(Symbols.add), |         child: const Icon(Symbols.add), | ||||||
|         onPressed: () { |         onPressed: () async { | ||||||
|           showModalBottomSheet( |           final user = await showModalBottomSheet<SnAccount?>( | ||||||
|             context: context, |             context: context, | ||||||
|             builder: (context) => _NewFriendWidget(), |             builder: (context) => AccountSelect( | ||||||
|  |               title: 'friendNew'.tr(), | ||||||
|  |             ), | ||||||
|           ); |           ); | ||||||
|  |           if (!mounted) return; | ||||||
|  |           if (user == null) return; | ||||||
|  |           _sendRequest(user); | ||||||
|         }, |         }, | ||||||
|       ), |       ), | ||||||
|       body: Column( |       body: Column( | ||||||
| @@ -231,8 +254,7 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|               trailing: const Icon(Symbols.chevron_right), |               trailing: const Icon(Symbols.chevron_right), | ||||||
|               onTap: _showBlocks, |               onTap: _showBlocks, | ||||||
|             ), |             ), | ||||||
|           if (_requests.isNotEmpty || _blocks.isNotEmpty) |           if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1), | ||||||
|             const Divider(height: 1), |  | ||||||
|           Expanded( |           Expanded( | ||||||
|             child: MediaQuery.removePadding( |             child: MediaQuery.removePadding( | ||||||
|               context: context, |               context: context, | ||||||
| @@ -264,16 +286,12 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|                               mainAxisAlignment: MainAxisAlignment.end, |                               mainAxisAlignment: MainAxisAlignment.end, | ||||||
|                               children: [ |                               children: [ | ||||||
|                                 InkWell( |                                 InkWell( | ||||||
|                                   onTap: _isUpdating |                                   onTap: _isUpdating ? null : () => _changeRelation(relation, 2), | ||||||
|                                       ? null |  | ||||||
|                                       : () => _changeRelation(relation, 2), |  | ||||||
|                                   child: Text('friendBlock').tr(), |                                   child: Text('friendBlock').tr(), | ||||||
|                                 ), |                                 ), | ||||||
|                                 const Gap(8), |                                 const Gap(8), | ||||||
|                                 InkWell( |                                 InkWell( | ||||||
|                                   onTap: _isUpdating |                                   onTap: _isUpdating ? null : () => _deleteRelation(relation), | ||||||
|                                       ? null |  | ||||||
|                                       : () => _deleteRelation(relation), |  | ||||||
|                                   child: Text('friendDeleteAction').tr(), |                                   child: Text('friendDeleteAction').tr(), | ||||||
|                                 ), |                                 ), | ||||||
|                               ], |                               ], | ||||||
| @@ -293,83 +311,9 @@ class _FriendScreenState extends State<FriendScreen> { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class _NewFriendWidget extends StatefulWidget { |  | ||||||
|   const _NewFriendWidget(); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   State<_NewFriendWidget> createState() => _NewFriendWidgetState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _NewFriendWidgetState extends State<_NewFriendWidget> { |  | ||||||
|   bool _isBusy = false; |  | ||||||
|  |  | ||||||
|   final TextEditingController _relatedController = TextEditingController(); |  | ||||||
|  |  | ||||||
|   Future<void> _sendRequest() async { |  | ||||||
|     if (_relatedController.text.isEmpty) return; |  | ||||||
|  |  | ||||||
|     setState(() => _isBusy = true); |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       final sn = context.read<SnNetworkProvider>(); |  | ||||||
|       await sn.client.post('/cgi/id/users/me/relations', data: { |  | ||||||
|         'related': _relatedController.text, |  | ||||||
|       }); |  | ||||||
|       if (!mounted) return; |  | ||||||
|       Navigator.pop(context, true); |  | ||||||
|       context.showSnackbar('friendRequestSent'.tr()); |  | ||||||
|     } catch (err) { |  | ||||||
|       if (!mounted) return; |  | ||||||
|       context.showErrorDialog(err); |  | ||||||
|     } finally { |  | ||||||
|       setState(() => _isBusy = false); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   void dispose() { |  | ||||||
|     super.dispose(); |  | ||||||
|     _relatedController.dispose(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     return StyledWidget(Column( |  | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|       children: [ |  | ||||||
|         Text( |  | ||||||
|           'friendNew', |  | ||||||
|           style: Theme.of(context).textTheme.titleLarge, |  | ||||||
|         ).tr(), |  | ||||||
|         const Gap(12), |  | ||||||
|         TextField( |  | ||||||
|           controller: _relatedController, |  | ||||||
|           readOnly: _isBusy, |  | ||||||
|           autocorrect: false, |  | ||||||
|           autofocus: true, |  | ||||||
|           textCapitalization: TextCapitalization.none, |  | ||||||
|           decoration: InputDecoration( |  | ||||||
|             labelText: 'fieldFriendRelatedName'.tr(), |  | ||||||
|             suffix: SizedBox( |  | ||||||
|               height: 24, |  | ||||||
|               child: IconButton( |  | ||||||
|                 onPressed: _isBusy ? null : () => _sendRequest(), |  | ||||||
|                 icon: Icon(Symbols.send), |  | ||||||
|                 visualDensity: |  | ||||||
|                     const VisualDensity(horizontal: -4, vertical: -4), |  | ||||||
|                 padding: EdgeInsets.zero, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |  | ||||||
|         ) |  | ||||||
|       ], |  | ||||||
|     )).padding(all: 24); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _FriendshipListWidget extends StatefulWidget { | class _FriendshipListWidget extends StatefulWidget { | ||||||
|   final List<SnRelationship> relations; |   final List<SnRelationship> relations; | ||||||
|  |  | ||||||
|   const _FriendshipListWidget({required this.relations}); |   const _FriendshipListWidget({required this.relations}); | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -476,9 +420,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> { | |||||||
|               mainAxisAlignment: MainAxisAlignment.center, |               mainAxisAlignment: MainAxisAlignment.center, | ||||||
|               crossAxisAlignment: CrossAxisAlignment.end, |               crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|               children: [ |               children: [ | ||||||
|                 Text(kFriendStatus[relation.status] ?? 'unknown') |                 Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75), | ||||||
|                     .tr() |  | ||||||
|                     .opacity(0.75), |  | ||||||
|                 if (relation.status == 0) |                 if (relation.status == 0) | ||||||
|                   Row( |                   Row( | ||||||
|                     mainAxisAlignment: MainAxisAlignment.end, |                     mainAxisAlignment: MainAxisAlignment.end, | ||||||
| @@ -499,8 +441,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> { | |||||||
|                     mainAxisAlignment: MainAxisAlignment.end, |                     mainAxisAlignment: MainAxisAlignment.end, | ||||||
|                     children: [ |                     children: [ | ||||||
|                       InkWell( |                       InkWell( | ||||||
|                         onTap: |                         onTap: _isBusy ? null : () => _changeRelation(relation, 1), | ||||||
|                             _isBusy ? null : () => _changeRelation(relation, 1), |  | ||||||
|                         child: Text('friendUnblock').tr(), |                         child: Text('friendUnblock').tr(), | ||||||
|                       ), |                       ), | ||||||
|                       const Gap(8), |                       const Gap(8), | ||||||
|   | |||||||
| @@ -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,6 +23,7 @@ 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'; | ||||||
| @@ -49,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', | ||||||
| @@ -64,6 +66,11 @@ 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 | ||||||
| @@ -230,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(); | ||||||
|  |  | ||||||
| @@ -407,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), | ||||||
|  |                       ], | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ); | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -7,6 +7,7 @@ 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'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  | import 'package:surface/providers/notification.dart'; | ||||||
| import 'package:surface/providers/sn_network.dart'; | import 'package:surface/providers/sn_network.dart'; | ||||||
| import 'package:surface/types/notification.dart'; | import 'package:surface/types/notification.dart'; | ||||||
| import 'package:surface/types/post.dart'; | import 'package:surface/types/post.dart'; | ||||||
| @@ -21,6 +22,16 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart'; | |||||||
| import '../providers/userinfo.dart'; | import '../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}); | ||||||
|  |  | ||||||
| @@ -36,13 +47,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; | ||||||
| @@ -51,6 +55,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | |||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       final sn = context.read<SnNetworkProvider>(); |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       final nty = context.read<NotificationProvider>(); | ||||||
|       final resp = await sn.client.get('/cgi/id/notifications?take=10'); |       final resp = await sn.client.get('/cgi/id/notifications?take=10'); | ||||||
|       _totalCount = resp.data['count']; |       _totalCount = resp.data['count']; | ||||||
|       _notifications.addAll( |       _notifications.addAll( | ||||||
| @@ -59,6 +64,7 @@ class _NotificationScreenState extends State<NotificationScreen> { | |||||||
|                 .cast<SnNotification>() ?? |                 .cast<SnNotification>() ?? | ||||||
|             [], |             [], | ||||||
|       ); |       ); | ||||||
|  |       nty.updateTray(); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       if (!mounted) return; |       if (!mounted) return; | ||||||
|       context.showErrorDialog(err); |       context.showErrorDialog(err); | ||||||
| @@ -85,9 +91,11 @@ class _NotificationScreenState extends State<NotificationScreen> { | |||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       final sn = context.read<SnNetworkProvider>(); |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       final nty = context.read<NotificationProvider>(); | ||||||
|       final resp = await sn.client.put('/cgi/id/notifications/read/all'); |       final resp = await sn.client.put('/cgi/id/notifications/read/all'); | ||||||
|       _notifications.clear(); |       _notifications.clear(); | ||||||
|       _fetchNotifications(); |       _fetchNotifications(); | ||||||
|  |       nty.clear(); | ||||||
|  |  | ||||||
|       if (!mounted) return; |       if (!mounted) return; | ||||||
|       context.showSnackbar( |       context.showSnackbar( | ||||||
|   | |||||||
| @@ -1,11 +1,17 @@ | |||||||
|  | import 'dart:io'; | ||||||
|  |  | ||||||
| import 'package:collection/collection.dart'; | import 'package:collection/collection.dart'; | ||||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | import 'package:dropdown_button2/dropdown_button2.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/gestures.dart'; | import 'package:flutter/gestures.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:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
|  | import 'package:hotkey_manager/hotkey_manager.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|  | import 'package:pasteboard/pasteboard.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:surface/controllers/post_write_controller.dart'; | import 'package:surface/controllers/post_write_controller.dart'; | ||||||
| import 'package:surface/providers/config.dart'; | import 'package:surface/providers/config.dart'; | ||||||
| @@ -20,13 +26,15 @@ import 'package:surface/widgets/post/post_meta_editor.dart'; | |||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.dart'; | ||||||
|  |  | ||||||
| class PostEditorExtraProps { | import '../../types/attachment.dart'; | ||||||
|  |  | ||||||
|  | class PostEditorExtra { | ||||||
|   final String? text; |   final String? text; | ||||||
|   final String? title; |   final String? title; | ||||||
|   final String? description; |   final String? description; | ||||||
|   final List<PostWriteMedia>? attachments; |   final List<PostWriteMedia>? attachments; | ||||||
|  |  | ||||||
|   const PostEditorExtraProps({ |   const PostEditorExtra({ | ||||||
|     this.text, |     this.text, | ||||||
|     this.title, |     this.title, | ||||||
|     this.description, |     this.description, | ||||||
| @@ -39,7 +47,7 @@ class PostEditorScreen extends StatefulWidget { | |||||||
|   final int? postEditId; |   final int? postEditId; | ||||||
|   final int? postReplyId; |   final int? postReplyId; | ||||||
|   final int? postRepostId; |   final int? postRepostId; | ||||||
|   final PostEditorExtraProps? extraProps; |   final PostEditorExtra? extraProps; | ||||||
|  |  | ||||||
|   const PostEditorScreen({ |   const PostEditorScreen({ | ||||||
|     super.key, |     super.key, | ||||||
| @@ -94,15 +102,39 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   final HotKey _pasteHotKey = HotKey( | ||||||
|  |     key: PhysicalKeyboardKey.keyV, | ||||||
|  |     modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control], | ||||||
|  |     scope: HotKeyScope.inapp, | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   void _registerHotKey() { | ||||||
|  |     if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; | ||||||
|  |     hotKeyManager.register(_pasteHotKey, keyDownHandler: (_) async { | ||||||
|  |       final imageBytes = await Pasteboard.image; | ||||||
|  |       if (imageBytes == null) return; | ||||||
|  |       _writeController.addAttachments([ | ||||||
|  |         PostWriteMedia.fromBytes( | ||||||
|  |           imageBytes, | ||||||
|  |           'attachmentPastedImage'.tr(), | ||||||
|  |           SnMediaType.image, | ||||||
|  |         ), | ||||||
|  |       ]); | ||||||
|  |       setState(() {}); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void dispose() { |   void dispose() { | ||||||
|     _writeController.dispose(); |     _writeController.dispose(); | ||||||
|  |     if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey); | ||||||
|     super.dispose(); |     super.dispose(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|  |     _registerHotKey(); | ||||||
|     if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) { |     if (!PostWriteController.kTitleMap.keys.contains(widget.mode)) { | ||||||
|       context.showErrorDialog('Unknown post type'); |       context.showErrorDialog('Unknown post type'); | ||||||
|       Navigator.pop(context); |       Navigator.pop(context); | ||||||
| @@ -153,6 +185,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | |||||||
|                       ), |                       ), | ||||||
|                 ), |                 ), | ||||||
|               ]), |               ]), | ||||||
|  |               maxLines: 2, | ||||||
|             ), |             ), | ||||||
|             actions: [ |             actions: [ | ||||||
|               IconButton( |               IconButton( | ||||||
| @@ -250,121 +283,130 @@ class _PostEditorScreenState extends State<PostEditorScreen> { | |||||||
|               ), |               ), | ||||||
|               const Divider(height: 1), |               const Divider(height: 1), | ||||||
|               Expanded( |               Expanded( | ||||||
|                 child: SingleChildScrollView( |                 child: Stack( | ||||||
|                   padding: EdgeInsets.only(bottom: 8), |                   children: [ | ||||||
|                   child: Column( |                     SingleChildScrollView( | ||||||
|                     children: [ |                       padding: EdgeInsets.only(bottom: 160), | ||||||
|                       // Replying Notice |                       child: Column( | ||||||
|                       if (_writeController.replyingPost != null) |                         children: [ | ||||||
|                         Column( |                           // Replying Notice | ||||||
|                           children: [ |                           if (_writeController.replyingPost != null) | ||||||
|                             ExpansionTile( |                             Column( | ||||||
|                               minTileHeight: 48, |                               children: [ | ||||||
|                               leading: const Icon(Symbols.reply).padding(left: 4), |                                 ExpansionTile( | ||||||
|                               title: Text('postReplyingNotice') |                                   minTileHeight: 48, | ||||||
|                                   .fontSize(15) |                                   leading: const Icon(Symbols.reply).padding(left: 4), | ||||||
|                                   .tr(args: ['@${_writeController.replyingPost!.publisher.name}']), |                                   title: Text('postReplyingNotice') | ||||||
|                               children: <Widget>[PostItem(data: _writeController.replyingPost!)], |                                       .fontSize(15) | ||||||
|                             ), |                                       .tr(args: ['@${_writeController.replyingPost!.publisher.name}']), | ||||||
|                             const Divider(height: 1), |                                   children: <Widget>[PostItem(data: _writeController.replyingPost!)], | ||||||
|                           ], |                                 ), | ||||||
|                         ), |                                 const Divider(height: 1), | ||||||
|                       // Reposting Notice |  | ||||||
|                       if (_writeController.repostingPost != null) |  | ||||||
|                         Column( |  | ||||||
|                           children: [ |  | ||||||
|                             ExpansionTile( |  | ||||||
|                               minTileHeight: 48, |  | ||||||
|                               leading: const Icon(Symbols.forward).padding(left: 4), |  | ||||||
|                               title: Text('postRepostingNotice') |  | ||||||
|                                   .fontSize(15) |  | ||||||
|                                   .tr(args: ['@${_writeController.repostingPost!.publisher.name}']), |  | ||||||
|                               children: <Widget>[ |  | ||||||
|                                 PostItem( |  | ||||||
|                                   data: _writeController.repostingPost!, |  | ||||||
|                                 ) |  | ||||||
|                               ], |                               ], | ||||||
|                             ), |                             ), | ||||||
|                             const Divider(height: 1), |                           // Reposting Notice | ||||||
|                           ], |                           if (_writeController.repostingPost != null) | ||||||
|                         ), |                             Column( | ||||||
|                       // Editing Notice |                               children: [ | ||||||
|                       if (_writeController.editingPost != null) |                                 ExpansionTile( | ||||||
|                         Column( |                                   minTileHeight: 48, | ||||||
|                           children: [ |                                   leading: const Icon(Symbols.forward).padding(left: 4), | ||||||
|                             ExpansionTile( |                                   title: Text('postRepostingNotice') | ||||||
|                               minTileHeight: 48, |                                       .fontSize(15) | ||||||
|                               leading: const Icon(Symbols.edit_note).padding(left: 4), |                                       .tr(args: ['@${_writeController.repostingPost!.publisher.name}']), | ||||||
|                               title: Text('postEditingNotice') |                                   children: <Widget>[ | ||||||
|                                   .fontSize(15) |                                     PostItem( | ||||||
|                                   .tr(args: ['@${_writeController.editingPost!.publisher.name}']), |                                       data: _writeController.repostingPost!, | ||||||
|                               children: <Widget>[PostItem(data: _writeController.editingPost!)], |                                     ) | ||||||
|  |                                   ], | ||||||
|  |                                 ), | ||||||
|  |                                 const Divider(height: 1), | ||||||
|  |                               ], | ||||||
|                             ), |                             ), | ||||||
|                             const Divider(height: 1), |                           // Editing Notice | ||||||
|                           ], |                           if (_writeController.editingPost != null) | ||||||
|                         ), |                             Column( | ||||||
|                       // Content Input Area |                               children: [ | ||||||
|                       Container( |                                 ExpansionTile( | ||||||
|                         constraints: const BoxConstraints(maxWidth: 640), |                                   minTileHeight: 48, | ||||||
|                         child: TextField( |                                   leading: const Icon(Symbols.edit_note).padding(left: 4), | ||||||
|                           controller: _writeController.contentController, |                                   title: Text('postEditingNotice') | ||||||
|                           maxLines: null, |                                       .fontSize(15) | ||||||
|                           decoration: InputDecoration( |                                       .tr(args: ['@${_writeController.editingPost!.publisher.name}']), | ||||||
|                             hintText: 'fieldPostContent'.tr(), |                                   children: <Widget>[PostItem(data: _writeController.editingPost!)], | ||||||
|                             hintStyle: TextStyle(fontSize: 14), |                                 ), | ||||||
|                             isCollapsed: true, |                                 const Divider(height: 1), | ||||||
|                             contentPadding: const EdgeInsets.symmetric( |                               ], | ||||||
|                               horizontal: 16, |                             ), | ||||||
|  |                           // Content Input Area | ||||||
|  |                           Container( | ||||||
|  |                             constraints: const BoxConstraints(maxWidth: 640), | ||||||
|  |                             child: TextField( | ||||||
|  |                               controller: _writeController.contentController, | ||||||
|  |                               maxLines: null, | ||||||
|  |                               decoration: InputDecoration( | ||||||
|  |                                 hintText: 'fieldPostContent'.tr(), | ||||||
|  |                                 hintStyle: TextStyle(fontSize: 14), | ||||||
|  |                                 isCollapsed: true, | ||||||
|  |                                 contentPadding: const EdgeInsets.symmetric( | ||||||
|  |                                   horizontal: 16, | ||||||
|  |                                 ), | ||||||
|  |                                 border: InputBorder.none, | ||||||
|  |                               ), | ||||||
|  |                               onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|                             ), |                             ), | ||||||
|                             border: InputBorder.none, |  | ||||||
|                           ), |                           ), | ||||||
|                           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |                         ] | ||||||
|                         ), |                             .expandIndexed( | ||||||
|  |                               (idx, ele) => [ | ||||||
|  |                                 if (idx != 0 || _writeController.isRelatedNull) const Gap(8), | ||||||
|  |                                 ele, | ||||||
|  |                               ], | ||||||
|  |                             ) | ||||||
|  |                             .toList(), | ||||||
|                       ), |                       ), | ||||||
|                     ] |                     ), | ||||||
|                         .expandIndexed( |                     if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null) | ||||||
|                           (idx, ele) => [ |                       Positioned( | ||||||
|                             if (idx != 0 || _writeController.isRelatedNull) const Gap(8), |                         bottom: 0, | ||||||
|                             ele, |                         left: 0, | ||||||
|                           ], |                         right: 0, | ||||||
|                         ) |                         child: PostMediaPendingList( | ||||||
|                         .toList(), |                           thumbnail: _writeController.thumbnail, | ||||||
|                   ), |                           attachments: _writeController.attachments, | ||||||
|  |                           isBusy: _writeController.isBusy, | ||||||
|  |                           onUpload: (int idx) async { | ||||||
|  |                             await _writeController.uploadSingleAttachment(context, idx); | ||||||
|  |                           }, | ||||||
|  |                           onPostSetThumbnail: (int? idx) { | ||||||
|  |                             _writeController.setThumbnail(idx); | ||||||
|  |                           }, | ||||||
|  |                           onInsertLink: (int idx) async { | ||||||
|  |                             _writeController.contentController.text += | ||||||
|  |                                 '\n'; | ||||||
|  |                           }, | ||||||
|  |                           onUpdate: (int idx, PostWriteMedia updatedMedia) async { | ||||||
|  |                             _writeController.setIsBusy(true); | ||||||
|  |                             try { | ||||||
|  |                               _writeController.setAttachmentAt(idx, updatedMedia); | ||||||
|  |                             } finally { | ||||||
|  |                               _writeController.setIsBusy(false); | ||||||
|  |                             } | ||||||
|  |                           }, | ||||||
|  |                           onRemove: (int idx) async { | ||||||
|  |                             _writeController.setIsBusy(true); | ||||||
|  |                             try { | ||||||
|  |                               _writeController.removeAttachmentAt(idx); | ||||||
|  |                             } finally { | ||||||
|  |                               _writeController.setIsBusy(false); | ||||||
|  |                             } | ||||||
|  |                           }, | ||||||
|  |                           onUpdateBusy: (state) => _writeController.setIsBusy(state), | ||||||
|  |                         ).padding(bottom: 8), | ||||||
|  |                       ), | ||||||
|  |                   ], | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|               if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null) |  | ||||||
|                 PostMediaPendingList( |  | ||||||
|                   thumbnail: _writeController.thumbnail, |  | ||||||
|                   attachments: _writeController.attachments, |  | ||||||
|                   isBusy: _writeController.isBusy, |  | ||||||
|                   onUpload: (int idx) async { |  | ||||||
|                     await _writeController.uploadSingleAttachment(context, idx); |  | ||||||
|                   }, |  | ||||||
|                   onPostSetThumbnail: (int? idx) { |  | ||||||
|                     _writeController.setThumbnail(idx); |  | ||||||
|                   }, |  | ||||||
|                   onInsertLink: (int idx) async { |  | ||||||
|                     _writeController.contentController.text += |  | ||||||
|                         '\n'; |  | ||||||
|                   }, |  | ||||||
|                   onUpdate: (int idx, PostWriteMedia updatedMedia) async { |  | ||||||
|                     _writeController.setIsBusy(true); |  | ||||||
|                     try { |  | ||||||
|                       _writeController.setAttachmentAt(idx, updatedMedia); |  | ||||||
|                     } finally { |  | ||||||
|                       _writeController.setIsBusy(false); |  | ||||||
|                     } |  | ||||||
|                   }, |  | ||||||
|                   onRemove: (int idx) async { |  | ||||||
|                     _writeController.setIsBusy(true); |  | ||||||
|                     try { |  | ||||||
|                       _writeController.removeAttachmentAt(idx); |  | ||||||
|                     } finally { |  | ||||||
|                       _writeController.setIsBusy(false); |  | ||||||
|                     } |  | ||||||
|                   }, |  | ||||||
|                   onUpdateBusy: (state) => _writeController.setIsBusy(state), |  | ||||||
|                 ).padding(bottom: 8), |  | ||||||
|               Material( |               Material( | ||||||
|                 elevation: 2, |                 elevation: 2, | ||||||
|                 child: Column( |                 child: Column( | ||||||
|   | |||||||
| @@ -8,9 +8,11 @@ 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/account.dart'; | ||||||
| import 'package:surface/types/post.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/account/account_select.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/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'; | ||||||
| @@ -229,13 +231,35 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   void _showMemberAdd() { |   Future<void> _addMember(SnAccount related) async { | ||||||
|     showModalBottomSheet( |     setState(() => _isBusy = true); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       await sn.client.post( | ||||||
|  |         '/cgi/id/realms/${widget.realm!.alias}/members', | ||||||
|  |         data: {'related': related.name}, | ||||||
|  |       ); | ||||||
|  |       if (!mounted) return; | ||||||
|  |       context.showSnackbar('realmMemberAdded'.tr()); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (!mounted) return; | ||||||
|  |       context.showErrorDialog(err); | ||||||
|  |     } finally { | ||||||
|  |       setState(() => _isBusy = false); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _showMemberAdd() async { | ||||||
|  |     final user = await showModalBottomSheet<SnAccount?>( | ||||||
|       context: context, |       context: context, | ||||||
|       builder: (context) => _NewRealmMemberWidget( |       builder: (context) => AccountSelect( | ||||||
|         realm: widget.realm!, |         title: 'realmMemberAdd'.tr(), | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|  |     if (!mounted) return; | ||||||
|  |     if (user == null) return; | ||||||
|  |     _addMember(user); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -293,85 +317,6 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class _NewRealmMemberWidget extends StatefulWidget { |  | ||||||
|   final SnRealm realm; |  | ||||||
|  |  | ||||||
|   const _NewRealmMemberWidget({required this.realm}); |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   State<_NewRealmMemberWidget> createState() => _NewRealmMemberWidgetState(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _NewRealmMemberWidgetState extends State<_NewRealmMemberWidget> { |  | ||||||
|   bool _isBusy = false; |  | ||||||
|  |  | ||||||
|   final TextEditingController _relatedController = TextEditingController(); |  | ||||||
|  |  | ||||||
|   Future<void> _performAction() async { |  | ||||||
|     if (_relatedController.text.isEmpty) return; |  | ||||||
|  |  | ||||||
|     setState(() => _isBusy = true); |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       final sn = context.read<SnNetworkProvider>(); |  | ||||||
|       await sn.client.post( |  | ||||||
|         '/cgi/id/realms/${widget.realm.alias}/members', |  | ||||||
|         data: { |  | ||||||
|           'related': _relatedController.text, |  | ||||||
|         }, |  | ||||||
|       ); |  | ||||||
|       if (!mounted) return; |  | ||||||
|       Navigator.pop(context, true); |  | ||||||
|       context.showSnackbar('channelMemberAdded'.tr()); |  | ||||||
|     } catch (err) { |  | ||||||
|       if (!mounted) return; |  | ||||||
|       context.showErrorDialog(err); |  | ||||||
|     } finally { |  | ||||||
|       setState(() => _isBusy = false); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   void dispose() { |  | ||||||
|     super.dispose(); |  | ||||||
|     _relatedController.dispose(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     return StyledWidget(Column( |  | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|       children: [ |  | ||||||
|         Text( |  | ||||||
|           'realmMemberAdd', |  | ||||||
|           style: Theme.of(context).textTheme.titleLarge, |  | ||||||
|         ).tr(), |  | ||||||
|         const Gap(12), |  | ||||||
|         TextField( |  | ||||||
|           controller: _relatedController, |  | ||||||
|           readOnly: _isBusy, |  | ||||||
|           autocorrect: false, |  | ||||||
|           autofocus: true, |  | ||||||
|           textCapitalization: TextCapitalization.none, |  | ||||||
|           decoration: InputDecoration( |  | ||||||
|             labelText: 'fieldMemberRelatedName'.tr(), |  | ||||||
|             suffix: SizedBox( |  | ||||||
|               height: 24, |  | ||||||
|               child: IconButton( |  | ||||||
|                 onPressed: _isBusy ? null : () => _performAction(), |  | ||||||
|                 icon: Icon(Symbols.send), |  | ||||||
|                 visualDensity: const VisualDensity(horizontal: -4, vertical: -4), |  | ||||||
|                 padding: EdgeInsets.zero, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |  | ||||||
|         ) |  | ||||||
|       ], |  | ||||||
|     )).padding(all: 24); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _RealmSettingsWidget extends StatefulWidget { | class _RealmSettingsWidget extends StatefulWidget { | ||||||
|   final SnRealm? realm; |   final SnRealm? realm; | ||||||
|   final Function() onUpdate; |   final Function() onUpdate; | ||||||
|   | |||||||
| @@ -82,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(), | ||||||
| @@ -147,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; | ||||||
| @@ -206,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(() {}); | ||||||
| @@ -342,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, | ||||||
| @@ -354,7 +400,7 @@ class _SettingsScreenState extends State<SettingsScreen> { | |||||||
|                                 ], |                                 ], | ||||||
|                               ), |                               ), | ||||||
|                             ), |                             ), | ||||||
|                           ) |                       ) | ||||||
|                           .toList(), |                           .toList(), | ||||||
|                       value: _serverUrlController.text, |                       value: _serverUrlController.text, | ||||||
|                       onChanged: (String? value) { |                       onChanged: (String? value) { | ||||||
| @@ -409,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; | ||||||
|   | |||||||
| @@ -8,9 +8,20 @@ import 'package:flutter/foundation.dart'; | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.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:material_symbols_icons/material_symbols_icons.dart'; | ||||||
|  | import 'package:provider/provider.dart'; | ||||||
| import 'package:receive_sharing_intent/receive_sharing_intent.dart'; | import 'package:receive_sharing_intent/receive_sharing_intent.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:surface/controllers/post_write_controller.dart'; | import 'package:surface/controllers/post_write_controller.dart'; | ||||||
|  | import 'package:surface/providers/channel.dart'; | ||||||
|  | import 'package:surface/providers/user_directory.dart'; | ||||||
|  | import 'package:surface/providers/userinfo.dart'; | ||||||
|  | import 'package:surface/screens/chat/room.dart'; | ||||||
| import 'package:surface/screens/post/post_editor.dart'; | import 'package:surface/screens/post/post_editor.dart'; | ||||||
|  | import 'package:surface/types/chat.dart'; | ||||||
|  | import 'package:surface/widgets/account/account_image.dart'; | ||||||
|  | import 'package:surface/widgets/dialog.dart'; | ||||||
|  | import 'package:surface/widgets/loading_indicator.dart'; | ||||||
|  |  | ||||||
| class AppSharingListener extends StatefulWidget { | class AppSharingListener extends StatefulWidget { | ||||||
|   final Widget child; |   final Widget child; | ||||||
| @@ -51,20 +62,39 @@ class _AppSharingListenerState extends State<AppSharingListener> { | |||||||
|                           pathParameters: { |                           pathParameters: { | ||||||
|                             'mode': 'stories', |                             'mode': 'stories', | ||||||
|                           }, |                           }, | ||||||
|                           extra: PostEditorExtraProps( |                           extra: PostEditorExtra( | ||||||
|                             text: value |                             text: value | ||||||
|                                 .where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type)) |                                 .where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type)) | ||||||
|                                 .map((e) => e.path).join('\n'), |                                 .map((e) => e.path) | ||||||
|  |                                 .join('\n'), | ||||||
|                             attachments: value |                             attachments: value | ||||||
|                                 .where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type)) |                                 .where((e) => [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image] | ||||||
|                                 .map((e) => PostWriteMedia.fromFile(XFile(e.path))).toList(), |                                     .contains(e.type)) | ||||||
|  |                                 .map((e) => PostWriteMedia.fromFile(XFile(e.path))) | ||||||
|  |                                 .toList(), | ||||||
|                           ), |                           ), | ||||||
|                         ); |                         ); | ||||||
|                         Navigator.pop(context); |                         Navigator.pop(context); | ||||||
|                       }, |                       }, | ||||||
|                     ), |                     ), | ||||||
|  |                     ListTile( | ||||||
|  |                       contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||||
|  |                       shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), | ||||||
|  |                       leading: Icon(Icons.chat_outlined), | ||||||
|  |                       trailing: const Icon(Icons.chevron_right), | ||||||
|  |                       title: Text('shareIntentSendChannel').tr(), | ||||||
|  |                       onTap: () { | ||||||
|  |                         showModalBottomSheet( | ||||||
|  |                           context: context, | ||||||
|  |                           builder: (context) => _ShareIntentChannelSelect(value: value), | ||||||
|  |                         ).then((val) { | ||||||
|  |                           if (!context.mounted) return; | ||||||
|  |                           if (val == true) Navigator.pop(context); | ||||||
|  |                         }); | ||||||
|  |                       }, | ||||||
|  |                     ), | ||||||
|                   ], |                   ], | ||||||
|                 ), |                 ).width(280), | ||||||
|               ) |               ) | ||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
| @@ -103,7 +133,7 @@ class _AppSharingListenerState extends State<AppSharingListener> { | |||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|     if(!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { |     if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { | ||||||
|       _initialize(); |       _initialize(); | ||||||
|       _initialHandle(); |       _initialHandle(); | ||||||
|     } |     } | ||||||
| @@ -120,3 +150,193 @@ class _AppSharingListenerState extends State<AppSharingListener> { | |||||||
|     return widget.child; |     return widget.child; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class _ShareIntentChannelSelect extends StatefulWidget { | ||||||
|  |   final Iterable<SharedMediaFile> value; | ||||||
|  |  | ||||||
|  |   const _ShareIntentChannelSelect({required this.value}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<_ShareIntentChannelSelect> createState() => _ShareIntentChannelSelectState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _ShareIntentChannelSelectState extends State<_ShareIntentChannelSelect> { | ||||||
|  |   bool _isBusy = true; | ||||||
|  |  | ||||||
|  |   List<SnChannel>? _channels; | ||||||
|  |   Map<int, SnChatMessage>? _lastMessages; | ||||||
|  |  | ||||||
|  |   void _refreshChannels() { | ||||||
|  |     final ua = context.read<UserProvider>(); | ||||||
|  |     if (!ua.isAuthorized) { | ||||||
|  |       setState(() => _isBusy = false); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     final chan = context.read<ChatChannelProvider>(); | ||||||
|  |     chan.fetchChannels().listen((channels) async { | ||||||
|  |       final lastMessages = await chan.getLastMessages(channels); | ||||||
|  |       _lastMessages = {for (final val in lastMessages) val.channelId: val}; | ||||||
|  |       channels.sort((a, b) { | ||||||
|  |         if (_lastMessages!.containsKey(a.id) && _lastMessages!.containsKey(b.id)) { | ||||||
|  |           return _lastMessages![b.id]!.createdAt.compareTo(_lastMessages![a.id]!.createdAt); | ||||||
|  |         } | ||||||
|  |         if (_lastMessages!.containsKey(a.id)) return -1; | ||||||
|  |         if (_lastMessages!.containsKey(b.id)) return 1; | ||||||
|  |         return 0; | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (!mounted) return; | ||||||
|  |       final ud = context.read<UserDirectoryProvider>(); | ||||||
|  |       for (final channel in channels) { | ||||||
|  |         if (channel.type == 1) { | ||||||
|  |           await ud.listAccount( | ||||||
|  |             channel.members | ||||||
|  |                     ?.cast<SnChannelMember?>() | ||||||
|  |                     .map((ele) => ele?.accountId) | ||||||
|  |                     .where((ele) => ele != null) | ||||||
|  |                     .toSet() ?? | ||||||
|  |                 {}, | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (mounted) setState(() => _channels = channels); | ||||||
|  |     }) | ||||||
|  |       ..onError((err) { | ||||||
|  |         if (!mounted) return; | ||||||
|  |         context.showErrorDialog(err); | ||||||
|  |         setState(() => _isBusy = false); | ||||||
|  |       }) | ||||||
|  |       ..onDone(() { | ||||||
|  |         if (!mounted) return; | ||||||
|  |         setState(() => _isBusy = false); | ||||||
|  |       }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |     _refreshChannels(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final ua = context.read<UserProvider>(); | ||||||
|  |     final ud = context.read<UserDirectoryProvider>(); | ||||||
|  |  | ||||||
|  |     return Column( | ||||||
|  |       children: [ | ||||||
|  |         Row( | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |           children: [ | ||||||
|  |             const Icon(Symbols.chat, size: 24), | ||||||
|  |             const Gap(16), | ||||||
|  |             Text('shareIntentSendChannel', style: Theme.of(context).textTheme.titleLarge).tr(), | ||||||
|  |           ], | ||||||
|  |         ).padding(horizontal: 20, top: 16, bottom: 12), | ||||||
|  |         LoadingIndicator(isActive: _isBusy), | ||||||
|  |         Expanded( | ||||||
|  |           child: MediaQuery.removePadding( | ||||||
|  |             context: context, | ||||||
|  |             removeTop: true, | ||||||
|  |             child: RefreshIndicator( | ||||||
|  |               onRefresh: () => Future.sync(() => _refreshChannels()), | ||||||
|  |               child: ListView.builder( | ||||||
|  |                 itemCount: _channels?.length ?? 0, | ||||||
|  |                 itemBuilder: (context, idx) { | ||||||
|  |                   final channel = _channels![idx]; | ||||||
|  |                   final lastMessage = _lastMessages?[channel.id]; | ||||||
|  |  | ||||||
|  |                   if (channel.type == 1) { | ||||||
|  |                     final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere( | ||||||
|  |                           (ele) => ele?.accountId != ua.user?.id, | ||||||
|  |                           orElse: () => null, | ||||||
|  |                         ); | ||||||
|  |  | ||||||
|  |                     return ListTile( | ||||||
|  |                       title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name), | ||||||
|  |                       subtitle: lastMessage != null | ||||||
|  |                           ? Text( | ||||||
|  |                               '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||||
|  |                               maxLines: 1, | ||||||
|  |                               overflow: TextOverflow.ellipsis, | ||||||
|  |                             ) | ||||||
|  |                           : Text( | ||||||
|  |                               'channelDirectMessageDescription'.tr(args: [ | ||||||
|  |                                 '@${ud.getAccountFromCache(otherMember?.accountId)?.name}', | ||||||
|  |                               ]), | ||||||
|  |                               maxLines: 1, | ||||||
|  |                               overflow: TextOverflow.ellipsis, | ||||||
|  |                             ), | ||||||
|  |                       contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||||
|  |                       leading: AccountImage( | ||||||
|  |                         content: ud.getAccountFromCache(otherMember?.accountId)?.avatar, | ||||||
|  |                       ), | ||||||
|  |                       onTap: () { | ||||||
|  |                         GoRouter.of(context).pushNamed( | ||||||
|  |                           'chatRoom', | ||||||
|  |                           pathParameters: { | ||||||
|  |                             'scope': channel.realm?.alias ?? 'global', | ||||||
|  |                             'alias': channel.alias, | ||||||
|  |                           }, | ||||||
|  |                         ).then((value) { | ||||||
|  |                           if (mounted) _refreshChannels(); | ||||||
|  |                         }); | ||||||
|  |                       }, | ||||||
|  |                     ); | ||||||
|  |                   } | ||||||
|  |  | ||||||
|  |                   return ListTile( | ||||||
|  |                     title: Text(channel.name), | ||||||
|  |                     subtitle: lastMessage != null | ||||||
|  |                         ? Text( | ||||||
|  |                             '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', | ||||||
|  |                             maxLines: 1, | ||||||
|  |                             overflow: TextOverflow.ellipsis, | ||||||
|  |                           ) | ||||||
|  |                         : Text( | ||||||
|  |                             channel.description, | ||||||
|  |                             maxLines: 1, | ||||||
|  |                             overflow: TextOverflow.ellipsis, | ||||||
|  |                           ), | ||||||
|  |                     contentPadding: const EdgeInsets.symmetric(horizontal: 16), | ||||||
|  |                     leading: AccountImage( | ||||||
|  |                       content: null, | ||||||
|  |                       fallbackWidget: const Icon(Symbols.chat, size: 20), | ||||||
|  |                     ), | ||||||
|  |                     onTap: () { | ||||||
|  |                       Navigator.pop(context, true); | ||||||
|  |                       GoRouter.of(context) | ||||||
|  |                           .pushNamed( | ||||||
|  |                         'chatRoom', | ||||||
|  |                         pathParameters: { | ||||||
|  |                           'scope': channel.realm?.alias ?? 'global', | ||||||
|  |                           'alias': channel.alias, | ||||||
|  |                         }, | ||||||
|  |                         extra: ChatRoomScreenExtra( | ||||||
|  |                           initialText: widget.value | ||||||
|  |                               .where((e) => [SharedMediaType.text, SharedMediaType.url].contains(e.type)) | ||||||
|  |                               .map((e) => e.path) | ||||||
|  |                               .join('\n'), | ||||||
|  |                           initialAttachments: widget.value | ||||||
|  |                               .where((e) => | ||||||
|  |                                   [SharedMediaType.video, SharedMediaType.file, SharedMediaType.image].contains(e.type)) | ||||||
|  |                               .map((e) => PostWriteMedia.fromFile(XFile(e.path))) | ||||||
|  |                               .toList(), | ||||||
|  |                         ), | ||||||
|  |                       ) | ||||||
|  |                           .then((value) { | ||||||
|  |                         if (value == true) _refreshChannels(); | ||||||
|  |                       }); | ||||||
|  |                     }, | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										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), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -33,7 +33,7 @@ Future<ThemeData> createAppTheme( | |||||||
|     brightness: brightness, |     brightness: brightness, | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false; |   final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false; | ||||||
|   final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true); |   final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true); | ||||||
|  |  | ||||||
|   return ThemeData( |   return ThemeData( | ||||||
| @@ -51,9 +51,9 @@ Future<ThemeData> createAppTheme( | |||||||
|     ), |     ), | ||||||
|     appBarTheme: AppBarTheme( |     appBarTheme: AppBarTheme( | ||||||
|       centerTitle: true, |       centerTitle: true, | ||||||
|       elevation: hasAppBarBlurry ? 0 : null, |       elevation: hasAppBarTransparent ? 0 : null, | ||||||
|       backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary, |       backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary, | ||||||
|       foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary, |       foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary, | ||||||
|     ), |     ), | ||||||
|     pageTransitionsTheme: PageTransitionsTheme( |     pageTransitionsTheme: PageTransitionsTheme( | ||||||
|       builders: { |       builders: { | ||||||
|   | |||||||
| @@ -15,12 +15,13 @@ 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, | ||||||
|     required Map<String, dynamic> permNodes, |     required Map<String, dynamic> permNodes, | ||||||
|  |     required String language, | ||||||
|     required SnAccountProfile? profile, |     required SnAccountProfile? profile, | ||||||
|     @Default([]) List<SnAccountBadge> badges, |     @Default([]) List<SnAccountBadge> badges, | ||||||
|     required DateTime? suspendedAt, |     required DateTime? suspendedAt, | ||||||
|   | |||||||
| @@ -33,6 +33,7 @@ mixin _$SnAccount { | |||||||
|   String get name => throw _privateConstructorUsedError; |   String get name => throw _privateConstructorUsedError; | ||||||
|   String get nick => throw _privateConstructorUsedError; |   String get nick => throw _privateConstructorUsedError; | ||||||
|   Map<String, dynamic> get permNodes => throw _privateConstructorUsedError; |   Map<String, dynamic> get permNodes => throw _privateConstructorUsedError; | ||||||
|  |   String get language => throw _privateConstructorUsedError; | ||||||
|   SnAccountProfile? get profile => throw _privateConstructorUsedError; |   SnAccountProfile? get profile => throw _privateConstructorUsedError; | ||||||
|   List<SnAccountBadge> get badges => throw _privateConstructorUsedError; |   List<SnAccountBadge> get badges => throw _privateConstructorUsedError; | ||||||
|   DateTime? get suspendedAt => throw _privateConstructorUsedError; |   DateTime? get suspendedAt => throw _privateConstructorUsedError; | ||||||
| @@ -69,6 +70,7 @@ abstract class $SnAccountCopyWith<$Res> { | |||||||
|       String name, |       String name, | ||||||
|       String nick, |       String nick, | ||||||
|       Map<String, dynamic> permNodes, |       Map<String, dynamic> permNodes, | ||||||
|  |       String language, | ||||||
|       SnAccountProfile? profile, |       SnAccountProfile? profile, | ||||||
|       List<SnAccountBadge> badges, |       List<SnAccountBadge> badges, | ||||||
|       DateTime? suspendedAt, |       DateTime? suspendedAt, | ||||||
| @@ -107,6 +109,7 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount> | |||||||
|     Object? name = null, |     Object? name = null, | ||||||
|     Object? nick = null, |     Object? nick = null, | ||||||
|     Object? permNodes = null, |     Object? permNodes = null, | ||||||
|  |     Object? language = null, | ||||||
|     Object? profile = freezed, |     Object? profile = freezed, | ||||||
|     Object? badges = null, |     Object? badges = null, | ||||||
|     Object? suspendedAt = freezed, |     Object? suspendedAt = freezed, | ||||||
| @@ -164,6 +167,10 @@ class _$SnAccountCopyWithImpl<$Res, $Val extends SnAccount> | |||||||
|           ? _value.permNodes |           ? _value.permNodes | ||||||
|           : permNodes // ignore: cast_nullable_to_non_nullable |           : permNodes // ignore: cast_nullable_to_non_nullable | ||||||
|               as Map<String, dynamic>, |               as Map<String, dynamic>, | ||||||
|  |       language: null == language | ||||||
|  |           ? _value.language | ||||||
|  |           : language // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as String, | ||||||
|       profile: freezed == profile |       profile: freezed == profile | ||||||
|           ? _value.profile |           ? _value.profile | ||||||
|           : profile // ignore: cast_nullable_to_non_nullable |           : profile // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -231,6 +238,7 @@ abstract class _$$SnAccountImplCopyWith<$Res> | |||||||
|       String name, |       String name, | ||||||
|       String nick, |       String nick, | ||||||
|       Map<String, dynamic> permNodes, |       Map<String, dynamic> permNodes, | ||||||
|  |       String language, | ||||||
|       SnAccountProfile? profile, |       SnAccountProfile? profile, | ||||||
|       List<SnAccountBadge> badges, |       List<SnAccountBadge> badges, | ||||||
|       DateTime? suspendedAt, |       DateTime? suspendedAt, | ||||||
| @@ -268,6 +276,7 @@ class __$$SnAccountImplCopyWithImpl<$Res> | |||||||
|     Object? name = null, |     Object? name = null, | ||||||
|     Object? nick = null, |     Object? nick = null, | ||||||
|     Object? permNodes = null, |     Object? permNodes = null, | ||||||
|  |     Object? language = null, | ||||||
|     Object? profile = freezed, |     Object? profile = freezed, | ||||||
|     Object? badges = null, |     Object? badges = null, | ||||||
|     Object? suspendedAt = freezed, |     Object? suspendedAt = freezed, | ||||||
| @@ -325,6 +334,10 @@ class __$$SnAccountImplCopyWithImpl<$Res> | |||||||
|           ? _value._permNodes |           ? _value._permNodes | ||||||
|           : permNodes // ignore: cast_nullable_to_non_nullable |           : permNodes // ignore: cast_nullable_to_non_nullable | ||||||
|               as Map<String, dynamic>, |               as Map<String, dynamic>, | ||||||
|  |       language: null == language | ||||||
|  |           ? _value.language | ||||||
|  |           : language // ignore: cast_nullable_to_non_nullable | ||||||
|  |               as String, | ||||||
|       profile: freezed == profile |       profile: freezed == profile | ||||||
|           ? _value.profile |           ? _value.profile | ||||||
|           : profile // ignore: cast_nullable_to_non_nullable |           : profile // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -367,12 +380,13 @@ 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, | ||||||
|       required final Map<String, dynamic> permNodes, |       required final Map<String, dynamic> permNodes, | ||||||
|  |       required this.language, | ||||||
|       required this.profile, |       required this.profile, | ||||||
|       final List<SnAccountBadge> badges = const [], |       final List<SnAccountBadge> badges = const [], | ||||||
|       required this.suspendedAt, |       required this.suspendedAt, | ||||||
| @@ -410,8 +424,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; | ||||||
| @@ -427,6 +443,8 @@ class _$SnAccountImpl extends _SnAccount { | |||||||
|     return EqualUnmodifiableMapView(_permNodes); |     return EqualUnmodifiableMapView(_permNodes); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   final String language; | ||||||
|   @override |   @override | ||||||
|   final SnAccountProfile? profile; |   final SnAccountProfile? profile; | ||||||
|   final List<SnAccountBadge> _badges; |   final List<SnAccountBadge> _badges; | ||||||
| @@ -451,7 +469,7 @@ class _$SnAccountImpl extends _SnAccount { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   String toString() { |   String toString() { | ||||||
|     return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)'; |     return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)'; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -477,6 +495,8 @@ class _$SnAccountImpl extends _SnAccount { | |||||||
|             (identical(other.nick, nick) || other.nick == nick) && |             (identical(other.nick, nick) || other.nick == nick) && | ||||||
|             const DeepCollectionEquality() |             const DeepCollectionEquality() | ||||||
|                 .equals(other._permNodes, _permNodes) && |                 .equals(other._permNodes, _permNodes) && | ||||||
|  |             (identical(other.language, language) || | ||||||
|  |                 other.language == language) && | ||||||
|             (identical(other.profile, profile) || other.profile == profile) && |             (identical(other.profile, profile) || other.profile == profile) && | ||||||
|             const DeepCollectionEquality().equals(other._badges, _badges) && |             const DeepCollectionEquality().equals(other._badges, _badges) && | ||||||
|             (identical(other.suspendedAt, suspendedAt) || |             (identical(other.suspendedAt, suspendedAt) || | ||||||
| @@ -507,6 +527,7 @@ class _$SnAccountImpl extends _SnAccount { | |||||||
|         name, |         name, | ||||||
|         nick, |         nick, | ||||||
|         const DeepCollectionEquality().hash(_permNodes), |         const DeepCollectionEquality().hash(_permNodes), | ||||||
|  |         language, | ||||||
|         profile, |         profile, | ||||||
|         const DeepCollectionEquality().hash(_badges), |         const DeepCollectionEquality().hash(_badges), | ||||||
|         suspendedAt, |         suspendedAt, | ||||||
| @@ -540,12 +561,13 @@ 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, | ||||||
|       required final Map<String, dynamic> permNodes, |       required final Map<String, dynamic> permNodes, | ||||||
|  |       required final String language, | ||||||
|       required final SnAccountProfile? profile, |       required final SnAccountProfile? profile, | ||||||
|       final List<SnAccountBadge> badges, |       final List<SnAccountBadge> badges, | ||||||
|       required final DateTime? suspendedAt, |       required final DateTime? suspendedAt, | ||||||
| @@ -584,6 +606,8 @@ abstract class _SnAccount extends SnAccount { | |||||||
|   @override |   @override | ||||||
|   Map<String, dynamic> get permNodes; |   Map<String, dynamic> get permNodes; | ||||||
|   @override |   @override | ||||||
|  |   String get language; | ||||||
|  |   @override | ||||||
|   SnAccountProfile? get profile; |   SnAccountProfile? get profile; | ||||||
|   @override |   @override | ||||||
|   List<SnAccountBadge> get badges; |   List<SnAccountBadge> get badges; | ||||||
|   | |||||||
| @@ -20,12 +20,13 @@ _$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, | ||||||
|       permNodes: json['perm_nodes'] as Map<String, dynamic>, |       permNodes: json['perm_nodes'] as Map<String, dynamic>, | ||||||
|  |       language: json['language'] as String, | ||||||
|       profile: json['profile'] == null |       profile: json['profile'] == null | ||||||
|           ? null |           ? null | ||||||
|           : SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>), |           : SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>), | ||||||
| @@ -56,6 +57,7 @@ Map<String, dynamic> _$$SnAccountImplToJson(_$SnAccountImpl instance) => | |||||||
|       'name': instance.name, |       'name': instance.name, | ||||||
|       'nick': instance.nick, |       'nick': instance.nick, | ||||||
|       'perm_nodes': instance.permNodes, |       'perm_nodes': instance.permNodes, | ||||||
|  |       'language': instance.language, | ||||||
|       'profile': instance.profile?.toJson(), |       'profile': instance.profile?.toJson(), | ||||||
|       'badges': instance.badges.map((e) => e.toJson()).toList(), |       'badges': instance.badges.map((e) => e.toJson()).toList(), | ||||||
|       'suspended_at': instance.suspendedAt?.toIso8601String(), |       'suspended_at': instance.suspendedAt?.toIso8601String(), | ||||||
|   | |||||||
| @@ -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, | ||||||
|  |     }; | ||||||
| @@ -196,68 +196,71 @@ class _AttachmentListState extends State<AttachmentList> { | |||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return Container( |         return AspectRatio( | ||||||
|           constraints: BoxConstraints(maxHeight: constraints.maxHeight), |           aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1, | ||||||
|           child: ScrollConfiguration( |           child: Container( | ||||||
|             behavior: _AttachmentListScrollBehavior(), |             constraints: BoxConstraints(maxHeight: constraints.maxHeight), | ||||||
|             child: ListView.separated( |             child: ScrollConfiguration( | ||||||
|               padding: widget.padding, |               behavior: _AttachmentListScrollBehavior(), | ||||||
|               shrinkWrap: true, |               child: ListView.separated( | ||||||
|               itemCount: widget.data.length, |                 padding: widget.padding, | ||||||
|               itemBuilder: (context, idx) { |                 shrinkWrap: true, | ||||||
|                 return Container( |                 itemCount: widget.data.length, | ||||||
|                   constraints: constraints.copyWith(maxWidth: widget.maxWidth), |                 itemBuilder: (context, idx) { | ||||||
|                   child: AspectRatio( |                   return Container( | ||||||
|                     aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(), |                     constraints: constraints.copyWith(maxWidth: widget.maxWidth), | ||||||
|                     child: GestureDetector( |                     child: AspectRatio( | ||||||
|                       onTap: () { |                       aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(), | ||||||
|                         if (widget.data[idx]?.mediaType != SnMediaType.image) return; |                       child: GestureDetector( | ||||||
|                         context.pushTransparentRoute( |                         onTap: () { | ||||||
|                           AttachmentZoomView( |                           if (widget.data[idx]?.mediaType != SnMediaType.image) return; | ||||||
|                             data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(), |                           context.pushTransparentRoute( | ||||||
|                             initialIndex: idx, |                             AttachmentZoomView( | ||||||
|                             heroTags: heroTags, |                               data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(), | ||||||
|                           ), |                               initialIndex: idx, | ||||||
|                           backgroundColor: Colors.black.withOpacity(0.7), |                               heroTags: heroTags, | ||||||
|                           rootNavigator: true, |  | ||||||
|                         ); |  | ||||||
|                       }, |  | ||||||
|                       child: Stack( |  | ||||||
|                         fit: StackFit.expand, |  | ||||||
|                         children: [ |  | ||||||
|                           Container( |  | ||||||
|                             decoration: BoxDecoration( |  | ||||||
|                               color: backgroundColor, |  | ||||||
|                               border: Border( |  | ||||||
|                                 top: borderSide, |  | ||||||
|                                 bottom: borderSide, |  | ||||||
|                               ), |  | ||||||
|                               borderRadius: AttachmentList.kDefaultRadius, |  | ||||||
|                             ), |                             ), | ||||||
|                             child: ClipRRect( |                             backgroundColor: Colors.black.withOpacity(0.7), | ||||||
|                               borderRadius: AttachmentList.kDefaultRadius, |                             rootNavigator: true, | ||||||
|                               child: AttachmentItem( |                           ); | ||||||
|                                 data: widget.data[idx], |                         }, | ||||||
|                                 heroTag: heroTags[idx], |                         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, |               ), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|         ); |         ); | ||||||
|   | |||||||
| @@ -152,6 +152,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> { | |||||||
|       child: GestureDetector( |       child: GestureDetector( | ||||||
|         behavior: HitTestBehavior.translucent, |         behavior: HitTestBehavior.translucent, | ||||||
|         child: Scaffold( |         child: Scaffold( | ||||||
|  |           backgroundColor: Colors.transparent, | ||||||
|           body: Stack( |           body: Stack( | ||||||
|             children: [ |             children: [ | ||||||
|               Builder(builder: (context) { |               Builder(builder: (context) { | ||||||
|   | |||||||
| @@ -1,17 +1,28 @@ | |||||||
|  | import 'dart:io'; | ||||||
|  | import 'dart:math' show min; | ||||||
|  |  | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/foundation.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:google_fonts/google_fonts.dart'; | ||||||
|  | import 'package:hotkey_manager/hotkey_manager.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|  | import 'package:pasteboard/pasteboard.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/controllers/chat_message_controller.dart'; | import 'package:surface/controllers/chat_message_controller.dart'; | ||||||
| import 'package:surface/controllers/post_write_controller.dart'; | import 'package:surface/controllers/post_write_controller.dart'; | ||||||
| import 'package:surface/providers/sn_attachment.dart'; | import 'package:surface/providers/sn_attachment.dart'; | ||||||
|  | import 'package:surface/providers/sn_network.dart'; | ||||||
|  | import 'package:surface/providers/sn_sticker.dart'; | ||||||
| import 'package:surface/providers/user_directory.dart'; | import 'package:surface/providers/user_directory.dart'; | ||||||
| import 'package:surface/types/attachment.dart'; | import 'package:surface/types/attachment.dart'; | ||||||
| import 'package:surface/types/chat.dart'; | import 'package:surface/types/chat.dart'; | ||||||
| import 'package:surface/widgets/dialog.dart'; | import 'package:surface/widgets/dialog.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/universal_image.dart'; | ||||||
|  |  | ||||||
| class ChatMessageInput extends StatefulWidget { | class ChatMessageInput extends StatefulWidget { | ||||||
|   final ChatMessageController controller; |   final ChatMessageController controller; | ||||||
| @@ -32,9 +43,30 @@ class ChatMessageInputState extends State<ChatMessageInput> { | |||||||
|   final TextEditingController _contentController = TextEditingController(); |   final TextEditingController _contentController = TextEditingController(); | ||||||
|   final FocusNode _focusNode = FocusNode(); |   final FocusNode _focusNode = FocusNode(); | ||||||
|  |  | ||||||
|  |   final HotKey _pasteHotKey = HotKey( | ||||||
|  |     key: PhysicalKeyboardKey.keyV, | ||||||
|  |     modifiers: [Platform.isMacOS ? HotKeyModifier.meta : HotKeyModifier.control], | ||||||
|  |     scope: HotKeyScope.inapp, | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   void _registerHotKey() { | ||||||
|  |     if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; | ||||||
|  |     hotKeyManager.register(_pasteHotKey, keyDownHandler: (_) async { | ||||||
|  |       final imageBytes = await Pasteboard.image; | ||||||
|  |       if (imageBytes == null) return; | ||||||
|  |       _attachments.add(PostWriteMedia.fromBytes( | ||||||
|  |         imageBytes, | ||||||
|  |         'attachmentPastedImage'.tr(), | ||||||
|  |         SnMediaType.image, | ||||||
|  |       )); | ||||||
|  |       setState(() {}); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void initState() { |   void initState() { | ||||||
|     super.initState(); |     super.initState(); | ||||||
|  |     _registerHotKey(); | ||||||
|     _contentController.addListener(() { |     _contentController.addListener(() { | ||||||
|       if (_contentController.text.isNotEmpty) { |       if (_contentController.text.isNotEmpty) { | ||||||
|         widget.controller.pingTypingStatus(); |         widget.controller.pingTypingStatus(); | ||||||
| @@ -46,6 +78,16 @@ class ChatMessageInputState extends State<ChatMessageInput> { | |||||||
|     setState(() => _replyingMessage = value); |     setState(() => _replyingMessage = value); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void setInitialText(String? value) { | ||||||
|  |     _contentController.text = value ?? ''; | ||||||
|  |     setState(() {}); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void setInitialAttachments(List<PostWriteMedia>? value) { | ||||||
|  |     _attachments.addAll(value ?? []); | ||||||
|  |     setState(() {}); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   void setEdit(SnChatMessage? value) { |   void setEdit(SnChatMessage? value) { | ||||||
|     _contentController.text = value?.body['text'] ?? ''; |     _contentController.text = value?.body['text'] ?? ''; | ||||||
|     _attachments.clear(); |     _attachments.clear(); | ||||||
| @@ -134,10 +176,34 @@ class ChatMessageInputState extends State<ChatMessageInput> { | |||||||
|  |  | ||||||
|   final List<PostWriteMedia> _attachments = List.empty(growable: true); |   final List<PostWriteMedia> _attachments = List.empty(growable: true); | ||||||
|  |  | ||||||
|  |   OverlayEntry? _overlayEntry; | ||||||
|  |  | ||||||
|  |   void _showEmojiPicker(BuildContext context) { | ||||||
|  |     final overlay = Overlay.of(context); | ||||||
|  |     _overlayEntry = OverlayEntry( | ||||||
|  |       builder: (context) => Positioned( | ||||||
|  |         bottom: 16 + MediaQuery.of(context).padding.bottom, | ||||||
|  |         right: 16, | ||||||
|  |         child: _StickerPicker( | ||||||
|  |           originalText: _contentController.text, | ||||||
|  |           onDismiss: () => _dismissEmojiPicker(), | ||||||
|  |           onInsert: (str) => _contentController.text = str, | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     overlay.insert(_overlayEntry!); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void _dismissEmojiPicker() { | ||||||
|  |     _overlayEntry?.remove(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   void dispose() { |   void dispose() { | ||||||
|     _contentController.dispose(); |     _contentController.dispose(); | ||||||
|     _focusNode.dispose(); |     _focusNode.dispose(); | ||||||
|  |     if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey); | ||||||
|     super.dispose(); |     super.dispose(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -279,6 +345,19 @@ class ChatMessageInputState extends State<ChatMessageInput> { | |||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|               const Gap(8), |               const Gap(8), | ||||||
|  |               IconButton( | ||||||
|  |                 icon: Icon( | ||||||
|  |                   Symbols.mood, | ||||||
|  |                   color: Theme.of(context).colorScheme.primary, | ||||||
|  |                 ), | ||||||
|  |                 visualDensity: const VisualDensity( | ||||||
|  |                   horizontal: -4, | ||||||
|  |                   vertical: -4, | ||||||
|  |                 ), | ||||||
|  |                 onPressed: () { | ||||||
|  |                   _showEmojiPicker(context); | ||||||
|  |                 }, | ||||||
|  |               ), | ||||||
|               AddPostMediaButton( |               AddPostMediaButton( | ||||||
|                 onAdd: (items) { |                 onAdd: (items) { | ||||||
|                   setState(() { |                   setState(() { | ||||||
| @@ -304,3 +383,107 @@ class ChatMessageInputState extends State<ChatMessageInput> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class _StickerPicker extends StatelessWidget { | ||||||
|  |   final String originalText; | ||||||
|  |   final Function? onDismiss; | ||||||
|  |   final Function(String)? onInsert; | ||||||
|  |  | ||||||
|  |   const _StickerPicker({this.onDismiss, required this.originalText, this.onInsert}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final sticker = context.read<SnStickerProvider>(); | ||||||
|  |     return GestureDetector( | ||||||
|  |       onTap: () { | ||||||
|  |         onDismiss?.call(); | ||||||
|  |       }, | ||||||
|  |       child: Container( | ||||||
|  |         constraints: BoxConstraints(maxWidth: min(360, MediaQuery.of(context).size.width), maxHeight: 240), | ||||||
|  |         child: Material( | ||||||
|  |           elevation: 8, | ||||||
|  |           borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |           child: ClipRRect( | ||||||
|  |             borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |             child: ListView( | ||||||
|  |               padding: EdgeInsets.zero, | ||||||
|  |               children: sticker.stickersByPack.entries | ||||||
|  |                   .map((e) { | ||||||
|  |                     return <Widget>[ | ||||||
|  |                       Container( | ||||||
|  |                         margin: EdgeInsets.only(bottom: 8), | ||||||
|  |                         padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||||
|  |                         color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||||
|  |                         child: Column( | ||||||
|  |                           mainAxisSize: MainAxisSize.min, | ||||||
|  |                           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                           children: [ | ||||||
|  |                             Text(e.value.first.pack.name).bold(), | ||||||
|  |                             Text(e.value.first.pack.description), | ||||||
|  |                           ], | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|  |                       GridView.builder( | ||||||
|  |                         physics: const NeverScrollableScrollPhysics(), | ||||||
|  |                         padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), | ||||||
|  |                         shrinkWrap: true, | ||||||
|  |                         gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( | ||||||
|  |                           maxCrossAxisExtent: 48, | ||||||
|  |                           childAspectRatio: 1.0, | ||||||
|  |                           mainAxisSpacing: 8, | ||||||
|  |                           crossAxisSpacing: 8, | ||||||
|  |                         ), | ||||||
|  |                         itemCount: e.value.length, | ||||||
|  |                         itemBuilder: (context, index) { | ||||||
|  |                           final sn = context.read<SnNetworkProvider>(); | ||||||
|  |                           final element = e.value[index]; | ||||||
|  |                           return GestureDetector( | ||||||
|  |                             onTap: () { | ||||||
|  |                               final withSpace = originalText.isNotEmpty; | ||||||
|  |                               onInsert?.call( | ||||||
|  |                                   '$originalText${withSpace ? ' ' : ''}:${element.pack.prefix}${element.alias}:'); | ||||||
|  |                               onDismiss?.call(); | ||||||
|  |                             }, | ||||||
|  |                             child: Tooltip( | ||||||
|  |                               richMessage: TextSpan( | ||||||
|  |                                 children: [ | ||||||
|  |                                   TextSpan( | ||||||
|  |                                       text: ':${element.pack.prefix}${element.alias}:\n', | ||||||
|  |                                       style: GoogleFonts.robotoMono()), | ||||||
|  |                                   TextSpan(text: element.name).bold(), | ||||||
|  |                                 ], | ||||||
|  |                               ), | ||||||
|  |                               child: Container( | ||||||
|  |                                 width: 48, | ||||||
|  |                                 height: 48, | ||||||
|  |                                 decoration: BoxDecoration( | ||||||
|  |                                   borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |                                   color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||||
|  |                                 ), | ||||||
|  |                                 child: ClipRRect( | ||||||
|  |                                   borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |                                   child: UniversalImage( | ||||||
|  |                                     sn.getAttachmentUrl(element.attachment.rid), | ||||||
|  |                                     width: 48, | ||||||
|  |                                     height: 48, | ||||||
|  |                                     cacheHeight: 48, | ||||||
|  |                                     cacheWidth: 48, | ||||||
|  |                                     fit: BoxFit.contain, | ||||||
|  |                                   ), | ||||||
|  |                                 ), | ||||||
|  |                               ), | ||||||
|  |                             ), | ||||||
|  |                           ); | ||||||
|  |                         }, | ||||||
|  |                       ), | ||||||
|  |                     ]; | ||||||
|  |                   }) | ||||||
|  |                   .expand((ele) => ele) | ||||||
|  |                   .toList(), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import 'package:gap/gap.dart'; | |||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package: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'; | ||||||
|  |  | ||||||
| @@ -13,6 +14,9 @@ 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, | ||||||
| @@ -22,45 +26,50 @@ class ConnectionIndicator extends StatelessWidget { | |||||||
|  |  | ||||||
|         return IgnorePointer( |         return IgnorePointer( | ||||||
|           ignoring: !show, |           ignoring: !show, | ||||||
|           child: GestureDetector( |           child: Center( | ||||||
|             child: Material( |             child: GestureDetector( | ||||||
|               elevation: 2, |               child: Material( | ||||||
|               shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), |                 elevation: 2, | ||||||
|               color: Theme.of(context).colorScheme.secondaryContainer, |                 shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), | ||||||
|               child: ua.isAuthorized |                 color: Theme.of(context).colorScheme.secondaryContainer, | ||||||
|                   ? Row( |                 child: ua.isAuthorized | ||||||
|                       mainAxisAlignment: MainAxisAlignment.center, |                     ? Row( | ||||||
|                       crossAxisAlignment: CrossAxisAlignment.center, |                         mainAxisSize: MainAxisSize.min, | ||||||
|                       children: [ |                         mainAxisAlignment: MainAxisAlignment.center, | ||||||
|                         if (ws.isBusy) |                         crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|                           Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) |                         children: [ | ||||||
|                         else if (!ws.isConnected) |                           if (ws.isBusy) | ||||||
|                           Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) |                             Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) | ||||||
|                         else |                           else if (!ws.isConnected) | ||||||
|                           Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer), |                             Text('serverDisconnected') | ||||||
|                         const Gap(8), |                                 .tr() | ||||||
|                         if (ws.isBusy) |                                 .textColor(Theme.of(context).colorScheme.onSecondaryContainer) | ||||||
|                           const CircularProgressIndicator(strokeWidth: 2.5) |                           else | ||||||
|                               .width(12) |                             Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer), | ||||||
|                               .height(12) |                           const Gap(8), | ||||||
|                               .padding(horizontal: 4, right: 4) |                           if (ws.isBusy) | ||||||
|                         else if (!ws.isConnected) |                             const CircularProgressIndicator(strokeWidth: 2.5) | ||||||
|                           const Icon(Symbols.power_off, size: 18) |                                 .width(12) | ||||||
|                         else |                                 .height(12) | ||||||
|                           const Icon(Symbols.power, size: 18), |                                 .padding(horizontal: 4, right: 4) | ||||||
|                       ], |                           else if (!ws.isConnected) | ||||||
|                     ).padding(horizontal: 8, vertical: 4) |                             const Icon(Symbols.power_off, size: 18) | ||||||
|                   : const SizedBox.shrink(), |                           else | ||||||
|             ).opacity(show ? 1 : 0, animate: true).animate( |                             const Icon(Symbols.power, size: 18), | ||||||
|                   const Duration(milliseconds: 300), |                         ], | ||||||
|                   Curves.easeInOut, |                       ).padding(horizontal: 8, vertical: 4) | ||||||
|                 ), |                     : const SizedBox.shrink(), | ||||||
|             onTap: () { |               ).opacity(show ? 1 : 0, animate: true).animate( | ||||||
|               if (!ws.isConnected && !ws.isBusy) { |                     const Duration(milliseconds: 300), | ||||||
|                 ws.connect(); |                     Curves.easeInOut, | ||||||
|               } |                   ), | ||||||
|             }, |               onTap: () { | ||||||
|           ), |                 if (!ws.isConnected && !ws.isBusy) { | ||||||
|  |                   ws.connect(); | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|  |           ).padding(left: marginLeft), | ||||||
|         ); |         ); | ||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ class ContextMenuArea extends StatelessWidget { | |||||||
|           // Leave padding for side navigation |           // 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]}'; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -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, | ||||||
|         ), |         ), | ||||||
| @@ -126,14 +129,27 @@ class MarkdownTextContent extends StatelessWidget { | |||||||
|                     future: st.lookupSticker(alias), |                     future: st.lookupSticker(alias), | ||||||
|                     builder: (context, snapshot) { |                     builder: (context, snapshot) { | ||||||
|                       if (snapshot.hasData) { |                       if (snapshot.hasData) { | ||||||
|                         return UniversalImage( |                         return GestureDetector( | ||||||
|                           sn.getAttachmentUrl(snapshot.data!.attachment.rid), |                             child: UniversalImage( | ||||||
|                           fit: BoxFit.cover, |                               sn.getAttachmentUrl(snapshot.data!.attachment.rid), | ||||||
|                           width: size, |                               fit: BoxFit.contain, | ||||||
|                           height: size, |                               width: size, | ||||||
|                           cacheHeight: size, |                               height: size, | ||||||
|                           cacheWidth: size, |                               cacheHeight: size, | ||||||
|                         ); |                               cacheWidth: size, | ||||||
|  |                             ), | ||||||
|  |                             onTap: () { | ||||||
|  |                               if (snapshot.data == null) return; | ||||||
|  |                               context.pushTransparentRoute( | ||||||
|  |                                 AttachmentZoomView( | ||||||
|  |                                   data: [snapshot.data!.attachment], | ||||||
|  |                                   initialIndex: 0, | ||||||
|  |                                   heroTags: [const Uuid().v4()], | ||||||
|  |                                 ), | ||||||
|  |                                 backgroundColor: Colors.black.withOpacity(0.7), | ||||||
|  |                                 rootNavigator: true, | ||||||
|  |                               ); | ||||||
|  |                             }); | ||||||
|                       } |                       } | ||||||
|                       return const SizedBox.shrink(); |                       return const SizedBox.shrink(); | ||||||
|                     }, |                     }, | ||||||
| @@ -142,7 +158,7 @@ class MarkdownTextContent extends StatelessWidget { | |||||||
|               ); |               ); | ||||||
|             case 'attachments': |             case 'attachments': | ||||||
|               final attachment = attachments?.firstWhere( |               final attachment = attachments?.firstWhere( | ||||||
|                     (ele) => ele?.rid == segments[1], |                 (ele) => ele?.rid == segments[1], | ||||||
|                 orElse: () => null, |                 orElse: () => null, | ||||||
|               ); |               ); | ||||||
|               if (attachment != null) { |               if (attachment != null) { | ||||||
|   | |||||||
| @@ -31,34 +31,37 @@ class _AppRailNavigationState extends State<AppRailNavigation> { | |||||||
|       builder: (context, _) { |       builder: (context, _) { | ||||||
|         final destinations = nav.destinations.where((ele) => ele.isPinned).toList(); |         final destinations = nav.destinations.where((ele) => ele.isPinned).toList(); | ||||||
|  |  | ||||||
|         return NavigationRail( |         return SizedBox( | ||||||
|           selectedIndex: |           width: 80, | ||||||
|               nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null, |           child: NavigationRail( | ||||||
|           destinations: [ |             selectedIndex: | ||||||
|             ...destinations.where((ele) => ele.isPinned).map((ele) { |                 nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null, | ||||||
|               return NavigationRailDestination( |             destinations: [ | ||||||
|                 icon: ele.icon, |               ...destinations.where((ele) => ele.isPinned).map((ele) { | ||||||
|                 label: Text(ele.label).tr(), |                 return NavigationRailDestination( | ||||||
|               ); |                   icon: ele.icon, | ||||||
|             }), |                   label: Text(ele.label).tr(), | ||||||
|           ], |                 ); | ||||||
|           trailing: Expanded( |               }), | ||||||
|             child: Align( |             ], | ||||||
|               alignment: Alignment.bottomCenter, |             trailing: Expanded( | ||||||
|               child: StyledWidget( |               child: Align( | ||||||
|                 IconButton( |                 alignment: Alignment.bottomCenter, | ||||||
|                   icon: const Icon(Symbols.menu), |                 child: StyledWidget( | ||||||
|                   onPressed: () { |                   IconButton( | ||||||
|                     Scaffold.of(context).openDrawer(); |                     icon: const Icon(Symbols.menu), | ||||||
|                   }, |                     onPressed: () { | ||||||
|                 ), |                       Scaffold.of(context).openDrawer(); | ||||||
|               ).padding(bottom: 16), |                     }, | ||||||
|  |                   ), | ||||||
|  |                 ).padding(bottom: 16), | ||||||
|  |               ), | ||||||
|             ), |             ), | ||||||
|  |             onDestinationSelected: (idx) { | ||||||
|  |               nav.setIndex(idx); | ||||||
|  |               GoRouter.of(context).goNamed(destinations[idx].screen); | ||||||
|  |             }, | ||||||
|           ), |           ), | ||||||
|           onDestinationSelected: (idx) { |  | ||||||
|             nav.setIndex(idx); |  | ||||||
|             GoRouter.of(context).goNamed(destinations[idx].screen); |  | ||||||
|           }, |  | ||||||
|         ); |         ); | ||||||
|       }, |       }, | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -140,6 +140,7 @@ class AppRootScaffold extends StatelessWidget { | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     final safeTop = MediaQuery.of(context).padding.top; |     final safeTop = MediaQuery.of(context).padding.top; | ||||||
|  |     final safeBottom = MediaQuery.of(context).padding.bottom; | ||||||
|  |  | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       key: globalRootScaffoldKey, |       key: globalRootScaffoldKey, | ||||||
| @@ -191,7 +192,10 @@ class AppRootScaffold extends StatelessWidget { | |||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
|           Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()), |           Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()), | ||||||
|           Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()), |           if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)) | ||||||
|  |             Positioned(bottom: safeBottom > 0 ? safeBottom : 16, left: 0, right: 0, child: ConnectionIndicator()) | ||||||
|  |           else | ||||||
|  |             Positioned(top: safeTop > 0 ? safeTop : 16, left: 0, right: 0, child: ConnectionIndicator()), | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|       drawer: !isExpandedDrawer ? AppNavigationDrawer() : null, |       drawer: !isExpandedDrawer ? AppNavigationDrawer() : null, | ||||||
|   | |||||||
| @@ -1,60 +1,181 @@ | |||||||
| 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_animate/flutter_animate.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||||
| import 'package:provider/provider.dart'; | import 'package:provider/provider.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/notification.dart'; | import 'package:surface/providers/notification.dart'; | ||||||
|  | import 'package:surface/providers/sn_network.dart'; | ||||||
| import 'package:surface/providers/userinfo.dart'; | import 'package:surface/providers/userinfo.dart'; | ||||||
|  | import 'package:surface/screens/notification.dart'; | ||||||
|  | import 'package:surface/types/notification.dart'; | ||||||
|  | import 'package:surface/widgets/dialog.dart'; | ||||||
|  | import 'package:surface/widgets/universal_image.dart'; | ||||||
|  |  | ||||||
| class NotifyIndicator extends StatelessWidget { | import 'markdown_content.dart'; | ||||||
|  |  | ||||||
|  | class NotifyIndicator extends StatefulWidget { | ||||||
|   const NotifyIndicator({super.key}); |   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 |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|  |     final sn = context.read<SnNetworkProvider>(); | ||||||
|     final ua = context.read<UserProvider>(); |     final ua = context.read<UserProvider>(); | ||||||
|     final nty = context.watch<NotificationProvider>(); |     final nty = context.watch<NotificationProvider>(); | ||||||
|  |  | ||||||
|     final show = nty.notifications.isNotEmpty && ua.isAuthorized; |     final isMobile = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE); | ||||||
|  |  | ||||||
|  |     final show = nty.showingCount > 0 && ua.isAuthorized; | ||||||
|  |  | ||||||
|  |     if (show) { | ||||||
|  |       _animationController.animateTo(1); | ||||||
|  |     } else { | ||||||
|  |       _animationController.animateTo(0); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return ListenableBuilder( |     return ListenableBuilder( | ||||||
|         listenable: nty, |         listenable: nty, | ||||||
|         builder: (context, _) { |         builder: (context, _) { | ||||||
|  |           final current = nty.notifications.lastOrNull; | ||||||
|  |  | ||||||
|           return IgnorePointer( |           return IgnorePointer( | ||||||
|             ignoring: !show, |             ignoring: !show, | ||||||
|             child: GestureDetector( |             child: GestureDetector( | ||||||
|               child: Material( |               child: Animate( | ||||||
|                 elevation: 2, |                 autoPlay: false, | ||||||
|                 shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), |                 controller: _animationController, | ||||||
|                 color: Theme.of(context).colorScheme.secondaryContainer, |                 effects: [ | ||||||
|                 child: ua.isAuthorized |                   SlideEffect( | ||||||
|                     ? Row( |                     begin: isMobile ? Offset(0, -1) : Offset(1, 0), | ||||||
|                         mainAxisAlignment: MainAxisAlignment.center, |                     end: Offset(0, 0), | ||||||
|                         crossAxisAlignment: CrossAxisAlignment.center, |                     duration: Duration(milliseconds: 300), | ||||||
|                         children: [ |                     curve: Curves.fastEaseInToSlowEaseOut, | ||||||
|                           Text( |  | ||||||
|                             nty.notifications.lastOrNull?.title ?? |  | ||||||
|                                 'notificationUnreadCount'.plural(nty.notifications.length), |  | ||||||
|                             maxLines: 1, |  | ||||||
|                             overflow: TextOverflow.ellipsis, |  | ||||||
|                           ), |  | ||||||
|                           if (nty.notifications.lastOrNull?.body != null) |  | ||||||
|                             Text( |  | ||||||
|                               nty.notifications.lastOrNull!.body, |  | ||||||
|                               maxLines: 1, |  | ||||||
|                               overflow: TextOverflow.ellipsis, |  | ||||||
|                             ).padding(left: 4), |  | ||||||
|                           const Gap(8), |  | ||||||
|                           const Icon(Symbols.notifications_unread, size: 18), |  | ||||||
|                         ], |  | ||||||
|                       ).padding(horizontal: 8, vertical: 4) |  | ||||||
|                     : const SizedBox.shrink(), |  | ||||||
|               ).opacity(show ? 1 : 0, animate: true).animate( |  | ||||||
|                     const Duration(milliseconds: 300), |  | ||||||
|                     Curves.easeInOut, |  | ||||||
|                   ), |                   ), | ||||||
|  |                   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: () { |               onTap: () { | ||||||
|                 nty.clear(); |                 nty.clear(); | ||||||
|  |                 if (current != null) { | ||||||
|  |                   _markOneAsRead(current); | ||||||
|  |                 } | ||||||
|               }, |               }, | ||||||
|             ), |             ), | ||||||
|           ); |           ); | ||||||
|   | |||||||
| @@ -1,10 +1,12 @@ | |||||||
| 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'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_animate/flutter_animate.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:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| @@ -34,6 +36,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; | ||||||
| @@ -186,6 +189,7 @@ class PostItem extends StatelessWidget { | |||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|             Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8), |             Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8), | ||||||
|  |             _PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12), | ||||||
|             _PostBottomAction( |             _PostBottomAction( | ||||||
|               data: data, |               data: data, | ||||||
|               showComments: showComments, |               showComments: showComments, | ||||||
| @@ -267,6 +271,7 @@ class PostItem extends StatelessWidget { | |||||||
|           LinkPreviewWidget( |           LinkPreviewWidget( | ||||||
|             text: data.body['content'], |             text: data.body['content'], | ||||||
|           ).padding(horizontal: 4), |           ).padding(horizontal: 4), | ||||||
|  |         _PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12), | ||||||
|         Container( |         Container( | ||||||
|           constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), |           constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), | ||||||
|           child: Column( |           child: Column( | ||||||
| @@ -817,6 +822,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( | ||||||
| @@ -1106,6 +1127,95 @@ class _PostTruncatedHint extends StatelessWidget { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class _PostFeaturedComment extends StatefulWidget { | ||||||
|  |   final SnPost data; | ||||||
|  |   final double? maxWidth; | ||||||
|  |  | ||||||
|  |   const _PostFeaturedComment({required this.data, this.maxWidth}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<_PostFeaturedComment> createState() => _PostFeaturedCommentState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _PostFeaturedCommentState extends State<_PostFeaturedComment> { | ||||||
|  |   SnPost? _featuredComment; | ||||||
|  |  | ||||||
|  |   Future<void> _fetchComments() async { | ||||||
|  |     try { | ||||||
|  |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: { | ||||||
|  |         'take': 1, | ||||||
|  |       }); | ||||||
|  |       setState(() => _featuredComment = SnPost.fromJson(resp.data[0])); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (!mounted) return; | ||||||
|  |       context.showErrorDialog(err); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |     if (widget.data.metric.replyCount > 0) { | ||||||
|  |       _fetchComments(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     if (widget.data.metric.replyCount == 0) return const SizedBox.shrink(); | ||||||
|  |     if (_featuredComment == null) return const SizedBox.shrink(); | ||||||
|  |  | ||||||
|  |     return AnimateWidgetExtensions(Container( | ||||||
|  |       constraints: BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity), | ||||||
|  |       margin: const EdgeInsets.only(top: 8), | ||||||
|  |       width: double.infinity, | ||||||
|  |       child: Material( | ||||||
|  |         borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |         color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||||
|  |         child: InkWell( | ||||||
|  |           borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |           onTap: () { | ||||||
|  |             showModalBottomSheet( | ||||||
|  |               context: context, | ||||||
|  |               useRootNavigator: true, | ||||||
|  |               builder: (context) => PostCommentListPopup( | ||||||
|  |                 postId: widget.data.id, | ||||||
|  |                 commentCount: widget.data.metric.replyCount, | ||||||
|  |               ), | ||||||
|  |             ); | ||||||
|  |           }, | ||||||
|  |           child: Column( | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |             children: [ | ||||||
|  |               Text('postFeaturedComment', style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 16)).tr(), | ||||||
|  |               const Gap(4), | ||||||
|  |               Row( | ||||||
|  |                 crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |                 children: [ | ||||||
|  |                   CircleAvatar( | ||||||
|  |                     radius: 12, | ||||||
|  |                     backgroundImage: UniversalImage.provider( | ||||||
|  |                       _featuredComment!.publisher.avatar, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   const Gap(8), | ||||||
|  |                   Expanded( | ||||||
|  |                     child: MarkdownTextContent( | ||||||
|  |                       content: _featuredComment!.body['content'], | ||||||
|  |                       isAutoWarp: true, | ||||||
|  |                     ), | ||||||
|  |                   ) | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ).padding(horizontal: 16, vertical: 8), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     )).animate().fadeIn(duration: 300.ms, curve: Curves.easeInOut); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| class _PostAbuseReportDialog extends StatefulWidget { | class _PostAbuseReportDialog extends StatefulWidget { | ||||||
|   final SnPost data; |   final SnPost data; | ||||||
|  |  | ||||||
| @@ -1181,3 +1291,96 @@ class _PostAbuseReportDialogState extends State<_PostAbuseReportDialog> { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | class _PostGetInsightSheet extends StatefulWidget { | ||||||
|  |   final int postId; | ||||||
|  |  | ||||||
|  |   const _PostGetInsightSheet({required this.postId}); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   State<_PostGetInsightSheet> createState() => _PostGetInsightSheetState(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _PostGetInsightSheetState extends State<_PostGetInsightSheet> { | ||||||
|  |   String? _response; | ||||||
|  |   String? _thinkingProcess; | ||||||
|  |  | ||||||
|  |   Future<void> _fetchResponse() async { | ||||||
|  |     try { | ||||||
|  |       final sn = context.read<SnNetworkProvider>(); | ||||||
|  |       final resp = await sn.client.get('/cgi/co/posts/${widget.postId}/insight', | ||||||
|  |           options: Options( | ||||||
|  |             sendTimeout: const Duration(minutes: 10), | ||||||
|  |             receiveTimeout: const Duration(minutes: 10), | ||||||
|  |           )); | ||||||
|  |       final out = resp.data['response'] as String; | ||||||
|  |       final document = XmlDocument.parse(out); | ||||||
|  |       _thinkingProcess = document.getElement('think')?.innerText.trim(); | ||||||
|  |       RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>'); | ||||||
|  |       setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim()); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (!mounted) return; | ||||||
|  |       context.showErrorDialog(err); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   void initState() { | ||||||
|  |     super.initState(); | ||||||
|  |     _fetchResponse(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Column( | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |       children: [ | ||||||
|  |         Row( | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |           children: [ | ||||||
|  |             const Icon(Symbols.book_4_spark, size: 24), | ||||||
|  |             const Gap(16), | ||||||
|  |             Text('postGetInsightTitle', style: Theme.of(context).textTheme.titleLarge).tr(), | ||||||
|  |           ], | ||||||
|  |         ).padding(horizontal: 20, top: 16, bottom: 12), | ||||||
|  |         const Gap(4), | ||||||
|  |         Text('postGetInsightDescription', style: Theme.of(context).textTheme.bodySmall).tr().padding(horizontal: 20), | ||||||
|  |         const Gap(4), | ||||||
|  |         if (_response == null) | ||||||
|  |           Expanded( | ||||||
|  |             child: Center( | ||||||
|  |               child: CircularProgressIndicator(), | ||||||
|  |             ), | ||||||
|  |           ) | ||||||
|  |         else | ||||||
|  |           Expanded( | ||||||
|  |             child: SingleChildScrollView( | ||||||
|  |               child: Column( | ||||||
|  |                 children: [ | ||||||
|  |                   if (_thinkingProcess != null && _thinkingProcess!.isNotEmpty) | ||||||
|  |                     ExpansionTile( | ||||||
|  |                       leading: const Icon(Symbols.info), | ||||||
|  |                       title: Text('aiThinkingProcess'.tr()), | ||||||
|  |                       tilePadding: const EdgeInsets.symmetric(horizontal: 20), | ||||||
|  |                       collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||||
|  |                       minTileHeight: 32, | ||||||
|  |                       children: [ | ||||||
|  |                         SelectableText( | ||||||
|  |                           _thinkingProcess!, | ||||||
|  |                           style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontStyle: FontStyle.italic), | ||||||
|  |                         ).padding(horizontal: 20, vertical: 8), | ||||||
|  |                       ], | ||||||
|  |                     ).padding(vertical: 8), | ||||||
|  |                   SelectionArea( | ||||||
|  |                     child: MarkdownTextContent( | ||||||
|  |                       content: _response!, | ||||||
|  |                     ), | ||||||
|  |                   ).padding(horizontal: 20, top: 8), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -61,7 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> { | |||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       HapticFeedback.mediumImpact(); |       HapticFeedback.heavyImpact(); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       // ignore: use_build_context_synchronously |       // ignore: use_build_context_synchronously | ||||||
|       if (context.mounted) context.showErrorDialog(err); |       if (context.mounted) context.showErrorDialog(err); | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
| @@ -11,9 +11,11 @@ | |||||||
| #include <file_selector_linux/file_selector_plugin.h> | #include <file_selector_linux/file_selector_plugin.h> | ||||||
| #include <flutter_udid/flutter_udid_plugin.h> | #include <flutter_udid/flutter_udid_plugin.h> | ||||||
| #include <flutter_webrtc/flutter_web_r_t_c_plugin.h> | #include <flutter_webrtc/flutter_web_r_t_c_plugin.h> | ||||||
|  | #include <hotkey_manager_linux/hotkey_manager_linux_plugin.h> | ||||||
| #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> | #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> | ||||||
| #include <media_kit_video/media_kit_video_plugin.h> | #include <media_kit_video/media_kit_video_plugin.h> | ||||||
| #include <pasteboard/pasteboard_plugin.h> | #include <pasteboard/pasteboard_plugin.h> | ||||||
|  | #include <tray_manager/tray_manager_plugin.h> | ||||||
| #include <url_launcher_linux/url_launcher_plugin.h> | #include <url_launcher_linux/url_launcher_plugin.h> | ||||||
|  |  | ||||||
| void fl_register_plugins(FlPluginRegistry* registry) { | void fl_register_plugins(FlPluginRegistry* registry) { | ||||||
| @@ -32,6 +34,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { | |||||||
|   g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = |   g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = | ||||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); |       fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); | ||||||
|   flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); |   flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); | ||||||
|  |   g_autoptr(FlPluginRegistrar) hotkey_manager_linux_registrar = | ||||||
|  |       fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerLinuxPlugin"); | ||||||
|  |   hotkey_manager_linux_plugin_register_with_registrar(hotkey_manager_linux_registrar); | ||||||
|   g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = |   g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = | ||||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); |       fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); | ||||||
|   media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); |   media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); | ||||||
| @@ -41,6 +46,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { | |||||||
|   g_autoptr(FlPluginRegistrar) pasteboard_registrar = |   g_autoptr(FlPluginRegistrar) pasteboard_registrar = | ||||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin"); |       fl_plugin_registry_get_registrar_for_plugin(registry, "PasteboardPlugin"); | ||||||
|   pasteboard_plugin_register_with_registrar(pasteboard_registrar); |   pasteboard_plugin_register_with_registrar(pasteboard_registrar); | ||||||
|  |   g_autoptr(FlPluginRegistrar) tray_manager_registrar = | ||||||
|  |       fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); | ||||||
|  |   tray_manager_plugin_register_with_registrar(tray_manager_registrar); | ||||||
|   g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = |   g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = | ||||||
|       fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); |       fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); | ||||||
|   url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); |   url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); | ||||||
|   | |||||||
| @@ -8,9 +8,11 @@ list(APPEND FLUTTER_PLUGIN_LIST | |||||||
|   file_selector_linux |   file_selector_linux | ||||||
|   flutter_udid |   flutter_udid | ||||||
|   flutter_webrtc |   flutter_webrtc | ||||||
|  |   hotkey_manager_linux | ||||||
|   media_kit_libs_linux |   media_kit_libs_linux | ||||||
|   media_kit_video |   media_kit_video | ||||||
|   pasteboard |   pasteboard | ||||||
|  |   tray_manager | ||||||
|   url_launcher_linux |   url_launcher_linux | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,14 +8,17 @@ 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 | ||||||
|  | import hotkey_manager_macos | ||||||
| import in_app_review | import in_app_review | ||||||
| import livekit_client | import livekit_client | ||||||
| import media_kit_libs_macos_video | import media_kit_libs_macos_video | ||||||
| @@ -27,6 +30,7 @@ import screen_brightness_macos | |||||||
| import share_plus | import share_plus | ||||||
| import shared_preferences_foundation | import shared_preferences_foundation | ||||||
| import sqflite_darwin | import sqflite_darwin | ||||||
|  | import tray_manager | ||||||
| import url_launcher_macos | import url_launcher_macos | ||||||
| import video_compress | import video_compress | ||||||
| import wakelock_plus | import wakelock_plus | ||||||
| @@ -35,14 +39,17 @@ 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")) | ||||||
|  |   HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin")) | ||||||
|   InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) |   InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) | ||||||
|   LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) |   LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) | ||||||
|   MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) |   MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) | ||||||
| @@ -54,6 +61,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { | |||||||
|   SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) |   SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) | ||||||
|   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) |   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) | ||||||
|   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) |   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) | ||||||
|  |   TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) | ||||||
|   UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) |   UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) | ||||||
|   VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) |   VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) | ||||||
|   WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) |   WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) | ||||||
|   | |||||||
| @@ -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.6.0): |   - Firebase/Analytics (11.7.0): | ||||||
|     - Firebase/Core |     - Firebase/Core | ||||||
|   - Firebase/Core (11.6.0): |   - Firebase/Core (11.7.0): | ||||||
|     - Firebase/CoreOnly |     - Firebase/CoreOnly | ||||||
|     - FirebaseAnalytics (~> 11.6.0) |     - FirebaseAnalytics (~> 11.7.0) | ||||||
|   - Firebase/CoreOnly (11.6.0): |   - Firebase/CoreOnly (11.7.0): | ||||||
|     - FirebaseCore (~> 11.6.0) |     - FirebaseCore (~> 11.7.0) | ||||||
|   - Firebase/Messaging (11.6.0): |   - Firebase/Messaging (11.7.0): | ||||||
|     - Firebase/CoreOnly |     - Firebase/CoreOnly | ||||||
|     - FirebaseMessaging (~> 11.6.0) |     - FirebaseMessaging (~> 11.7.0) | ||||||
|   - firebase_analytics (11.4.0): |   - firebase_analytics (11.4.2): | ||||||
|     - Firebase/Analytics (= 11.6.0) |     - Firebase/Analytics (= 11.7.0) | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - firebase_core (3.10.0): |   - firebase_core (3.11.0): | ||||||
|     - Firebase/CoreOnly (~> 11.6.0) |     - Firebase/CoreOnly (~> 11.7.0) | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - firebase_messaging (15.2.0): |   - firebase_messaging (15.2.2): | ||||||
|     - Firebase/CoreOnly (~> 11.6.0) |     - Firebase/CoreOnly (~> 11.7.0) | ||||||
|     - Firebase/Messaging (~> 11.6.0) |     - Firebase/Messaging (~> 11.7.0) | ||||||
|     - firebase_core |     - firebase_core | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - FirebaseAnalytics (11.6.0): |   - FirebaseAnalytics (11.7.0): | ||||||
|     - FirebaseAnalytics/AdIdSupport (= 11.6.0) |     - FirebaseAnalytics/AdIdSupport (= 11.7.0) | ||||||
|     - FirebaseCore (~> 11.6.0) |     - FirebaseCore (~> 11.7.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.6.0): |   - FirebaseAnalytics/AdIdSupport (11.7.0): | ||||||
|     - FirebaseCore (~> 11.6.0) |     - FirebaseCore (~> 11.7.0) | ||||||
|     - FirebaseInstallations (~> 11.0) |     - FirebaseInstallations (~> 11.0) | ||||||
|     - GoogleAppMeasurement (= 11.6.0) |     - GoogleAppMeasurement (= 11.7.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.6.0): |   - FirebaseCore (11.7.0): | ||||||
|     - FirebaseCoreInternal (~> 11.6.0) |     - FirebaseCoreInternal (~> 11.7.0) | ||||||
|     - GoogleUtilities/Environment (~> 8.0) |     - GoogleUtilities/Environment (~> 8.0) | ||||||
|     - GoogleUtilities/Logger (~> 8.0) |     - GoogleUtilities/Logger (~> 8.0) | ||||||
|   - FirebaseCoreInternal (11.6.0): |   - FirebaseCoreInternal (11.7.0): | ||||||
|     - "GoogleUtilities/NSData+zlib (~> 8.0)" |     - "GoogleUtilities/NSData+zlib (~> 8.0)" | ||||||
|   - FirebaseInstallations (11.6.0): |   - FirebaseInstallations (11.7.0): | ||||||
|     - FirebaseCore (~> 11.6.0) |     - FirebaseCore (~> 11.7.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.6.0): |   - FirebaseMessaging (11.7.0): | ||||||
|     - FirebaseCore (~> 11.6.0) |     - FirebaseCore (~> 11.7.0) | ||||||
|     - FirebaseInstallations (~> 11.0) |     - FirebaseInstallations (~> 11.0) | ||||||
|     - GoogleDataTransport (~> 10.0) |     - GoogleDataTransport (~> 10.0) | ||||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) |     - GoogleUtilities/AppDelegateSwizzler (~> 8.0) | ||||||
| @@ -72,6 +74,9 @@ 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 | ||||||
| @@ -82,21 +87,21 @@ PODS: | |||||||
|   - gal (1.0.0): |   - gal (1.0.0): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - GoogleAppMeasurement (11.6.0): |   - GoogleAppMeasurement (11.7.0): | ||||||
|     - GoogleAppMeasurement/AdIdSupport (= 11.6.0) |     - GoogleAppMeasurement/AdIdSupport (= 11.7.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.6.0): |   - GoogleAppMeasurement/AdIdSupport (11.7.0): | ||||||
|     - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0) |     - GoogleAppMeasurement/WithoutAdIdSupport (= 11.7.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.6.0): |   - GoogleAppMeasurement/WithoutAdIdSupport (11.7.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) | ||||||
| @@ -132,6 +137,10 @@ PODS: | |||||||
|   - GoogleUtilities/UserDefaults (8.0.2): |   - GoogleUtilities/UserDefaults (8.0.2): | ||||||
|     - GoogleUtilities/Logger |     - GoogleUtilities/Logger | ||||||
|     - GoogleUtilities/Privacy |     - GoogleUtilities/Privacy | ||||||
|  |   - HotKey (0.2.1) | ||||||
|  |   - hotkey_manager_macos (0.0.1): | ||||||
|  |     - FlutterMacOS | ||||||
|  |     - HotKey | ||||||
|   - in_app_review (2.0.0): |   - in_app_review (2.0.0): | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - livekit_client (2.3.5): |   - livekit_client (2.3.5): | ||||||
| @@ -149,6 +158,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): | ||||||
| @@ -168,6 +178,8 @@ PODS: | |||||||
|   - sqflite_darwin (0.0.4): |   - sqflite_darwin (0.0.4): | ||||||
|     - Flutter |     - Flutter | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|  |   - tray_manager (0.0.1): | ||||||
|  |     - FlutterMacOS | ||||||
|   - url_launcher_macos (0.0.1): |   - url_launcher_macos (0.0.1): | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - video_compress (0.3.0): |   - video_compress (0.3.0): | ||||||
| @@ -181,15 +193,18 @@ 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`) | ||||||
|   - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) |   - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) | ||||||
|  |   - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) | ||||||
|   - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) |   - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) | ||||||
|   - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) |   - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) | ||||||
|   - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) |   - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) | ||||||
| @@ -202,6 +217,7 @@ DEPENDENCIES: | |||||||
|   - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) |   - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) | ||||||
|   - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) |   - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) | ||||||
|   - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) |   - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) | ||||||
|  |   - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) | ||||||
|   - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) |   - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) | ||||||
|   - video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`) |   - video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`) | ||||||
|   - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) |   - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) | ||||||
| @@ -217,7 +233,9 @@ SPEC REPOS: | |||||||
|     - GoogleAppMeasurement |     - GoogleAppMeasurement | ||||||
|     - GoogleDataTransport |     - GoogleDataTransport | ||||||
|     - GoogleUtilities |     - GoogleUtilities | ||||||
|  |     - HotKey | ||||||
|     - nanopb |     - nanopb | ||||||
|  |     - OrderedSet | ||||||
|     - PromisesObjC |     - PromisesObjC | ||||||
|     - SAMKeychain |     - SAMKeychain | ||||||
|     - WebRTC-SDK |     - WebRTC-SDK | ||||||
| @@ -231,6 +249,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 +261,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: | ||||||
| @@ -249,6 +271,8 @@ EXTERNAL SOURCES: | |||||||
|     :path: Flutter/ephemeral |     :path: Flutter/ephemeral | ||||||
|   gal: |   gal: | ||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin |     :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin | ||||||
|  |   hotkey_manager_macos: | ||||||
|  |     :path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos | ||||||
|   in_app_review: |   in_app_review: | ||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos |     :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos | ||||||
|   livekit_client: |   livekit_client: | ||||||
| @@ -273,6 +297,8 @@ EXTERNAL SOURCES: | |||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin |     :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin | ||||||
|   sqflite_darwin: |   sqflite_darwin: | ||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin |     :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin | ||||||
|  |   tray_manager: | ||||||
|  |     :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos | ||||||
|   url_launcher_macos: |   url_launcher_macos: | ||||||
|     :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos |     :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos | ||||||
|   video_compress: |   video_compress: | ||||||
| @@ -285,30 +311,35 @@ 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: 374a441a91ead896215703a674d58cdb3e9d772b |   Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4 | ||||||
|   firebase_analytics: 5249f87da6fed852901581aab2602e0280ec2fdb |   firebase_analytics: 41d88c024a7756462a803e36236ba74f24cdc2c5 | ||||||
|   firebase_core: 6d9bb8b0ea817e8fe0d928177d42275b45fdba6f |   firebase_core: 751d3d919b95d4ae46ab049d0d64d42d4eec086b | ||||||
|   firebase_messaging: ae8e88b586e4d50abc7cac5bacf74d21967fd226 |   firebase_messaging: cc174f19945e9541e140e3cb0118448e59b38c6c | ||||||
|   FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 |   FirebaseAnalytics: bc9e565af9044ba8d6c6e4157e4edca8e5fdf7ec | ||||||
|   FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa |   FirebaseCore: 3227e35f4197a924206fbcdc0349325baf4f5de4 | ||||||
|   FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 |   FirebaseCoreInternal: d6c17dafc8dc33614733a8b52df78fcb4394c881 | ||||||
|   FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c |   FirebaseInstallations: 9347e719c3d52d8d7b9074b2c32407dd027305e9 | ||||||
|   FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021 |   FirebaseMessaging: 00ece041b71ddb52a2862ffdee73fb6e9824bd0c | ||||||
|  |   flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b | ||||||
|   flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52 |   flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52 | ||||||
|   flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8 |   flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8 | ||||||
|   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 |   FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 | ||||||
|   gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 |   gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 | ||||||
|   GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3 |   GoogleAppMeasurement: 0471a5b5bff51f3a91b1e76df22c952d04c63967 | ||||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 |   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||||
|   GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d |   GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d | ||||||
|  |   HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 | ||||||
|  |   hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160 | ||||||
|   in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 |   in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 | ||||||
|   livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1 |   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 | ||||||
| @@ -318,6 +349,7 @@ SPEC CHECKSUMS: | |||||||
|   share_plus: 1fa619de8392a4398bfaf176d441853922614e89 |   share_plus: 1fa619de8392a4398bfaf176d441853922614e89 | ||||||
|   shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 |   shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 | ||||||
|   sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d |   sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d | ||||||
|  |   tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 | ||||||
|   url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 |   url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 | ||||||
|   video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f |   video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f | ||||||
|   wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 |   wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 | ||||||
|   | |||||||
							
								
								
									
										252
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										252
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -13,10 +13,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: _flutterfire_internals |       name: _flutterfire_internals | ||||||
|       sha256: "27899c95f9e7ec06c8310e6e0eac967707714b9f1450c4a58fa00ca011a4a8ae" |       sha256: e051259913915ea5bc8fe18664596bea08592fd123930605d562969cd7315fcd | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.3.49" |     version: "1.3.51" | ||||||
|   _macros: |   _macros: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: dart |     description: dart | ||||||
| @@ -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: | ||||||
| @@ -362,10 +362,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: device_info_plus |       name: device_info_plus | ||||||
|       sha256: b37d37c2f912ad4e8ec694187de87d05de2a3cb82b465ff1f65f65a2d05de544 |       sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "11.2.1" |     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: "498c6cb8468e348a556709c745d92a52173ab3a9b906aa0593393f0787f201ea" |       sha256: "47428047a0778f72af53a3c7cb5d556e1cb25e2327cc8aa40d544971dc6245b2" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "11.4.0" |     version: "11.4.2" | ||||||
|   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: ccbb350554e98afdb4b59852689292d194d31232a2647b5012a66622b3711df9 |       sha256: "1076f4b041f76143e14878c70f0758f17fe5910c0cd992db9e93bd3c3584512b" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.3.0" |     version: "4.3.2" | ||||||
|   firebase_analytics_web: |   firebase_analytics_web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: firebase_analytics_web |       name: firebase_analytics_web | ||||||
|       sha256: "68e1f18fc16482c211c658e739c25f015b202a260d9ad8249c6d3d7963b8105f" |       sha256: "8f6dd64ea6d28b7f5b9e739d183a9e1c7f17027794a3e9aba1879621d42426ef" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.5.10+6" |     version: "0.5.10+8" | ||||||
|   firebase_core: |   firebase_core: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: firebase_core |       name: firebase_core | ||||||
|       sha256: "0307c1fde82e2b8b97e0be2dab93612aff9a72f31ebe9bfac66ed8b37ef7c568" |       sha256: "93dc4dd12f9b02c5767f235307f609e61ed9211047132d07f9e02c668f0bfc33" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.10.0" |     version: "3.11.0" | ||||||
|   firebase_core_platform_interface: |   firebase_core_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -578,34 +578,34 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: firebase_core_web |       name: firebase_core_web | ||||||
|       sha256: fbc008cf390d909b823763064b63afefe9f02d8afdb13eb3f485b871afee956b |       sha256: "0e13c80f0de8acaa5d0519cbe23c8b4cc138a2d5d508b5755c861bdfc9762678" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.19.0" |     version: "2.20.0" | ||||||
|   firebase_messaging: |   firebase_messaging: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: firebase_messaging |       name: firebase_messaging | ||||||
|       sha256: "48a8a59197c1c5174060ba9aa1e0036e9b5a0d28a0cc22d19c1fcabc67fafe3c" |       sha256: "3dee3b0cbfe719e64773cb7d1cad57c58b2235a8c136f5715fe733a54058c783" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "15.2.0" |     version: "15.2.2" | ||||||
|   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: "9770a8e91f54296829dcaa61ce9b7c2f9ae9abbf99976dd3103a60470d5264dd" |       sha256: e9ea726b9bb864fc6223bb66422bd9877b9973ae51967754a769b0d01e201c1e | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.6.0" |     version: "4.6.2" | ||||||
|   firebase_messaging_web: |   firebase_messaging_web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: firebase_messaging_web |       name: firebase_messaging_web | ||||||
|       sha256: "329ca4ef45ec616abe6f1d5e58feed0934a50840a65aa327052354ad3c64ed77" |       sha256: "5f7b40e8bf861a37f8b8196e347d8a919750421a45f0b45d1bb74e98fa72726e" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.10.0" |     version: "3.10.2" | ||||||
|   fixnum: |   fixnum: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -675,6 +675,70 @@ 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: | ||||||
| @@ -700,10 +764,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: flutter_markdown |       name: flutter_markdown | ||||||
|       sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487 |       sha256: b3ff1ef5fb3924ee02b4d38b974ffae3969d50603e68787684ee9dd45f6f144a | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.7.5" |     version: "0.7.6+1" | ||||||
|   flutter_native_splash: |   flutter_native_splash: | ||||||
|     dependency: "direct dev" |     dependency: "direct dev" | ||||||
|     description: |     description: | ||||||
| @@ -766,10 +830,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: flutter_webrtc |       name: flutter_webrtc | ||||||
|       sha256: "188401cc3275bc4f1f965babdff6cac612a4b46572f1e49f49db8af5361d5712" |       sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.12.6" |     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: | ||||||
| @@ -874,8 +938,48 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.7.0" |     version: "0.7.0" | ||||||
|   html: |   hotkey_manager: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: hotkey_manager | ||||||
|  |       sha256: "06f0655b76c8dd322fb7101dc615afbdbf39c3d3414df9e059c33892104479cd" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.2.3" | ||||||
|  |   hotkey_manager_linux: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: hotkey_manager_linux | ||||||
|  |       sha256: "83676bda8210a3377bc6f1977f193bc1dbdd4c46f1bdd02875f44b6eff9a8473" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.2.0" | ||||||
|  |   hotkey_manager_macos: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: hotkey_manager_macos | ||||||
|  |       sha256: "03b5967e64357b9ac05188ea4a5df6fe4ed4205762cb80aaccf8916ee1713c96" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.2.0" | ||||||
|  |   hotkey_manager_platform_interface: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: hotkey_manager_platform_interface | ||||||
|  |       sha256: "98ffca25b8cc9081552902747b2942e3bc37855389a4218c9d50ca316b653b13" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.2.0" | ||||||
|  |   hotkey_manager_windows: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: hotkey_manager_windows | ||||||
|  |       sha256: "0d03ced9fe563ed0b68f0a0e1b22c9ffe26eb8053cb960e401f68a4f070e0117" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.2.0" | ||||||
|  |   html: | ||||||
|  |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: html |       name: html | ||||||
|       sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" |       sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" | ||||||
| @@ -886,10 +990,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: | ||||||
| @@ -966,10 +1070,10 @@ 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: | ||||||
| @@ -1218,6 +1322,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.2.5" |     version: "1.2.5" | ||||||
|  |   menu_base: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: menu_base | ||||||
|  |       sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.1.1" | ||||||
|   meta: |   meta: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1270,18 +1382,18 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: package_info_plus |       name: package_info_plus | ||||||
|       sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790" |       sha256: c447a3c3e7be4addf129b8f9ab6a4bd5d166b78918223e223b61fddf4d07e254 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "8.1.3" |     version: "8.2.0" | ||||||
|   package_info_plus_platform_interface: |   package_info_plus_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: package_info_plus_platform_interface |       name: package_info_plus_platform_interface | ||||||
|       sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b |       sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.2" |     version: "3.1.0" | ||||||
|   pasteboard: |   pasteboard: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -1646,18 +1758,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: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5" |       sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.4.2" |     version: "2.4.4" | ||||||
|   shared_preferences_foundation: |   shared_preferences_foundation: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1714,6 +1826,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.1" |     version: "2.0.1" | ||||||
|  |   shortid: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: shortid | ||||||
|  |       sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.1.2" | ||||||
|   sky_engine: |   sky_engine: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: flutter |     description: flutter | ||||||
| @@ -1887,6 +2007,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.0.2" |     version: "1.0.2" | ||||||
|  |   tray_manager: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: tray_manager | ||||||
|  |       sha256: "80be6c508159a6f3c57983de795209ac13453e9832fd574143b06dceee188ed2" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.3.2" | ||||||
|   typed_data: |   typed_data: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1895,6 +2023,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.4.0" |     version: "1.4.0" | ||||||
|  |   uni_platform: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: uni_platform | ||||||
|  |       sha256: e02213a7ee5352212412ca026afd41d269eb00d982faa552f419ffc2debfad84 | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "0.1.3" | ||||||
|   universal_io: |   universal_io: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1995,10 +2131,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: vector_graphics |       name: vector_graphics | ||||||
|       sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" |       sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.1.15" |     version: "1.1.18" | ||||||
|   vector_graphics_codec: |   vector_graphics_codec: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -2107,10 +2243,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 +2259,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 +2288,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 | ||||||
|   | |||||||
| @@ -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+56 | version: 2.3.2+63 | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: ^3.5.4 |   sdk: ^3.5.4 | ||||||
| @@ -115,6 +115,11 @@ 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 | ||||||
|  |   tray_manager: ^0.3.2 | ||||||
|  |   hotkey_manager: ^0.2.3 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
| @@ -149,6 +154,8 @@ flutter: | |||||||
|     - assets/icon/icon.png |     - assets/icon/icon.png | ||||||
|     - assets/icon/icon-dark.png |     - assets/icon/icon-dark.png | ||||||
|     - assets/icon/icon-light-radius.png |     - assets/icon/icon-light-radius.png | ||||||
|  |     - assets/icon/tray-icon.ico | ||||||
|  |     - assets/icon/tray-icon.png | ||||||
|     - assets/translations/ |     - assets/translations/ | ||||||
|  |  | ||||||
|   # An image asset can refer to one or more resolution-specific "variants", see |   # An image asset can refer to one or more resolution-specific "variants", see | ||||||
|   | |||||||
| @@ -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,9 +11,11 @@ | |||||||
| #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> | ||||||
|  | #include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h> | ||||||
| #include <livekit_client/live_kit_plugin.h> | #include <livekit_client/live_kit_plugin.h> | ||||||
| #include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h> | #include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h> | ||||||
| #include <media_kit_video/media_kit_video_plugin_c_api.h> | #include <media_kit_video/media_kit_video_plugin_c_api.h> | ||||||
| @@ -21,6 +23,7 @@ | |||||||
| #include <permission_handler_windows/permission_handler_windows_plugin.h> | #include <permission_handler_windows/permission_handler_windows_plugin.h> | ||||||
| #include <screen_brightness_windows/screen_brightness_windows_plugin.h> | #include <screen_brightness_windows/screen_brightness_windows_plugin.h> | ||||||
| #include <share_plus/share_plus_windows_plugin_c_api.h> | #include <share_plus/share_plus_windows_plugin_c_api.h> | ||||||
|  | #include <tray_manager/tray_manager_plugin.h> | ||||||
| #include <url_launcher_windows/url_launcher_windows.h> | #include <url_launcher_windows/url_launcher_windows.h> | ||||||
|  |  | ||||||
| void RegisterPlugins(flutter::PluginRegistry* registry) { | void RegisterPlugins(flutter::PluginRegistry* registry) { | ||||||
| @@ -34,12 +37,16 @@ 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( | ||||||
|       registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); |       registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); | ||||||
|   GalPluginCApiRegisterWithRegistrar( |   GalPluginCApiRegisterWithRegistrar( | ||||||
|       registry->GetRegistrarForPlugin("GalPluginCApi")); |       registry->GetRegistrarForPlugin("GalPluginCApi")); | ||||||
|  |   HotkeyManagerWindowsPluginCApiRegisterWithRegistrar( | ||||||
|  |       registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi")); | ||||||
|   LiveKitPluginRegisterWithRegistrar( |   LiveKitPluginRegisterWithRegistrar( | ||||||
|       registry->GetRegistrarForPlugin("LiveKitPlugin")); |       registry->GetRegistrarForPlugin("LiveKitPlugin")); | ||||||
|   MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( |   MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( | ||||||
| @@ -54,6 +61,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { | |||||||
|       registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); |       registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); | ||||||
|   SharePlusWindowsPluginCApiRegisterWithRegistrar( |   SharePlusWindowsPluginCApiRegisterWithRegistrar( | ||||||
|       registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); |       registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); | ||||||
|  |   TrayManagerPluginRegisterWithRegistrar( | ||||||
|  |       registry->GetRegistrarForPlugin("TrayManagerPlugin")); | ||||||
|   UrlLauncherWindowsRegisterWithRegistrar( |   UrlLauncherWindowsRegisterWithRegistrar( | ||||||
|       registry->GetRegistrarForPlugin("UrlLauncherWindows")); |       registry->GetRegistrarForPlugin("UrlLauncherWindows")); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -8,9 +8,11 @@ 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 | ||||||
|  |   hotkey_manager_windows | ||||||
|   livekit_client |   livekit_client | ||||||
|   media_kit_libs_windows_video |   media_kit_libs_windows_video | ||||||
|   media_kit_video |   media_kit_video | ||||||
| @@ -18,6 +20,7 @@ list(APPEND FLUTTER_PLUGIN_LIST | |||||||
|   permission_handler_windows |   permission_handler_windows | ||||||
|   screen_brightness_windows |   screen_brightness_windows | ||||||
|   share_plus |   share_plus | ||||||
|  |   tray_manager | ||||||
|   url_launcher_windows |   url_launcher_windows | ||||||
| ) | ) | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user