Compare commits
	
		
			35 Commits
		
	
	
		
			2.2.2+60
			...
			f0a3bbe023
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f0a3bbe023 | |||
| df81c84438 | |||
| 8b12395fca | |||
| cb2b71d194 | |||
| 7ed508e2bb | |||
| dad869967e | |||
| 2d5b3b554e | |||
| 74882116e3 | |||
| a97c3bce3a | |||
| 1aa70827dc | |||
| fe028860e9 | |||
| a2d2ce4d38 | |||
| 167c11b9eb | |||
| 8cb3933fcc | |||
| 3818328afe | |||
| 11627e2455 | |||
| 3f82c06ff8 | |||
| 2350f59131 | |||
| 9fe7c9530a | |||
| 52f1826e91 | |||
| 28a4c86dbf | |||
| 85e48ce03b | |||
| efef61a8ea | |||
| 10ead95af9 | |||
| 838ee4d55d | |||
| 13e42429a9 | |||
| c6ce3fe2b7 | |||
| ae9a7eb0fd | |||
| 5d6fb2442f | |||
| 5a85985534 | |||
| c80499db03 | |||
| b8dcdb2315 | |||
| b7b921f1f4 | |||
| 319d5c7d7f | |||
| 4b5b001739 | 
@@ -26,7 +26,7 @@
 | 
				
			|||||||
        <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": "BaLoading",
 | 
				
			||||||
    "name": "Cat ate chips",
 | 
					    "name": "BaLoading",
 | 
				
			||||||
    "attachment_id": "d0b692cc64054463",
 | 
					    "attachment_id": "2JCI2uh21mKkfk9P",
 | 
				
			||||||
    "pack_id": 2
 | 
					    "pack_id": 3
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										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
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										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
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										
											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  | 
@@ -154,9 +154,12 @@
 | 
				
			|||||||
  "fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
 | 
					  "fieldPublisherBelongToRealmUnset": "Unset Publisher Belongs to Realm",
 | 
				
			||||||
  "writePostTypeStory": "Post a story",
 | 
					  "writePostTypeStory": "Post a story",
 | 
				
			||||||
  "writePostTypeArticle": "Write an article",
 | 
					  "writePostTypeArticle": "Write an article",
 | 
				
			||||||
 | 
					  "writePostTypeQuestion": "Ask a question",
 | 
				
			||||||
 | 
					  "writePostTypeVideo": "Post a video",
 | 
				
			||||||
  "fieldPostPublisher": "Post publisher",
 | 
					  "fieldPostPublisher": "Post publisher",
 | 
				
			||||||
  "fieldPostContent": "What happened?!",
 | 
					  "fieldPostContent": "What happened?!",
 | 
				
			||||||
  "fieldPostTitle": "Title",
 | 
					  "fieldPostTitle": "Title",
 | 
				
			||||||
 | 
					  "fieldPostQuestionReward": "Answer Rewards (Source Points)",
 | 
				
			||||||
  "fieldPostDescription": "Description",
 | 
					  "fieldPostDescription": "Description",
 | 
				
			||||||
  "fieldPostTags": "Tags",
 | 
					  "fieldPostTags": "Tags",
 | 
				
			||||||
  "fieldPostCategories": "Categories",
 | 
					  "fieldPostCategories": "Categories",
 | 
				
			||||||
@@ -166,9 +169,9 @@
 | 
				
			|||||||
  "postPosted": "Post has been posted.",
 | 
					  "postPosted": "Post has been posted.",
 | 
				
			||||||
  "postPublishedAt": "Published At",
 | 
					  "postPublishedAt": "Published At",
 | 
				
			||||||
  "postPublishedUntil": "Published Until",
 | 
					  "postPublishedUntil": "Published Until",
 | 
				
			||||||
  "postEditingNotice": "You're about to editing a post that posted {}.",
 | 
					  "postEditingNotice": "You're about to editing a post that posted by {}.",
 | 
				
			||||||
  "postReplyingNotice": "You're about to reply to a post that posted {}.",
 | 
					  "postReplyingNotice": "You're about to reply to a post that posted by {}.",
 | 
				
			||||||
  "postRepostingNotice": "You're about to repost a post that posted {}.",
 | 
					  "postRepostingNotice": "You're about to repost a post that posted by {}.",
 | 
				
			||||||
  "postReact": "React",
 | 
					  "postReact": "React",
 | 
				
			||||||
  "postReactions": "Reactions of Post",
 | 
					  "postReactions": "Reactions of Post",
 | 
				
			||||||
  "postReactionUpvote": {
 | 
					  "postReactionUpvote": {
 | 
				
			||||||
@@ -241,6 +244,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.",
 | 
				
			||||||
@@ -562,6 +567,7 @@
 | 
				
			|||||||
  "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",
 | 
				
			||||||
@@ -574,6 +580,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",
 | 
				
			||||||
@@ -604,5 +611,13 @@
 | 
				
			|||||||
    "one": "{} Source Point",
 | 
					    "one": "{} Source Point",
 | 
				
			||||||
    "other": "{} Source Points"
 | 
					    "other": "{} Source Points"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "aiThinkingProcess": "AI Thinking Process"
 | 
					  "aiThinkingProcess": "AI Thinking Process",
 | 
				
			||||||
 | 
					  "accountSettingsApplied": "Account settings have been applied.",
 | 
				
			||||||
 | 
					  "trayMenuExit": "Exit",
 | 
				
			||||||
 | 
					  "postQuestionUnanswered": "Unanswered Question",
 | 
				
			||||||
 | 
					  "postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
 | 
				
			||||||
 | 
					  "postQuestionAnswered": "Answered Question",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelect": "Select as Answer",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
 | 
				
			||||||
 | 
					  "postVideoUpload": "Upload Video"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -138,9 +138,12 @@
 | 
				
			|||||||
  "fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
 | 
					  "fieldPublisherBelongToRealmUnset": "未设置发布者所属领域",
 | 
				
			||||||
  "writePostTypeStory": "发动态",
 | 
					  "writePostTypeStory": "发动态",
 | 
				
			||||||
  "writePostTypeArticle": "写文章",
 | 
					  "writePostTypeArticle": "写文章",
 | 
				
			||||||
 | 
					  "writePostTypeQuestion": "提问题",
 | 
				
			||||||
 | 
					  "writePostTypeVideo": "发视频",
 | 
				
			||||||
  "fieldPostPublisher": "帖子发布者",
 | 
					  "fieldPostPublisher": "帖子发布者",
 | 
				
			||||||
  "fieldPostContent": "发生什么事了?!",
 | 
					  "fieldPostContent": "发生什么事了?!",
 | 
				
			||||||
  "fieldPostTitle": "标题",
 | 
					  "fieldPostTitle": "标题",
 | 
				
			||||||
 | 
					  "fieldPostQuestionReward": "回答奖励源点",
 | 
				
			||||||
  "fieldPostDescription": "描述",
 | 
					  "fieldPostDescription": "描述",
 | 
				
			||||||
  "fieldPostTags": "标签",
 | 
					  "fieldPostTags": "标签",
 | 
				
			||||||
  "fieldPostCategories": "分类",
 | 
					  "fieldPostCategories": "分类",
 | 
				
			||||||
@@ -239,6 +242,8 @@
 | 
				
			|||||||
  "settingsMisc": "杂项",
 | 
					  "settingsMisc": "杂项",
 | 
				
			||||||
  "settingsMiscAbout": "关于",
 | 
					  "settingsMiscAbout": "关于",
 | 
				
			||||||
  "settingsMiscAboutDescription": "查看 Solian 的版本信息。",
 | 
					  "settingsMiscAboutDescription": "查看 Solian 的版本信息。",
 | 
				
			||||||
 | 
					  "settingsAccountLanguage": "帐号偏好语言",
 | 
				
			||||||
 | 
					  "settingsAccountLanguageDescription": "设置邮件、通知和其他帐号相关内容的语言。",
 | 
				
			||||||
  "sensitiveContent": "敏感内容",
 | 
					  "sensitiveContent": "敏感内容",
 | 
				
			||||||
  "sensitiveContentCollapsed": "敏感内容已折叠。",
 | 
					  "sensitiveContentCollapsed": "敏感内容已折叠。",
 | 
				
			||||||
  "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
 | 
					  "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
 | 
				
			||||||
@@ -560,6 +565,7 @@
 | 
				
			|||||||
  "shareIntent": "分享",
 | 
					  "shareIntent": "分享",
 | 
				
			||||||
  "shareIntentDescription": "您想对您分享的内容做些什么?",
 | 
					  "shareIntentDescription": "您想对您分享的内容做些什么?",
 | 
				
			||||||
  "shareIntentPostStory": "发布动态",
 | 
					  "shareIntentPostStory": "发布动态",
 | 
				
			||||||
 | 
					  "shareIntentSendChannel": "分享到聊天频道",
 | 
				
			||||||
  "updateAvailable": "检测到更新可用",
 | 
					  "updateAvailable": "检测到更新可用",
 | 
				
			||||||
  "updateOngoing": "正在更新,请稍后……",
 | 
					  "updateOngoing": "正在更新,请稍后……",
 | 
				
			||||||
  "custom": "自定义",
 | 
					  "custom": "自定义",
 | 
				
			||||||
@@ -572,6 +578,7 @@
 | 
				
			|||||||
  "colorSchemeWhite": "白色",
 | 
					  "colorSchemeWhite": "白色",
 | 
				
			||||||
  "colorSchemeBlack": "黑色",
 | 
					  "colorSchemeBlack": "黑色",
 | 
				
			||||||
  "colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
 | 
					  "colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
 | 
				
			||||||
 | 
					  "postFeaturedComment": "精选评论",
 | 
				
			||||||
  "postCategoryTechnology": "技术",
 | 
					  "postCategoryTechnology": "技术",
 | 
				
			||||||
  "postCategoryGaming": "游戏",
 | 
					  "postCategoryGaming": "游戏",
 | 
				
			||||||
  "postCategoryLife": "生活",
 | 
					  "postCategoryLife": "生活",
 | 
				
			||||||
@@ -602,5 +609,14 @@
 | 
				
			|||||||
    "one": "{} 源点",
 | 
					    "one": "{} 源点",
 | 
				
			||||||
    "other": "{} 源点"
 | 
					    "other": "{} 源点"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "aiThinkingProcess": "AI 思考过程"
 | 
					  "aiThinkingProcess": "AI 思考过程",
 | 
				
			||||||
 | 
					  "accountSettingsApplied": "帐号设置已应用。",
 | 
				
			||||||
 | 
					  "trayMenuExit": "退出",
 | 
				
			||||||
 | 
					  "postQuestionUnanswered": "未解答的问题",
 | 
				
			||||||
 | 
					  "postQuestionUnansweredWithReward": "未解答的问题,悬赏源点 {}",
 | 
				
			||||||
 | 
					  "postQuestionAnswered": "已解答的问题",
 | 
				
			||||||
 | 
					  "postQuestionAnswerTitle": "精选解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelect": "选择解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelected": "解答已选择,奖励已发放。",
 | 
				
			||||||
 | 
					  "postVideoUpload": "上传视频"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -138,9 +138,11 @@
 | 
				
			|||||||
  "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
 | 
					  "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
 | 
				
			||||||
  "writePostTypeStory": "發動態",
 | 
					  "writePostTypeStory": "發動態",
 | 
				
			||||||
  "writePostTypeArticle": "寫文章",
 | 
					  "writePostTypeArticle": "寫文章",
 | 
				
			||||||
 | 
					  "writePostTypeQuestion": "提問題",
 | 
				
			||||||
  "fieldPostPublisher": "帖子發佈者",
 | 
					  "fieldPostPublisher": "帖子發佈者",
 | 
				
			||||||
  "fieldPostContent": "發生什麼事了?!",
 | 
					  "fieldPostContent": "發生什麼事了?!",
 | 
				
			||||||
  "fieldPostTitle": "標題",
 | 
					  "fieldPostTitle": "標題",
 | 
				
			||||||
 | 
					  "fieldPostQuestionReward": "回答獎勵源點",
 | 
				
			||||||
  "fieldPostDescription": "描述",
 | 
					  "fieldPostDescription": "描述",
 | 
				
			||||||
  "fieldPostTags": "標籤",
 | 
					  "fieldPostTags": "標籤",
 | 
				
			||||||
  "fieldPostCategories": "分類",
 | 
					  "fieldPostCategories": "分類",
 | 
				
			||||||
@@ -239,6 +241,8 @@
 | 
				
			|||||||
  "settingsMisc": "雜項",
 | 
					  "settingsMisc": "雜項",
 | 
				
			||||||
  "settingsMiscAbout": "關於",
 | 
					  "settingsMiscAbout": "關於",
 | 
				
			||||||
  "settingsMiscAboutDescription": "查看 Solian 的版本信息。",
 | 
					  "settingsMiscAboutDescription": "查看 Solian 的版本信息。",
 | 
				
			||||||
 | 
					  "settingsAccountLanguage": "帳號偏好語言",
 | 
				
			||||||
 | 
					  "settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
 | 
				
			||||||
  "sensitiveContent": "敏感內容",
 | 
					  "sensitiveContent": "敏感內容",
 | 
				
			||||||
  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
					  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
				
			||||||
  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
 | 
					  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
 | 
				
			||||||
@@ -560,6 +564,7 @@
 | 
				
			|||||||
  "shareIntent": "分享",
 | 
					  "shareIntent": "分享",
 | 
				
			||||||
  "shareIntentDescription": "您想對您分享的內容做些什麼?",
 | 
					  "shareIntentDescription": "您想對您分享的內容做些什麼?",
 | 
				
			||||||
  "shareIntentPostStory": "發佈動態",
 | 
					  "shareIntentPostStory": "發佈動態",
 | 
				
			||||||
 | 
					  "shareIntentSendChannel": "分享到聊天頻道",
 | 
				
			||||||
  "updateAvailable": "檢測到更新可用",
 | 
					  "updateAvailable": "檢測到更新可用",
 | 
				
			||||||
  "updateOngoing": "正在更新,請稍後……",
 | 
					  "updateOngoing": "正在更新,請稍後……",
 | 
				
			||||||
  "custom": "自定義",
 | 
					  "custom": "自定義",
 | 
				
			||||||
@@ -572,6 +577,7 @@
 | 
				
			|||||||
  "colorSchemeWhite": "白色",
 | 
					  "colorSchemeWhite": "白色",
 | 
				
			||||||
  "colorSchemeBlack": "黑色",
 | 
					  "colorSchemeBlack": "黑色",
 | 
				
			||||||
  "colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
 | 
					  "colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
 | 
				
			||||||
 | 
					  "postFeaturedComment": "精選評論",
 | 
				
			||||||
  "postCategoryTechnology": "技術",
 | 
					  "postCategoryTechnology": "技術",
 | 
				
			||||||
  "postCategoryGaming": "遊戲",
 | 
					  "postCategoryGaming": "遊戲",
 | 
				
			||||||
  "postCategoryLife": "生活",
 | 
					  "postCategoryLife": "生活",
 | 
				
			||||||
@@ -602,5 +608,13 @@
 | 
				
			|||||||
    "one": "{} 源點",
 | 
					    "one": "{} 源點",
 | 
				
			||||||
    "other": "{} 源點"
 | 
					    "other": "{} 源點"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "aiThinkingProcess": "AI 思考過程"
 | 
					  "aiThinkingProcess": "AI 思考過程",
 | 
				
			||||||
 | 
					  "accountSettingsApplied": "帳號設置已應用。",
 | 
				
			||||||
 | 
					  "trayMenuExit": "退出",
 | 
				
			||||||
 | 
					  "postQuestionUnanswered": "未解答的問題",
 | 
				
			||||||
 | 
					  "postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
 | 
				
			||||||
 | 
					  "postQuestionAnswered": "已解答的問題",
 | 
				
			||||||
 | 
					  "postQuestionAnswerTitle": "精選解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelect": "選擇解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelected": "解答已選擇,獎勵已發放。"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -138,9 +138,11 @@
 | 
				
			|||||||
  "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
 | 
					  "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
 | 
				
			||||||
  "writePostTypeStory": "發動態",
 | 
					  "writePostTypeStory": "發動態",
 | 
				
			||||||
  "writePostTypeArticle": "寫文章",
 | 
					  "writePostTypeArticle": "寫文章",
 | 
				
			||||||
 | 
					  "writePostTypeQuestion": "提問題",
 | 
				
			||||||
  "fieldPostPublisher": "帖子發佈者",
 | 
					  "fieldPostPublisher": "帖子發佈者",
 | 
				
			||||||
  "fieldPostContent": "發生什麼事了?!",
 | 
					  "fieldPostContent": "發生什麼事了?!",
 | 
				
			||||||
  "fieldPostTitle": "標題",
 | 
					  "fieldPostTitle": "標題",
 | 
				
			||||||
 | 
					  "fieldPostQuestionReward": "回答獎勵源點",
 | 
				
			||||||
  "fieldPostDescription": "描述",
 | 
					  "fieldPostDescription": "描述",
 | 
				
			||||||
  "fieldPostTags": "標籤",
 | 
					  "fieldPostTags": "標籤",
 | 
				
			||||||
  "fieldPostCategories": "分類",
 | 
					  "fieldPostCategories": "分類",
 | 
				
			||||||
@@ -239,6 +241,8 @@
 | 
				
			|||||||
  "settingsMisc": "雜項",
 | 
					  "settingsMisc": "雜項",
 | 
				
			||||||
  "settingsMiscAbout": "關於",
 | 
					  "settingsMiscAbout": "關於",
 | 
				
			||||||
  "settingsMiscAboutDescription": "查看 Solian 的版本信息。",
 | 
					  "settingsMiscAboutDescription": "查看 Solian 的版本信息。",
 | 
				
			||||||
 | 
					  "settingsAccountLanguage": "帳號偏好語言",
 | 
				
			||||||
 | 
					  "settingsAccountLanguageDescription": "設置郵件、通知和其他帳號相關內容的語言。",
 | 
				
			||||||
  "sensitiveContent": "敏感內容",
 | 
					  "sensitiveContent": "敏感內容",
 | 
				
			||||||
  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
					  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
				
			||||||
  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
 | 
					  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
 | 
				
			||||||
@@ -560,6 +564,7 @@
 | 
				
			|||||||
  "shareIntent": "分享",
 | 
					  "shareIntent": "分享",
 | 
				
			||||||
  "shareIntentDescription": "您想對您分享的內容做些什麼?",
 | 
					  "shareIntentDescription": "您想對您分享的內容做些什麼?",
 | 
				
			||||||
  "shareIntentPostStory": "發佈動態",
 | 
					  "shareIntentPostStory": "發佈動態",
 | 
				
			||||||
 | 
					  "shareIntentSendChannel": "分享到聊天頻道",
 | 
				
			||||||
  "updateAvailable": "檢測到更新可用",
 | 
					  "updateAvailable": "檢測到更新可用",
 | 
				
			||||||
  "updateOngoing": "正在更新,請稍後……",
 | 
					  "updateOngoing": "正在更新,請稍後……",
 | 
				
			||||||
  "custom": "自定義",
 | 
					  "custom": "自定義",
 | 
				
			||||||
@@ -572,6 +577,7 @@
 | 
				
			|||||||
  "colorSchemeWhite": "白色",
 | 
					  "colorSchemeWhite": "白色",
 | 
				
			||||||
  "colorSchemeBlack": "黑色",
 | 
					  "colorSchemeBlack": "黑色",
 | 
				
			||||||
  "colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
 | 
					  "colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
 | 
				
			||||||
 | 
					  "postFeaturedComment": "精選評論",
 | 
				
			||||||
  "postCategoryTechnology": "技術",
 | 
					  "postCategoryTechnology": "技術",
 | 
				
			||||||
  "postCategoryGaming": "遊戲",
 | 
					  "postCategoryGaming": "遊戲",
 | 
				
			||||||
  "postCategoryLife": "生活",
 | 
					  "postCategoryLife": "生活",
 | 
				
			||||||
@@ -602,5 +608,13 @@
 | 
				
			|||||||
    "one": "{} 源點",
 | 
					    "one": "{} 源點",
 | 
				
			||||||
    "other": "{} 源點"
 | 
					    "other": "{} 源點"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "aiThinkingProcess": "AI 思考過程"
 | 
					  "aiThinkingProcess": "AI 思考過程",
 | 
				
			||||||
 | 
					  "accountSettingsApplied": "帳號設置已應用。",
 | 
				
			||||||
 | 
					  "trayMenuExit": "退出",
 | 
				
			||||||
 | 
					  "postQuestionUnanswered": "未解答的問題",
 | 
				
			||||||
 | 
					  "postQuestionUnansweredWithReward": "未解答的問題,懸賞源點 {}",
 | 
				
			||||||
 | 
					  "postQuestionAnswered": "已解答的問題",
 | 
				
			||||||
 | 
					  "postQuestionAnswerTitle": "精選解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelect": "選擇解答",
 | 
				
			||||||
 | 
					  "postQuestionAnswerSelected": "解答已選擇,獎勵已發放。"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,6 @@ PODS:
 | 
				
			|||||||
  - Alamofire (5.10.2)
 | 
					  - Alamofire (5.10.2)
 | 
				
			||||||
  - connectivity_plus (0.0.1):
 | 
					  - connectivity_plus (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - FlutterMacOS
 | 
					 | 
				
			||||||
  - croppy (0.0.1):
 | 
					  - croppy (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - device_info_plus (0.0.1):
 | 
					  - device_info_plus (0.0.1):
 | 
				
			||||||
@@ -43,58 +42,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.1):
 | 
					  - 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.1):
 | 
					  - firebase_core (3.11.0):
 | 
				
			||||||
    - Firebase/CoreOnly (= 11.6.0)
 | 
					    - Firebase/CoreOnly (= 11.7.0)
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - firebase_messaging (15.2.1):
 | 
					  - 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)
 | 
				
			||||||
@@ -123,21 +122,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)
 | 
				
			||||||
@@ -179,8 +178,8 @@ 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.6):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - flutter_webrtc
 | 
					    - flutter_webrtc
 | 
				
			||||||
    - WebRTC-SDK (= 125.6422.06)
 | 
					    - WebRTC-SDK (= 125.6422.06)
 | 
				
			||||||
@@ -237,7 +236,7 @@ PODS:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
DEPENDENCIES:
 | 
					DEPENDENCIES:
 | 
				
			||||||
  - Alamofire
 | 
					  - Alamofire
 | 
				
			||||||
  - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
 | 
					  - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
 | 
				
			||||||
  - croppy (from `.symlinks/plugins/croppy/ios`)
 | 
					  - croppy (from `.symlinks/plugins/croppy/ios`)
 | 
				
			||||||
  - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
 | 
					  - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
 | 
				
			||||||
  - file_picker (from `.symlinks/plugins/file_picker/ios`)
 | 
					  - file_picker (from `.symlinks/plugins/file_picker/ios`)
 | 
				
			||||||
@@ -300,7 +299,7 @@ SPEC REPOS:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
EXTERNAL SOURCES:
 | 
					EXTERNAL SOURCES:
 | 
				
			||||||
  connectivity_plus:
 | 
					  connectivity_plus:
 | 
				
			||||||
    :path: ".symlinks/plugins/connectivity_plus/darwin"
 | 
					    :path: ".symlinks/plugins/connectivity_plus/ios"
 | 
				
			||||||
  croppy:
 | 
					  croppy:
 | 
				
			||||||
    :path: ".symlinks/plugins/croppy/ios"
 | 
					    :path: ".symlinks/plugins/croppy/ios"
 | 
				
			||||||
  device_info_plus:
 | 
					  device_info_plus:
 | 
				
			||||||
@@ -374,22 +373,22 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
SPEC CHECKSUMS:
 | 
					SPEC CHECKSUMS:
 | 
				
			||||||
  Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
 | 
					  Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
 | 
				
			||||||
  connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
 | 
					  connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
 | 
				
			||||||
  croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
 | 
					  croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
 | 
				
			||||||
  device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
 | 
					  device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
 | 
				
			||||||
  DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
 | 
					  DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
 | 
				
			||||||
  DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
 | 
					  DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
 | 
				
			||||||
  file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
 | 
					  file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
 | 
				
			||||||
  file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
 | 
					  file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
 | 
				
			||||||
  Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
 | 
					  Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4
 | 
				
			||||||
  firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e
 | 
					  firebase_analytics: 7236e6115c1b4e62c2270faa29c052a317e31107
 | 
				
			||||||
  firebase_core: e2aa06dbd854d961f8ce46c2e20933bee1bf2d2b
 | 
					  firebase_core: aa979ae726f00b3ef4ccf59dfb96170af84efbd4
 | 
				
			||||||
  firebase_messaging: 96cf6d67121b3f39746b2a4f29a26c0eee4af70e
 | 
					  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_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
 | 
				
			||||||
@@ -397,14 +396,14 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  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: 148b2cf67a09aaf475ba8e5bf1667fe10dc35f81
 | 
				
			||||||
  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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -71,7 +71,7 @@ class ChatMessageController extends ChangeNotifier {
 | 
				
			|||||||
      resp.data as Map<String, dynamic>,
 | 
					      resp.data as Map<String, dynamic>,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _wsSubscription = _ws.stream.stream.listen((event) {
 | 
					    _wsSubscription = _ws.pk.stream.listen((event) {
 | 
				
			||||||
      switch (event.method) {
 | 
					      switch (event.method) {
 | 
				
			||||||
        case 'events.new':
 | 
					        case 'events.new':
 | 
				
			||||||
          if (event.payload?['channel_id'] != channel?.id) break;
 | 
					          if (event.payload?['channel_id'] != channel?.id) break;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -144,6 +144,8 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
  static const Map<String, String> kTitleMap = {
 | 
					  static const Map<String, String> kTitleMap = {
 | 
				
			||||||
    'stories': 'writePostTypeStory',
 | 
					    'stories': 'writePostTypeStory',
 | 
				
			||||||
    'articles': 'writePostTypeArticle',
 | 
					    'articles': 'writePostTypeArticle',
 | 
				
			||||||
 | 
					    'questions': 'writePostTypeQuestion',
 | 
				
			||||||
 | 
					    'videos': 'writePostTypeVideo',
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static const kAttachmentProgressWeight = 0.9;
 | 
					  static const kAttachmentProgressWeight = 0.9;
 | 
				
			||||||
@@ -153,6 +155,7 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
  final TextEditingController titleController = TextEditingController();
 | 
					  final TextEditingController titleController = TextEditingController();
 | 
				
			||||||
  final TextEditingController descriptionController = TextEditingController();
 | 
					  final TextEditingController descriptionController = TextEditingController();
 | 
				
			||||||
  final TextEditingController aliasController = TextEditingController();
 | 
					  final TextEditingController aliasController = TextEditingController();
 | 
				
			||||||
 | 
					  final TextEditingController rewardController = TextEditingController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool _temporarySaveActive = false;
 | 
					  bool _temporarySaveActive = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -168,6 +171,7 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
    contentController.addListener(() {
 | 
					    contentController.addListener(() {
 | 
				
			||||||
      _temporaryPlanSave();
 | 
					      _temporaryPlanSave();
 | 
				
			||||||
 | 
					      notifyListeners();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    if (doLoadFromTemporary) _temporaryLoad();
 | 
					    if (doLoadFromTemporary) _temporaryLoad();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -194,6 +198,7 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
  PostWriteMedia? thumbnail;
 | 
					  PostWriteMedia? thumbnail;
 | 
				
			||||||
  List<PostWriteMedia> attachments = List.empty(growable: true);
 | 
					  List<PostWriteMedia> attachments = List.empty(growable: true);
 | 
				
			||||||
  DateTime? publishedAt, publishedUntil;
 | 
					  DateTime? publishedAt, publishedUntil;
 | 
				
			||||||
 | 
					  SnAttachment? videoAttachment;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> fetchRelatedPost(
 | 
					  Future<void> fetchRelatedPost(
 | 
				
			||||||
    BuildContext context, {
 | 
					    BuildContext context, {
 | 
				
			||||||
@@ -214,6 +219,8 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
        descriptionController.text = post.body['description'] ?? '';
 | 
					        descriptionController.text = post.body['description'] ?? '';
 | 
				
			||||||
        contentController.text = post.body['content'] ?? '';
 | 
					        contentController.text = post.body['content'] ?? '';
 | 
				
			||||||
        aliasController.text = post.alias ?? '';
 | 
					        aliasController.text = post.alias ?? '';
 | 
				
			||||||
 | 
					        rewardController.text = post.body['reward']?.toString() ?? '';
 | 
				
			||||||
 | 
					        videoAttachment = post.preload?.video;
 | 
				
			||||||
        publishedAt = post.publishedAt;
 | 
					        publishedAt = post.publishedAt;
 | 
				
			||||||
        publishedUntil = post.publishedUntil;
 | 
					        publishedUntil = post.publishedUntil;
 | 
				
			||||||
        visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
 | 
					        visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
 | 
				
			||||||
@@ -347,6 +354,7 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
          if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
 | 
					          if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
 | 
				
			||||||
          if (titleController.text.isNotEmpty) 'title': titleController.text,
 | 
					          if (titleController.text.isNotEmpty) 'title': titleController.text,
 | 
				
			||||||
          if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
 | 
					          if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
 | 
				
			||||||
 | 
					          if (rewardController.text.isNotEmpty) 'reward': rewardController.text,
 | 
				
			||||||
          if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
 | 
					          if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
 | 
				
			||||||
          'attachments':
 | 
					          'attachments':
 | 
				
			||||||
              attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
 | 
					              attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
 | 
				
			||||||
@@ -375,6 +383,7 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
      aliasController.text = data['alias'] ?? '';
 | 
					      aliasController.text = data['alias'] ?? '';
 | 
				
			||||||
      titleController.text = data['title'] ?? '';
 | 
					      titleController.text = data['title'] ?? '';
 | 
				
			||||||
      descriptionController.text = data['description'] ?? '';
 | 
					      descriptionController.text = data['description'] ?? '';
 | 
				
			||||||
 | 
					      rewardController.text = data['reward']?.toString() ?? '';
 | 
				
			||||||
      if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
 | 
					      if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
 | 
				
			||||||
      attachments
 | 
					      attachments
 | 
				
			||||||
          .addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
 | 
					          .addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
 | 
				
			||||||
@@ -473,6 +482,8 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
    progress = kAttachmentProgressWeight;
 | 
					    progress = kAttachmentProgressWeight;
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final reward = double.tryParse(rewardController.text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Posting the content
 | 
					    // Posting the content
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final baseProgressVal = progress!;
 | 
					      final baseProgressVal = progress!;
 | 
				
			||||||
@@ -498,6 +509,8 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
          if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
 | 
					          if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
 | 
				
			||||||
          if (replyingPost != null) 'reply_to': replyingPost!.id,
 | 
					          if (replyingPost != null) 'reply_to': replyingPost!.id,
 | 
				
			||||||
          if (repostingPost != null) 'repost_to': repostingPost!.id,
 | 
					          if (repostingPost != null) 'repost_to': repostingPost!.id,
 | 
				
			||||||
 | 
					          if (reward != null) 'reward': reward,
 | 
				
			||||||
 | 
					          if (videoAttachment != null) 'video': videoAttachment!.rid,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        onSendProgress: (count, total) {
 | 
					        onSendProgress: (count, total) {
 | 
				
			||||||
          progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
 | 
					          progress = baseProgressVal + (count / total) * (kPostingProgressWeight / 2);
 | 
				
			||||||
@@ -624,6 +637,11 @@ class PostWriteController extends ChangeNotifier {
 | 
				
			|||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void setVideoAttachment(SnAttachment? value) {
 | 
				
			||||||
 | 
					    videoAttachment = value;
 | 
				
			||||||
 | 
					    notifyListeners();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void reset() {
 | 
					  void reset() {
 | 
				
			||||||
    publishedAt = null;
 | 
					    publishedAt = null;
 | 
				
			||||||
    publishedUntil = null;
 | 
					    publishedUntil = null;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										106
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										106
									
								
								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')) {
 | 
				
			||||||
@@ -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>();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.pk.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 (showingTrayCount == 0) {
 | 
				
			||||||
 | 
					      trayManager.setTitle('');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      trayManager.setTitle(' $showingTrayCount');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void clear() {
 | 
					  void clear() {
 | 
				
			||||||
 | 
					    showingCount = 0;
 | 
				
			||||||
    notifications.clear();
 | 
					    notifications.clear();
 | 
				
			||||||
 | 
					    updateTray();
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,9 @@ class SnPostContentProvider {
 | 
				
			|||||||
      if (out[i].body['thumbnail'] != null) {
 | 
					      if (out[i].body['thumbnail'] != null) {
 | 
				
			||||||
        rids.add(out[i].body['thumbnail']);
 | 
					        rids.add(out[i].body['thumbnail']);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      if (out[i].body['video'] != null) {
 | 
				
			||||||
 | 
					        rids.add(out[i].body['video']);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      if (out[i].repostTo != null) {
 | 
					      if (out[i].repostTo != null) {
 | 
				
			||||||
        out[i] = out[i].copyWith(
 | 
					        out[i] = out[i].copyWith(
 | 
				
			||||||
          repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
 | 
					          repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
 | 
				
			||||||
@@ -36,6 +39,7 @@ class SnPostContentProvider {
 | 
				
			|||||||
        preload: SnPostPreload(
 | 
					        preload: SnPostPreload(
 | 
				
			||||||
          thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
 | 
					          thumbnail: attachments.where((ele) => ele?.rid == out[i].body['thumbnail']).firstOrNull,
 | 
				
			||||||
          attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
					          attachments: attachments.where((ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
				
			||||||
 | 
					          video: attachments.where((ele) => ele?.rid == out[i].body['video']).firstOrNull,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -53,6 +57,9 @@ class SnPostContentProvider {
 | 
				
			|||||||
    if (out.body['thumbnail'] != null) {
 | 
					    if (out.body['thumbnail'] != null) {
 | 
				
			||||||
      rids.add(out.body['thumbnail']);
 | 
					      rids.add(out.body['thumbnail']);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (out.body['video'] != null) {
 | 
				
			||||||
 | 
					      rids.add(out.body['video']);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    if (out.repostTo != null) {
 | 
					    if (out.repostTo != null) {
 | 
				
			||||||
      out = out.copyWith(
 | 
					      out = out.copyWith(
 | 
				
			||||||
        repostTo: await _preloadRelatedDataSingle(out.repostTo!),
 | 
					        repostTo: await _preloadRelatedDataSingle(out.repostTo!),
 | 
				
			||||||
@@ -64,6 +71,7 @@ class SnPostContentProvider {
 | 
				
			|||||||
      preload: SnPostPreload(
 | 
					      preload: SnPostPreload(
 | 
				
			||||||
        thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
 | 
					        thumbnail: attachments.where((ele) => ele?.rid == out.body['thumbnail']).firstOrNull,
 | 
				
			||||||
        attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
					        attachments: attachments.where((ele) => out.body['attachments']?.contains(ele?.rid) ?? false).toList(),
 | 
				
			||||||
 | 
					        video: attachments.where((ele) => ele?.rid == out.body['video']).firstOrNull,
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,8 @@ class WebSocketProvider extends ChangeNotifier {
 | 
				
			|||||||
  late final SnNetworkProvider _sn;
 | 
					  late final SnNetworkProvider _sn;
 | 
				
			||||||
  late final UserProvider _ua;
 | 
					  late final UserProvider _ua;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  StreamController<WebSocketPackage> stream = StreamController.broadcast();
 | 
					  StreamController<WebSocketPackage> pk = StreamController.broadcast();
 | 
				
			||||||
 | 
					  Stream<dynamic>? _wsStream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  WebSocketProvider(BuildContext context) {
 | 
					  WebSocketProvider(BuildContext context) {
 | 
				
			||||||
    _sn = context.read<SnNetworkProvider>();
 | 
					    _sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
@@ -33,7 +34,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();
 | 
				
			||||||
@@ -50,6 +60,7 @@ class WebSocketProvider extends ChangeNotifier {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      conn = WebSocketChannel.connect(uri);
 | 
					      conn = WebSocketChannel.connect(uri);
 | 
				
			||||||
      await conn!.ready;
 | 
					      await conn!.ready;
 | 
				
			||||||
 | 
					      _wsStream = conn!.stream.asBroadcastStream();
 | 
				
			||||||
      listen();
 | 
					      listen();
 | 
				
			||||||
      log('[WebSocket] Connected to server!');
 | 
					      log('[WebSocket] Connected to server!');
 | 
				
			||||||
      isConnected = true;
 | 
					      isConnected = true;
 | 
				
			||||||
@@ -70,6 +81,7 @@ class WebSocketProvider extends ChangeNotifier {
 | 
				
			|||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      isBusy = false;
 | 
					      isBusy = false;
 | 
				
			||||||
      notifyListeners();
 | 
					      notifyListeners();
 | 
				
			||||||
 | 
					      _connectCompleter!.complete();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -83,11 +95,12 @@ class WebSocketProvider extends ChangeNotifier {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void listen() {
 | 
					  void listen() {
 | 
				
			||||||
    conn?.stream.listen(
 | 
					    if (_wsStream == null) return;
 | 
				
			||||||
 | 
					    _wsStream!.listen(
 | 
				
			||||||
      (event) {
 | 
					      (event) {
 | 
				
			||||||
        final packet = WebSocketPackage.fromJson(jsonDecode(event));
 | 
					        final packet = WebSocketPackage.fromJson(jsonDecode(event));
 | 
				
			||||||
        log('Websocket incoming message: ${packet.method} ${packet.message}');
 | 
					        log('Websocket incoming message: ${packet.method} ${packet.message}');
 | 
				
			||||||
        stream.sink.add(packet);
 | 
					        pk.sink.add(packet);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      onDone: () {
 | 
					      onDone: () {
 | 
				
			||||||
        isConnected = false;
 | 
					        isConnected = false;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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]);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -73,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(
 | 
				
			||||||
@@ -156,6 +156,7 @@ final _appRoutes = [
 | 
				
			|||||||
        builder: (context, state) => ChatRoomScreen(
 | 
					        builder: (context, state) => ChatRoomScreen(
 | 
				
			||||||
          scope: state.pathParameters['scope']!,
 | 
					          scope: state.pathParameters['scope']!,
 | 
				
			||||||
          alias: state.pathParameters['alias']!,
 | 
					          alias: state.pathParameters['alias']!,
 | 
				
			||||||
 | 
					          extra: state.extra as ChatRoomScreenExtra?,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
@@ -191,11 +192,6 @@ final _appRoutes = [
 | 
				
			|||||||
      child: const RealmScreen(),
 | 
					      child: const RealmScreen(),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    routes: [
 | 
					    routes: [
 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/:alias',
 | 
					 | 
				
			||||||
        name: 'realmDetail',
 | 
					 | 
				
			||||||
        builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/manage',
 | 
					        path: '/manage',
 | 
				
			||||||
        name: 'realmManage',
 | 
					        name: 'realmManage',
 | 
				
			||||||
@@ -203,6 +199,11 @@ final _appRoutes = [
 | 
				
			|||||||
          editingRealmAlias: state.uri.queryParameters['editing'],
 | 
					          editingRealmAlias: state.uri.queryParameters['editing'],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
 | 
					      GoRoute(
 | 
				
			||||||
 | 
					        path: '/:alias',
 | 
				
			||||||
 | 
					        name: 'realmDetail',
 | 
				
			||||||
 | 
					        builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
 | 
					  GoRoute(path: '/news', name: 'news', builder: (context, state) => const NewsScreen(), routes: [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,7 +28,19 @@ class AccountScreen extends StatelessWidget {
 | 
				
			|||||||
    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
 | 
					        flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
 | 
				
			||||||
            ? Stack(
 | 
					            ? Stack(
 | 
				
			||||||
                fit: StackFit.expand,
 | 
					                fit: StackFit.expand,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,41 @@
 | 
				
			|||||||
 | 
					import 'package:collection/collection.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/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					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:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/userinfo.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:intl/locale.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AccountSettingsScreen extends StatelessWidget {
 | 
					class AccountSettingsScreen extends StatelessWidget {
 | 
				
			||||||
  const AccountSettingsScreen({super.key});
 | 
					  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
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    final ua = context.watch<UserProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppScaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        leading: PageBackButton(),
 | 
					        leading: PageBackButton(),
 | 
				
			||||||
@@ -21,6 +45,42 @@ class AccountSettingsScreen extends StatelessWidget {
 | 
				
			|||||||
        child: Column(
 | 
					        child: Column(
 | 
				
			||||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
					          crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
          children: [
 | 
					          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(
 | 
					            ListTile(
 | 
				
			||||||
              title: Text('accountProfileEdit').tr(),
 | 
					              title: Text('accountProfileEdit').tr(),
 | 
				
			||||||
              subtitle: Text('accountProfileEditSubtitle').tr(),
 | 
					              subtitle: Text('accountProfileEditSubtitle').tr(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -155,12 +155,16 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
                text: TextSpan(children: [
 | 
					                text: TextSpan(children: [
 | 
				
			||||||
                  TextSpan(
 | 
					                  TextSpan(
 | 
				
			||||||
                    text: 'call'.tr(),
 | 
					                    text: 'call'.tr(),
 | 
				
			||||||
                    style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
 | 
					                    style: Theme.of(context).textTheme.titleLarge!.copyWith(
 | 
				
			||||||
 | 
					                          color: Theme.of(context).appBarTheme.foregroundColor,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  const TextSpan(text: '\n'),
 | 
					                  const TextSpan(text: '\n'),
 | 
				
			||||||
                  TextSpan(
 | 
					                  TextSpan(
 | 
				
			||||||
                    text: call.lastDuration.toString(),
 | 
					                    text: call.lastDuration.toString(),
 | 
				
			||||||
                    style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
 | 
					                    style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
				
			||||||
 | 
					                          color: Theme.of(context).appBarTheme.foregroundColor,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ]),
 | 
					                ]),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,3 +1,4 @@
 | 
				
			|||||||
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
import 'package:dio/dio.dart';
 | 
					import 'package:dio/dio.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';
 | 
				
			||||||
@@ -17,6 +18,7 @@ import 'package:uuid/uuid.dart';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class ChatManageScreen extends StatefulWidget {
 | 
					class ChatManageScreen extends StatefulWidget {
 | 
				
			||||||
  final String? editingChannelAlias;
 | 
					  final String? editingChannelAlias;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ChatManageScreen({super.key, this.editingChannelAlias});
 | 
					  const ChatManageScreen({super.key, this.editingChannelAlias});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -33,6 +35,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
  List<SnRealm>? _realms;
 | 
					  List<SnRealm>? _realms;
 | 
				
			||||||
  SnRealm? _belongToRealm;
 | 
					  SnRealm? _belongToRealm;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  SnChannel? _editingChannel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _fetchRealms() async {
 | 
					  Future<void> _fetchRealms() async {
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
@@ -41,6 +45,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
      _realms = List<SnRealm>.from(
 | 
					      _realms = List<SnRealm>.from(
 | 
				
			||||||
        resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
 | 
					        resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					      if (_editingChannel != null) {
 | 
				
			||||||
 | 
					        _belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      if (mounted) context.showErrorDialog(err);
 | 
					      if (mounted) context.showErrorDialog(err);
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
@@ -48,8 +55,6 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  SnChannel? _editingChannel;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Future<void> _fetchChannel() async {
 | 
					  Future<void> _fetchChannel() async {
 | 
				
			||||||
    setState(() => _isBusy = true);
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -124,9 +129,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return AppScaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      appBar: AppBar(
 | 
					      appBar: AppBar(
 | 
				
			||||||
        title: widget.editingChannelAlias != null
 | 
					        title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(),
 | 
				
			||||||
            ? Text('screenChatManage').tr()
 | 
					 | 
				
			||||||
            : Text('screenChatNew').tr(),
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body: SingleChildScrollView(
 | 
					      body: SingleChildScrollView(
 | 
				
			||||||
        child: Column(
 | 
					        child: Column(
 | 
				
			||||||
@@ -138,8 +141,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                leadingPadding: const EdgeInsets.only(left: 10, right: 20),
 | 
					                leadingPadding: const EdgeInsets.only(left: 10, right: 20),
 | 
				
			||||||
                dividerColor: Colors.transparent,
 | 
					                dividerColor: Colors.transparent,
 | 
				
			||||||
                content: Text(
 | 
					                content: Text(
 | 
				
			||||||
                  'channelEditingNotice'
 | 
					                  'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']),
 | 
				
			||||||
                      .tr(args: ['#${_editingChannel!.alias}']),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                actions: [
 | 
					                actions: [
 | 
				
			||||||
                  TextButton(
 | 
					                  TextButton(
 | 
				
			||||||
@@ -162,6 +164,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                items: [
 | 
					                items: [
 | 
				
			||||||
                  ...(_realms?.map(
 | 
					                  ...(_realms?.map(
 | 
				
			||||||
                        (SnRealm item) => DropdownMenuItem<SnRealm>(
 | 
					                        (SnRealm item) => DropdownMenuItem<SnRealm>(
 | 
				
			||||||
 | 
					                          enabled: _editingChannel == null || _editingChannel?.realmId == item.id,
 | 
				
			||||||
                          value: item,
 | 
					                          value: item,
 | 
				
			||||||
                          child: Row(
 | 
					                          child: Row(
 | 
				
			||||||
                            children: [
 | 
					                            children: [
 | 
				
			||||||
@@ -179,15 +182,12 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                                  mainAxisSize: MainAxisSize.min,
 | 
					                                  mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
                                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
					                                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                                  children: [
 | 
					                                  children: [
 | 
				
			||||||
                                    Text(item.name).textStyle(Theme.of(context)
 | 
					                                    Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!),
 | 
				
			||||||
                                        .textTheme
 | 
					 | 
				
			||||||
                                        .bodyMedium!),
 | 
					 | 
				
			||||||
                                    Text(
 | 
					                                    Text(
 | 
				
			||||||
                                      item.description,
 | 
					                                      item.description,
 | 
				
			||||||
                                      maxLines: 1,
 | 
					                                      maxLines: 1,
 | 
				
			||||||
                                      overflow: TextOverflow.ellipsis,
 | 
					                                      overflow: TextOverflow.ellipsis,
 | 
				
			||||||
                                    ).textStyle(
 | 
					                                    ).textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
				
			||||||
                                        Theme.of(context).textTheme.bodySmall!),
 | 
					 | 
				
			||||||
                                  ],
 | 
					                                  ],
 | 
				
			||||||
                                ),
 | 
					                                ),
 | 
				
			||||||
                              ),
 | 
					                              ),
 | 
				
			||||||
@@ -197,14 +197,14 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                      ) ??
 | 
					                      ) ??
 | 
				
			||||||
                      []),
 | 
					                      []),
 | 
				
			||||||
                  DropdownMenuItem<SnRealm>(
 | 
					                  DropdownMenuItem<SnRealm>(
 | 
				
			||||||
 | 
					                    enabled: _editingChannel == null,
 | 
				
			||||||
                    value: null,
 | 
					                    value: null,
 | 
				
			||||||
                    child: Row(
 | 
					                    child: Row(
 | 
				
			||||||
                      children: [
 | 
					                      children: [
 | 
				
			||||||
                        CircleAvatar(
 | 
					                        CircleAvatar(
 | 
				
			||||||
                          radius: 16,
 | 
					                          radius: 16,
 | 
				
			||||||
                          backgroundColor: Colors.transparent,
 | 
					                          backgroundColor: Colors.transparent,
 | 
				
			||||||
                          foregroundColor:
 | 
					                          foregroundColor: Theme.of(context).colorScheme.onSurface,
 | 
				
			||||||
                              Theme.of(context).colorScheme.onSurface,
 | 
					 | 
				
			||||||
                          child: const Icon(Symbols.clear),
 | 
					                          child: const Icon(Symbols.clear),
 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        const Gap(12),
 | 
					                        const Gap(12),
 | 
				
			||||||
@@ -213,9 +213,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                            mainAxisSize: MainAxisSize.min,
 | 
					                            mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
                            crossAxisAlignment: CrossAxisAlignment.start,
 | 
					                            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                            children: [
 | 
					                            children: [
 | 
				
			||||||
                              Text('fieldChatBelongToRealmUnset')
 | 
					                              Text('fieldChatBelongToRealmUnset').tr().textStyle(
 | 
				
			||||||
                                  .tr()
 | 
					 | 
				
			||||||
                                  .textStyle(
 | 
					 | 
				
			||||||
                                    Theme.of(context).textTheme.bodyMedium!,
 | 
					                                    Theme.of(context).textTheme.bodyMedium!,
 | 
				
			||||||
                                  ),
 | 
					                                  ),
 | 
				
			||||||
                            ],
 | 
					                            ],
 | 
				
			||||||
@@ -231,10 +229,10 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
                buttonStyleData: const ButtonStyleData(
 | 
					                buttonStyleData: const ButtonStyleData(
 | 
				
			||||||
                  padding: EdgeInsets.only(right: 16),
 | 
					                  padding: EdgeInsets.only(right: 16),
 | 
				
			||||||
                  height: 60,
 | 
					                  height: 48,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                menuItemStyleData: const MenuItemStyleData(
 | 
					                menuItemStyleData: const MenuItemStyleData(
 | 
				
			||||||
                  height: 60,
 | 
					                  height: 48,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
@@ -250,8 +248,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                    helperText: 'fieldChatAliasHint'.tr(),
 | 
					                    helperText: 'fieldChatAliasHint'.tr(),
 | 
				
			||||||
                    helperMaxLines: 2,
 | 
					                    helperMaxLines: 2,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  onTapOutside: (_) =>
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const Gap(4),
 | 
					                const Gap(4),
 | 
				
			||||||
                TextField(
 | 
					                TextField(
 | 
				
			||||||
@@ -260,8 +257,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                    border: const UnderlineInputBorder(),
 | 
					                    border: const UnderlineInputBorder(),
 | 
				
			||||||
                    labelText: 'fieldChatName'.tr(),
 | 
					                    labelText: 'fieldChatName'.tr(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  onTapOutside: (_) =>
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const Gap(4),
 | 
					                const Gap(4),
 | 
				
			||||||
                TextField(
 | 
					                TextField(
 | 
				
			||||||
@@ -272,8 +268,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
				
			|||||||
                    border: const UnderlineInputBorder(),
 | 
					                    border: const UnderlineInputBorder(),
 | 
				
			||||||
                    labelText: 'fieldChatDescription'.tr(),
 | 
					                    labelText: 'fieldChatDescription'.tr(),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  onTapOutside: (_) =>
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                const Gap(12),
 | 
					                const Gap(12),
 | 
				
			||||||
                Row(
 | 
					                Row(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,12 +186,27 @@ 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>();
 | 
				
			||||||
    _wsSubscription = ws.stream.stream.listen((event) {
 | 
					    _wsSubscription = ws.pk.stream.listen((event) {
 | 
				
			||||||
      switch (event.method) {
 | 
					      switch (event.method) {
 | 
				
			||||||
        case 'calls.new':
 | 
					        case 'calls.new':
 | 
				
			||||||
          final payload = SnChatCall.fromJson(event.payload!);
 | 
					          final payload = SnChatCall.fromJson(event.payload!);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
import 'package:animations/animations.dart';
 | 
					 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
 | 
					import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
 | 
				
			||||||
@@ -7,10 +6,8 @@ import 'package:go_router/go_router.dart';
 | 
				
			|||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:surface/providers/config.dart';
 | 
					 | 
				
			||||||
import 'package:surface/providers/post.dart';
 | 
					import 'package:surface/providers/post.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
import 'package:surface/screens/post/post_detail.dart';
 | 
					 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
					import 'package:surface/widgets/app_bar_leading.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
@@ -97,8 +94,6 @@ 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(
 | 
				
			||||||
@@ -166,6 +161,48 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					          Row(
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              Text('writePostTypeQuestion').tr(),
 | 
				
			||||||
 | 
					              const Gap(20),
 | 
				
			||||||
 | 
					              FloatingActionButton(
 | 
				
			||||||
 | 
					                heroTag: null,
 | 
				
			||||||
 | 
					                tooltip: 'writePostTypeQuestion'.tr(),
 | 
				
			||||||
 | 
					                onPressed: () {
 | 
				
			||||||
 | 
					                  GoRouter.of(context).pushNamed('postEditor', pathParameters: {
 | 
				
			||||||
 | 
					                    'mode': 'questions',
 | 
				
			||||||
 | 
					                  }).then((value) {
 | 
				
			||||||
 | 
					                    if (value == true) {
 | 
				
			||||||
 | 
					                      _refreshPosts();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                  _fabKey.currentState!.toggle();
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                child: const Icon(Symbols.question_answer),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          Row(
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              Text('writePostTypeVideo').tr(),
 | 
				
			||||||
 | 
					              const Gap(20),
 | 
				
			||||||
 | 
					              FloatingActionButton(
 | 
				
			||||||
 | 
					                heroTag: null,
 | 
				
			||||||
 | 
					                tooltip: 'writePostTypeVideo'.tr(),
 | 
				
			||||||
 | 
					                onPressed: () {
 | 
				
			||||||
 | 
					                  GoRouter.of(context).pushNamed('postEditor', pathParameters: {
 | 
				
			||||||
 | 
					                    'mode': 'videos',
 | 
				
			||||||
 | 
					                  }).then((value) {
 | 
				
			||||||
 | 
					                    if (value == true) {
 | 
				
			||||||
 | 
					                      _refreshPosts();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                  _fabKey.currentState!.toggle();
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                child: const Icon(Symbols.video_call),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body: RefreshIndicator(
 | 
					      body: RefreshIndicator(
 | 
				
			||||||
@@ -225,34 +262,15 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
				
			|||||||
              onFetchData: _fetchPosts,
 | 
					              onFetchData: _fetchPosts,
 | 
				
			||||||
              itemBuilder: (context, idx) {
 | 
					              itemBuilder: (context, idx) {
 | 
				
			||||||
                return Center(
 | 
					                return Center(
 | 
				
			||||||
                  child: OpenContainer(
 | 
					                  child: OpenablePostItem(
 | 
				
			||||||
                    closedBuilder: (_, __) => Container(
 | 
					                    data: _posts[idx],
 | 
				
			||||||
                      constraints: const BoxConstraints(maxWidth: 640),
 | 
					                    maxWidth: 640,
 | 
				
			||||||
                      child: PostItem(
 | 
					                    onChanged: (data) {
 | 
				
			||||||
                        data: _posts[idx],
 | 
					                      setState(() => _posts[idx] = data);
 | 
				
			||||||
                        maxWidth: 640,
 | 
					                    },
 | 
				
			||||||
                        onChanged: (data) {
 | 
					                    onDeleted: () {
 | 
				
			||||||
                          setState(() => _posts[idx] = data);
 | 
					                      _refreshPosts();
 | 
				
			||||||
                        },
 | 
					                    },
 | 
				
			||||||
                        onDeleted: () {
 | 
					 | 
				
			||||||
                          _refreshPosts();
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                    openBuilder: (_, close) => PostDetailScreen(
 | 
					 | 
				
			||||||
                      slug: _posts[idx].id.toString(),
 | 
					 | 
				
			||||||
                      preload: _posts[idx],
 | 
					 | 
				
			||||||
                      onBack: close,
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                    openColor: Colors.transparent,
 | 
					 | 
				
			||||||
                    openElevation: 0,
 | 
					 | 
				
			||||||
                    transitionType: ContainerTransitionType.fade,
 | 
					 | 
				
			||||||
                    closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
 | 
					 | 
				
			||||||
                          cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                    closedShape: const RoundedRectangleBorder(
 | 
					 | 
				
			||||||
                      borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -288,6 +288,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
 | 
				
			|||||||
              child: InkWell(
 | 
					              child: InkWell(
 | 
				
			||||||
                borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
					                borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
                child: Column(
 | 
					                child: Column(
 | 
				
			||||||
 | 
					                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                  spacing: 4,
 | 
					                  spacing: 4,
 | 
				
			||||||
                  children: [
 | 
					                  children: [
 | 
				
			||||||
                    Text(
 | 
					                    Text(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,15 +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,
 | 
					 | 
				
			||||||
    'passport.security.otp': Symbols.password,
 | 
					 | 
				
			||||||
    'interactive.subscription': Symbols.subscriptions,
 | 
					 | 
				
			||||||
    'interactive.feedback': Symbols.add_reaction,
 | 
					 | 
				
			||||||
    'messaging.callStart': Symbols.call_received,
 | 
					 | 
				
			||||||
    'wallet.transaction.new': Symbols.receipt,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  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;
 | 
				
			||||||
@@ -53,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(
 | 
				
			||||||
@@ -61,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);
 | 
				
			||||||
@@ -87,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(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -183,7 +183,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
 | 
				
			|||||||
            if (_data != null)
 | 
					            if (_data != null)
 | 
				
			||||||
              PostCommentSliverList(
 | 
					              PostCommentSliverList(
 | 
				
			||||||
                key: _childListKey,
 | 
					                key: _childListKey,
 | 
				
			||||||
                parentPostId: _data!.id,
 | 
					                parentPost: _data!,
 | 
				
			||||||
                maxWidth: 640,
 | 
					                maxWidth: 640,
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
 | 
					            SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,32 +1,46 @@
 | 
				
			|||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:collection/collection.dart';
 | 
					import 'package:collection/collection.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:flutter_context_menu/flutter_context_menu.dart';
 | 
				
			||||||
import 'package:gap/gap.dart';
 | 
					import 'package:gap/gap.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:responsive_framework/responsive_framework.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';
 | 
				
			||||||
 | 
					import 'package:surface/providers/sn_attachment.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/attachment.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
 | 
				
			||||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
					import 'package:surface/widgets/loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/markdown_content.dart';
 | 
				
			||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/post_item.dart';
 | 
					 | 
				
			||||||
import 'package:surface/widgets/post/post_media_pending_list.dart';
 | 
					import 'package:surface/widgets/post/post_media_pending_list.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/post_meta_editor.dart';
 | 
					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';
 | 
				
			||||||
 | 
					import 'package:uuid/uuid.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostEditorExtraProps {
 | 
					import '../../widgets/attachment/attachment_input.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 +53,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 +108,49 @@ 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(() {});
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _showPublisherPopup() {
 | 
				
			||||||
 | 
					    showModalBottomSheet(
 | 
				
			||||||
 | 
					      context: context,
 | 
				
			||||||
 | 
					      builder: (context) => _PostPublisherPopup(
 | 
				
			||||||
 | 
					        controller: _writeController,
 | 
				
			||||||
 | 
					        publishers: _publishers,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @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 +201,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
				
			|||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ]),
 | 
					              ]),
 | 
				
			||||||
 | 
					              maxLines: 2,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            actions: [
 | 
					            actions: [
 | 
				
			||||||
              IconButton(
 | 
					              IconButton(
 | 
				
			||||||
@@ -164,207 +213,92 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
				
			|||||||
          ),
 | 
					          ),
 | 
				
			||||||
          body: Column(
 | 
					          body: Column(
 | 
				
			||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
              DropdownButtonHideUnderline(
 | 
					              if (_writeController.editingPost != null)
 | 
				
			||||||
                child: DropdownButton2<SnPublisher>(
 | 
					                Container(
 | 
				
			||||||
                  isExpanded: true,
 | 
					                  padding: const EdgeInsets.only(top: 4, bottom: 4, left: 20, right: 20),
 | 
				
			||||||
                  hint: Text(
 | 
					                  decoration: BoxDecoration(
 | 
				
			||||||
                    'fieldPostPublisher',
 | 
					                    border: Border(
 | 
				
			||||||
                    style: TextStyle(
 | 
					                      bottom: BorderSide(
 | 
				
			||||||
                      fontSize: 14,
 | 
					                        color: Theme.of(context).dividerColor,
 | 
				
			||||||
                      color: Theme.of(context).hintColor,
 | 
					                        width: 1 / MediaQuery.of(context).devicePixelRatio,
 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ).tr(),
 | 
					 | 
				
			||||||
                  items: <DropdownMenuItem<SnPublisher>>[
 | 
					 | 
				
			||||||
                    ...(_publishers?.map(
 | 
					 | 
				
			||||||
                          (item) => DropdownMenuItem<SnPublisher>(
 | 
					 | 
				
			||||||
                            enabled: _writeController.editingPost == null,
 | 
					 | 
				
			||||||
                            value: item,
 | 
					 | 
				
			||||||
                            child: Row(
 | 
					 | 
				
			||||||
                              children: [
 | 
					 | 
				
			||||||
                                AccountImage(content: item.avatar, radius: 16),
 | 
					 | 
				
			||||||
                                const Gap(8),
 | 
					 | 
				
			||||||
                                Expanded(
 | 
					 | 
				
			||||||
                                  child: Column(
 | 
					 | 
				
			||||||
                                    mainAxisSize: MainAxisSize.min,
 | 
					 | 
				
			||||||
                                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
                                    children: [
 | 
					 | 
				
			||||||
                                      Text(item.nick).textStyle(Theme.of(context).textTheme.bodyMedium!),
 | 
					 | 
				
			||||||
                                      Text('@${item.name}')
 | 
					 | 
				
			||||||
                                          .textStyle(Theme.of(context).textTheme.bodySmall!)
 | 
					 | 
				
			||||||
                                          .fontSize(12),
 | 
					 | 
				
			||||||
                                    ],
 | 
					 | 
				
			||||||
                                  ),
 | 
					 | 
				
			||||||
                                ),
 | 
					 | 
				
			||||||
                              ],
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ) ??
 | 
					 | 
				
			||||||
                        []),
 | 
					 | 
				
			||||||
                    DropdownMenuItem<SnPublisher>(
 | 
					 | 
				
			||||||
                      value: null,
 | 
					 | 
				
			||||||
                      child: Row(
 | 
					 | 
				
			||||||
                        children: [
 | 
					 | 
				
			||||||
                          CircleAvatar(
 | 
					 | 
				
			||||||
                            radius: 16,
 | 
					 | 
				
			||||||
                            backgroundColor: Colors.transparent,
 | 
					 | 
				
			||||||
                            foregroundColor: Theme.of(context).colorScheme.onSurface,
 | 
					 | 
				
			||||||
                            child: const Icon(Symbols.add),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                          const Gap(8),
 | 
					 | 
				
			||||||
                          Expanded(
 | 
					 | 
				
			||||||
                            child: Column(
 | 
					 | 
				
			||||||
                              mainAxisSize: MainAxisSize.min,
 | 
					 | 
				
			||||||
                              crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
                              children: [
 | 
					 | 
				
			||||||
                                Text('publishersNew').tr().textStyle(Theme.of(context).textTheme.bodyMedium!),
 | 
					 | 
				
			||||||
                              ],
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ],
 | 
					 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                  ],
 | 
					 | 
				
			||||||
                  value: _writeController.publisher,
 | 
					 | 
				
			||||||
                  onChanged: (SnPublisher? value) {
 | 
					 | 
				
			||||||
                    if (value == null) {
 | 
					 | 
				
			||||||
                      GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
 | 
					 | 
				
			||||||
                        if (value == true) {
 | 
					 | 
				
			||||||
                          _publishers = null;
 | 
					 | 
				
			||||||
                          _fetchPublishers();
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                      });
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                      _writeController.setPublisher(value);
 | 
					 | 
				
			||||||
                      final config = context.read<ConfigProvider>();
 | 
					 | 
				
			||||||
                      config.prefs.setInt('int_last_publisher_id', value.id);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                  buttonStyleData: const ButtonStyleData(
 | 
					 | 
				
			||||||
                    padding: EdgeInsets.only(right: 16),
 | 
					 | 
				
			||||||
                    height: 48,
 | 
					 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  menuItemStyleData: const MenuItemStyleData(
 | 
					                  child: Row(
 | 
				
			||||||
                    height: 48,
 | 
					                    crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              const Divider(height: 1),
 | 
					 | 
				
			||||||
              Expanded(
 | 
					 | 
				
			||||||
                child: SingleChildScrollView(
 | 
					 | 
				
			||||||
                  padding: EdgeInsets.only(bottom: 8),
 | 
					 | 
				
			||||||
                  child: Column(
 | 
					 | 
				
			||||||
                    children: [
 | 
					                    children: [
 | 
				
			||||||
                      // Replying Notice
 | 
					                      const Icon(Icons.edit, size: 16),
 | 
				
			||||||
                      if (_writeController.replyingPost != null)
 | 
					                      const Gap(10),
 | 
				
			||||||
                        Column(
 | 
					                      Text('postEditingNotice').tr(args: ['@${_writeController.editingPost!.publisher.name}']),
 | 
				
			||||||
                          children: [
 | 
					                    ],
 | 
				
			||||||
                            ExpansionTile(
 | 
					 | 
				
			||||||
                              minTileHeight: 48,
 | 
					 | 
				
			||||||
                              leading: const Icon(Symbols.reply).padding(left: 4),
 | 
					 | 
				
			||||||
                              title: Text('postReplyingNotice')
 | 
					 | 
				
			||||||
                                  .fontSize(15)
 | 
					 | 
				
			||||||
                                  .tr(args: ['@${_writeController.replyingPost!.publisher.name}']),
 | 
					 | 
				
			||||||
                              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),
 | 
					 | 
				
			||||||
                          ],
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      // Editing Notice
 | 
					 | 
				
			||||||
                      if (_writeController.editingPost != null)
 | 
					 | 
				
			||||||
                        Column(
 | 
					 | 
				
			||||||
                          children: [
 | 
					 | 
				
			||||||
                            ExpansionTile(
 | 
					 | 
				
			||||||
                              minTileHeight: 48,
 | 
					 | 
				
			||||||
                              leading: const Icon(Symbols.edit_note).padding(left: 4),
 | 
					 | 
				
			||||||
                              title: Text('postEditingNotice')
 | 
					 | 
				
			||||||
                                  .fontSize(15)
 | 
					 | 
				
			||||||
                                  .tr(args: ['@${_writeController.editingPost!.publisher.name}']),
 | 
					 | 
				
			||||||
                              children: <Widget>[PostItem(data: _writeController.editingPost!)],
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                            const Divider(height: 1),
 | 
					 | 
				
			||||||
                          ],
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      // 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(),
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                    ]
 | 
					 | 
				
			||||||
                        .expandIndexed(
 | 
					 | 
				
			||||||
                          (idx, ele) => [
 | 
					 | 
				
			||||||
                            if (idx != 0 || _writeController.isRelatedNull) const Gap(8),
 | 
					 | 
				
			||||||
                            ele,
 | 
					 | 
				
			||||||
                          ],
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        .toList(),
 | 
					 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
					              Expanded(
 | 
				
			||||||
 | 
					                child: Stack(
 | 
				
			||||||
 | 
					                  children: [
 | 
				
			||||||
 | 
					                    SingleChildScrollView(
 | 
				
			||||||
 | 
					                      padding: EdgeInsets.only(bottom: 160),
 | 
				
			||||||
 | 
					                      child: switch (_writeController.mode) {
 | 
				
			||||||
 | 
					                        'stories' => _PostStoryEditor(
 | 
				
			||||||
 | 
					                            controller: _writeController,
 | 
				
			||||||
 | 
					                            onTapPublisher: _showPublisherPopup,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        'articles' => _PostArticleEditor(
 | 
				
			||||||
 | 
					                            controller: _writeController,
 | 
				
			||||||
 | 
					                            onTapPublisher: _showPublisherPopup,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        'questions' => _PostQuestionEditor(
 | 
				
			||||||
 | 
					                            controller: _writeController,
 | 
				
			||||||
 | 
					                            onTapPublisher: _showPublisherPopup,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        'videos' => _PostVideoEditor(
 | 
				
			||||||
 | 
					                            controller: _writeController,
 | 
				
			||||||
 | 
					                            onTapPublisher: _showPublisherPopup,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        _ => const Placeholder(),
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    if (_writeController.attachments.isNotEmpty || _writeController.thumbnail != null)
 | 
				
			||||||
 | 
					                      Positioned(
 | 
				
			||||||
 | 
					                        bottom: 0,
 | 
				
			||||||
 | 
					                        left: 0,
 | 
				
			||||||
 | 
					                        right: 0,
 | 
				
			||||||
 | 
					                        child: 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),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              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(
 | 
				
			||||||
@@ -466,3 +400,525 @@ class _PostEditorActionScrollBehavior extends MaterialScrollBehavior {
 | 
				
			|||||||
        PointerDeviceKind.mouse,
 | 
					        PointerDeviceKind.mouse,
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostPublisherPopup extends StatelessWidget {
 | 
				
			||||||
 | 
					  final PostWriteController controller;
 | 
				
			||||||
 | 
					  final List<SnPublisher>? publishers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostPublisherPopup({required this.controller, this.publishers});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Column(
 | 
				
			||||||
 | 
					      crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        Row(
 | 
				
			||||||
 | 
					          crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            const Icon(Symbols.face, size: 24),
 | 
				
			||||||
 | 
					            const Gap(16),
 | 
				
			||||||
 | 
					            Text('accountPublishers', style: Theme.of(context).textTheme.titleLarge).tr(),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
				
			||||||
 | 
					        Expanded(
 | 
				
			||||||
 | 
					          child: ListView.builder(
 | 
				
			||||||
 | 
					            itemCount: publishers?.length ?? 0,
 | 
				
			||||||
 | 
					            itemBuilder: (context, idx) {
 | 
				
			||||||
 | 
					              final publisher = publishers![idx];
 | 
				
			||||||
 | 
					              return ListTile(
 | 
				
			||||||
 | 
					                title: Text(publisher.nick),
 | 
				
			||||||
 | 
					                subtitle: Text('@${publisher.name}'),
 | 
				
			||||||
 | 
					                leading: AccountImage(content: publisher.avatar, radius: 18),
 | 
				
			||||||
 | 
					                onTap: () {
 | 
				
			||||||
 | 
					                  controller.setPublisher(publisher);
 | 
				
			||||||
 | 
					                  Navigator.pop(context, true);
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostStoryEditor extends StatelessWidget {
 | 
				
			||||||
 | 
					  final PostWriteController controller;
 | 
				
			||||||
 | 
					  final Function? onTapPublisher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostStoryEditor({required this.controller, this.onTapPublisher});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Container(
 | 
				
			||||||
 | 
					      padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
				
			||||||
 | 
					      constraints: const BoxConstraints(maxWidth: 640),
 | 
				
			||||||
 | 
					      child: Row(
 | 
				
			||||||
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Material(
 | 
				
			||||||
 | 
					            elevation: 2,
 | 
				
			||||||
 | 
					            borderRadius: const BorderRadius.all(Radius.circular(24)),
 | 
				
			||||||
 | 
					            child: GestureDetector(
 | 
				
			||||||
 | 
					              onTap: () {
 | 
				
			||||||
 | 
					                onTapPublisher?.call();
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              child: AccountImage(
 | 
				
			||||||
 | 
					                content: controller.publisher?.avatar,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          Expanded(
 | 
				
			||||||
 | 
					            child: Column(
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                const Gap(6),
 | 
				
			||||||
 | 
					                TextField(
 | 
				
			||||||
 | 
					                  controller: controller.titleController,
 | 
				
			||||||
 | 
					                  decoration: InputDecoration.collapsed(
 | 
				
			||||||
 | 
					                    hintText: 'fieldPostTitle'.tr(),
 | 
				
			||||||
 | 
					                    border: InputBorder.none,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  style: Theme.of(context).textTheme.titleLarge,
 | 
				
			||||||
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					                const Gap(8),
 | 
				
			||||||
 | 
					                TextField(
 | 
				
			||||||
 | 
					                  controller: controller.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(),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ).padding(bottom: 8),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostArticleEditor extends StatelessWidget {
 | 
				
			||||||
 | 
					  final PostWriteController controller;
 | 
				
			||||||
 | 
					  final Function? onTapPublisher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostArticleEditor({required this.controller, this.onTapPublisher});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    final editorWidgets = <Widget>[
 | 
				
			||||||
 | 
					      Material(
 | 
				
			||||||
 | 
					        color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
				
			||||||
 | 
					        child: InkWell(
 | 
				
			||||||
 | 
					          child: Row(
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              AccountImage(content: controller.publisher?.avatar, radius: 20),
 | 
				
			||||||
 | 
					              const Gap(8),
 | 
				
			||||||
 | 
					              Expanded(
 | 
				
			||||||
 | 
					                child: Column(
 | 
				
			||||||
 | 
					                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                  children: [
 | 
				
			||||||
 | 
					                    Text(controller.publisher?.nick ?? 'loading'.tr()).bold(),
 | 
				
			||||||
 | 
					                    Text('@${controller.publisher?.name}'),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ).padding(horizontal: 12, vertical: 8),
 | 
				
			||||||
 | 
					          onTap: () {
 | 
				
			||||||
 | 
					            onTapPublisher?.call();
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      const Gap(16),
 | 
				
			||||||
 | 
					      TextField(
 | 
				
			||||||
 | 
					        controller: controller.titleController,
 | 
				
			||||||
 | 
					        decoration: InputDecoration.collapsed(
 | 
				
			||||||
 | 
					          hintText: 'fieldPostTitle'.tr(),
 | 
				
			||||||
 | 
					          border: InputBorder.none,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        style: Theme.of(context).textTheme.titleLarge,
 | 
				
			||||||
 | 
					        onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					      ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					      const Gap(8),
 | 
				
			||||||
 | 
					      TextField(
 | 
				
			||||||
 | 
					        controller: controller.descriptionController,
 | 
				
			||||||
 | 
					        decoration: InputDecoration.collapsed(
 | 
				
			||||||
 | 
					          hintText: 'fieldPostDescription'.tr(),
 | 
				
			||||||
 | 
					          border: InputBorder.none,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        maxLines: null,
 | 
				
			||||||
 | 
					        keyboardType: TextInputType.multiline,
 | 
				
			||||||
 | 
					        style: Theme.of(context).textTheme.bodyLarge,
 | 
				
			||||||
 | 
					        onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					      ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					      const Gap(4),
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (ResponsiveBreakpoints.of(context).largerThan(MOBILE)) {
 | 
				
			||||||
 | 
					      return Container(
 | 
				
			||||||
 | 
					        constraints: const BoxConstraints(maxWidth: 640 * 2 + 8),
 | 
				
			||||||
 | 
					        child: Column(
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            ...editorWidgets,
 | 
				
			||||||
 | 
					            Row(
 | 
				
			||||||
 | 
					              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                Expanded(
 | 
				
			||||||
 | 
					                  child: TextField(
 | 
				
			||||||
 | 
					                    controller: controller.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(),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                const Gap(8),
 | 
				
			||||||
 | 
					                Expanded(
 | 
				
			||||||
 | 
					                  child: MarkdownTextContent(
 | 
				
			||||||
 | 
					                    content: controller.contentController.text,
 | 
				
			||||||
 | 
					                  ).padding(horizontal: 24),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Column(
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        ...editorWidgets,
 | 
				
			||||||
 | 
					        Container(
 | 
				
			||||||
 | 
					          padding: const EdgeInsets.only(top: 8),
 | 
				
			||||||
 | 
					          constraints: const BoxConstraints(maxWidth: 640),
 | 
				
			||||||
 | 
					          child: TextField(
 | 
				
			||||||
 | 
					            controller: controller.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(),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostQuestionEditor extends StatelessWidget {
 | 
				
			||||||
 | 
					  final PostWriteController controller;
 | 
				
			||||||
 | 
					  final Function? onTapPublisher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostQuestionEditor({required this.controller, this.onTapPublisher});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Container(
 | 
				
			||||||
 | 
					      padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
				
			||||||
 | 
					      constraints: const BoxConstraints(maxWidth: 640),
 | 
				
			||||||
 | 
					      child: Row(
 | 
				
			||||||
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Material(
 | 
				
			||||||
 | 
					            elevation: 1,
 | 
				
			||||||
 | 
					            borderRadius: const BorderRadius.all(Radius.circular(24)),
 | 
				
			||||||
 | 
					            child: GestureDetector(
 | 
				
			||||||
 | 
					              onTap: () {
 | 
				
			||||||
 | 
					                onTapPublisher?.call();
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              child: AccountImage(
 | 
				
			||||||
 | 
					                content: controller.publisher?.avatar,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          Expanded(
 | 
				
			||||||
 | 
					            child: Column(
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                const Gap(6),
 | 
				
			||||||
 | 
					                TextField(
 | 
				
			||||||
 | 
					                  controller: controller.titleController,
 | 
				
			||||||
 | 
					                  decoration: InputDecoration.collapsed(
 | 
				
			||||||
 | 
					                    hintText: 'fieldPostTitle'.tr(),
 | 
				
			||||||
 | 
					                    border: InputBorder.none,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  style: Theme.of(context).textTheme.titleLarge,
 | 
				
			||||||
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					                const Gap(8),
 | 
				
			||||||
 | 
					                TextField(
 | 
				
			||||||
 | 
					                  controller: controller.rewardController,
 | 
				
			||||||
 | 
					                  decoration: InputDecoration(
 | 
				
			||||||
 | 
					                    hintText: 'fieldPostQuestionReward'.tr(),
 | 
				
			||||||
 | 
					                    suffixText: 'walletCurrencyShort'.tr(),
 | 
				
			||||||
 | 
					                    border: InputBorder.none,
 | 
				
			||||||
 | 
					                    isCollapsed: true,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					                ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					                const Gap(8),
 | 
				
			||||||
 | 
					                TextField(
 | 
				
			||||||
 | 
					                  controller: controller.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(),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ).padding(top: 8),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostVideoEditor extends StatelessWidget {
 | 
				
			||||||
 | 
					  final PostWriteController controller;
 | 
				
			||||||
 | 
					  final Function? onTapPublisher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostVideoEditor({required this.controller, this.onTapPublisher});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _selectVideo(BuildContext context) async {
 | 
				
			||||||
 | 
					    final video = await showDialog<SnAttachment?>(
 | 
				
			||||||
 | 
					      context: context,
 | 
				
			||||||
 | 
					      builder: (context) => AttachmentInputDialog(
 | 
				
			||||||
 | 
					        title: 'postVideoUpload'.tr(),
 | 
				
			||||||
 | 
					        pool: 'interactive',
 | 
				
			||||||
 | 
					        mediaType: SnMediaType.video,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (!context.mounted) return;
 | 
				
			||||||
 | 
					    if (video == null) return;
 | 
				
			||||||
 | 
					    controller.setVideoAttachment(video);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _setAlt(BuildContext context) async {
 | 
				
			||||||
 | 
					    if (controller.videoAttachment == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final result = await showDialog<SnAttachment?>(
 | 
				
			||||||
 | 
					      context: context,
 | 
				
			||||||
 | 
					      builder: (context) => PendingAttachmentAltDialog(media: PostWriteMedia(controller.videoAttachment)),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (result == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    controller.setVideoAttachment(result);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _createBoost(BuildContext context) async {
 | 
				
			||||||
 | 
					    if (controller.videoAttachment == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final result = await showDialog<SnAttachmentBoost?>(
 | 
				
			||||||
 | 
					      context: context,
 | 
				
			||||||
 | 
					      builder: (context) => PendingAttachmentBoostDialog(media: PostWriteMedia(controller.videoAttachment)),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (result == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final newAttach = controller.videoAttachment!.copyWith(
 | 
				
			||||||
 | 
					      boosts: [...controller.videoAttachment!.boosts, result],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    controller.setVideoAttachment(newAttach);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _setThumbnail(BuildContext context) async {
 | 
				
			||||||
 | 
					    if (controller.videoAttachment == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final thumbnail = await showDialog<SnAttachment?>(
 | 
				
			||||||
 | 
					      context: context,
 | 
				
			||||||
 | 
					      builder: (context) => AttachmentInputDialog(
 | 
				
			||||||
 | 
					        title: 'attachmentSetThumbnail'.tr(),
 | 
				
			||||||
 | 
					        pool: 'interactive',
 | 
				
			||||||
 | 
					        analyzeNow: true,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (thumbnail == null) return;
 | 
				
			||||||
 | 
					    if (!context.mounted) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final attach = context.read<SnAttachmentProvider>();
 | 
				
			||||||
 | 
					      final newAttach = await attach.updateOne(
 | 
				
			||||||
 | 
					        controller.videoAttachment!,
 | 
				
			||||||
 | 
					        thumbnailId: thumbnail.id,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      controller.setVideoAttachment(newAttach);
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      if (!context.mounted) return;
 | 
				
			||||||
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _deleteAttachment(BuildContext context) async {
 | 
				
			||||||
 | 
					    if (controller.videoAttachment == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					      await sn.client.delete('/cgi/uc/attachments/${controller.videoAttachment!.id}');
 | 
				
			||||||
 | 
					      controller.setVideoAttachment(null);
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      if (!context.mounted) return;
 | 
				
			||||||
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Column(
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        Material(
 | 
				
			||||||
 | 
					          color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
				
			||||||
 | 
					          child: InkWell(
 | 
				
			||||||
 | 
					            child: Row(
 | 
				
			||||||
 | 
					              children: [
 | 
				
			||||||
 | 
					                AccountImage(content: controller.publisher?.avatar, radius: 20),
 | 
				
			||||||
 | 
					                const Gap(8),
 | 
				
			||||||
 | 
					                Expanded(
 | 
				
			||||||
 | 
					                  child: Column(
 | 
				
			||||||
 | 
					                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      Text(controller.publisher?.nick ?? 'loading'.tr()).bold(),
 | 
				
			||||||
 | 
					                      Text('@${controller.publisher?.name}'),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ).padding(horizontal: 12, vertical: 8),
 | 
				
			||||||
 | 
					            onTap: () {
 | 
				
			||||||
 | 
					              onTapPublisher?.call();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        const Gap(16),
 | 
				
			||||||
 | 
					        TextField(
 | 
				
			||||||
 | 
					          controller: controller.titleController,
 | 
				
			||||||
 | 
					          decoration: InputDecoration.collapsed(
 | 
				
			||||||
 | 
					            hintText: 'fieldPostTitle'.tr(),
 | 
				
			||||||
 | 
					            border: InputBorder.none,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          style: Theme.of(context).textTheme.titleLarge,
 | 
				
			||||||
 | 
					          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					        ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					        const Gap(8),
 | 
				
			||||||
 | 
					        TextField(
 | 
				
			||||||
 | 
					          controller: controller.descriptionController,
 | 
				
			||||||
 | 
					          decoration: InputDecoration.collapsed(
 | 
				
			||||||
 | 
					            hintText: 'fieldPostDescription'.tr(),
 | 
				
			||||||
 | 
					            border: InputBorder.none,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          maxLines: null,
 | 
				
			||||||
 | 
					          keyboardType: TextInputType.multiline,
 | 
				
			||||||
 | 
					          style: Theme.of(context).textTheme.bodyLarge,
 | 
				
			||||||
 | 
					          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
 | 
					        ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					        const Gap(12),
 | 
				
			||||||
 | 
					        Container(
 | 
				
			||||||
 | 
					          margin: const EdgeInsets.only(left: 16, right: 16),
 | 
				
			||||||
 | 
					          decoration: BoxDecoration(
 | 
				
			||||||
 | 
					            borderRadius: BorderRadius.circular(16),
 | 
				
			||||||
 | 
					            border: Border.all(color: Theme.of(context).dividerColor),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          child: ContextMenuRegion(
 | 
				
			||||||
 | 
					            contextMenu: ContextMenu(
 | 
				
			||||||
 | 
					              entries: [
 | 
				
			||||||
 | 
					                MenuItem(
 | 
				
			||||||
 | 
					                  label: 'attachmentSetAlt'.tr(),
 | 
				
			||||||
 | 
					                  icon: Symbols.description,
 | 
				
			||||||
 | 
					                  onSelected: () {
 | 
				
			||||||
 | 
					                    _setAlt(context);
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                MenuItem(
 | 
				
			||||||
 | 
					                  label: 'attachmentBoost'.tr(),
 | 
				
			||||||
 | 
					                  icon: Symbols.bolt,
 | 
				
			||||||
 | 
					                  onSelected: () {
 | 
				
			||||||
 | 
					                    _createBoost(context);
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                MenuItem(
 | 
				
			||||||
 | 
					                  label: 'attachmentSetThumbnail'.tr(),
 | 
				
			||||||
 | 
					                  icon: Symbols.image,
 | 
				
			||||||
 | 
					                  onSelected: () {
 | 
				
			||||||
 | 
					                    _setThumbnail(context);
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                MenuItem(
 | 
				
			||||||
 | 
					                  label: 'attachmentCopyRandomId'.tr(),
 | 
				
			||||||
 | 
					                  icon: Symbols.content_copy,
 | 
				
			||||||
 | 
					                  onSelected: () {
 | 
				
			||||||
 | 
					                    Clipboard.setData(ClipboardData(text: controller.videoAttachment!.rid));
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                MenuItem(
 | 
				
			||||||
 | 
					                  label: 'delete'.tr(),
 | 
				
			||||||
 | 
					                  icon: Symbols.delete,
 | 
				
			||||||
 | 
					                  onSelected: () => _deleteAttachment(context),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                MenuItem(
 | 
				
			||||||
 | 
					                  label: 'unlink'.tr(),
 | 
				
			||||||
 | 
					                  icon: Symbols.link_off,
 | 
				
			||||||
 | 
					                  onSelected: () {
 | 
				
			||||||
 | 
					                    controller.setVideoAttachment(null);
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            child: InkWell(
 | 
				
			||||||
 | 
					              borderRadius: BorderRadius.circular(16),
 | 
				
			||||||
 | 
					              onTap: controller.videoAttachment != null ? () => _selectVideo(context) : null,
 | 
				
			||||||
 | 
					              child: AspectRatio(
 | 
				
			||||||
 | 
					                aspectRatio: 16 / 9,
 | 
				
			||||||
 | 
					                child: controller.videoAttachment == null
 | 
				
			||||||
 | 
					                    ? Center(
 | 
				
			||||||
 | 
					                        child: Row(
 | 
				
			||||||
 | 
					                          mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                          crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					                          mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            const Icon(Icons.add),
 | 
				
			||||||
 | 
					                            const Gap(4),
 | 
				
			||||||
 | 
					                            Text('postVideoUpload'.tr()),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      )
 | 
				
			||||||
 | 
					                    : ClipRRect(
 | 
				
			||||||
 | 
					                        borderRadius: BorderRadius.circular(16),
 | 
				
			||||||
 | 
					                        child: AttachmentItem(
 | 
				
			||||||
 | 
					                          data: controller.videoAttachment!,
 | 
				
			||||||
 | 
					                          heroTag: const Uuid().v4(),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:gap/gap.dart';
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
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';
 | 
				
			||||||
@@ -134,7 +133,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
 | 
				
			|||||||
      body: Stack(
 | 
					      body: Stack(
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          InfiniteList(
 | 
					          InfiniteList(
 | 
				
			||||||
            padding: const EdgeInsets.only(top: 100),
 | 
					            padding: const EdgeInsets.only(top: 100 + 8),
 | 
				
			||||||
            itemCount: _posts.length,
 | 
					            itemCount: _posts.length,
 | 
				
			||||||
            isLoading: _isBusy,
 | 
					            isLoading: _isBusy,
 | 
				
			||||||
            hasReachedMax: _postCount != null && _posts.length >= _postCount!,
 | 
					            hasReachedMax: _postCount != null && _posts.length >= _postCount!,
 | 
				
			||||||
@@ -142,27 +141,18 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
 | 
				
			|||||||
              _fetchPosts();
 | 
					              _fetchPosts();
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            itemBuilder: (context, idx) {
 | 
					            itemBuilder: (context, idx) {
 | 
				
			||||||
              return GestureDetector(
 | 
					              return OpenablePostItem(
 | 
				
			||||||
                child: PostItem(
 | 
					                data: _posts[idx],
 | 
				
			||||||
                  data: _posts[idx],
 | 
					                maxWidth: 640,
 | 
				
			||||||
                  maxWidth: 640,
 | 
					                onChanged: (data) {
 | 
				
			||||||
                  onChanged: (data) {
 | 
					                  setState(() => _posts[idx] = data);
 | 
				
			||||||
                    setState(() => _posts[idx] = data);
 | 
					                },
 | 
				
			||||||
                  },
 | 
					                onDeleted: () {
 | 
				
			||||||
                  onDeleted: () {
 | 
					                  _refreshPosts();
 | 
				
			||||||
                    _refreshPosts();
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                onTap: () {
 | 
					 | 
				
			||||||
                  GoRouter.of(context).pushNamed(
 | 
					 | 
				
			||||||
                    'postDetail',
 | 
					 | 
				
			||||||
                    pathParameters: {'slug': _posts[idx].id.toString()},
 | 
					 | 
				
			||||||
                    extra: _posts[idx],
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              );
 | 
					              );
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            separatorBuilder: (context, index) => const Divider(height: 1),
 | 
					            separatorBuilder: (_, __) => const Gap(8),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          Positioned(
 | 
					          Positioned(
 | 
				
			||||||
            top: 16,
 | 
					            top: 16,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -287,8 +287,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
				
			|||||||
                  Theme(
 | 
					                  Theme(
 | 
				
			||||||
                    data: Theme.of(context).copyWith(
 | 
					                    data: Theme.of(context).copyWith(
 | 
				
			||||||
                      appBarTheme: Theme.of(context).appBarTheme.copyWith(
 | 
					                      appBarTheme: Theme.of(context).appBarTheme.copyWith(
 | 
				
			||||||
                        foregroundColor: Colors.white,
 | 
					                            foregroundColor: Colors.white,
 | 
				
			||||||
                      ),
 | 
					                          ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    child: SliverAppBar(
 | 
					                    child: SliverAppBar(
 | 
				
			||||||
                      expandedHeight: _appBarHeight,
 | 
					                      expandedHeight: _appBarHeight,
 | 
				
			||||||
@@ -597,25 +597,16 @@ class _PublisherPostList extends StatelessWidget {
 | 
				
			|||||||
      hasReachedMax: postCount != null && posts.length >= postCount!,
 | 
					      hasReachedMax: postCount != null && posts.length >= postCount!,
 | 
				
			||||||
      onFetchData: fetchPosts,
 | 
					      onFetchData: fetchPosts,
 | 
				
			||||||
      itemBuilder: (context, idx) {
 | 
					      itemBuilder: (context, idx) {
 | 
				
			||||||
        return GestureDetector(
 | 
					        return OpenablePostItem(
 | 
				
			||||||
          child: PostItem(
 | 
					          data: posts[idx],
 | 
				
			||||||
            data: posts[idx],
 | 
					          maxWidth: 640,
 | 
				
			||||||
            maxWidth: 640,
 | 
					          onChanged: (data) {
 | 
				
			||||||
            onChanged: (data) {
 | 
					            onChanged(idx, data);
 | 
				
			||||||
              onChanged(idx, data);
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            onDeleted: onDeleted,
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          onTap: () {
 | 
					 | 
				
			||||||
            GoRouter.of(context).pushNamed(
 | 
					 | 
				
			||||||
              'postDetail',
 | 
					 | 
				
			||||||
              pathParameters: {'slug': posts[idx].id.toString()},
 | 
					 | 
				
			||||||
              extra: posts[idx],
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 | 
					          onDeleted: onDeleted,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      separatorBuilder: (context, index) => const Divider(height: 1),
 | 
					      separatorBuilder: (_, __) => const Gap(8),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
 | 
					                      });
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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: {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,7 @@ class SnAccount with _$SnAccount {
 | 
				
			|||||||
    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
 | 
				
			||||||
@@ -373,6 +386,7 @@ class _$SnAccountImpl extends _SnAccount {
 | 
				
			|||||||
      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,
 | 
				
			||||||
@@ -429,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;
 | 
				
			||||||
@@ -453,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
 | 
				
			||||||
@@ -479,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) ||
 | 
				
			||||||
@@ -509,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,
 | 
				
			||||||
@@ -548,6 +567,7 @@ abstract class _SnAccount extends SnAccount {
 | 
				
			|||||||
      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,
 | 
				
			||||||
@@ -586,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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,6 +26,7 @@ _$SnAccountImpl _$$SnAccountImplFromJson(Map<String, dynamic> json) =>
 | 
				
			|||||||
      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(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -89,6 +89,7 @@ class SnPostPreload with _$SnPostPreload {
 | 
				
			|||||||
  const factory SnPostPreload({
 | 
					  const factory SnPostPreload({
 | 
				
			||||||
    required SnAttachment? thumbnail,
 | 
					    required SnAttachment? thumbnail,
 | 
				
			||||||
    required List<SnAttachment?>? attachments,
 | 
					    required List<SnAttachment?>? attachments,
 | 
				
			||||||
 | 
					    required SnAttachment? video,
 | 
				
			||||||
  }) = _SnPostPreload;
 | 
					  }) = _SnPostPreload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory SnPostPreload.fromJson(Map<String, Object?> json) =>
 | 
					  factory SnPostPreload.fromJson(Map<String, Object?> json) =>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1567,6 +1567,7 @@ SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) {
 | 
				
			|||||||
mixin _$SnPostPreload {
 | 
					mixin _$SnPostPreload {
 | 
				
			||||||
  SnAttachment? get thumbnail => throw _privateConstructorUsedError;
 | 
					  SnAttachment? get thumbnail => throw _privateConstructorUsedError;
 | 
				
			||||||
  List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
 | 
					  List<SnAttachment?>? get attachments => throw _privateConstructorUsedError;
 | 
				
			||||||
 | 
					  SnAttachment? get video => throw _privateConstructorUsedError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Serializes this SnPostPreload to a JSON map.
 | 
					  /// Serializes this SnPostPreload to a JSON map.
 | 
				
			||||||
  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
 | 
					  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
 | 
				
			||||||
@@ -1584,9 +1585,13 @@ abstract class $SnPostPreloadCopyWith<$Res> {
 | 
				
			|||||||
          SnPostPreload value, $Res Function(SnPostPreload) then) =
 | 
					          SnPostPreload value, $Res Function(SnPostPreload) then) =
 | 
				
			||||||
      _$SnPostPreloadCopyWithImpl<$Res, SnPostPreload>;
 | 
					      _$SnPostPreloadCopyWithImpl<$Res, SnPostPreload>;
 | 
				
			||||||
  @useResult
 | 
					  @useResult
 | 
				
			||||||
  $Res call({SnAttachment? thumbnail, List<SnAttachment?>? attachments});
 | 
					  $Res call(
 | 
				
			||||||
 | 
					      {SnAttachment? thumbnail,
 | 
				
			||||||
 | 
					      List<SnAttachment?>? attachments,
 | 
				
			||||||
 | 
					      SnAttachment? video});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $SnAttachmentCopyWith<$Res>? get thumbnail;
 | 
					  $SnAttachmentCopyWith<$Res>? get thumbnail;
 | 
				
			||||||
 | 
					  $SnAttachmentCopyWith<$Res>? get video;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
@@ -1606,6 +1611,7 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
 | 
				
			|||||||
  $Res call({
 | 
					  $Res call({
 | 
				
			||||||
    Object? thumbnail = freezed,
 | 
					    Object? thumbnail = freezed,
 | 
				
			||||||
    Object? attachments = freezed,
 | 
					    Object? attachments = freezed,
 | 
				
			||||||
 | 
					    Object? video = freezed,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
    return _then(_value.copyWith(
 | 
					    return _then(_value.copyWith(
 | 
				
			||||||
      thumbnail: freezed == thumbnail
 | 
					      thumbnail: freezed == thumbnail
 | 
				
			||||||
@@ -1616,6 +1622,10 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
 | 
				
			|||||||
          ? _value.attachments
 | 
					          ? _value.attachments
 | 
				
			||||||
          : attachments // ignore: cast_nullable_to_non_nullable
 | 
					          : attachments // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
              as List<SnAttachment?>?,
 | 
					              as List<SnAttachment?>?,
 | 
				
			||||||
 | 
					      video: freezed == video
 | 
				
			||||||
 | 
					          ? _value.video
 | 
				
			||||||
 | 
					          : video // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					              as SnAttachment?,
 | 
				
			||||||
    ) as $Val);
 | 
					    ) as $Val);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1632,6 +1642,20 @@ class _$SnPostPreloadCopyWithImpl<$Res, $Val extends SnPostPreload>
 | 
				
			|||||||
      return _then(_value.copyWith(thumbnail: value) as $Val);
 | 
					      return _then(_value.copyWith(thumbnail: value) as $Val);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Create a copy of SnPostPreload
 | 
				
			||||||
 | 
					  /// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  @pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					  $SnAttachmentCopyWith<$Res>? get video {
 | 
				
			||||||
 | 
					    if (_value.video == null) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return $SnAttachmentCopyWith<$Res>(_value.video!, (value) {
 | 
				
			||||||
 | 
					      return _then(_value.copyWith(video: value) as $Val);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
@@ -1642,10 +1666,15 @@ abstract class _$$SnPostPreloadImplCopyWith<$Res>
 | 
				
			|||||||
      __$$SnPostPreloadImplCopyWithImpl<$Res>;
 | 
					      __$$SnPostPreloadImplCopyWithImpl<$Res>;
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  @useResult
 | 
					  @useResult
 | 
				
			||||||
  $Res call({SnAttachment? thumbnail, List<SnAttachment?>? attachments});
 | 
					  $Res call(
 | 
				
			||||||
 | 
					      {SnAttachment? thumbnail,
 | 
				
			||||||
 | 
					      List<SnAttachment?>? attachments,
 | 
				
			||||||
 | 
					      SnAttachment? video});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  $SnAttachmentCopyWith<$Res>? get thumbnail;
 | 
					  $SnAttachmentCopyWith<$Res>? get thumbnail;
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  $SnAttachmentCopyWith<$Res>? get video;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
@@ -1663,6 +1692,7 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
 | 
				
			|||||||
  $Res call({
 | 
					  $Res call({
 | 
				
			||||||
    Object? thumbnail = freezed,
 | 
					    Object? thumbnail = freezed,
 | 
				
			||||||
    Object? attachments = freezed,
 | 
					    Object? attachments = freezed,
 | 
				
			||||||
 | 
					    Object? video = freezed,
 | 
				
			||||||
  }) {
 | 
					  }) {
 | 
				
			||||||
    return _then(_$SnPostPreloadImpl(
 | 
					    return _then(_$SnPostPreloadImpl(
 | 
				
			||||||
      thumbnail: freezed == thumbnail
 | 
					      thumbnail: freezed == thumbnail
 | 
				
			||||||
@@ -1673,6 +1703,10 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
 | 
				
			|||||||
          ? _value._attachments
 | 
					          ? _value._attachments
 | 
				
			||||||
          : attachments // ignore: cast_nullable_to_non_nullable
 | 
					          : attachments // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
              as List<SnAttachment?>?,
 | 
					              as List<SnAttachment?>?,
 | 
				
			||||||
 | 
					      video: freezed == video
 | 
				
			||||||
 | 
					          ? _value.video
 | 
				
			||||||
 | 
					          : video // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					              as SnAttachment?,
 | 
				
			||||||
    ));
 | 
					    ));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1682,7 +1716,8 @@ class __$$SnPostPreloadImplCopyWithImpl<$Res>
 | 
				
			|||||||
class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
					class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
				
			||||||
  const _$SnPostPreloadImpl(
 | 
					  const _$SnPostPreloadImpl(
 | 
				
			||||||
      {required this.thumbnail,
 | 
					      {required this.thumbnail,
 | 
				
			||||||
      required final List<SnAttachment?>? attachments})
 | 
					      required final List<SnAttachment?>? attachments,
 | 
				
			||||||
 | 
					      required this.video})
 | 
				
			||||||
      : _attachments = attachments;
 | 
					      : _attachments = attachments;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) =>
 | 
					  factory _$SnPostPreloadImpl.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
@@ -1700,9 +1735,12 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
				
			|||||||
    return EqualUnmodifiableListView(value);
 | 
					    return EqualUnmodifiableListView(value);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  final SnAttachment? video;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
    return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments)';
 | 
					    return 'SnPostPreload(thumbnail: $thumbnail, attachments: $attachments, video: $video)';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -1713,13 +1751,14 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
				
			|||||||
            (identical(other.thumbnail, thumbnail) ||
 | 
					            (identical(other.thumbnail, thumbnail) ||
 | 
				
			||||||
                other.thumbnail == thumbnail) &&
 | 
					                other.thumbnail == thumbnail) &&
 | 
				
			||||||
            const DeepCollectionEquality()
 | 
					            const DeepCollectionEquality()
 | 
				
			||||||
                .equals(other._attachments, _attachments));
 | 
					                .equals(other._attachments, _attachments) &&
 | 
				
			||||||
 | 
					            (identical(other.video, video) || other.video == video));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
					  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode => Object.hash(runtimeType, thumbnail,
 | 
					  int get hashCode => Object.hash(runtimeType, thumbnail,
 | 
				
			||||||
      const DeepCollectionEquality().hash(_attachments));
 | 
					      const DeepCollectionEquality().hash(_attachments), video);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Create a copy of SnPostPreload
 | 
					  /// Create a copy of SnPostPreload
 | 
				
			||||||
  /// with the given fields replaced by the non-null parameter values.
 | 
					  /// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
@@ -1740,7 +1779,8 @@ class _$SnPostPreloadImpl implements _SnPostPreload {
 | 
				
			|||||||
abstract class _SnPostPreload implements SnPostPreload {
 | 
					abstract class _SnPostPreload implements SnPostPreload {
 | 
				
			||||||
  const factory _SnPostPreload(
 | 
					  const factory _SnPostPreload(
 | 
				
			||||||
      {required final SnAttachment? thumbnail,
 | 
					      {required final SnAttachment? thumbnail,
 | 
				
			||||||
      required final List<SnAttachment?>? attachments}) = _$SnPostPreloadImpl;
 | 
					      required final List<SnAttachment?>? attachments,
 | 
				
			||||||
 | 
					      required final SnAttachment? video}) = _$SnPostPreloadImpl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  factory _SnPostPreload.fromJson(Map<String, dynamic> json) =
 | 
					  factory _SnPostPreload.fromJson(Map<String, dynamic> json) =
 | 
				
			||||||
      _$SnPostPreloadImpl.fromJson;
 | 
					      _$SnPostPreloadImpl.fromJson;
 | 
				
			||||||
@@ -1749,6 +1789,8 @@ abstract class _SnPostPreload implements SnPostPreload {
 | 
				
			|||||||
  SnAttachment? get thumbnail;
 | 
					  SnAttachment? get thumbnail;
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  List<SnAttachment?>? get attachments;
 | 
					  List<SnAttachment?>? get attachments;
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  SnAttachment? get video;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Create a copy of SnPostPreload
 | 
					  /// Create a copy of SnPostPreload
 | 
				
			||||||
  /// with the given fields replaced by the non-null parameter values.
 | 
					  /// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -165,12 +165,16 @@ _$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
 | 
				
			|||||||
              ? null
 | 
					              ? null
 | 
				
			||||||
              : SnAttachment.fromJson(e as Map<String, dynamic>))
 | 
					              : SnAttachment.fromJson(e as Map<String, dynamic>))
 | 
				
			||||||
          .toList(),
 | 
					          .toList(),
 | 
				
			||||||
 | 
					      video: json['video'] == null
 | 
				
			||||||
 | 
					          ? null
 | 
				
			||||||
 | 
					          : SnAttachment.fromJson(json['video'] as Map<String, dynamic>),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
 | 
					Map<String, dynamic> _$$SnPostPreloadImplToJson(_$SnPostPreloadImpl instance) =>
 | 
				
			||||||
    <String, dynamic>{
 | 
					    <String, dynamic>{
 | 
				
			||||||
      'thumbnail': instance.thumbnail?.toJson(),
 | 
					      'thumbnail': instance.thumbnail?.toJson(),
 | 
				
			||||||
      'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
 | 
					      'attachments': instance.attachments?.map((e) => e?.toJson()).toList(),
 | 
				
			||||||
 | 
					      'video': instance.video?.toJson(),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(
 | 
					_$SnBodyImpl _$$SnBodyImplFromJson(Map<String, dynamic> json) => _$SnBodyImpl(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,12 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:surface/providers/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/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';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -47,10 +50,19 @@ class _AccountSelectState extends State<AccountSelect> {
 | 
				
			|||||||
  Future<void> _getFriends() async {
 | 
					  Future<void> _getFriends() async {
 | 
				
			||||||
    final sn = context.read<SnNetworkProvider>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
    final resp = await sn.client.get('/cgi/id/users/me/relations?status=1');
 | 
					    final resp = await sn.client.get('/cgi/id/users/me/relations?status=1');
 | 
				
			||||||
 | 
					    if (!mounted) return;
 | 
				
			||||||
 | 
					    final ua = context.read<UserProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setState(() {
 | 
					    setState(() {
 | 
				
			||||||
      _relativeUsers.addAll(
 | 
					      _relativeUsers.addAll(
 | 
				
			||||||
        resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
 | 
					        resp.data?.map((e) {
 | 
				
			||||||
 | 
					          final rel = SnRelationship.fromJson(e);
 | 
				
			||||||
 | 
					          if (rel.relatedId == ua.user?.id) {
 | 
				
			||||||
 | 
					            return rel.account!;
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            return rel.related!;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }).cast<SnAccount>(),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -96,10 +108,14 @@ class _AccountSelectState extends State<AccountSelect> {
 | 
				
			|||||||
      child: Column(
 | 
					      child: Column(
 | 
				
			||||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          Text(
 | 
					          Row(
 | 
				
			||||||
            widget.title,
 | 
					            crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
            style: Theme.of(context).textTheme.headlineSmall,
 | 
					            children: [
 | 
				
			||||||
          ).padding(left: 24, right: 24, top: 16, bottom: 16),
 | 
					              const Icon(Symbols.group, size: 24),
 | 
				
			||||||
 | 
					              const Gap(16),
 | 
				
			||||||
 | 
					              Text(widget.title, style: Theme.of(context).textTheme.titleLarge),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
				
			||||||
          Container(
 | 
					          Container(
 | 
				
			||||||
            color: Theme.of(context).colorScheme.secondaryContainer,
 | 
					            color: Theme.of(context).colorScheme.secondaryContainer,
 | 
				
			||||||
            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
 | 
					            padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
 | 
				
			||||||
@@ -117,13 +133,9 @@ class _AccountSelectState extends State<AccountSelect> {
 | 
				
			|||||||
          ),
 | 
					          ),
 | 
				
			||||||
          Expanded(
 | 
					          Expanded(
 | 
				
			||||||
            child: ListView.builder(
 | 
					            child: ListView.builder(
 | 
				
			||||||
              itemCount: _pendingUsers.isEmpty
 | 
					              itemCount: _pendingUsers.isEmpty ? _relativeUsers.length : _pendingUsers.length,
 | 
				
			||||||
                  ? _relativeUsers.length
 | 
					 | 
				
			||||||
                  : _pendingUsers.length,
 | 
					 | 
				
			||||||
              itemBuilder: (context, index) {
 | 
					              itemBuilder: (context, index) {
 | 
				
			||||||
                var user = _pendingUsers.isEmpty
 | 
					                var user = _pendingUsers.isEmpty ? _relativeUsers[index] : _pendingUsers[index];
 | 
				
			||||||
                    ? _relativeUsers[index]
 | 
					 | 
				
			||||||
                    : _pendingUsers[index];
 | 
					 | 
				
			||||||
                return ListTile(
 | 
					                return ListTile(
 | 
				
			||||||
                  title: Text(user.nick),
 | 
					                  title: Text(user.nick),
 | 
				
			||||||
                  subtitle: Text(user.name),
 | 
					                  subtitle: Text(user.name),
 | 
				
			||||||
@@ -142,8 +154,7 @@ class _AccountSelectState extends State<AccountSelect> {
 | 
				
			|||||||
                          }
 | 
					                          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                          setState(() {
 | 
					                          setState(() {
 | 
				
			||||||
                            final idx = _selectedUsers
 | 
					                            final idx = _selectedUsers.indexWhere((x) => x.id == user.id);
 | 
				
			||||||
                                .indexWhere((x) => x.id == user.id);
 | 
					 | 
				
			||||||
                            if (idx != -1) {
 | 
					                            if (idx != -1) {
 | 
				
			||||||
                              _selectedUsers.removeAt(idx);
 | 
					                              _selectedUsers.removeAt(idx);
 | 
				
			||||||
                            } else {
 | 
					                            } else {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,12 +6,22 @@ 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/sn_attachment.dart';
 | 
					import 'package:surface/providers/sn_attachment.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/attachment.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AttachmentInputDialog extends StatefulWidget {
 | 
					class AttachmentInputDialog extends StatefulWidget {
 | 
				
			||||||
  final String? title;
 | 
					  final String? title;
 | 
				
			||||||
final bool? analyzeNow;
 | 
					  final bool? analyzeNow;
 | 
				
			||||||
  const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false});
 | 
					  final SnMediaType? mediaType;
 | 
				
			||||||
 | 
					  final String pool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const AttachmentInputDialog({
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					    required this.title,
 | 
				
			||||||
 | 
					    required this.pool,
 | 
				
			||||||
 | 
					    this.analyzeNow = false,
 | 
				
			||||||
 | 
					    this.mediaType = SnMediaType.image,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
 | 
					  State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
 | 
				
			||||||
@@ -20,13 +30,18 @@ final bool? analyzeNow;
 | 
				
			|||||||
class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
					class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
				
			||||||
  final _randomIdController = TextEditingController();
 | 
					  final _randomIdController = TextEditingController();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  XFile? _thumbnailFile;
 | 
					  XFile? _file;
 | 
				
			||||||
 | 
					  double? _progress;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _pickImage() async {
 | 
					  void _pickMedia() async {
 | 
				
			||||||
    final picker = ImagePicker();
 | 
					    final picker = ImagePicker();
 | 
				
			||||||
    final result = await picker.pickImage(source: ImageSource.gallery);
 | 
					    final result = switch (widget.mediaType) {
 | 
				
			||||||
 | 
					      SnMediaType.image => await picker.pickImage(source: ImageSource.gallery),
 | 
				
			||||||
 | 
					      SnMediaType.video => await picker.pickVideo(source: ImageSource.gallery),
 | 
				
			||||||
 | 
					      _ => await picker.pickMedia(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
    if (result == null) return;
 | 
					    if (result == null) return;
 | 
				
			||||||
    setState(() => _thumbnailFile = result);
 | 
					    setState(() => _file = result);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool _isBusy = false;
 | 
					  bool _isBusy = false;
 | 
				
			||||||
@@ -46,15 +61,20 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
				
			|||||||
        if (!mounted) return;
 | 
					        if (!mounted) return;
 | 
				
			||||||
        context.showErrorDialog(err);
 | 
					        context.showErrorDialog(err);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else if (_thumbnailFile != null) {
 | 
					    } else if (_file != null) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        final attachment = await attach.directUploadOne(
 | 
					        final place = await attach.chunkedUploadInitialize(await _file!.length(), _file!.name, widget.pool, null);
 | 
				
			||||||
          (await _thumbnailFile!.readAsBytes()).buffer.asUint8List(),
 | 
					
 | 
				
			||||||
          _thumbnailFile!.path,
 | 
					        final attachment = await attach.chunkedUploadParts(
 | 
				
			||||||
          'interactive',
 | 
					          _file!,
 | 
				
			||||||
          null,
 | 
					          place.$1,
 | 
				
			||||||
 | 
					          place.$2,
 | 
				
			||||||
          analyzeNow: widget.analyzeNow ?? false,
 | 
					          analyzeNow: widget.analyzeNow ?? false,
 | 
				
			||||||
 | 
					          onProgress: (value) {
 | 
				
			||||||
 | 
					            setState(() => _progress = value);
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!mounted) return;
 | 
					        if (!mounted) return;
 | 
				
			||||||
        Navigator.pop(context, attachment);
 | 
					        Navigator.pop(context, attachment);
 | 
				
			||||||
      } catch (err) {
 | 
					      } catch (err) {
 | 
				
			||||||
@@ -67,7 +87,7 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return AlertDialog(
 | 
					    return AlertDialog(
 | 
				
			||||||
      title: Text(widget.title ?? 'attachmentInputDialog').tr(),
 | 
					      title: Text(widget.title ?? 'attachmentInputDialog'.tr()),
 | 
				
			||||||
      content: Column(
 | 
					      content: Column(
 | 
				
			||||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
        mainAxisSize: MainAxisSize.min,
 | 
					        mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
@@ -86,24 +106,35 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
 | 
				
			|||||||
          const Gap(24),
 | 
					          const Gap(24),
 | 
				
			||||||
          Text('attachmentInputNew').tr().fontSize(14),
 | 
					          Text('attachmentInputNew').tr().fontSize(14),
 | 
				
			||||||
          Card(
 | 
					          Card(
 | 
				
			||||||
            child: ListTile(
 | 
					            child: Column(
 | 
				
			||||||
              contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
 | 
					              children: [
 | 
				
			||||||
              leading: const Icon(Symbols.add_photo_alternate),
 | 
					                ListTile(
 | 
				
			||||||
              trailing: const Icon(Symbols.chevron_right),
 | 
					                  contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
 | 
				
			||||||
              title: Text('addAttachmentFromAlbum').tr(),
 | 
					                  leading: const Icon(Symbols.add_photo_alternate),
 | 
				
			||||||
              subtitle: _thumbnailFile == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
 | 
					                  trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
              onTap: () {
 | 
					                  title: Text('addAttachmentFromAlbum').tr(),
 | 
				
			||||||
                _pickImage();
 | 
					                  subtitle: _file == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
 | 
				
			||||||
              },
 | 
					                  onTap: () {
 | 
				
			||||||
 | 
					                    _pickMedia();
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 | 
					          if (_isBusy)
 | 
				
			||||||
 | 
					            LinearProgressIndicator(
 | 
				
			||||||
 | 
					              value: _progress,
 | 
				
			||||||
 | 
					              borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					            ).padding(top: 16),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      actions: [
 | 
					      actions: [
 | 
				
			||||||
        TextButton(
 | 
					        TextButton(
 | 
				
			||||||
          onPressed: _isBusy ? null : () {
 | 
					          onPressed: _isBusy
 | 
				
			||||||
            Navigator.pop(context);
 | 
					              ? null
 | 
				
			||||||
          },
 | 
					              : () {
 | 
				
			||||||
 | 
					                  Navigator.pop(context);
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
          child: Text('dialogDismiss').tr(),
 | 
					          child: Text('dialogDismiss').tr(),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        TextButton(
 | 
					        TextButton(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,35 @@ 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();
 | 
				
			||||||
 | 
					    _dismissEmojiPicker();
 | 
				
			||||||
 | 
					    if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS)) hotKeyManager.unregister(_pasteHotKey);
 | 
				
			||||||
    super.dispose();
 | 
					    super.dispose();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -270,6 +337,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
 | 
				
			|||||||
                        : 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]),
 | 
					                        : 'fieldChatMessage'.tr(args: [widget.controller.channel?.name ?? 'loading'.tr()]),
 | 
				
			||||||
                    border: InputBorder.none,
 | 
					                    border: InputBorder.none,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
 | 
					                  textInputAction: TextInputAction.send,
 | 
				
			||||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
					                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                  onSubmitted: (_) {
 | 
					                  onSubmitted: (_) {
 | 
				
			||||||
                    if (_isBusy) return;
 | 
					                    if (_isBusy) return;
 | 
				
			||||||
@@ -279,6 +347,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 +385,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(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,17 +8,23 @@ import 'package:styled_widget/styled_widget.dart';
 | 
				
			|||||||
import 'package:surface/providers/post.dart';
 | 
					import 'package:surface/providers/post.dart';
 | 
				
			||||||
import 'package:surface/providers/userinfo.dart';
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/post_item.dart';
 | 
					import 'package:surface/widgets/post/post_item.dart';
 | 
				
			||||||
import 'package:surface/widgets/post/post_mini_editor.dart';
 | 
					import 'package:surface/widgets/post/post_mini_editor.dart';
 | 
				
			||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
					import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import '../../providers/sn_network.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostCommentSliverList extends StatefulWidget {
 | 
					class PostCommentSliverList extends StatefulWidget {
 | 
				
			||||||
  final int parentPostId;
 | 
					  final SnPost parentPost;
 | 
				
			||||||
  final double? maxWidth;
 | 
					  final double? maxWidth;
 | 
				
			||||||
 | 
					  final Function(SnPost)? onSelectAnswer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const PostCommentSliverList({
 | 
					  const PostCommentSliverList({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.parentPostId,
 | 
					    required this.parentPost,
 | 
				
			||||||
    this.maxWidth,
 | 
					    this.maxWidth,
 | 
				
			||||||
 | 
					    this.onSelectAnswer,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -37,7 +43,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
 | 
				
			|||||||
    setState(() => _isBusy = true);
 | 
					    setState(() => _isBusy = true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final pt = context.read<SnPostContentProvider>();
 | 
					    final pt = context.read<SnPostContentProvider>();
 | 
				
			||||||
    final result = await pt.listPostReplies(widget.parentPostId);
 | 
					    final result = await pt.listPostReplies(widget.parentPost.id);
 | 
				
			||||||
    final List<SnPost> out = result.$1;
 | 
					    final List<SnPost> out = result.$1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!mounted) return;
 | 
					    if (!mounted) return;
 | 
				
			||||||
@@ -48,6 +54,21 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
 | 
				
			|||||||
    if (mounted) setState(() => _isBusy = false);
 | 
					    if (mounted) setState(() => _isBusy = false);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _selectAnswer(SnPost answer) async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					      await sn.client.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
 | 
				
			||||||
 | 
					        'publisher': answer.publisherId,
 | 
				
			||||||
 | 
					        'answer_id': answer.id,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      await refresh();
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> refresh() async {
 | 
					  Future<void> refresh() async {
 | 
				
			||||||
    _posts.clear();
 | 
					    _posts.clear();
 | 
				
			||||||
    _fetchPosts();
 | 
					    _fetchPosts();
 | 
				
			||||||
@@ -71,6 +92,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
 | 
				
			|||||||
          child: PostItem(
 | 
					          child: PostItem(
 | 
				
			||||||
            data: _posts[idx],
 | 
					            data: _posts[idx],
 | 
				
			||||||
            maxWidth: widget.maxWidth,
 | 
					            maxWidth: widget.maxWidth,
 | 
				
			||||||
 | 
					            onSelectAnswer: widget.parentPost.type == 'question' ? () => _selectAnswer(_posts[idx]) : null,
 | 
				
			||||||
            onChanged: (data) {
 | 
					            onChanged: (data) {
 | 
				
			||||||
              setState(() => _posts[idx] = data);
 | 
					              setState(() => _posts[idx] = data);
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
@@ -94,11 +116,12 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostCommentListPopup extends StatefulWidget {
 | 
					class PostCommentListPopup extends StatefulWidget {
 | 
				
			||||||
  final int postId;
 | 
					  final SnPost post;
 | 
				
			||||||
  final int commentCount;
 | 
					  final int commentCount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const PostCommentListPopup({
 | 
					  const PostCommentListPopup({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
    required this.postId,
 | 
					    required this.post,
 | 
				
			||||||
    this.commentCount = 0,
 | 
					    this.commentCount = 0,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -122,9 +145,7 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
 | 
				
			|||||||
          children: [
 | 
					          children: [
 | 
				
			||||||
            const Icon(Symbols.comment, size: 24),
 | 
					            const Icon(Symbols.comment, size: 24),
 | 
				
			||||||
            const Gap(16),
 | 
					            const Gap(16),
 | 
				
			||||||
            Text('postCommentsDetailed')
 | 
					            Text('postCommentsDetailed').plural(widget.commentCount).textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
				
			||||||
                .plural(widget.commentCount)
 | 
					 | 
				
			||||||
                .textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
					 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
					        ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
				
			||||||
        Expanded(
 | 
					        Expanded(
 | 
				
			||||||
@@ -143,7 +164,7 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
 | 
				
			|||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    child: PostMiniEditor(
 | 
					                    child: PostMiniEditor(
 | 
				
			||||||
                      postReplyId: widget.postId,
 | 
					                      postReplyId: widget.post.id,
 | 
				
			||||||
                      onPost: () {
 | 
					                      onPost: () {
 | 
				
			||||||
                        _childListKey.currentState!.refresh();
 | 
					                        _childListKey.currentState!.refresh();
 | 
				
			||||||
                      },
 | 
					                      },
 | 
				
			||||||
@@ -151,8 +172,8 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
 | 
				
			|||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              PostCommentSliverList(
 | 
					              PostCommentSliverList(
 | 
				
			||||||
 | 
					                parentPost: widget.post,
 | 
				
			||||||
                key: _childListKey,
 | 
					                key: _childListKey,
 | 
				
			||||||
                parentPostId: widget.postId,
 | 
					 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,13 @@
 | 
				
			|||||||
import 'dart:developer';
 | 
					 | 
				
			||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
import 'dart:math' as math;
 | 
					import 'dart:math' as math;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import 'package:animations/animations.dart';
 | 
				
			||||||
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: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';
 | 
				
			||||||
@@ -22,10 +23,12 @@ import 'package:styled_widget/styled_widget.dart';
 | 
				
			|||||||
import 'package:surface/providers/config.dart';
 | 
					import 'package:surface/providers/config.dart';
 | 
				
			||||||
import 'package:surface/providers/sn_network.dart';
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
import 'package:surface/providers/userinfo.dart';
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/post/post_detail.dart';
 | 
				
			||||||
import 'package:surface/types/attachment.dart';
 | 
					import 'package:surface/types/attachment.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/types/reaction.dart';
 | 
					import 'package:surface/types/reaction.dart';
 | 
				
			||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
				
			||||||
import 'package:surface/widgets/attachment/attachment_list.dart';
 | 
					import 'package:surface/widgets/attachment/attachment_list.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/link_preview.dart';
 | 
					import 'package:surface/widgets/link_preview.dart';
 | 
				
			||||||
@@ -38,6 +41,65 @@ 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';
 | 
					import 'package:xml/xml.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OpenablePostItem extends StatelessWidget {
 | 
				
			||||||
 | 
					  final SnPost data;
 | 
				
			||||||
 | 
					  final bool showReactions;
 | 
				
			||||||
 | 
					  final bool showComments;
 | 
				
			||||||
 | 
					  final bool showMenu;
 | 
				
			||||||
 | 
					  final bool showFullPost;
 | 
				
			||||||
 | 
					  final double? maxWidth;
 | 
				
			||||||
 | 
					  final Function(SnPost data)? onChanged;
 | 
				
			||||||
 | 
					  final Function()? onDeleted;
 | 
				
			||||||
 | 
					  final Function()? onSelectAnswer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const OpenablePostItem({
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					    required this.data,
 | 
				
			||||||
 | 
					    this.showReactions = true,
 | 
				
			||||||
 | 
					    this.showComments = true,
 | 
				
			||||||
 | 
					    this.showMenu = true,
 | 
				
			||||||
 | 
					    this.showFullPost = false,
 | 
				
			||||||
 | 
					    this.maxWidth,
 | 
				
			||||||
 | 
					    this.onChanged,
 | 
				
			||||||
 | 
					    this.onDeleted,
 | 
				
			||||||
 | 
					    this.onSelectAnswer,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    final cfg = context.read<ConfigProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return OpenContainer(
 | 
				
			||||||
 | 
					      closedBuilder: (_, __) => Container(
 | 
				
			||||||
 | 
					        constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
 | 
				
			||||||
 | 
					        child: PostItem(
 | 
				
			||||||
 | 
					          data: data,
 | 
				
			||||||
 | 
					          maxWidth: maxWidth,
 | 
				
			||||||
 | 
					          showComments: showComments,
 | 
				
			||||||
 | 
					          showFullPost: showFullPost,
 | 
				
			||||||
 | 
					          onChanged: onChanged,
 | 
				
			||||||
 | 
					          onDeleted: onDeleted,
 | 
				
			||||||
 | 
					          onSelectAnswer: onSelectAnswer,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      openBuilder: (_, close) => PostDetailScreen(
 | 
				
			||||||
 | 
					        slug: data.id.toString(),
 | 
				
			||||||
 | 
					        preload: data,
 | 
				
			||||||
 | 
					        onBack: close,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      openColor: Colors.transparent,
 | 
				
			||||||
 | 
					      openElevation: 0,
 | 
				
			||||||
 | 
					      transitionType: ContainerTransitionType.fade,
 | 
				
			||||||
 | 
					      closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
 | 
				
			||||||
 | 
					            cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					      closedShape: const RoundedRectangleBorder(
 | 
				
			||||||
 | 
					        borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostItem extends StatelessWidget {
 | 
					class PostItem extends StatelessWidget {
 | 
				
			||||||
  final SnPost data;
 | 
					  final SnPost data;
 | 
				
			||||||
  final bool showReactions;
 | 
					  final bool showReactions;
 | 
				
			||||||
@@ -47,6 +109,7 @@ class PostItem extends StatelessWidget {
 | 
				
			|||||||
  final double? maxWidth;
 | 
					  final double? maxWidth;
 | 
				
			||||||
  final Function(SnPost data)? onChanged;
 | 
					  final Function(SnPost data)? onChanged;
 | 
				
			||||||
  final Function()? onDeleted;
 | 
					  final Function()? onDeleted;
 | 
				
			||||||
 | 
					  final Function()? onSelectAnswer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const PostItem({
 | 
					  const PostItem({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
@@ -58,6 +121,7 @@ class PostItem extends StatelessWidget {
 | 
				
			|||||||
    this.maxWidth,
 | 
					    this.maxWidth,
 | 
				
			||||||
    this.onChanged,
 | 
					    this.onChanged,
 | 
				
			||||||
    this.onDeleted,
 | 
					    this.onDeleted,
 | 
				
			||||||
 | 
					    this.onSelectAnswer,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _onChanged(SnPost data) {
 | 
					  void _onChanged(SnPost data) {
 | 
				
			||||||
@@ -142,10 +206,12 @@ class PostItem extends StatelessWidget {
 | 
				
			|||||||
              isRelativeDate: !showFullPost,
 | 
					              isRelativeDate: !showFullPost,
 | 
				
			||||||
              onShare: () => _doShare(context),
 | 
					              onShare: () => _doShare(context),
 | 
				
			||||||
              onShareImage: () => _doShareViaPicture(context),
 | 
					              onShareImage: () => _doShareViaPicture(context),
 | 
				
			||||||
 | 
					              onSelectAnswer: onSelectAnswer,
 | 
				
			||||||
              onDeleted: () {
 | 
					              onDeleted: () {
 | 
				
			||||||
                if (onDeleted != null) {}
 | 
					                if (onDeleted != null) {}
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ).padding(horizontal: 12, top: 8, bottom: 8),
 | 
					            ).padding(horizontal: 12, top: 8, bottom: 8),
 | 
				
			||||||
 | 
					            if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
 | 
				
			||||||
            Container(
 | 
					            Container(
 | 
				
			||||||
              width: double.infinity,
 | 
					              width: double.infinity,
 | 
				
			||||||
              margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
 | 
					              margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
 | 
				
			||||||
@@ -189,6 +255,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,
 | 
				
			||||||
@@ -223,10 +290,13 @@ class PostItem extends StatelessWidget {
 | 
				
			|||||||
                showMenu: showMenu,
 | 
					                showMenu: showMenu,
 | 
				
			||||||
                onShare: () => _doShare(context),
 | 
					                onShare: () => _doShare(context),
 | 
				
			||||||
                onShareImage: () => _doShareViaPicture(context),
 | 
					                onShareImage: () => _doShareViaPicture(context),
 | 
				
			||||||
 | 
					                onSelectAnswer: onSelectAnswer,
 | 
				
			||||||
                onDeleted: () {
 | 
					                onDeleted: () {
 | 
				
			||||||
                  if (onDeleted != null) onDeleted!();
 | 
					                  if (onDeleted != null) onDeleted!();
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ).padding(horizontal: 12, vertical: 8),
 | 
					              ).padding(horizontal: 12, vertical: 8),
 | 
				
			||||||
 | 
					              if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
 | 
				
			||||||
 | 
					              if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
 | 
				
			||||||
              if (data.body['title'] != null || data.body['description'] != null)
 | 
					              if (data.body['title'] != null || data.body['description'] != null)
 | 
				
			||||||
                _PostHeadline(
 | 
					                _PostHeadline(
 | 
				
			||||||
                  data: data,
 | 
					                  data: data,
 | 
				
			||||||
@@ -270,6 +340,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(
 | 
				
			||||||
@@ -331,6 +402,7 @@ class PostShareImageWidget extends StatelessWidget {
 | 
				
			|||||||
            showMenu: false,
 | 
					            showMenu: false,
 | 
				
			||||||
            isRelativeDate: false,
 | 
					            isRelativeDate: false,
 | 
				
			||||||
          ).padding(horizontal: 16, bottom: 8),
 | 
					          ).padding(horizontal: 16, bottom: 8),
 | 
				
			||||||
 | 
					          if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
 | 
				
			||||||
          _PostHeadline(
 | 
					          _PostHeadline(
 | 
				
			||||||
            data: data,
 | 
					            data: data,
 | 
				
			||||||
            isEnlarge: data.type == 'article',
 | 
					            isEnlarge: data.type == 'article',
 | 
				
			||||||
@@ -436,6 +508,30 @@ class PostShareImageWidget extends StatelessWidget {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostQuestionHint extends StatelessWidget {
 | 
				
			||||||
 | 
					  final SnPost data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostQuestionHint({required this.data});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Row(
 | 
				
			||||||
 | 
					      children: [
 | 
				
			||||||
 | 
					        Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, size: 20),
 | 
				
			||||||
 | 
					        const Gap(4),
 | 
				
			||||||
 | 
					        if (data.body['answer'] == null && data.body['reward']?.toDouble() != null)
 | 
				
			||||||
 | 
					          Text('postQuestionUnansweredWithReward'.tr(args: [
 | 
				
			||||||
 | 
					            '${data.body['reward']}',
 | 
				
			||||||
 | 
					          ])).opacity(0.75)
 | 
				
			||||||
 | 
					        else if (data.body['answer'] == null)
 | 
				
			||||||
 | 
					          Text('postQuestionUnanswered'.tr()).opacity(0.75)
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					          Text('postQuestionAnswered'.tr()).opacity(0.75),
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    ).opacity(0.75);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _PostBottomAction extends StatelessWidget {
 | 
					class _PostBottomAction extends StatelessWidget {
 | 
				
			||||||
  final SnPost data;
 | 
					  final SnPost data;
 | 
				
			||||||
  final bool showComments;
 | 
					  final bool showComments;
 | 
				
			||||||
@@ -527,7 +623,7 @@ class _PostBottomAction extends StatelessWidget {
 | 
				
			|||||||
                      context: context,
 | 
					                      context: context,
 | 
				
			||||||
                      useRootNavigator: true,
 | 
					                      useRootNavigator: true,
 | 
				
			||||||
                      builder: (context) => PostCommentListPopup(
 | 
					                      builder: (context) => PostCommentListPopup(
 | 
				
			||||||
                        postId: data.id,
 | 
					                        post: data,
 | 
				
			||||||
                        commentCount: data.metric.replyCount,
 | 
					                        commentCount: data.metric.replyCount,
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    );
 | 
					                    );
 | 
				
			||||||
@@ -650,6 +746,7 @@ class _PostContentHeader extends StatelessWidget {
 | 
				
			|||||||
  final bool showMenu;
 | 
					  final bool showMenu;
 | 
				
			||||||
  final Function onDeleted;
 | 
					  final Function onDeleted;
 | 
				
			||||||
  final Function() onShare, onShareImage;
 | 
					  final Function() onShare, onShareImage;
 | 
				
			||||||
 | 
					  final Function()? onSelectAnswer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const _PostContentHeader({
 | 
					  const _PostContentHeader({
 | 
				
			||||||
    required this.data,
 | 
					    required this.data,
 | 
				
			||||||
@@ -660,6 +757,7 @@ class _PostContentHeader extends StatelessWidget {
 | 
				
			|||||||
    required this.onDeleted,
 | 
					    required this.onDeleted,
 | 
				
			||||||
    required this.onShare,
 | 
					    required this.onShare,
 | 
				
			||||||
    required this.onShareImage,
 | 
					    required this.onShareImage,
 | 
				
			||||||
 | 
					    this.onSelectAnswer,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> _deletePost(BuildContext context) async {
 | 
					  Future<void> _deletePost(BuildContext context) async {
 | 
				
			||||||
@@ -758,6 +856,20 @@ class _PostContentHeader extends StatelessWidget {
 | 
				
			|||||||
              visualDensity: VisualDensity(horizontal: -4, vertical: -4),
 | 
					              visualDensity: VisualDensity(horizontal: -4, vertical: -4),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            itemBuilder: (BuildContext context) => <PopupMenuEntry>[
 | 
					            itemBuilder: (BuildContext context) => <PopupMenuEntry>[
 | 
				
			||||||
 | 
					              if (isAuthor && onSelectAnswer != null)
 | 
				
			||||||
 | 
					                PopupMenuItem(
 | 
				
			||||||
 | 
					                  child: Row(
 | 
				
			||||||
 | 
					                    children: [
 | 
				
			||||||
 | 
					                      const Icon(Symbols.check_circle),
 | 
				
			||||||
 | 
					                      const Gap(16),
 | 
				
			||||||
 | 
					                      Text('postQuestionAnswerSelect').tr(),
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  onTap: () {
 | 
				
			||||||
 | 
					                    onSelectAnswer?.call();
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              if (isAuthor && onSelectAnswer != null) PopupMenuDivider(),
 | 
				
			||||||
              if (isAuthor)
 | 
					              if (isAuthor)
 | 
				
			||||||
                PopupMenuItem(
 | 
					                PopupMenuItem(
 | 
				
			||||||
                  child: Row(
 | 
					                  child: Row(
 | 
				
			||||||
@@ -831,7 +943,7 @@ class _PostContentHeader extends StatelessWidget {
 | 
				
			|||||||
                onTap: () {
 | 
					                onTap: () {
 | 
				
			||||||
                  showModalBottomSheet(
 | 
					                  showModalBottomSheet(
 | 
				
			||||||
                    context: context,
 | 
					                    context: context,
 | 
				
			||||||
                    builder: (context) => _PostGetInsightSheet(postId: data.id),
 | 
					                    builder: (context) => _PostGetInsightPopup(postId: data.id),
 | 
				
			||||||
                  );
 | 
					                  );
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
@@ -1125,6 +1237,118 @@ 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;
 | 
				
			||||||
 | 
					  bool _isAnswer = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _fetchComments() async {
 | 
				
			||||||
 | 
					    // If this is a answered question, fetch the answer instead
 | 
				
			||||||
 | 
					    if (widget.data.type == 'question' && widget.data.body['answer'] != null) {
 | 
				
			||||||
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					      final resp = await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
 | 
				
			||||||
 | 
					      _isAnswer = true;
 | 
				
			||||||
 | 
					      setState(() => _featuredComment = SnPost.fromJson(resp.data));
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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: _isAnswer ? Colors.green.withOpacity(0.5) : Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
				
			||||||
 | 
					        child: InkWell(
 | 
				
			||||||
 | 
					          borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					          onTap: () {
 | 
				
			||||||
 | 
					            showModalBottomSheet(
 | 
				
			||||||
 | 
					              context: context,
 | 
				
			||||||
 | 
					              useRootNavigator: true,
 | 
				
			||||||
 | 
					              builder: (context) => PostCommentListPopup(
 | 
				
			||||||
 | 
					                post: widget.data,
 | 
				
			||||||
 | 
					                commentCount: widget.data.metric.replyCount,
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          child: Column(
 | 
				
			||||||
 | 
					            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              Row(
 | 
				
			||||||
 | 
					                crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  const Gap(2),
 | 
				
			||||||
 | 
					                  Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, size: 20),
 | 
				
			||||||
 | 
					                  const Gap(10),
 | 
				
			||||||
 | 
					                  Text(
 | 
				
			||||||
 | 
					                    _isAnswer ? 'postQuestionAnswerTitle' : 'postFeaturedComment',
 | 
				
			||||||
 | 
					                    style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 15),
 | 
				
			||||||
 | 
					                  ).tr(),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              const Gap(4),
 | 
				
			||||||
 | 
					              Row(
 | 
				
			||||||
 | 
					                crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  CircleAvatar(
 | 
				
			||||||
 | 
					                    radius: 12,
 | 
				
			||||||
 | 
					                    backgroundImage: UniversalImage.provider(
 | 
				
			||||||
 | 
					                      sn.getAttachmentUrl(_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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1201,16 +1425,16 @@ class _PostAbuseReportDialogState extends State<_PostAbuseReportDialog> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _PostGetInsightSheet extends StatefulWidget {
 | 
					class _PostGetInsightPopup extends StatefulWidget {
 | 
				
			||||||
  final int postId;
 | 
					  final int postId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const _PostGetInsightSheet({required this.postId});
 | 
					  const _PostGetInsightPopup({required this.postId});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  State<_PostGetInsightSheet> createState() => _PostGetInsightSheetState();
 | 
					  State<_PostGetInsightPopup> createState() => _PostGetInsightPopupState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _PostGetInsightSheetState extends State<_PostGetInsightSheet> {
 | 
					class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
 | 
				
			||||||
  String? _response;
 | 
					  String? _response;
 | 
				
			||||||
  String? _thinkingProcess;
 | 
					  String? _thinkingProcess;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1223,8 +1447,14 @@ class _PostGetInsightSheetState extends State<_PostGetInsightSheet> {
 | 
				
			|||||||
            receiveTimeout: const Duration(minutes: 10),
 | 
					            receiveTimeout: const Duration(minutes: 10),
 | 
				
			||||||
          ));
 | 
					          ));
 | 
				
			||||||
      final out = resp.data['response'] as String;
 | 
					      final out = resp.data['response'] as String;
 | 
				
			||||||
      final document = XmlDocument.parse(out);
 | 
					
 | 
				
			||||||
      _thinkingProcess = document.getElement('think')?.innerText.trim();
 | 
					      try {
 | 
				
			||||||
 | 
					        final document = XmlDocument.parse(out);
 | 
				
			||||||
 | 
					        _thinkingProcess = document.getElement('think')?.innerText.trim();
 | 
				
			||||||
 | 
					      } catch (_) {
 | 
				
			||||||
 | 
					        // ignore
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
 | 
					      RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
 | 
				
			||||||
      setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
 | 
					      setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
@@ -1293,3 +1523,29 @@ class _PostGetInsightSheetState extends State<_PostGetInsightSheet> {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _PostVideoPlayer extends StatelessWidget {
 | 
				
			||||||
 | 
					  final SnPost data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _PostVideoPlayer({required this.data});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Container(
 | 
				
			||||||
 | 
					      decoration: BoxDecoration(
 | 
				
			||||||
 | 
					        borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					        border: Border.all(
 | 
				
			||||||
 | 
					          color: Theme.of(context).dividerColor,
 | 
				
			||||||
 | 
					          width: 1,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      child: AspectRatio(
 | 
				
			||||||
 | 
					        aspectRatio: 16 / 9,
 | 
				
			||||||
 | 
					        child: ClipRRect(
 | 
				
			||||||
 | 
					          borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					          child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -95,6 +95,7 @@ class PostMediaPendingList extends StatelessWidget {
 | 
				
			|||||||
      context: context,
 | 
					      context: context,
 | 
				
			||||||
      builder: (context) => AttachmentInputDialog(
 | 
					      builder: (context) => AttachmentInputDialog(
 | 
				
			||||||
        title: 'attachmentSetThumbnail'.tr(),
 | 
					        title: 'attachmentSetThumbnail'.tr(),
 | 
				
			||||||
 | 
					        pool: 'interactive',
 | 
				
			||||||
        analyzeNow: true,
 | 
					        analyzeNow: true,
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@@ -292,7 +293,7 @@ class PostMediaPendingList extends StatelessWidget {
 | 
				
			|||||||
      constraints: const BoxConstraints(maxHeight: 120),
 | 
					      constraints: const BoxConstraints(maxHeight: 120),
 | 
				
			||||||
      child: Row(
 | 
					      child: Row(
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          const Gap(8),
 | 
					          const Gap(16),
 | 
				
			||||||
          if (thumbnail != null)
 | 
					          if (thumbnail != null)
 | 
				
			||||||
            ContextMenuArea(
 | 
					            ContextMenuArea(
 | 
				
			||||||
              contextMenu: _createContextMenu(context, -1, thumbnail!),
 | 
					              contextMenu: _createContextMenu(context, -1, thumbnail!),
 | 
				
			||||||
@@ -337,15 +338,10 @@ class _PostMediaPendingItem extends StatelessWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final sn = context.read<SnNetworkProvider>();
 | 
					    final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Container(
 | 
					    return Material(
 | 
				
			||||||
      decoration: BoxDecoration(
 | 
					      elevation: 4,
 | 
				
			||||||
        border: Border.all(
 | 
					      color: Theme.of(context).colorScheme.surfaceContainer,
 | 
				
			||||||
          color: Theme.of(context).dividerColor,
 | 
					      borderRadius: BorderRadius.circular(8),
 | 
				
			||||||
          width: 1,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        borderRadius: BorderRadius.circular(8),
 | 
					 | 
				
			||||||
        color: Theme.of(context).colorScheme.surfaceContainer,
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      child: ClipRRect(
 | 
					      child: ClipRRect(
 | 
				
			||||||
        borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					        borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
        child: Row(
 | 
					        child: Row(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,7 @@ const Map<int, String> kPostVisibilityLevel = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class PostMetaEditor extends StatelessWidget {
 | 
					class PostMetaEditor extends StatelessWidget {
 | 
				
			||||||
  final PostWriteController controller;
 | 
					  final PostWriteController controller;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const PostMetaEditor({super.key, required this.controller});
 | 
					  const PostMetaEditor({super.key, required this.controller});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<DateTime?> _selectDate(
 | 
					  Future<DateTime?> _selectDate(
 | 
				
			||||||
@@ -87,28 +88,6 @@ class PostMetaEditor extends StatelessWidget {
 | 
				
			|||||||
          padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
 | 
					          padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
 | 
				
			||||||
          child: Column(
 | 
					          child: Column(
 | 
				
			||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
              TextField(
 | 
					 | 
				
			||||||
                controller: controller.titleController,
 | 
					 | 
				
			||||||
                decoration: InputDecoration(
 | 
					 | 
				
			||||||
                  labelText: 'fieldPostTitle'.tr(),
 | 
					 | 
				
			||||||
                  border: UnderlineInputBorder(),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                onTapOutside: (_) =>
 | 
					 | 
				
			||||||
                    FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
              ).padding(horizontal: 24),
 | 
					 | 
				
			||||||
              if (controller.mode == 'articles') const Gap(4),
 | 
					 | 
				
			||||||
              if (controller.mode == 'articles')
 | 
					 | 
				
			||||||
                TextField(
 | 
					 | 
				
			||||||
                  controller: controller.descriptionController,
 | 
					 | 
				
			||||||
                  maxLines: null,
 | 
					 | 
				
			||||||
                  decoration: InputDecoration(
 | 
					 | 
				
			||||||
                    labelText: 'fieldPostDescription'.tr(),
 | 
					 | 
				
			||||||
                    border: UnderlineInputBorder(),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  onTapOutside: (_) =>
 | 
					 | 
				
			||||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
                ).padding(horizontal: 24),
 | 
					 | 
				
			||||||
              const Gap(4),
 | 
					 | 
				
			||||||
              PostTagsField(
 | 
					              PostTagsField(
 | 
				
			||||||
                initialTags: controller.tags,
 | 
					                initialTags: controller.tags,
 | 
				
			||||||
                labelText: 'fieldPostTags'.tr(),
 | 
					                labelText: 'fieldPostTags'.tr(),
 | 
				
			||||||
@@ -133,8 +112,7 @@ class PostMetaEditor extends StatelessWidget {
 | 
				
			|||||||
                  helperMaxLines: 2,
 | 
					                  helperMaxLines: 2,
 | 
				
			||||||
                  border: UnderlineInputBorder(),
 | 
					                  border: UnderlineInputBorder(),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                onTapOutside: (_) =>
 | 
					                onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
				
			||||||
                    FocusManager.instance.primaryFocus?.unfocus(),
 | 
					 | 
				
			||||||
              ).padding(horizontal: 24),
 | 
					              ).padding(horizontal: 24),
 | 
				
			||||||
              const Gap(12),
 | 
					              const Gap(12),
 | 
				
			||||||
              ListTile(
 | 
					              ListTile(
 | 
				
			||||||
@@ -182,8 +160,7 @@ class PostMetaEditor extends StatelessWidget {
 | 
				
			|||||||
                  leading: Icon(Symbols.person),
 | 
					                  leading: Icon(Symbols.person),
 | 
				
			||||||
                  trailing: Icon(Symbols.chevron_right),
 | 
					                  trailing: Icon(Symbols.chevron_right),
 | 
				
			||||||
                  title: Text('postVisibleUsers').tr(),
 | 
					                  title: Text('postVisibleUsers').tr(),
 | 
				
			||||||
                  subtitle: Text('postSelectedUsers')
 | 
					                  subtitle: Text('postSelectedUsers').plural(controller.visibleUsers.length),
 | 
				
			||||||
                      .plural(controller.visibleUsers.length),
 | 
					 | 
				
			||||||
                  onTap: () {
 | 
					                  onTap: () {
 | 
				
			||||||
                    _selectVisibleUser(context);
 | 
					                    _selectVisibleUser(context);
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
@@ -194,8 +171,7 @@ class PostMetaEditor extends StatelessWidget {
 | 
				
			|||||||
                  leading: Icon(Symbols.person),
 | 
					                  leading: Icon(Symbols.person),
 | 
				
			||||||
                  trailing: Icon(Symbols.chevron_right),
 | 
					                  trailing: Icon(Symbols.chevron_right),
 | 
				
			||||||
                  title: Text('postInvisibleUsers').tr(),
 | 
					                  title: Text('postInvisibleUsers').tr(),
 | 
				
			||||||
                  subtitle: Text('postSelectedUsers')
 | 
					                  subtitle: Text('postSelectedUsers').plural(controller.invisibleUsers.length),
 | 
				
			||||||
                      .plural(controller.invisibleUsers.length),
 | 
					 | 
				
			||||||
                  onTap: () {
 | 
					                  onTap: () {
 | 
				
			||||||
                    _selectInvisibleUser(context);
 | 
					                    _selectInvisibleUser(context);
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
@@ -204,9 +180,7 @@ class PostMetaEditor extends StatelessWidget {
 | 
				
			|||||||
                leading: const Icon(Symbols.event_available),
 | 
					                leading: const Icon(Symbols.event_available),
 | 
				
			||||||
                title: Text('postPublishedAt').tr(),
 | 
					                title: Text('postPublishedAt').tr(),
 | 
				
			||||||
                subtitle: Text(
 | 
					                subtitle: Text(
 | 
				
			||||||
                  controller.publishedAt != null
 | 
					                  controller.publishedAt != null ? dateFormatter.format(controller.publishedAt!) : 'unset'.tr(),
 | 
				
			||||||
                      ? dateFormatter.format(controller.publishedAt!)
 | 
					 | 
				
			||||||
                      : 'unset'.tr(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                trailing: controller.publishedAt != null
 | 
					                trailing: controller.publishedAt != null
 | 
				
			||||||
                    ? IconButton(
 | 
					                    ? IconButton(
 | 
				
			||||||
@@ -230,9 +204,7 @@ class PostMetaEditor extends StatelessWidget {
 | 
				
			|||||||
                leading: const Icon(Symbols.event_busy),
 | 
					                leading: const Icon(Symbols.event_busy),
 | 
				
			||||||
                title: Text('postPublishedUntil').tr(),
 | 
					                title: Text('postPublishedUntil').tr(),
 | 
				
			||||||
                subtitle: Text(
 | 
					                subtitle: Text(
 | 
				
			||||||
                  controller.publishedUntil != null
 | 
					                  controller.publishedUntil != null ? dateFormatter.format(controller.publishedUntil!) : 'unset'.tr(),
 | 
				
			||||||
                      ? dateFormatter.format(controller.publishedUntil!)
 | 
					 | 
				
			||||||
                      : 'unset'.tr(),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                trailing: controller.publishedUntil != null
 | 
					                trailing: controller.publishedUntil != null
 | 
				
			||||||
                    ? IconButton(
 | 
					                    ? IconButton(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,7 @@ 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
 | 
				
			||||||
@@ -29,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
 | 
				
			||||||
@@ -47,6 +49,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
				
			|||||||
  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"))
 | 
				
			||||||
@@ -58,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"))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,6 @@ PODS:
 | 
				
			|||||||
  - bitsdojo_window_macos (0.0.1):
 | 
					  - bitsdojo_window_macos (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - connectivity_plus (0.0.1):
 | 
					  - connectivity_plus (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - croppy (0.0.1):
 | 
					  - croppy (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
@@ -14,59 +13,59 @@ PODS:
 | 
				
			|||||||
    - 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.1):
 | 
					  - 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.1):
 | 
					  - firebase_core (3.11.0):
 | 
				
			||||||
    - Firebase/CoreOnly (~> 11.6.0)
 | 
					    - Firebase/CoreOnly (~> 11.7.0)
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - firebase_messaging (15.2.1):
 | 
					  - 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)
 | 
				
			||||||
@@ -87,21 +86,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)
 | 
				
			||||||
@@ -137,9 +136,13 @@ 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.6):
 | 
				
			||||||
    - flutter_webrtc
 | 
					    - flutter_webrtc
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
    - WebRTC-SDK (= 125.6422.06)
 | 
					    - WebRTC-SDK (= 125.6422.06)
 | 
				
			||||||
@@ -174,6 +177,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):
 | 
				
			||||||
@@ -184,7 +189,7 @@ PODS:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
DEPENDENCIES:
 | 
					DEPENDENCIES:
 | 
				
			||||||
  - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
 | 
					  - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
 | 
				
			||||||
  - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`)
 | 
					  - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
 | 
				
			||||||
  - 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_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
 | 
				
			||||||
@@ -198,6 +203,7 @@ DEPENDENCIES:
 | 
				
			|||||||
  - 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`)
 | 
				
			||||||
@@ -210,6 +216,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`)
 | 
				
			||||||
@@ -225,6 +232,7 @@ SPEC REPOS:
 | 
				
			|||||||
    - GoogleAppMeasurement
 | 
					    - GoogleAppMeasurement
 | 
				
			||||||
    - GoogleDataTransport
 | 
					    - GoogleDataTransport
 | 
				
			||||||
    - GoogleUtilities
 | 
					    - GoogleUtilities
 | 
				
			||||||
 | 
					    - HotKey
 | 
				
			||||||
    - nanopb
 | 
					    - nanopb
 | 
				
			||||||
    - OrderedSet
 | 
					    - OrderedSet
 | 
				
			||||||
    - PromisesObjC
 | 
					    - PromisesObjC
 | 
				
			||||||
@@ -235,7 +243,7 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
  bitsdojo_window_macos:
 | 
					  bitsdojo_window_macos:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
 | 
				
			||||||
  connectivity_plus:
 | 
					  connectivity_plus:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
 | 
				
			||||||
  croppy:
 | 
					  croppy:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/croppy/macos
 | 
				
			||||||
  device_info_plus:
 | 
					  device_info_plus:
 | 
				
			||||||
@@ -262,6 +270,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:
 | 
				
			||||||
@@ -286,6 +296,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:
 | 
				
			||||||
@@ -295,31 +307,33 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
SPEC CHECKSUMS:
 | 
					SPEC CHECKSUMS:
 | 
				
			||||||
  bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
 | 
					  bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
 | 
				
			||||||
  connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695
 | 
					  connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802
 | 
				
			||||||
  croppy: 25a638bd7d05411d8c697f481568f261037694fc
 | 
					  croppy: 25a638bd7d05411d8c697f481568f261037694fc
 | 
				
			||||||
  device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
 | 
					  device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
 | 
				
			||||||
  file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
 | 
					  file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
 | 
				
			||||||
  file_saver: 44e6fbf666677faf097302460e214e977fdd977b
 | 
					  file_saver: 44e6fbf666677faf097302460e214e977fdd977b
 | 
				
			||||||
  file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
 | 
					  file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
 | 
				
			||||||
  Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
 | 
					  Firebase: a64bf6a8546e6eab54f1c715cd6151f39d2329f4
 | 
				
			||||||
  firebase_analytics: 91efc58e8e37964469fdd59ad11ba36bc97e75d6
 | 
					  firebase_analytics: 41d88c024a7756462a803e36236ba74f24cdc2c5
 | 
				
			||||||
  firebase_core: 75e003524565fb5bd80c9960bc5892e8475821cd
 | 
					  firebase_core: 751d3d919b95d4ae46ab049d0d64d42d4eec086b
 | 
				
			||||||
  firebase_messaging: 082a385eb98b5bb843a566cb30404859c4bd6e25
 | 
					  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_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: 0ad107154753a5a76802d2222c040223ad049499
 | 
				
			||||||
  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
 | 
				
			||||||
@@ -334,6 +348,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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										168
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										168
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -13,10 +13,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: _flutterfire_internals
 | 
					      name: _flutterfire_internals
 | 
				
			||||||
      sha256: e4f2a7ef31b0ab2c89d2bde35ef3e6e6aff1dce5e66069c6540b0e9cfe33ee6b
 | 
					      sha256: e051259913915ea5bc8fe18664596bea08592fd123930605d562969cd7315fcd
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.3.50"
 | 
					    version: "1.3.51"
 | 
				
			||||||
  _macros:
 | 
					  _macros:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description: dart
 | 
					    description: dart
 | 
				
			||||||
@@ -214,6 +214,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.4.3"
 | 
					    version: "0.4.3"
 | 
				
			||||||
 | 
					  chalkdart:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: chalkdart
 | 
				
			||||||
 | 
					      sha256: "08c910ee341fcdd1e46f20ddce59b13c1d85f5d97f2fd2f12014c46ede670e40"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.3.2"
 | 
				
			||||||
  characters:
 | 
					  characters:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -266,10 +274,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: connectivity_plus
 | 
					      name: connectivity_plus
 | 
				
			||||||
      sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
 | 
					      sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.1.2"
 | 
					    version: "6.1.3"
 | 
				
			||||||
  connectivity_plus_platform_interface:
 | 
					  connectivity_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -362,10 +370,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: device_info_plus
 | 
					      name: device_info_plus
 | 
				
			||||||
      sha256: e3fc9a65820fef83035af8ee8c09004a719d5d1d54e6de978fcb0d84bbeb241a
 | 
					      sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "11.2.2"
 | 
					    version: "11.3.0"
 | 
				
			||||||
  device_info_plus_platform_interface:
 | 
					  device_info_plus_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -490,10 +498,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: file_picker
 | 
					      name: file_picker
 | 
				
			||||||
      sha256: c9943dd7d702ab4199d199bc151a2d79c86db031a02ad84566dab58c494d2adc
 | 
					      sha256: cacfdc5abe93e64d418caa9256eef663499ad791bb688d9fd12c85a311968fba
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "8.3.1"
 | 
					    version: "8.3.2"
 | 
				
			||||||
  file_saver:
 | 
					  file_saver:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -538,34 +546,34 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: firebase_analytics
 | 
					      name: firebase_analytics
 | 
				
			||||||
      sha256: eac382bbcd5ae78c1d1ce5619d13f5a7424429f4bf55df9e3ad5110da34d1060
 | 
					      sha256: "47428047a0778f72af53a3c7cb5d556e1cb25e2327cc8aa40d544971dc6245b2"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "11.4.1"
 | 
					    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: a34db46c367265c4c961626e4b128bfb7b7e50958e7add4c27ba103f5f81b9b0
 | 
					      sha256: "1076f4b041f76143e14878c70f0758f17fe5910c0cd992db9e93bd3c3584512b"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.3.1"
 | 
					    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: b6b4cef08e45e4c7d48476d9fc49fe9577081809a59026fe95b1a1b1eea165fa
 | 
					      sha256: "8f6dd64ea6d28b7f5b9e739d183a9e1c7f17027794a3e9aba1879621d42426ef"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.5.10+7"
 | 
					    version: "0.5.10+8"
 | 
				
			||||||
  firebase_core:
 | 
					  firebase_core:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: firebase_core
 | 
					      name: firebase_core
 | 
				
			||||||
      sha256: d851c1ca98fd5a4c07c747f8c65dacc2edd84a4d9ac055d32a5f0342529069f5
 | 
					      sha256: "93dc4dd12f9b02c5767f235307f609e61ed9211047132d07f9e02c668f0bfc33"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.10.1"
 | 
					    version: "3.11.0"
 | 
				
			||||||
  firebase_core_platform_interface:
 | 
					  firebase_core_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -578,34 +586,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: e20ea2a0ecf9b0971575ab3ab42a6e285a94e50092c555b090c1a588a81b4d54
 | 
					      sha256: "3dee3b0cbfe719e64773cb7d1cad57c58b2235a8c136f5715fe733a54058c783"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "15.2.1"
 | 
					    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: c57a92b5ae1857ef4fe4ae2e73452b44d32e984e15ab8b53415ea1bb514bdabd
 | 
					      sha256: e9ea726b9bb864fc6223bb66422bd9877b9973ae51967754a769b0d01e201c1e
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.6.1"
 | 
					    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: "83694a990d8525d6b01039240b97757298369622ca0253ad0ebcfed221bf8ee0"
 | 
					      sha256: "5f7b40e8bf861a37f8b8196e347d8a919750421a45f0b45d1bb74e98fa72726e"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.10.1"
 | 
					    version: "3.10.2"
 | 
				
			||||||
  fixnum:
 | 
					  fixnum:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -764,10 +772,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:
 | 
				
			||||||
@@ -830,10 +838,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_webrtc
 | 
					      name: flutter_webrtc
 | 
				
			||||||
      sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03"
 | 
					      sha256: e917067abeef2400e6a7a03db53a6e1418551e54809f18ab80447ac323eb77e4
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.12.7"
 | 
					    version: "0.12.8"
 | 
				
			||||||
  freezed:
 | 
					  freezed:
 | 
				
			||||||
    dependency: "direct dev"
 | 
					    dependency: "direct dev"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -886,10 +894,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: go_router
 | 
					      name: go_router
 | 
				
			||||||
      sha256: "9b736a9fa879d8ad6df7932cbdcc58237c173ab004ef90d8377923d7ad731eaa"
 | 
					      sha256: "04539267a740931c6d4479a10d466717ca5901c6fdfd3fcda09391bbb8ebd651"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "14.7.2"
 | 
					    version: "14.8.0"
 | 
				
			||||||
  google_fonts:
 | 
					  google_fonts:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -934,10 +942,50 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: home_widget
 | 
					      name: home_widget
 | 
				
			||||||
      sha256: b313e3304c0429669fddf1286e1fbf61a64b873f38ba30b3eb890ef0d7560b12
 | 
					      sha256: "7430f7549d42cef2e729bd3c779de748b93f1eb78b1abfe6bca8fffd1cfce3e9"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.7.0"
 | 
					    version: "0.7.0+1"
 | 
				
			||||||
 | 
					  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
 | 
				
			||||||
 | 
					    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:
 | 
					  html:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1150,10 +1198,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: livekit_client
 | 
					      name: livekit_client
 | 
				
			||||||
      sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de"
 | 
					      sha256: "0cfb2f48eff7a93ea8927696dc6f218aebd2fcd1fcc1b1a7b2f53ff3597fdb52"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.3.5"
 | 
					    version: "2.3.6"
 | 
				
			||||||
  logging:
 | 
					  logging:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1206,10 +1254,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: material_symbols_icons
 | 
					      name: material_symbols_icons
 | 
				
			||||||
      sha256: "89aac72d25dd49303f71b3b1e70f8374791846729365b25bebc2a2531e5b86cd"
 | 
					      sha256: "1403944c2a68dbdf934fca2b2115a372e70a4a185c136ab71a9d16e2108d0b14"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.2801.1"
 | 
					    version: "4.2805.1"
 | 
				
			||||||
  media_kit:
 | 
					  media_kit:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1282,6 +1330,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:
 | 
				
			||||||
@@ -1334,18 +1390,18 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: package_info_plus
 | 
					      name: package_info_plus
 | 
				
			||||||
      sha256: b15fad91c4d3d1f2b48c053dd41cb82da007c27407dc9ab5f9aa59881d0e39d4
 | 
					      sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "8.1.4"
 | 
					    version: "8.2.1"
 | 
				
			||||||
  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:
 | 
				
			||||||
@@ -1778,6 +1834,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
 | 
				
			||||||
@@ -1951,6 +2015,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:
 | 
				
			||||||
@@ -1959,6 +2031,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:
 | 
				
			||||||
@@ -2059,10 +2139,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: vector_graphics
 | 
					      name: vector_graphics
 | 
				
			||||||
      sha256: "7ed22c21d7fdcc88dd6ba7860384af438cd220b251ad65dfc142ab722fabef61"
 | 
					      sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.1.16"
 | 
					    version: "1.1.18"
 | 
				
			||||||
  vector_graphics_codec:
 | 
					  vector_graphics_codec:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -2179,10 +2259,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: webrtc_interface
 | 
					      name: webrtc_interface
 | 
				
			||||||
      sha256: abec3ab7956bd5ac539cf34a42fa0c82ea26675847c0966bb85160400eea9388
 | 
					      sha256: "10fc6dc0ac16f909f5e434c18902415211d759313c87261f1e4ec5b4f6a04c26"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.2.0"
 | 
					    version: "1.2.1"
 | 
				
			||||||
  win32:
 | 
					  win32:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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+60
 | 
					version: 2.3.2+65
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ^3.5.4
 | 
					  sdk: ^3.5.4
 | 
				
			||||||
@@ -118,6 +118,8 @@ dependencies:
 | 
				
			|||||||
  flutter_inappwebview: ^6.1.5
 | 
					  flutter_inappwebview: ^6.1.5
 | 
				
			||||||
  html: ^0.15.5
 | 
					  html: ^0.15.5
 | 
				
			||||||
  xml: ^6.5.0
 | 
					  xml: ^6.5.0
 | 
				
			||||||
 | 
					  tray_manager: ^0.3.2
 | 
				
			||||||
 | 
					  hotkey_manager: ^0.2.3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
@@ -152,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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@
 | 
				
			|||||||
#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>
 | 
				
			||||||
@@ -22,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) {
 | 
				
			||||||
@@ -43,6 +45,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
 | 
				
			|||||||
      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(
 | 
				
			||||||
@@ -57,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"));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
 | 
				
			|||||||
  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
 | 
				
			||||||
@@ -19,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