Compare commits
	
		
			36 Commits
		
	
	
		
			2.2.2+52
			...
			5377161fb0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5377161fb0 | |||
| 963e538ae5 | |||
| a355e3bf90 | |||
| cb4a2598c8 | |||
| 950612dc07 | |||
| cbd1eaf1af | |||
| ac41cbd99f | |||
| 9f9c90abc4 | |||
| 87029e3538 | |||
| 127d9adc09 | |||
| c82dc7ad85 | |||
| 36bcff7a7c | |||
| 38201b547a | |||
| ed0334fcda | |||
| fbb486b90b | |||
| 9b34f385d5 | |||
| bb7b731602 | |||
| 19076f8136 | |||
| dc77a936ce | |||
| 7f58710c6f | |||
| 068ddcdcdc | |||
| f4e9252ca0 | |||
| 3b1e918117 | |||
| ed7981fdaf | |||
| 9698ca53e4 | |||
| ddc1dc7daf | |||
| 1625a957f8 | |||
| 2dc50d627e | |||
| 2ffde9a3dd | |||
| 5967a91ae1 | |||
| 32c1effcb5 | |||
| 9d0e19c56f | |||
| acf4e634fe | |||
| 25942c2338 | |||
| a4f81f6ba1 | |||
| c1b9090e51 | 
@@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "sync": {
 | 
			
		||||
    "region": "solian-next",
 | 
			
		||||
    "region": "solian",
 | 
			
		||||
    "configPath": "roadsign.toml"
 | 
			
		||||
  },
 | 
			
		||||
  "deployments": [
 | 
			
		||||
    {
 | 
			
		||||
      "region": "solian-next",
 | 
			
		||||
      "site": "solian-next-web",
 | 
			
		||||
      "region": "solian",
 | 
			
		||||
      "site": "solian-web",
 | 
			
		||||
      "path": "build/web"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,12 @@
 | 
			
		||||
        android:label="Solian"
 | 
			
		||||
        android:name="${applicationName}"
 | 
			
		||||
        android:icon="@mipmap/ic_launcher"
 | 
			
		||||
        android:enableOnBackInvokedCallback="true"
 | 
			
		||||
        android:requestLegacyExternalStorage="true">
 | 
			
		||||
        <meta-data
 | 
			
		||||
            android:name="flutterEmbedding"
 | 
			
		||||
            android:value="2" />
 | 
			
		||||
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name=".MainActivity"
 | 
			
		||||
            android:exported="true"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								api/Reader/List News Sources.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/Reader/List News Sources.bru
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
meta {
 | 
			
		||||
  name: List News Sources
 | 
			
		||||
  type: http
 | 
			
		||||
  seq: 3
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
get {
 | 
			
		||||
  url: {{endpoint}}/cgi/re/well-known/sources
 | 
			
		||||
  body: none
 | 
			
		||||
  auth: none
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								api/Reader/List News.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								api/Reader/List News.bru
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
meta {
 | 
			
		||||
  name: List News
 | 
			
		||||
  type: http
 | 
			
		||||
  seq: 2
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
get {
 | 
			
		||||
  url: {{endpoint}}/cgi/re/news?take=10&offset=0&source=shadiao
 | 
			
		||||
  body: none
 | 
			
		||||
  auth: none
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
params:query {
 | 
			
		||||
  take: 10
 | 
			
		||||
  offset: 0
 | 
			
		||||
  source: shadiao
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								api/Reader/Trigger Scan News.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								api/Reader/Trigger Scan News.bru
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
meta {
 | 
			
		||||
  name: Trigger Scan News
 | 
			
		||||
  type: http
 | 
			
		||||
  seq: 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
post {
 | 
			
		||||
  url: {{endpoint}}/cgi/re/admin/scan
 | 
			
		||||
  body: json
 | 
			
		||||
  auth: inherit
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body:json {
 | 
			
		||||
  {
 | 
			
		||||
    "eager": true
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -17,6 +17,7 @@
 | 
			
		||||
  "screenAccountProfileEdit": "Edit Profile",
 | 
			
		||||
  "screenAbuseReport": "Abuse Reports",
 | 
			
		||||
  "screenSettings": "Settings",
 | 
			
		||||
  "screenNews": "News",
 | 
			
		||||
  "screenAlbum": "Album",
 | 
			
		||||
  "screenChat": "Chat",
 | 
			
		||||
  "screenChatManage": "Edit Channel",
 | 
			
		||||
@@ -193,6 +194,13 @@
 | 
			
		||||
  "settingsColorSchemeDescription": "Set the application primary color.",
 | 
			
		||||
  "settingsColorSeed": "Color Seed",
 | 
			
		||||
  "settingsColorSeedDescription": "Select one of the present color schemes.",
 | 
			
		||||
  "settingsFeatures": "Features",
 | 
			
		||||
  "settingsNotifyWithHaptic": "Haptic when Notified",
 | 
			
		||||
  "settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.",
 | 
			
		||||
  "settingsExpandPostLink": "Expand Post Link",
 | 
			
		||||
  "settingsExpandPostLinkDescription": "Expand the post link in the post list.",
 | 
			
		||||
  "settingsExpandChatLink": "Expand Chat Link",
 | 
			
		||||
  "settingsExpandChatLinkDescription": "Expand the chat link in the chat list.",
 | 
			
		||||
  "settingsNetwork": "Network",
 | 
			
		||||
  "settingsNetworkServer": "HyperNet Server",
 | 
			
		||||
  "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
 | 
			
		||||
@@ -215,8 +223,9 @@
 | 
			
		||||
  "sensitiveContentCollapsed": "Sensitive content has been collapsed.",
 | 
			
		||||
  "sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
 | 
			
		||||
  "sensitiveContentReveal": "Reveal",
 | 
			
		||||
  "serverConnecting": "Connecting to server...",
 | 
			
		||||
  "serverDisconnected": "Lost connection from server",
 | 
			
		||||
  "serverConnecting": "Connecting...",
 | 
			
		||||
  "serverDisconnected": "Connection Lost",
 | 
			
		||||
  "serverConnected": "Connected",
 | 
			
		||||
  "fieldChatAlias": "Channel Alias",
 | 
			
		||||
  "fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.",
 | 
			
		||||
  "fieldChatName": "Name",
 | 
			
		||||
@@ -294,6 +303,7 @@
 | 
			
		||||
  "addAttachmentFromCameraPhoto": "Take photo",
 | 
			
		||||
  "addAttachmentFromCameraVideo": "Take video",
 | 
			
		||||
  "addAttachmentFromRandomId": "Link via RID",
 | 
			
		||||
  "attachmentDetailInfo": "Attachment details",
 | 
			
		||||
  "attachmentPastedImage": "Pasted Image",
 | 
			
		||||
  "attachmentInsertLink": "Insert Link",
 | 
			
		||||
  "attachmentSetAsPostThumbnail": "Set as post thumbnail",
 | 
			
		||||
@@ -549,5 +559,10 @@
 | 
			
		||||
  "postCategoryKnowledge": "Knowledge",
 | 
			
		||||
  "postCategoryLiterature": "Literature",
 | 
			
		||||
  "postCategoryFunny": "Funny",
 | 
			
		||||
  "postCategoryUncategorized": "Uncategorized"
 | 
			
		||||
  "postCategoryUncategorized": "Uncategorized",
 | 
			
		||||
  "newsAllSources": "All News",
 | 
			
		||||
  "newsReadingProviderSwap": "Swap",
 | 
			
		||||
  "newsReadingFromReader": "You're reading from HyperNet.Reader",
 | 
			
		||||
  "newsReadingFromOriginal": "You're reading the original article",
 | 
			
		||||
  "newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author."
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@
 | 
			
		||||
  "screenAccountProfileEdit": "编辑资料",
 | 
			
		||||
  "screenAbuseReport": "滥用检举",
 | 
			
		||||
  "screenSettings": "设置",
 | 
			
		||||
  "screenNews": "新闻",
 | 
			
		||||
  "screenAlbum": "相册",
 | 
			
		||||
  "screenChat": "聊天",
 | 
			
		||||
  "screenChatManage": "编辑聊天频道",
 | 
			
		||||
@@ -191,6 +192,13 @@
 | 
			
		||||
  "settingsColorSchemeDescription": "设置应用主题色。",
 | 
			
		||||
  "settingsColorSeed": "预设色彩主题",
 | 
			
		||||
  "settingsColorSeedDescription": "选择一个预设色彩主题。",
 | 
			
		||||
  "settingsFeatures": "功能",
 | 
			
		||||
  "settingsNotifyWithHaptic": "新通知时振动",
 | 
			
		||||
  "settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
 | 
			
		||||
  "settingsExpandPostLink": "展开帖子链接",
 | 
			
		||||
  "settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。",
 | 
			
		||||
  "settingsExpandChatLink": "展开聊天链接",
 | 
			
		||||
  "settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。",
 | 
			
		||||
  "settingsNetwork": "网络",
 | 
			
		||||
  "settingsNetworkServer": "HyperNet 服务器",
 | 
			
		||||
  "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
 | 
			
		||||
@@ -213,8 +221,9 @@
 | 
			
		||||
  "sensitiveContentCollapsed": "敏感内容已折叠。",
 | 
			
		||||
  "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
 | 
			
		||||
  "sensitiveContentReveal": "显示内容",
 | 
			
		||||
  "serverConnecting": "正在连接服务器…",
 | 
			
		||||
  "serverDisconnected": "已与服务器断开连接",
 | 
			
		||||
  "serverConnecting": "正在连接…",
 | 
			
		||||
  "serverDisconnected": "已断开连接",
 | 
			
		||||
  "serverConnected": "已连接",
 | 
			
		||||
  "fieldChatAlias": "频道别名",
 | 
			
		||||
  "fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
 | 
			
		||||
  "fieldChatName": "名称",
 | 
			
		||||
@@ -292,6 +301,7 @@
 | 
			
		||||
  "addAttachmentFromCameraPhoto": "拍摄照片",
 | 
			
		||||
  "addAttachmentFromCameraVideo": "拍摄视频",
 | 
			
		||||
  "addAttachmentFromRandomId": "通过访问 ID 链接",
 | 
			
		||||
  "attachmentDetailInfo": "附件详细信息",
 | 
			
		||||
  "attachmentPastedImage": "粘贴的图片",
 | 
			
		||||
  "attachmentInsertLink": "插入连接",
 | 
			
		||||
  "attachmentSetAsPostThumbnail": "设置为帖子缩略图",
 | 
			
		||||
@@ -547,5 +557,10 @@
 | 
			
		||||
  "postCategoryKnowledge": "知识",
 | 
			
		||||
  "postCategoryLiterature": "文学",
 | 
			
		||||
  "postCategoryFunny": "搞笑",
 | 
			
		||||
  "postCategoryUncategorized": "未分类"
 | 
			
		||||
  "postCategoryUncategorized": "未分类",
 | 
			
		||||
  "newsAllSources": "所有新闻",
 | 
			
		||||
  "newsReadingProviderSwap": "切换",
 | 
			
		||||
  "newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
 | 
			
		||||
  "newsReadingFromOriginal": "你正在阅读原始文章",
 | 
			
		||||
  "newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@
 | 
			
		||||
  "screenAccountProfileEdit": "編輯資料",
 | 
			
		||||
  "screenAbuseReport": "濫用檢舉",
 | 
			
		||||
  "screenSettings": "設置",
 | 
			
		||||
  "screenNews": "新聞",
 | 
			
		||||
  "screenAlbum": "相冊",
 | 
			
		||||
  "screenChat": "聊天",
 | 
			
		||||
  "screenChatManage": "編輯聊天頻道",
 | 
			
		||||
@@ -191,6 +192,13 @@
 | 
			
		||||
  "settingsColorSchemeDescription": "設置應用主題色。",
 | 
			
		||||
  "settingsColorSeed": "預設色彩主題",
 | 
			
		||||
  "settingsColorSeedDescription": "選擇一個預設色彩主題。",
 | 
			
		||||
  "settingsFeatures": "功能",
 | 
			
		||||
  "settingsNotifyWithHaptic": "新通知時振動",
 | 
			
		||||
  "settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
 | 
			
		||||
  "settingsExpandPostLink": "展開帖子鏈接",
 | 
			
		||||
  "settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
 | 
			
		||||
  "settingsExpandChatLink": "展開聊天鏈接",
 | 
			
		||||
  "settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
 | 
			
		||||
  "settingsNetwork": "網絡",
 | 
			
		||||
  "settingsNetworkServer": "HyperNet 服務器",
 | 
			
		||||
  "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
 | 
			
		||||
@@ -213,8 +221,9 @@
 | 
			
		||||
  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
			
		||||
  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
 | 
			
		||||
  "sensitiveContentReveal": "顯示內容",
 | 
			
		||||
  "serverConnecting": "正在連接服務器…",
 | 
			
		||||
  "serverDisconnected": "已與服務器斷開連接",
 | 
			
		||||
  "serverConnecting": "正在連接…",
 | 
			
		||||
  "serverDisconnected": "已斷開連接",
 | 
			
		||||
  "serverConnected": "已連接",
 | 
			
		||||
  "fieldChatAlias": "頻道別名",
 | 
			
		||||
  "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
 | 
			
		||||
  "fieldChatName": "名稱",
 | 
			
		||||
@@ -292,6 +301,7 @@
 | 
			
		||||
  "addAttachmentFromCameraPhoto": "拍攝照片",
 | 
			
		||||
  "addAttachmentFromCameraVideo": "拍攝視頻",
 | 
			
		||||
  "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
 | 
			
		||||
  "attachmentDetailInfo": "附件詳細信息",
 | 
			
		||||
  "attachmentPastedImage": "粘貼的圖片",
 | 
			
		||||
  "attachmentInsertLink": "插入連接",
 | 
			
		||||
  "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
 | 
			
		||||
@@ -547,5 +557,10 @@
 | 
			
		||||
  "postCategoryKnowledge": "知識",
 | 
			
		||||
  "postCategoryLiterature": "文學",
 | 
			
		||||
  "postCategoryFunny": "搞笑",
 | 
			
		||||
  "postCategoryUncategorized": "未分類"
 | 
			
		||||
  "postCategoryUncategorized": "未分類",
 | 
			
		||||
  "newsAllSources": "所有新聞",
 | 
			
		||||
  "newsReadingProviderSwap": "切換",
 | 
			
		||||
  "newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章",
 | 
			
		||||
  "newsReadingFromOriginal": "你正在閲讀原始文章",
 | 
			
		||||
  "newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@
 | 
			
		||||
  "screenAccountProfileEdit": "編輯資料",
 | 
			
		||||
  "screenAbuseReport": "濫用檢舉",
 | 
			
		||||
  "screenSettings": "設置",
 | 
			
		||||
  "screenNews": "新聞",
 | 
			
		||||
  "screenAlbum": "相冊",
 | 
			
		||||
  "screenChat": "聊天",
 | 
			
		||||
  "screenChatManage": "編輯聊天頻道",
 | 
			
		||||
@@ -191,6 +192,13 @@
 | 
			
		||||
  "settingsColorSchemeDescription": "設置應用主題色。",
 | 
			
		||||
  "settingsColorSeed": "預設色彩主題",
 | 
			
		||||
  "settingsColorSeedDescription": "選擇一個預設色彩主題。",
 | 
			
		||||
  "settingsFeatures": "功能",
 | 
			
		||||
  "settingsNotifyWithHaptic": "新通知時振動",
 | 
			
		||||
  "settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
 | 
			
		||||
  "settingsExpandPostLink": "展開帖子鏈接",
 | 
			
		||||
  "settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
 | 
			
		||||
  "settingsExpandChatLink": "展開聊天鏈接",
 | 
			
		||||
  "settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
 | 
			
		||||
  "settingsNetwork": "網絡",
 | 
			
		||||
  "settingsNetworkServer": "HyperNet 服務器",
 | 
			
		||||
  "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
 | 
			
		||||
@@ -213,8 +221,9 @@
 | 
			
		||||
  "sensitiveContentCollapsed": "敏感內容已摺疊。",
 | 
			
		||||
  "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
 | 
			
		||||
  "sensitiveContentReveal": "顯示內容",
 | 
			
		||||
  "serverConnecting": "正在連接服務器…",
 | 
			
		||||
  "serverDisconnected": "已與服務器斷開連接",
 | 
			
		||||
  "serverConnecting": "正在連接…",
 | 
			
		||||
  "serverDisconnected": "已斷開連接",
 | 
			
		||||
  "serverConnected": "已連接",
 | 
			
		||||
  "fieldChatAlias": "頻道別名",
 | 
			
		||||
  "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
 | 
			
		||||
  "fieldChatName": "名稱",
 | 
			
		||||
@@ -292,6 +301,7 @@
 | 
			
		||||
  "addAttachmentFromCameraPhoto": "拍攝照片",
 | 
			
		||||
  "addAttachmentFromCameraVideo": "拍攝視頻",
 | 
			
		||||
  "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
 | 
			
		||||
  "attachmentDetailInfo": "附件詳細信息",
 | 
			
		||||
  "attachmentPastedImage": "粘貼的圖片",
 | 
			
		||||
  "attachmentInsertLink": "插入連接",
 | 
			
		||||
  "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
 | 
			
		||||
@@ -547,5 +557,10 @@
 | 
			
		||||
  "postCategoryKnowledge": "知識",
 | 
			
		||||
  "postCategoryLiterature": "文學",
 | 
			
		||||
  "postCategoryFunny": "搞笑",
 | 
			
		||||
  "postCategoryUncategorized": "未分類"
 | 
			
		||||
  "postCategoryUncategorized": "未分類",
 | 
			
		||||
  "newsAllSources": "所有新聞",
 | 
			
		||||
  "newsReadingProviderSwap": "切換",
 | 
			
		||||
  "newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章",
 | 
			
		||||
  "newsReadingFromOriginal": "你正在閱讀原始文章",
 | 
			
		||||
  "newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										100
									
								
								ios/Podfile.lock
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								ios/Podfile.lock
									
									
									
									
									
								
							@@ -43,58 +43,58 @@ PODS:
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - file_saver (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - Firebase/Analytics (11.4.0):
 | 
			
		||||
  - Firebase/Analytics (11.6.0):
 | 
			
		||||
    - Firebase/Core
 | 
			
		||||
  - Firebase/Core (11.4.0):
 | 
			
		||||
  - Firebase/Core (11.6.0):
 | 
			
		||||
    - Firebase/CoreOnly
 | 
			
		||||
    - FirebaseAnalytics (~> 11.4.0)
 | 
			
		||||
  - Firebase/CoreOnly (11.4.0):
 | 
			
		||||
    - FirebaseCore (= 11.4.0)
 | 
			
		||||
  - Firebase/Messaging (11.4.0):
 | 
			
		||||
    - FirebaseAnalytics (~> 11.6.0)
 | 
			
		||||
  - Firebase/CoreOnly (11.6.0):
 | 
			
		||||
    - FirebaseCore (~> 11.6.0)
 | 
			
		||||
  - Firebase/Messaging (11.6.0):
 | 
			
		||||
    - Firebase/CoreOnly
 | 
			
		||||
    - FirebaseMessaging (~> 11.4.0)
 | 
			
		||||
  - firebase_analytics (11.3.6):
 | 
			
		||||
    - Firebase/Analytics (= 11.4.0)
 | 
			
		||||
    - FirebaseMessaging (~> 11.6.0)
 | 
			
		||||
  - firebase_analytics (11.4.0):
 | 
			
		||||
    - Firebase/Analytics (= 11.6.0)
 | 
			
		||||
    - firebase_core
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - firebase_core (3.9.0):
 | 
			
		||||
    - Firebase/CoreOnly (= 11.4.0)
 | 
			
		||||
  - firebase_core (3.10.0):
 | 
			
		||||
    - Firebase/CoreOnly (= 11.6.0)
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - firebase_messaging (15.1.6):
 | 
			
		||||
    - Firebase/Messaging (= 11.4.0)
 | 
			
		||||
  - firebase_messaging (15.2.0):
 | 
			
		||||
    - Firebase/Messaging (= 11.6.0)
 | 
			
		||||
    - firebase_core
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - FirebaseAnalytics (11.4.0):
 | 
			
		||||
    - FirebaseAnalytics/AdIdSupport (= 11.4.0)
 | 
			
		||||
    - FirebaseCore (~> 11.0)
 | 
			
		||||
  - FirebaseAnalytics (11.6.0):
 | 
			
		||||
    - FirebaseAnalytics/AdIdSupport (= 11.6.0)
 | 
			
		||||
    - FirebaseCore (~> 11.6.0)
 | 
			
		||||
    - FirebaseInstallations (~> 11.0)
 | 
			
		||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/Network (~> 8.0)
 | 
			
		||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
			
		||||
    - nanopb (~> 3.30910.0)
 | 
			
		||||
  - FirebaseAnalytics/AdIdSupport (11.4.0):
 | 
			
		||||
    - FirebaseCore (~> 11.0)
 | 
			
		||||
  - FirebaseAnalytics/AdIdSupport (11.6.0):
 | 
			
		||||
    - FirebaseCore (~> 11.6.0)
 | 
			
		||||
    - FirebaseInstallations (~> 11.0)
 | 
			
		||||
    - GoogleAppMeasurement (= 11.4.0)
 | 
			
		||||
    - GoogleAppMeasurement (= 11.6.0)
 | 
			
		||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/Network (~> 8.0)
 | 
			
		||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
			
		||||
    - nanopb (~> 3.30910.0)
 | 
			
		||||
  - FirebaseCore (11.4.0):
 | 
			
		||||
    - FirebaseCoreInternal (~> 11.0)
 | 
			
		||||
  - FirebaseCore (11.6.0):
 | 
			
		||||
    - FirebaseCoreInternal (~> 11.6.0)
 | 
			
		||||
    - GoogleUtilities/Environment (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/Logger (~> 8.0)
 | 
			
		||||
  - FirebaseCoreInternal (11.6.0):
 | 
			
		||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
			
		||||
  - FirebaseInstallations (11.4.0):
 | 
			
		||||
    - FirebaseCore (~> 11.0)
 | 
			
		||||
  - FirebaseInstallations (11.6.0):
 | 
			
		||||
    - FirebaseCore (~> 11.6.0)
 | 
			
		||||
    - GoogleUtilities/Environment (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/UserDefaults (~> 8.0)
 | 
			
		||||
    - PromisesObjC (~> 2.4)
 | 
			
		||||
  - FirebaseMessaging (11.4.0):
 | 
			
		||||
    - FirebaseCore (~> 11.0)
 | 
			
		||||
  - FirebaseMessaging (11.6.0):
 | 
			
		||||
    - FirebaseCore (~> 11.6.0)
 | 
			
		||||
    - FirebaseInstallations (~> 11.0)
 | 
			
		||||
    - GoogleDataTransport (~> 10.0)
 | 
			
		||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
			
		||||
@@ -105,32 +105,39 @@ PODS:
 | 
			
		||||
  - Flutter (1.0.0)
 | 
			
		||||
  - flutter_app_update (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - flutter_inappwebview_ios (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - flutter_inappwebview_ios/Core (= 0.0.1)
 | 
			
		||||
    - OrderedSet (~> 6.0.3)
 | 
			
		||||
  - flutter_inappwebview_ios/Core (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - OrderedSet (~> 6.0.3)
 | 
			
		||||
  - flutter_native_splash (2.4.3):
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - flutter_udid (0.0.1):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - SAMKeychain
 | 
			
		||||
  - flutter_webrtc (0.12.2):
 | 
			
		||||
  - flutter_webrtc (0.12.6):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - WebRTC-SDK (= 125.6422.06)
 | 
			
		||||
  - gal (1.0.0):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - GoogleAppMeasurement (11.4.0):
 | 
			
		||||
    - GoogleAppMeasurement/AdIdSupport (= 11.4.0)
 | 
			
		||||
  - GoogleAppMeasurement (11.6.0):
 | 
			
		||||
    - GoogleAppMeasurement/AdIdSupport (= 11.6.0)
 | 
			
		||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/Network (~> 8.0)
 | 
			
		||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
			
		||||
    - nanopb (~> 3.30910.0)
 | 
			
		||||
  - GoogleAppMeasurement/AdIdSupport (11.4.0):
 | 
			
		||||
    - GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0)
 | 
			
		||||
  - GoogleAppMeasurement/AdIdSupport (11.6.0):
 | 
			
		||||
    - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
 | 
			
		||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/Network (~> 8.0)
 | 
			
		||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
			
		||||
    - nanopb (~> 3.30910.0)
 | 
			
		||||
  - GoogleAppMeasurement/WithoutAdIdSupport (11.4.0):
 | 
			
		||||
  - GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
 | 
			
		||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/Network (~> 8.0)
 | 
			
		||||
@@ -173,7 +180,7 @@ PODS:
 | 
			
		||||
  - in_app_review (2.0.0):
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - Kingfisher (8.1.3)
 | 
			
		||||
  - livekit_client (2.3.4):
 | 
			
		||||
  - livekit_client (2.3.5):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - flutter_webrtc
 | 
			
		||||
    - WebRTC-SDK (= 125.6422.06)
 | 
			
		||||
@@ -188,6 +195,7 @@ PODS:
 | 
			
		||||
    - nanopb/encode (= 3.30910.0)
 | 
			
		||||
  - nanopb/decode (3.30910.0)
 | 
			
		||||
  - nanopb/encode (3.30910.0)
 | 
			
		||||
  - OrderedSet (6.0.3)
 | 
			
		||||
  - package_info_plus (0.4.5):
 | 
			
		||||
    - Flutter
 | 
			
		||||
  - pasteboard (0.0.1):
 | 
			
		||||
@@ -239,6 +247,7 @@ DEPENDENCIES:
 | 
			
		||||
  - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
 | 
			
		||||
  - Flutter (from `Flutter`)
 | 
			
		||||
  - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
 | 
			
		||||
  - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
 | 
			
		||||
  - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
 | 
			
		||||
  - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
 | 
			
		||||
  - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
 | 
			
		||||
@@ -282,6 +291,7 @@ SPEC REPOS:
 | 
			
		||||
    - GoogleUtilities
 | 
			
		||||
    - Kingfisher
 | 
			
		||||
    - nanopb
 | 
			
		||||
    - OrderedSet
 | 
			
		||||
    - PromisesObjC
 | 
			
		||||
    - SAMKeychain
 | 
			
		||||
    - SDWebImage
 | 
			
		||||
@@ -309,6 +319,8 @@ EXTERNAL SOURCES:
 | 
			
		||||
    :path: Flutter
 | 
			
		||||
  flutter_app_update:
 | 
			
		||||
    :path: ".symlinks/plugins/flutter_app_update/ios"
 | 
			
		||||
  flutter_inappwebview_ios:
 | 
			
		||||
    :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
 | 
			
		||||
  flutter_native_splash:
 | 
			
		||||
    :path: ".symlinks/plugins/flutter_native_splash/ios"
 | 
			
		||||
  flutter_udid:
 | 
			
		||||
@@ -369,33 +381,35 @@ SPEC CHECKSUMS:
 | 
			
		||||
  DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
 | 
			
		||||
  file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
 | 
			
		||||
  file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
 | 
			
		||||
  Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
 | 
			
		||||
  firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75
 | 
			
		||||
  firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10
 | 
			
		||||
  firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9
 | 
			
		||||
  FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
 | 
			
		||||
  FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
 | 
			
		||||
  Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
 | 
			
		||||
  firebase_analytics: 07bd7cfbac54bfcdccf2bb2530f9a65486f7ef3f
 | 
			
		||||
  firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10
 | 
			
		||||
  firebase_messaging: e2f0ba891b1509668c07f5099761518a5af8fe3c
 | 
			
		||||
  FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
 | 
			
		||||
  FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
 | 
			
		||||
  FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
 | 
			
		||||
  FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
 | 
			
		||||
  FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
 | 
			
		||||
  FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
 | 
			
		||||
  FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
 | 
			
		||||
  Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
 | 
			
		||||
  flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
 | 
			
		||||
  flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
 | 
			
		||||
  flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
 | 
			
		||||
  flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
 | 
			
		||||
  flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563
 | 
			
		||||
  flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
 | 
			
		||||
  gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
 | 
			
		||||
  GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
 | 
			
		||||
  GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
 | 
			
		||||
  GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
 | 
			
		||||
  GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
 | 
			
		||||
  home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
 | 
			
		||||
  image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
 | 
			
		||||
  in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
 | 
			
		||||
  Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
 | 
			
		||||
  livekit_client: 4eaa7a2968fc7e7c57888f43d90394547cc8d9e9
 | 
			
		||||
  livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
 | 
			
		||||
  media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
 | 
			
		||||
  media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
 | 
			
		||||
  media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
 | 
			
		||||
  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
			
		||||
  OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
 | 
			
		||||
  package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
 | 
			
		||||
  pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
 | 
			
		||||
  path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
 | 
			
		||||
 
 | 
			
		||||
@@ -74,6 +74,7 @@ class ChatMessageController extends ChangeNotifier {
 | 
			
		||||
    _wsSubscription = _ws.stream.stream.listen((event) {
 | 
			
		||||
      switch (event.method) {
 | 
			
		||||
        case 'events.new':
 | 
			
		||||
          if (event.payload?['channel_id'] != channel?.id) break;
 | 
			
		||||
          final payload = SnChatMessage.fromJson(event.payload!);
 | 
			
		||||
          _addMessage(payload);
 | 
			
		||||
          break;
 | 
			
		||||
 
 | 
			
		||||
@@ -260,7 +260,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
 | 
			
		||||
    try {
 | 
			
		||||
      final cfg = context.read<ConfigProvider>();
 | 
			
		||||
      WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
        cfg.calcDrawerSize(context);
 | 
			
		||||
        cfg.calcDrawerSize(context, withMediaQuery: true);
 | 
			
		||||
      });
 | 
			
		||||
      final home = context.read<HomeWidgetProvider>();
 | 
			
		||||
      await home.initialize();
 | 
			
		||||
@@ -279,6 +279,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
 | 
			
		||||
      await ws.tryConnect();
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      final notify = context.read<NotificationProvider>();
 | 
			
		||||
      notify.listen();
 | 
			
		||||
      await notify.registerPushNotifications();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,9 @@ const kAppbarTransparentStoreKey = 'app_bar_transparent';
 | 
			
		||||
const kAppBackgroundStoreKey = 'app_has_background';
 | 
			
		||||
const kAppColorSchemeStoreKey = 'app_color_scheme';
 | 
			
		||||
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
 | 
			
		||||
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
 | 
			
		||||
const kAppExpandPostLink = 'app_expand_post_link';
 | 
			
		||||
const kAppExpandChatLink = 'app_expand_chat_link';
 | 
			
		||||
 | 
			
		||||
const Map<String, FilterQuality> kImageQualityLevel = {
 | 
			
		||||
  'settingsImageQualityLowest': FilterQuality.none,
 | 
			
		||||
@@ -38,14 +41,22 @@ class ConfigProvider extends ChangeNotifier {
 | 
			
		||||
  bool drawerIsCollapsed = false;
 | 
			
		||||
  bool drawerIsExpanded = false;
 | 
			
		||||
 | 
			
		||||
  void calcDrawerSize(BuildContext context) {
 | 
			
		||||
  void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
 | 
			
		||||
    bool newDrawerIsCollapsed = false;
 | 
			
		||||
    bool newDrawerIsExpanded = false;
 | 
			
		||||
    if (withMediaQuery) {
 | 
			
		||||
      newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450;
 | 
			
		||||
      newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451;
 | 
			
		||||
    } else {
 | 
			
		||||
      final rpb = ResponsiveBreakpoints.of(context);
 | 
			
		||||
    final newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
 | 
			
		||||
    final newDrawerIsExpanded = rpb.largerThan(TABLET)
 | 
			
		||||
      newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
 | 
			
		||||
      newDrawerIsCollapsed = rpb.largerThan(TABLET)
 | 
			
		||||
          ? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
 | 
			
		||||
              ? false
 | 
			
		||||
              : true
 | 
			
		||||
          : false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) {
 | 
			
		||||
      drawerIsExpanded = newDrawerIsExpanded;
 | 
			
		||||
      drawerIsCollapsed = newDrawerIsCollapsed;
 | 
			
		||||
 
 | 
			
		||||
@@ -58,6 +58,11 @@ class NavigationProvider extends ChangeNotifier {
 | 
			
		||||
      screen: 'realm',
 | 
			
		||||
      label: 'screenRealm',
 | 
			
		||||
    ),
 | 
			
		||||
    AppNavDestination(
 | 
			
		||||
      icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
 | 
			
		||||
      screen: 'news',
 | 
			
		||||
      label: 'screenNews',
 | 
			
		||||
    ),
 | 
			
		||||
    AppNavDestination(
 | 
			
		||||
      icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
 | 
			
		||||
      screen: 'album',
 | 
			
		||||
@@ -83,8 +88,7 @@ class NavigationProvider extends ChangeNotifier {
 | 
			
		||||
 | 
			
		||||
  List<AppNavDestination> destinations = [];
 | 
			
		||||
 | 
			
		||||
  int get pinnedDestinationCount =>
 | 
			
		||||
      destinations.where((ele) => ele.isPinned).length;
 | 
			
		||||
  int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
 | 
			
		||||
 | 
			
		||||
  NavigationProvider() {
 | 
			
		||||
    buildDestinations(kDefaultPinnedDestination);
 | 
			
		||||
@@ -113,17 +117,13 @@ class NavigationProvider extends ChangeNotifier {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool isIndexInRange(int min, int max) {
 | 
			
		||||
    return _currentIndex != null &&
 | 
			
		||||
        _currentIndex! >= min &&
 | 
			
		||||
        _currentIndex! < max;
 | 
			
		||||
    return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void autoDetectIndex(GoRouter? state) {
 | 
			
		||||
    if (state == null) return;
 | 
			
		||||
    final idx = destinations.indexWhere(
 | 
			
		||||
      (ele) =>
 | 
			
		||||
          ele.screen ==
 | 
			
		||||
          state.routerDelegate.currentConfiguration.last.route.name,
 | 
			
		||||
      (ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name,
 | 
			
		||||
    );
 | 
			
		||||
    _currentIndex = idx == -1 ? null : idx;
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
 
 | 
			
		||||
@@ -4,18 +4,26 @@ import 'dart:io';
 | 
			
		||||
import 'package:firebase_messaging/firebase_messaging.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:flutter_udid/flutter_udid.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:surface/providers/config.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/providers/websocket.dart';
 | 
			
		||||
import 'package:surface/types/notification.dart';
 | 
			
		||||
 | 
			
		||||
class NotificationProvider extends ChangeNotifier {
 | 
			
		||||
  late final SnNetworkProvider _sn;
 | 
			
		||||
  late final UserProvider _ua;
 | 
			
		||||
  late final WebSocketProvider _ws;
 | 
			
		||||
  late final ConfigProvider _cfg;
 | 
			
		||||
 | 
			
		||||
  NotificationProvider(BuildContext context) {
 | 
			
		||||
    _sn = context.read<SnNetworkProvider>();
 | 
			
		||||
    _ua = context.read<UserProvider>();
 | 
			
		||||
    _ws = context.read<WebSocketProvider>();
 | 
			
		||||
    _cfg = context.read<ConfigProvider>();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> registerPushNotifications() async {
 | 
			
		||||
@@ -62,4 +70,23 @@ class NotificationProvider extends ChangeNotifier {
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  List<SnNotification> notifications = List.empty(growable: true);
 | 
			
		||||
 | 
			
		||||
  void listen() {
 | 
			
		||||
    _ws.stream.stream.listen((event) {
 | 
			
		||||
      if (event.method == 'notifications.new') {
 | 
			
		||||
        final notification = SnNotification.fromJson(event.payload!);
 | 
			
		||||
        notifications.add(notification);
 | 
			
		||||
        notifyListeners();
 | 
			
		||||
        final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
 | 
			
		||||
        if (doHaptic) HapticFeedback.lightImpact();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void clear() {
 | 
			
		||||
    notifications.clear();
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										188
									
								
								lib/router.dart
									
									
									
									
									
								
							
							
						
						
									
										188
									
								
								lib/router.dart
									
									
									
									
									
								
							@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:surface/screens/abuse_report.dart';
 | 
			
		||||
import 'package:surface/screens/account.dart';
 | 
			
		||||
import 'package:surface/screens/account/pfp.dart';
 | 
			
		||||
import 'package:surface/screens/account/profile_page.dart';
 | 
			
		||||
import 'package:surface/screens/account/profile_edit.dart';
 | 
			
		||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
 | 
			
		||||
import 'package:surface/screens/account/publishers/publisher_new.dart';
 | 
			
		||||
@@ -19,6 +19,8 @@ import 'package:surface/screens/chat/room.dart';
 | 
			
		||||
import 'package:surface/screens/explore.dart';
 | 
			
		||||
import 'package:surface/screens/friend.dart';
 | 
			
		||||
import 'package:surface/screens/home.dart';
 | 
			
		||||
import 'package:surface/screens/news/news_detail.dart';
 | 
			
		||||
import 'package:surface/screens/news/news_list.dart';
 | 
			
		||||
import 'package:surface/screens/notification.dart';
 | 
			
		||||
import 'package:surface/screens/post/post_detail.dart';
 | 
			
		||||
import 'package:surface/screens/post/post_editor.dart';
 | 
			
		||||
@@ -31,35 +33,33 @@ import 'package:surface/screens/settings.dart';
 | 
			
		||||
import 'package:surface/screens/sharing.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/about.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_background.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
Widget _fadeThroughTransition(
 | 
			
		||||
    BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
 | 
			
		||||
  return FadeThroughTransition(
 | 
			
		||||
    animation: animation,
 | 
			
		||||
    secondaryAnimation: secondaryAnimation,
 | 
			
		||||
    fillColor: Colors.transparent,
 | 
			
		||||
    child: child,
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final _appRoutes = [
 | 
			
		||||
  ShellRoute(
 | 
			
		||||
    builder: (context, state, child) => AppPageScaffold(
 | 
			
		||||
      body: child,
 | 
			
		||||
      showAppBar: false,
 | 
			
		||||
    ),
 | 
			
		||||
    routes: [
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/',
 | 
			
		||||
    name: 'home',
 | 
			
		||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
			
		||||
          child: const HomeScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
    builder: (context, state) => const HomeScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/posts',
 | 
			
		||||
    name: 'explore',
 | 
			
		||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
			
		||||
          child: const ExploreScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
    builder: (context, state) => const ExploreScreen(),
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/write/:mode',
 | 
			
		||||
        name: 'postEditor',
 | 
			
		||||
            builder: (context, state) => AppBackground(
 | 
			
		||||
              child: PostEditorScreen(
 | 
			
		||||
        builder: (context, state) => PostEditorScreen(
 | 
			
		||||
          mode: state.pathParameters['mode']!,
 | 
			
		||||
          postEditId: int.tryParse(
 | 
			
		||||
            state.uri.queryParameters['editing'] ?? '',
 | 
			
		||||
@@ -73,216 +73,160 @@ final _appRoutes = [
 | 
			
		||||
          extraProps: state.extra as PostEditorExtraProps?,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
          ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/search',
 | 
			
		||||
        name: 'postSearch',
 | 
			
		||||
            builder: (context, state) => AppBackground(
 | 
			
		||||
              child: PostSearchScreen(
 | 
			
		||||
        builder: (context, state) => PostSearchScreen(
 | 
			
		||||
          initialTags: state.uri.queryParameters['tags']?.split(','),
 | 
			
		||||
          initialCategories: state.uri.queryParameters['categories']?.split(','),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
          ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/publishers/:name',
 | 
			
		||||
        name: 'postPublisher',
 | 
			
		||||
            builder: (context, state) => AppBackground(
 | 
			
		||||
              child: PostPublisherScreen(name: state.pathParameters['name']!),
 | 
			
		||||
            ),
 | 
			
		||||
        builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:slug',
 | 
			
		||||
        name: 'postDetail',
 | 
			
		||||
            builder: (context, state) => AppBackground(
 | 
			
		||||
              child: PostDetailScreen(
 | 
			
		||||
        builder: (context, state) => PostDetailScreen(
 | 
			
		||||
          slug: state.pathParameters['slug']!,
 | 
			
		||||
          preload: state.extra as SnPost?,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
          ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/account',
 | 
			
		||||
    name: 'account',
 | 
			
		||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
			
		||||
          child: const AccountScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
        routes: [],
 | 
			
		||||
    builder: (context, state) => const AccountScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/chat',
 | 
			
		||||
    name: 'chat',
 | 
			
		||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
			
		||||
          child: const ChatScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
    builder: (context, state) => const ChatScreen(),
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:scope/:alias',
 | 
			
		||||
        name: 'chatRoom',
 | 
			
		||||
            builder: (context, state) => AppBackground(
 | 
			
		||||
              child: ChatRoomScreen(
 | 
			
		||||
        builder: (context, state) => ChatRoomScreen(
 | 
			
		||||
          scope: state.pathParameters['scope']!,
 | 
			
		||||
          alias: state.pathParameters['alias']!,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
          ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:scope/:alias/call',
 | 
			
		||||
        name: 'chatCallRoom',
 | 
			
		||||
            builder: (context, state) => AppBackground(
 | 
			
		||||
              child: CallRoomScreen(
 | 
			
		||||
        builder: (context, state) => CallRoomScreen(
 | 
			
		||||
          scope: state.pathParameters['scope']!,
 | 
			
		||||
          alias: state.pathParameters['alias']!,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
          ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:scope/:alias/detail',
 | 
			
		||||
        name: 'channelDetail',
 | 
			
		||||
            builder: (context, state) => AppBackground(
 | 
			
		||||
              child: ChannelDetailScreen(
 | 
			
		||||
        builder: (context, state) => ChannelDetailScreen(
 | 
			
		||||
          scope: state.pathParameters['scope']!,
 | 
			
		||||
          alias: state.pathParameters['alias']!,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
          ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/manage',
 | 
			
		||||
        name: 'chatManage',
 | 
			
		||||
            pageBuilder: (context, state) => CustomTransitionPage(
 | 
			
		||||
              child: ChatManageScreen(
 | 
			
		||||
        builder: (context, state) => ChatManageScreen(
 | 
			
		||||
          editingChannelAlias: state.uri.queryParameters['editing'],
 | 
			
		||||
        ),
 | 
			
		||||
              transitionsBuilder: (context, animation, secondaryAnimation, child) {
 | 
			
		||||
                return FadeThroughTransition(
 | 
			
		||||
                  animation: animation,
 | 
			
		||||
                  secondaryAnimation: secondaryAnimation,
 | 
			
		||||
                  fillColor: Colors.transparent,
 | 
			
		||||
                  child: AppBackground(
 | 
			
		||||
                    child: child,
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/:alias',
 | 
			
		||||
            name: 'realmDetail',
 | 
			
		||||
            builder: (context, state) => AppBackground(
 | 
			
		||||
              child: RealmDetailScreen(alias: state.pathParameters['alias']!),
 | 
			
		||||
            ),
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/realm',
 | 
			
		||||
    name: 'realm',
 | 
			
		||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
			
		||||
    pageBuilder: (context, state) => CustomTransitionPage(
 | 
			
		||||
      transitionsBuilder: _fadeThroughTransition,
 | 
			
		||||
      child: const RealmScreen(),
 | 
			
		||||
    ),
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:alias',
 | 
			
		||||
        name: 'realmDetail',
 | 
			
		||||
        builder: (context, state) => RealmDetailScreen(alias: state.pathParameters['alias']!),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/manage',
 | 
			
		||||
        name: 'realmManage',
 | 
			
		||||
            pageBuilder: (context, state) => CustomTransitionPage(
 | 
			
		||||
              child: RealmManageScreen(
 | 
			
		||||
        builder: (context, state) => RealmManageScreen(
 | 
			
		||||
          editingRealmAlias: state.uri.queryParameters['editing'],
 | 
			
		||||
        ),
 | 
			
		||||
              transitionsBuilder: (context, animation, secondaryAnimation, child) {
 | 
			
		||||
                return FadeThroughTransition(
 | 
			
		||||
                  animation: animation,
 | 
			
		||||
                  secondaryAnimation: secondaryAnimation,
 | 
			
		||||
                  fillColor: Colors.transparent,
 | 
			
		||||
                  child: AppBackground(
 | 
			
		||||
                    child: child,
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/news',
 | 
			
		||||
    name: 'news',
 | 
			
		||||
    builder: (context, state) => const NewsScreen(),
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:hash',
 | 
			
		||||
        name: 'newsDetail',
 | 
			
		||||
        builder: (context, state) => NewsDetailScreen(
 | 
			
		||||
          hash: state.pathParameters['hash']!,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    ]
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/album',
 | 
			
		||||
    name: 'album',
 | 
			
		||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
			
		||||
          child: const AlbumScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
    builder: (context, state) => const AlbumScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/friend',
 | 
			
		||||
    name: 'friend',
 | 
			
		||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
			
		||||
          child: const FriendScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
    builder: (context, state) => const FriendScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/notification',
 | 
			
		||||
    name: 'notification',
 | 
			
		||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
			
		||||
          child: const NotificationScreen(),
 | 
			
		||||
    builder: (context, state) => const NotificationScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
  ShellRoute(
 | 
			
		||||
    builder: (context, state, child) => AppPageScaffold(body: child),
 | 
			
		||||
    routes: [
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/auth/login',
 | 
			
		||||
    name: 'authLogin',
 | 
			
		||||
        builder: (context, state) => const AppBackground(
 | 
			
		||||
          child: LoginScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
    builder: (context, state) => LoginScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/auth/register',
 | 
			
		||||
    name: 'authRegister',
 | 
			
		||||
        builder: (context, state) => const AppBackground(
 | 
			
		||||
          child: RegisterScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
    builder: (context, state) => RegisterScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/reports',
 | 
			
		||||
    name: 'abuseReport',
 | 
			
		||||
        builder: (context, state) => const AppBackground(
 | 
			
		||||
          child: AbuseReportScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
    builder: (context, state) => AbuseReportScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/account/profile/edit',
 | 
			
		||||
    name: 'accountProfileEdit',
 | 
			
		||||
        builder: (context, state) => const AppBackground(
 | 
			
		||||
          child: ProfileEditScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
    builder: (context, state) => ProfileEditScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/account/publishers',
 | 
			
		||||
    name: 'accountPublishers',
 | 
			
		||||
        builder: (context, state) => const AppBackground(
 | 
			
		||||
          child: PublisherScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
    builder: (context, state) => PublisherScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/account/publishers/new',
 | 
			
		||||
    name: 'accountPublisherNew',
 | 
			
		||||
        builder: (context, state) => const AppBackground(
 | 
			
		||||
          child: AccountPublisherNewScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
    builder: (context, state) => AccountPublisherNewScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/account/publishers/edit/:name',
 | 
			
		||||
    name: 'accountPublisherEdit',
 | 
			
		||||
        builder: (context, state) => AppBackground(
 | 
			
		||||
          child: AccountPublisherEditScreen(
 | 
			
		||||
    builder: (context, state) => AccountPublisherEditScreen(
 | 
			
		||||
      name: state.pathParameters['name']!,
 | 
			
		||||
    ),
 | 
			
		||||
  ),
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/account/:name',
 | 
			
		||||
    name: 'accountProfilePage',
 | 
			
		||||
@@ -290,29 +234,15 @@ final _appRoutes = [
 | 
			
		||||
      child: UserScreen(name: state.pathParameters['name']!),
 | 
			
		||||
    ),
 | 
			
		||||
  ),
 | 
			
		||||
  ShellRoute(
 | 
			
		||||
    builder: (context, state, child) => AppPageScaffold(body: child),
 | 
			
		||||
    routes: [
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/settings',
 | 
			
		||||
    name: 'settings',
 | 
			
		||||
        builder: (context, state) => const AppBackground(
 | 
			
		||||
          child: SettingsScreen(),
 | 
			
		||||
    builder: (context, state) => SettingsScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
  ),
 | 
			
		||||
  ShellRoute(
 | 
			
		||||
    builder: (context, state, child) => AppPageScaffold(body: child),
 | 
			
		||||
    routes: [
 | 
			
		||||
  GoRoute(
 | 
			
		||||
    path: '/about',
 | 
			
		||||
    name: 'about',
 | 
			
		||||
        builder: (context, state) => const AppBackground(
 | 
			
		||||
          child: AboutScreen(),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    ],
 | 
			
		||||
    builder: (context, state) => AboutScreen(),
 | 
			
		||||
  ),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
import '../types/account.dart';
 | 
			
		||||
 | 
			
		||||
@@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('screenAbuseReport').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          ListTile(
 | 
			
		||||
@@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
 | 
			
		||||
          else
 | 
			
		||||
            Expanded(
 | 
			
		||||
              child: ListView.builder(
 | 
			
		||||
                padding: EdgeInsets.only(top: 8),
 | 
			
		||||
                itemCount: _reports.length,
 | 
			
		||||
                itemBuilder: (context, idx) {
 | 
			
		||||
                  return ListTile(
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import 'package:surface/providers/websocket.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
class AccountScreen extends StatelessWidget {
 | 
			
		||||
  const AccountScreen({super.key});
 | 
			
		||||
@@ -20,7 +21,7 @@ class AccountScreen extends StatelessWidget {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final ua = context.watch<UserProvider>();
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        title: Text("screenAccount").tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
 | 
			
		||||
class ProfileEditScreen extends StatefulWidget {
 | 
			
		||||
@@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
            onDateTimeChanged: (DateTime newDate) {
 | 
			
		||||
              setState(() {
 | 
			
		||||
                _birthday = newDate;
 | 
			
		||||
                _birthdayController.text =
 | 
			
		||||
                    DateFormat(_kDateFormat).format(_birthday!);
 | 
			
		||||
                _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
 | 
			
		||||
              });
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
@@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
    if (image == null) return;
 | 
			
		||||
    if (!mounted) return;
 | 
			
		||||
 | 
			
		||||
    final ImageProvider imageProvider =
 | 
			
		||||
        kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
 | 
			
		||||
    final aspectRatios = place == 'banner'
 | 
			
		||||
        ? [CropAspectRatio(width: 16, height: 7)]
 | 
			
		||||
        : [CropAspectRatio(width: 1, height: 1)];
 | 
			
		||||
    final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
 | 
			
		||||
    final aspectRatios =
 | 
			
		||||
        place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
 | 
			
		||||
    final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
 | 
			
		||||
        ? await showCupertinoImageCropper(
 | 
			
		||||
            // ignore: use_build_context_synchronously
 | 
			
		||||
@@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
 | 
			
		||||
    final rawBytes =
 | 
			
		||||
        (await result.uiImage.toByteData(format: ImageByteFormat.png))!
 | 
			
		||||
            .buffer
 | 
			
		||||
            .asUint8List();
 | 
			
		||||
    final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final attachment = await attach.directUploadOne(
 | 
			
		||||
@@ -212,7 +207,12 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    return SingleChildScrollView(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('screenAccountProfileEdit').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: SingleChildScrollView(
 | 
			
		||||
        child: Column(
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
          children: [
 | 
			
		||||
@@ -229,8 +229,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
                      child: AspectRatio(
 | 
			
		||||
                        aspectRatio: 16 / 9,
 | 
			
		||||
                        child: Container(
 | 
			
		||||
                        color:
 | 
			
		||||
                            Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
                          color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
                          child: _banner != null
 | 
			
		||||
                              ? AutoResizeUniversalImage(
 | 
			
		||||
                                  sn.getAttachmentUrl(_banner!),
 | 
			
		||||
@@ -343,6 +342,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
 | 
			
		||||
            ).padding(horizontal: padding),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -241,6 +241,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      backgroundColor: Colors.transparent,
 | 
			
		||||
      body: CustomScrollView(
 | 
			
		||||
        controller: _scrollController,
 | 
			
		||||
        slivers: [
 | 
			
		||||
@@ -594,7 +595,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
 | 
			
		||||
                subtitle: Text('@${ele.name}'),
 | 
			
		||||
                trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  GoRouter.of(context).pushNamed(
 | 
			
		||||
                  GoRouter.of(context).goNamed(
 | 
			
		||||
                    'postPublisher',
 | 
			
		||||
                    pathParameters: {'name': ele.name},
 | 
			
		||||
                  );
 | 
			
		||||
@@ -18,6 +18,7 @@ import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
 | 
			
		||||
class AccountPublisherEditScreen extends StatefulWidget {
 | 
			
		||||
@@ -176,7 +177,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      body: SingleChildScrollView(
 | 
			
		||||
        child: Column(
 | 
			
		||||
          children: [
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
class AccountPublisherNewScreen extends StatefulWidget {
 | 
			
		||||
  const AccountPublisherNewScreen({super.key});
 | 
			
		||||
@@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return  AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('screenAccountPublisherNew').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: SingleChildScrollView(
 | 
			
		||||
        child: Column(
 | 
			
		||||
          children: [
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
class PublisherScreen extends StatefulWidget {
 | 
			
		||||
  const PublisherScreen({super.key});
 | 
			
		||||
@@ -32,8 +33,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final resp = await sn.client.get('/cgi/co/publishers/me');
 | 
			
		||||
      final List<SnPublisher> out = List<SnPublisher>.from(
 | 
			
		||||
          resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
 | 
			
		||||
      final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
 | 
			
		||||
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
 | 
			
		||||
@@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('screenAccountPublishers').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          ListTile(
 | 
			
		||||
@@ -62,9 +66,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
			
		||||
            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
            leading: const Icon(Symbols.add_circle),
 | 
			
		||||
            onTap: () {
 | 
			
		||||
              GoRouter.of(context)
 | 
			
		||||
                  .pushNamed('accountPublisherNew')
 | 
			
		||||
                  .then((value) {
 | 
			
		||||
              GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
 | 
			
		||||
                if (value == true) {
 | 
			
		||||
                  _publishers.clear();
 | 
			
		||||
                  _fetchPublishers();
 | 
			
		||||
@@ -75,6 +77,9 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
			
		||||
          const Divider(height: 1),
 | 
			
		||||
          LoadingIndicator(isActive: _isBusy),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: MediaQuery.removePadding(
 | 
			
		||||
              context: context,
 | 
			
		||||
              removeTop: true,
 | 
			
		||||
              child: RefreshIndicator(
 | 
			
		||||
                onRefresh: () {
 | 
			
		||||
                  _publishers.clear();
 | 
			
		||||
@@ -120,6 +125,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:uuid/uuid.dart';
 | 
			
		||||
 | 
			
		||||
class AlbumScreen extends StatefulWidget {
 | 
			
		||||
@@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      body: CustomScrollView(
 | 
			
		||||
        controller: _scrollController,
 | 
			
		||||
        slivers: [
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/types/auth.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
			
		||||
 | 
			
		||||
import '../../providers/websocket.dart';
 | 
			
		||||
@@ -35,7 +36,12 @@ class _LoginScreenState extends State<LoginScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Theme(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('screenAuthLogin').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: Theme(
 | 
			
		||||
        data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
 | 
			
		||||
        child: SingleChildScrollView(
 | 
			
		||||
          child: PageTransitionSwitcher(
 | 
			
		||||
@@ -96,6 +102,7 @@ class _LoginScreenState extends State<LoginScreen> {
 | 
			
		||||
            },
 | 
			
		||||
          ).padding(all: 24),
 | 
			
		||||
        ).center(),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -441,7 +448,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
 | 
			
		||||
 | 
			
		||||
      widget.onNext();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if(mounted) context.showErrorDialog(err);
 | 
			
		||||
      if (mounted) context.showErrorDialog(err);
 | 
			
		||||
      return;
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
			
		||||
 | 
			
		||||
class RegisterScreen extends StatefulWidget {
 | 
			
		||||
@@ -54,7 +55,12 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return StyledWidget(Container(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('screenAuthRegister').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: StyledWidget(Container(
 | 
			
		||||
        constraints: const BoxConstraints(maxWidth: 380),
 | 
			
		||||
        child: SingleChildScrollView(
 | 
			
		||||
          child: Column(
 | 
			
		||||
@@ -180,10 +186,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
			
		||||
                          'termAcceptNextWithAgree'.tr(),
 | 
			
		||||
                          textAlign: TextAlign.end,
 | 
			
		||||
                          style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
			
		||||
                          color: Theme.of(context)
 | 
			
		||||
                              .colorScheme
 | 
			
		||||
                              .onSurface
 | 
			
		||||
                              .withAlpha((255 * 0.75).round()),
 | 
			
		||||
                                color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
 | 
			
		||||
                              ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        Material(
 | 
			
		||||
@@ -223,6 +226,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
    )).padding(all: 24).center();
 | 
			
		||||
      )).padding(all: 24).center(),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ import 'package:surface/widgets/account/account_select.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/unauthorized_hint.dart';
 | 
			
		||||
import 'package:uuid/uuid.dart';
 | 
			
		||||
 | 
			
		||||
@@ -120,7 +121,7 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
    final ua = context.read<UserProvider>();
 | 
			
		||||
 | 
			
		||||
    if (!ua.isAuthorized) {
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
      return AppScaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          leading: AutoAppBarLeading(),
 | 
			
		||||
          title: Text('screenChat').tr(),
 | 
			
		||||
@@ -131,7 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        title: Text('screenChat').tr(),
 | 
			
		||||
@@ -195,6 +196,9 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
        children: [
 | 
			
		||||
          LoadingIndicator(isActive: _isBusy),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: MediaQuery.removePadding(
 | 
			
		||||
              context: context,
 | 
			
		||||
              removeTop: true,
 | 
			
		||||
              child: RefreshIndicator(
 | 
			
		||||
                onRefresh: () => Future.sync(() => _refreshChannels()),
 | 
			
		||||
                child: ListView.builder(
 | 
			
		||||
@@ -276,6 +280,7 @@ class _ChatScreenState extends State<ChatScreen> {
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -9,10 +9,12 @@ import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/chat_call.dart';
 | 
			
		||||
import 'package:surface/widgets/chat/call/call_controls.dart';
 | 
			
		||||
import 'package:surface/widgets/chat/call/call_participant.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
class CallRoomScreen extends StatefulWidget {
 | 
			
		||||
  final String scope;
 | 
			
		||||
  final String alias;
 | 
			
		||||
 | 
			
		||||
  const CallRoomScreen({super.key, required this.scope, required this.alias});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -35,8 +37,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
    return Stack(
 | 
			
		||||
      children: [
 | 
			
		||||
        Container(
 | 
			
		||||
          color:
 | 
			
		||||
              Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
 | 
			
		||||
          color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
 | 
			
		||||
          child: call.focusTrack != null
 | 
			
		||||
              ? InteractiveParticipantWidget(
 | 
			
		||||
                  isFixedAvatar: false,
 | 
			
		||||
@@ -71,8 +72,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
                      color: Theme.of(context).cardColor,
 | 
			
		||||
                      participant: track,
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        if (track.participant.sid !=
 | 
			
		||||
                            call.focusTrack?.participant.sid) {
 | 
			
		||||
                        if (track.participant.sid != call.focusTrack?.participant.sid) {
 | 
			
		||||
                          call.setFocusTrack(track);
 | 
			
		||||
                        }
 | 
			
		||||
                      },
 | 
			
		||||
@@ -114,14 +114,10 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
            child: ClipRRect(
 | 
			
		||||
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
              child: InteractiveParticipantWidget(
 | 
			
		||||
                color: Theme.of(context)
 | 
			
		||||
                    .colorScheme
 | 
			
		||||
                    .surfaceContainerHigh
 | 
			
		||||
                    .withOpacity(0.75),
 | 
			
		||||
                color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
 | 
			
		||||
                participant: track,
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  if (track.participant.sid !=
 | 
			
		||||
                      call.focusTrack?.participant.sid) {
 | 
			
		||||
                  if (track.participant.sid != call.focusTrack?.participant.sid) {
 | 
			
		||||
                    call.setFocusTrack(track);
 | 
			
		||||
                  }
 | 
			
		||||
                },
 | 
			
		||||
@@ -152,31 +148,24 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
    return ListenableBuilder(
 | 
			
		||||
        listenable: call,
 | 
			
		||||
        builder: (context, _) {
 | 
			
		||||
          return Scaffold(
 | 
			
		||||
          return AppScaffold(
 | 
			
		||||
            appBar: AppBar(
 | 
			
		||||
              title: RichText(
 | 
			
		||||
                textAlign: TextAlign.center,
 | 
			
		||||
                text: TextSpan(children: [
 | 
			
		||||
                  TextSpan(
 | 
			
		||||
                    text: 'call'.tr(),
 | 
			
		||||
                    style: Theme.of(context)
 | 
			
		||||
                        .textTheme
 | 
			
		||||
                        .titleLarge!
 | 
			
		||||
                        .copyWith(color: Colors.white),
 | 
			
		||||
                    style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
 | 
			
		||||
                  ),
 | 
			
		||||
                  const TextSpan(text: '\n'),
 | 
			
		||||
                  TextSpan(
 | 
			
		||||
                    text: call.lastDuration.toString(),
 | 
			
		||||
                    style: Theme.of(context)
 | 
			
		||||
                        .textTheme
 | 
			
		||||
                        .bodySmall!
 | 
			
		||||
                        .copyWith(color: Colors.white),
 | 
			
		||||
                    style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
 | 
			
		||||
                  ),
 | 
			
		||||
                ]),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            body: SafeArea(
 | 
			
		||||
              child: GestureDetector(
 | 
			
		||||
            body: GestureDetector(
 | 
			
		||||
              behavior: HitTestBehavior.translucent,
 | 
			
		||||
              child: Column(
 | 
			
		||||
                children: [
 | 
			
		||||
@@ -190,8 +179,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
                        Builder(builder: (context) {
 | 
			
		||||
                          final call = context.read<ChatCallProvider>();
 | 
			
		||||
                          final connectionQuality =
 | 
			
		||||
                                call.room.localParticipant?.connectionQuality ??
 | 
			
		||||
                                    livekit.ConnectionQuality.unknown;
 | 
			
		||||
                              call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown;
 | 
			
		||||
                          return Expanded(
 | 
			
		||||
                            child: Column(
 | 
			
		||||
                              mainAxisSize: MainAxisSize.min,
 | 
			
		||||
@@ -213,35 +201,24 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
                                  children: [
 | 
			
		||||
                                    Text(
 | 
			
		||||
                                      {
 | 
			
		||||
                                          livekit.ConnectionState.disconnected:
 | 
			
		||||
                                              'callStatusDisconnected'.tr(),
 | 
			
		||||
                                          livekit.ConnectionState.connected:
 | 
			
		||||
                                              'callStatusConnected'.tr(),
 | 
			
		||||
                                          livekit.ConnectionState.connecting:
 | 
			
		||||
                                              'callStatusConnecting'.tr(),
 | 
			
		||||
                                          livekit.ConnectionState.reconnecting:
 | 
			
		||||
                                              'callStatusReconnecting'.tr(),
 | 
			
		||||
                                        livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(),
 | 
			
		||||
                                        livekit.ConnectionState.connected: 'callStatusConnected'.tr(),
 | 
			
		||||
                                        livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(),
 | 
			
		||||
                                        livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(),
 | 
			
		||||
                                      }[call.room.connectionState]!,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    const Gap(6),
 | 
			
		||||
                                      if (connectionQuality !=
 | 
			
		||||
                                          livekit.ConnectionQuality.unknown)
 | 
			
		||||
                                    if (connectionQuality != livekit.ConnectionQuality.unknown)
 | 
			
		||||
                                      Icon(
 | 
			
		||||
                                        {
 | 
			
		||||
                                            livekit.ConnectionQuality.excellent:
 | 
			
		||||
                                                Icons.signal_cellular_alt,
 | 
			
		||||
                                            livekit.ConnectionQuality.good:
 | 
			
		||||
                                                Icons.signal_cellular_alt_2_bar,
 | 
			
		||||
                                            livekit.ConnectionQuality.poor:
 | 
			
		||||
                                                Icons.signal_cellular_alt_1_bar,
 | 
			
		||||
                                          livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt,
 | 
			
		||||
                                          livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar,
 | 
			
		||||
                                          livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar,
 | 
			
		||||
                                        }[connectionQuality],
 | 
			
		||||
                                        color: {
 | 
			
		||||
                                            livekit.ConnectionQuality.excellent:
 | 
			
		||||
                                                Colors.green,
 | 
			
		||||
                                            livekit.ConnectionQuality.good:
 | 
			
		||||
                                                Colors.orange,
 | 
			
		||||
                                            livekit.ConnectionQuality.poor:
 | 
			
		||||
                                                Colors.red,
 | 
			
		||||
                                          livekit.ConnectionQuality.excellent: Colors.green,
 | 
			
		||||
                                          livekit.ConnectionQuality.good: Colors.orange,
 | 
			
		||||
                                          livekit.ConnectionQuality.poor: Colors.red,
 | 
			
		||||
                                        }[connectionQuality],
 | 
			
		||||
                                        size: 16,
 | 
			
		||||
                                      )
 | 
			
		||||
@@ -263,9 +240,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
                        Row(
 | 
			
		||||
                          children: [
 | 
			
		||||
                            IconButton(
 | 
			
		||||
                                icon: _layoutMode == 0
 | 
			
		||||
                                    ? const Icon(Icons.view_list)
 | 
			
		||||
                                    : const Icon(Icons.grid_view),
 | 
			
		||||
                              icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view),
 | 
			
		||||
                              onPressed: () {
 | 
			
		||||
                                _switchLayout();
 | 
			
		||||
                              },
 | 
			
		||||
@@ -277,8 +252,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
                  ),
 | 
			
		||||
                  Expanded(
 | 
			
		||||
                    child: Material(
 | 
			
		||||
                        color:
 | 
			
		||||
                            Theme.of(context).colorScheme.surfaceContainerLow,
 | 
			
		||||
                      color: Theme.of(context).colorScheme.surfaceContainerLow,
 | 
			
		||||
                      child: Builder(
 | 
			
		||||
                        builder: (context) {
 | 
			
		||||
                          switch (_layoutMode) {
 | 
			
		||||
@@ -303,7 +277,6 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
			
		||||
              ),
 | 
			
		||||
              onTap: () {},
 | 
			
		||||
            ),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import 'package:surface/types/chat.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
			
		||||
 | 
			
		||||
class ChannelDetailScreen extends StatefulWidget {
 | 
			
		||||
@@ -189,7 +190,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
 | 
			
		||||
 | 
			
		||||
    final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import 'package:surface/types/realm.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:uuid/uuid.dart';
 | 
			
		||||
 | 
			
		||||
class ChatManageScreen extends StatefulWidget {
 | 
			
		||||
@@ -121,7 +122,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: widget.editingChannelAlias != null
 | 
			
		||||
            ? Text('screenChatManage').tr()
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ import 'package:surface/widgets/chat/chat_message_input.dart';
 | 
			
		||||
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
			
		||||
 | 
			
		||||
import '../../providers/user_directory.dart';
 | 
			
		||||
@@ -211,7 +212,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
			
		||||
    final call = context.watch<ChatCallProvider>();
 | 
			
		||||
    final ud = context.read<UserDirectoryProvider>();
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text(
 | 
			
		||||
          _channel?.type == 1
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import 'package:animations/animations.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
 | 
			
		||||
@@ -8,9 +9,11 @@ import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/post.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/screens/post/post_detail.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_item.dart';
 | 
			
		||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
			
		||||
 | 
			
		||||
@@ -93,7 +96,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      floatingActionButtonLocation: ExpandableFab.location,
 | 
			
		||||
      floatingActionButton: ExpandableFab(
 | 
			
		||||
        key: _fabKey,
 | 
			
		||||
@@ -210,6 +213,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            const SliverGap(12),
 | 
			
		||||
            SliverInfiniteList(
 | 
			
		||||
              itemCount: _posts.length,
 | 
			
		||||
              isLoading: _isBusy,
 | 
			
		||||
@@ -217,7 +221,10 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
			
		||||
              hasReachedMax: _postCount != null && _posts.length >= _postCount!,
 | 
			
		||||
              onFetchData: _fetchPosts,
 | 
			
		||||
              itemBuilder: (context, idx) {
 | 
			
		||||
                return GestureDetector(
 | 
			
		||||
                return Center(
 | 
			
		||||
                  child: OpenContainer(
 | 
			
		||||
                    closedBuilder: (_, __) => Container(
 | 
			
		||||
                      constraints: const BoxConstraints(maxWidth: 640),
 | 
			
		||||
                      child: PostItem(
 | 
			
		||||
                        data: _posts[idx],
 | 
			
		||||
                        maxWidth: 640,
 | 
			
		||||
@@ -228,16 +235,23 @@ class _ExploreScreenState extends State<ExploreScreen> {
 | 
			
		||||
                          _refreshPosts();
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).pushNamed(
 | 
			
		||||
                      'postDetail',
 | 
			
		||||
                      pathParameters: {'slug': _posts[idx].id.toString()},
 | 
			
		||||
                      extra: _posts[idx],
 | 
			
		||||
                    ),
 | 
			
		||||
                    openBuilder: (_, close) => PostDetailScreen(
 | 
			
		||||
                      slug: _posts[idx].id.toString(),
 | 
			
		||||
                      preload: _posts[idx],
 | 
			
		||||
                      onBack: close,
 | 
			
		||||
                    ),
 | 
			
		||||
                    openColor: Colors.transparent,
 | 
			
		||||
                    openElevation: 0,
 | 
			
		||||
                    closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75),
 | 
			
		||||
                    transitionType: ContainerTransitionType.fade,
 | 
			
		||||
                    closedShape: const RoundedRectangleBorder(
 | 
			
		||||
                      borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
              separatorBuilder: (context, index) => const Divider(height: 1),
 | 
			
		||||
              separatorBuilder: (_, __) => const Gap(8),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
import '../providers/userinfo.dart';
 | 
			
		||||
import '../widgets/unauthorized_hint.dart';
 | 
			
		||||
@@ -180,7 +181,7 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
			
		||||
    final ua = context.read<UserProvider>();
 | 
			
		||||
 | 
			
		||||
    if (!ua.isAuthorized) {
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
      return AppScaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          leading: AutoAppBarLeading(),
 | 
			
		||||
          title: Text('screenFriend').tr(),
 | 
			
		||||
@@ -191,7 +192,7 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        title: Text('screenFriend').tr(),
 | 
			
		||||
@@ -233,6 +234,9 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
			
		||||
          if (_requests.isNotEmpty || _blocks.isNotEmpty)
 | 
			
		||||
            const Divider(height: 1),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: MediaQuery.removePadding(
 | 
			
		||||
              context: context,
 | 
			
		||||
              removeTop: true,
 | 
			
		||||
              child: RefreshIndicator(
 | 
			
		||||
                onRefresh: () => Future.wait([
 | 
			
		||||
                  _fetchRelations(),
 | 
			
		||||
@@ -282,6 +286,7 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ import 'package:surface/types/check_in.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_item.dart';
 | 
			
		||||
 | 
			
		||||
class HomeScreenDashEntry {
 | 
			
		||||
@@ -67,7 +68,7 @@ class _HomeScreenState extends State<HomeScreen> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        title: Text("screenHome").tr(),
 | 
			
		||||
@@ -387,6 +388,8 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
 | 
			
		||||
                        Text(
 | 
			
		||||
                          'dailyCheckInNone',
 | 
			
		||||
                          style: Theme.of(context).textTheme.bodyLarge,
 | 
			
		||||
                          maxLines: 2,
 | 
			
		||||
                          overflow: TextOverflow.ellipsis,
 | 
			
		||||
                        ).tr(),
 | 
			
		||||
                      ],
 | 
			
		||||
                    )
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										230
									
								
								lib/screens/news/news_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								lib/screens/news/news_detail.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,230 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/gestures.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:html/dom.dart' as dom;
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:relative_time/relative_time.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/types/news.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
			
		||||
 | 
			
		||||
class NewsDetailScreen extends StatefulWidget {
 | 
			
		||||
  final String hash;
 | 
			
		||||
 | 
			
		||||
  const NewsDetailScreen({super.key, required this.hash});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<NewsDetailScreen> createState() => _NewsDetailScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _NewsDetailScreenState extends State<NewsDetailScreen> {
 | 
			
		||||
  SnNewsArticle? _article;
 | 
			
		||||
  dom.Document? _articleFragment;
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchArticle() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
 | 
			
		||||
      _article = SnNewsArticle.fromJson(resp.data);
 | 
			
		||||
      _articleFragment = parse(_article!.content);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err).then((_) {
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        Navigator.pop(context);
 | 
			
		||||
      });
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() {});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  List<Widget> _parseHtmlToWidgets(Iterable<dom.Element>? elements) {
 | 
			
		||||
    if (elements == null) return [];
 | 
			
		||||
 | 
			
		||||
    final List<Widget> widgets = [];
 | 
			
		||||
 | 
			
		||||
    for (final node in elements) {
 | 
			
		||||
      switch (node.localName) {
 | 
			
		||||
        case 'h1':
 | 
			
		||||
        case 'h2':
 | 
			
		||||
        case 'h3':
 | 
			
		||||
        case 'h4':
 | 
			
		||||
        case 'h5':
 | 
			
		||||
        case 'h6':
 | 
			
		||||
          widgets.add(Text(node.text.trim(), style: Theme.of(context).textTheme.titleMedium));
 | 
			
		||||
          break;
 | 
			
		||||
        case 'p':
 | 
			
		||||
          if (node.text.trim().isEmpty) continue;
 | 
			
		||||
          widgets.add(
 | 
			
		||||
            Text.rich(
 | 
			
		||||
              TextSpan(
 | 
			
		||||
                text: node.text.trim(),
 | 
			
		||||
                children: [
 | 
			
		||||
                  for (final child in node.children)
 | 
			
		||||
                    switch (child.localName) {
 | 
			
		||||
                      'a' => TextSpan(
 | 
			
		||||
                          text: child.text.trim(),
 | 
			
		||||
                          style: const TextStyle(decoration: TextDecoration.underline),
 | 
			
		||||
                          recognizer: TapGestureRecognizer()
 | 
			
		||||
                            ..onTap = () {
 | 
			
		||||
                              launchUrlString(child.attributes['href']!);
 | 
			
		||||
                            },
 | 
			
		||||
                        ),
 | 
			
		||||
                      _ => TextSpan(text: child.text.trim()),
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
              style: Theme.of(context).textTheme.bodyLarge,
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
          break;
 | 
			
		||||
        case 'a':
 | 
			
		||||
          // drop single link
 | 
			
		||||
          break;
 | 
			
		||||
        case 'div':
 | 
			
		||||
          // ignore div text, normally it is not meaningful
 | 
			
		||||
          widgets.addAll(_parseHtmlToWidgets(node.children));
 | 
			
		||||
          break;
 | 
			
		||||
        case 'hr':
 | 
			
		||||
          widgets.add(const Divider());
 | 
			
		||||
          break;
 | 
			
		||||
        case 'img':
 | 
			
		||||
          var src = node.attributes['src'];
 | 
			
		||||
          if (src == null) break;
 | 
			
		||||
          final width = double.tryParse(node.attributes['width'] ?? 'null');
 | 
			
		||||
          final height = double.tryParse(node.attributes['height'] ?? 'null');
 | 
			
		||||
          final ratio = width != null && height != null ? width / height : 1.0;
 | 
			
		||||
          if (!src.startsWith('http')) {
 | 
			
		||||
            final baseUri = Uri.parse(_article!.url);
 | 
			
		||||
            final baseUrl = '${baseUri.scheme}://${baseUri.host}';
 | 
			
		||||
            src = '$baseUrl/$src';
 | 
			
		||||
          }
 | 
			
		||||
          widgets.add(
 | 
			
		||||
            AspectRatio(
 | 
			
		||||
              aspectRatio: ratio,
 | 
			
		||||
              child: Container(
 | 
			
		||||
                decoration: BoxDecoration(
 | 
			
		||||
                  borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                  border: Border.all(
 | 
			
		||||
                    color: Theme.of(context).dividerColor,
 | 
			
		||||
                    width: 1,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                height: height ?? double.infinity,
 | 
			
		||||
                child: ClipRRect(
 | 
			
		||||
                  borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                  child: AutoResizeUniversalImage(src, fit: BoxFit.cover),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          widgets.addAll(_parseHtmlToWidgets(node.children));
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return widgets;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _fetchArticle();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool _isReadingFromReader = true;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text(_article?.title ?? 'loading'.tr()),
 | 
			
		||||
      ),
 | 
			
		||||
      body: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          MaterialBanner(
 | 
			
		||||
            dividerColor: Colors.transparent,
 | 
			
		||||
            leading: const Icon(Icons.info),
 | 
			
		||||
            content: Text(_isReadingFromReader ? 'newsReadingFromReader'.tr() : 'newsReadingFromOriginal'.tr()),
 | 
			
		||||
            actions: [
 | 
			
		||||
              TextButton(
 | 
			
		||||
                child: Text('newsReadingProviderSwap').tr(),
 | 
			
		||||
                onPressed: () {
 | 
			
		||||
                  setState(() => _isReadingFromReader = !_isReadingFromReader);
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
          if (_articleFragment != null && _isReadingFromReader)
 | 
			
		||||
            Expanded(
 | 
			
		||||
              child: SingleChildScrollView(
 | 
			
		||||
                child: Column(
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                  spacing: 8,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
 | 
			
		||||
                    Builder(builder: (context) {
 | 
			
		||||
                      final htmlDescription = parse(_article!.description);
 | 
			
		||||
                      return Text(
 | 
			
		||||
                        htmlDescription.children.map((ele) => ele.text.trim()).join(),
 | 
			
		||||
                        style: Theme.of(context).textTheme.bodyMedium,
 | 
			
		||||
                      );
 | 
			
		||||
                    }),
 | 
			
		||||
                    Builder(builder: (context) {
 | 
			
		||||
                      final date = _article!.publishedAt ?? _article!.createdAt;
 | 
			
		||||
                      return Row(
 | 
			
		||||
                        spacing: 2,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
			
		||||
                          Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
 | 
			
		||||
                          Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ).opacity(0.75);
 | 
			
		||||
                    }),
 | 
			
		||||
                    Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75),
 | 
			
		||||
                    const Divider(),
 | 
			
		||||
                    ..._parseHtmlToWidgets(_articleFragment!.children),
 | 
			
		||||
                    const Divider(),
 | 
			
		||||
                    InkWell(
 | 
			
		||||
                      child: Row(
 | 
			
		||||
                        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Text(
 | 
			
		||||
                            'Reference from original website',
 | 
			
		||||
                            style: TextStyle(decoration: TextDecoration.underline),
 | 
			
		||||
                          ),
 | 
			
		||||
                          const Gap(4),
 | 
			
		||||
                          Icon(Icons.launch, size: 16),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ).opacity(0.85),
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        launchUrlString(_article!.url);
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                    Gap(MediaQuery.of(context).padding.bottom),
 | 
			
		||||
                  ],
 | 
			
		||||
                ).padding(horizontal: 12, vertical: 16),
 | 
			
		||||
              ),
 | 
			
		||||
            )
 | 
			
		||||
          else if (_article != null)
 | 
			
		||||
            Expanded(
 | 
			
		||||
              child: InAppWebView(
 | 
			
		||||
                key: GlobalKey(),
 | 
			
		||||
                initialUrlRequest: URLRequest(url: WebUri(_article!.url)),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										221
									
								
								lib/screens/news/news_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								lib/screens/news/news_list.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,221 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:html/parser.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:relative_time/relative_time.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/types/news.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
			
		||||
 | 
			
		||||
class NewsScreen extends StatefulWidget {
 | 
			
		||||
  const NewsScreen({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<NewsScreen> createState() => _NewsScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _NewsScreenState extends State<NewsScreen> {
 | 
			
		||||
  List<SnNewsSource>? _sources;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _fetchSources();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchSources() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final resp = await sn.client.get('/cgi/re/well-known/sources');
 | 
			
		||||
      _sources = List<SnNewsSource>.from(
 | 
			
		||||
        resp.data?.map((e) => SnNewsSource.fromJson(e)) ?? [],
 | 
			
		||||
      );
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() {});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if (_sources == null) {
 | 
			
		||||
      return AppScaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          leading: AutoAppBarLeading(),
 | 
			
		||||
          title: Text('screenNews').tr(),
 | 
			
		||||
        ),
 | 
			
		||||
        body: Center(
 | 
			
		||||
          child: CircularProgressIndicator(),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return DefaultTabController(
 | 
			
		||||
      length: _sources!.length + 1,
 | 
			
		||||
      child: AppScaffold(
 | 
			
		||||
        body: NestedScrollView(
 | 
			
		||||
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
 | 
			
		||||
            return <Widget>[
 | 
			
		||||
              SliverOverlapAbsorber(
 | 
			
		||||
                handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
 | 
			
		||||
                sliver: SliverAppBar(
 | 
			
		||||
                  leading: AutoAppBarLeading(),
 | 
			
		||||
                  title: Text('screenNews').tr(),
 | 
			
		||||
                  bottom: TabBar(
 | 
			
		||||
                    isScrollable: true,
 | 
			
		||||
                    tabs: [
 | 
			
		||||
                      Tab(child: Text('newsAllSources'.tr())),
 | 
			
		||||
                      for (final source in _sources!) Tab(child: Text(source.label)),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ];
 | 
			
		||||
          },
 | 
			
		||||
          body: TabBarView(
 | 
			
		||||
            children: [
 | 
			
		||||
              _NewsArticleListWidget(allSources: _sources!),
 | 
			
		||||
              for (final source in _sources!)
 | 
			
		||||
                _NewsArticleListWidget(
 | 
			
		||||
                  source: source.id,
 | 
			
		||||
                  allSources: _sources!,
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _NewsArticleListWidget extends StatefulWidget {
 | 
			
		||||
  final String? source;
 | 
			
		||||
  final List<SnNewsSource> allSources;
 | 
			
		||||
 | 
			
		||||
  const _NewsArticleListWidget({this.source, required this.allSources});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<_NewsArticleListWidget> createState() => _NewsArticleListWidgetState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
 | 
			
		||||
  bool _isBusy = false;
 | 
			
		||||
 | 
			
		||||
  int? _totalCount;
 | 
			
		||||
  final List<SnNewsArticle> _articles = List.empty(growable: true);
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchArticles() async {
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final resp = await sn.client.get('/cgi/re/news', queryParameters: {
 | 
			
		||||
        'take': 10,
 | 
			
		||||
        'offset': _articles.length,
 | 
			
		||||
        if (widget.source != null) 'source': widget.source,
 | 
			
		||||
      });
 | 
			
		||||
      _totalCount = resp.data['count'];
 | 
			
		||||
      _articles.addAll(List<SnNewsArticle>.from(
 | 
			
		||||
        resp.data['data']?.map((e) => SnNewsArticle.fromJson(e)) ?? [],
 | 
			
		||||
      ));
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _fetchArticles();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return MediaQuery.removePadding(
 | 
			
		||||
      context: context,
 | 
			
		||||
      removeTop: true,
 | 
			
		||||
      child: RefreshIndicator(
 | 
			
		||||
        onRefresh: _fetchArticles,
 | 
			
		||||
        child: InfiniteList(
 | 
			
		||||
          isLoading: _isBusy,
 | 
			
		||||
          itemCount: _articles.length,
 | 
			
		||||
          hasReachedMax: _totalCount != null && _articles.length >= _totalCount!,
 | 
			
		||||
          onFetchData: () {
 | 
			
		||||
            _fetchArticles();
 | 
			
		||||
          },
 | 
			
		||||
          itemBuilder: (context, index) {
 | 
			
		||||
            final article = _articles[index];
 | 
			
		||||
 | 
			
		||||
            final baseUri = Uri.parse(article.url);
 | 
			
		||||
            final baseUrl = '${baseUri.scheme}://${baseUri.host}';
 | 
			
		||||
 | 
			
		||||
            final htmlDescription = parse(article.description);
 | 
			
		||||
            final date = article.publishedAt ?? article.createdAt;
 | 
			
		||||
 | 
			
		||||
            return Card(
 | 
			
		||||
              child: InkWell(
 | 
			
		||||
                radius: 8,
 | 
			
		||||
                onTap: () {
 | 
			
		||||
                  GoRouter.of(context).pushNamed(
 | 
			
		||||
                    'newsDetail',
 | 
			
		||||
                    pathParameters: {'hash': article.hash},
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
                child: Column(
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
 | 
			
		||||
                      ClipRRect(
 | 
			
		||||
                        borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                        child: AspectRatio(
 | 
			
		||||
                          aspectRatio: 16 / 9,
 | 
			
		||||
                          child: AutoResizeUniversalImage(
 | 
			
		||||
                            article.thumbnail.startsWith('http') ? article.thumbnail : '$baseUrl/${article.thumbnail}',
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    const Gap(16),
 | 
			
		||||
                    Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
 | 
			
		||||
                    const Gap(8),
 | 
			
		||||
                    Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
 | 
			
		||||
                        .textStyle(Theme.of(context).textTheme.bodyMedium!)
 | 
			
		||||
                        .padding(horizontal: 16),
 | 
			
		||||
                    const Gap(8),
 | 
			
		||||
                    Row(
 | 
			
		||||
                      spacing: 2,
 | 
			
		||||
                      children: [
 | 
			
		||||
                        Text(widget.allSources.where((x) => x.id == article.source).first.label)
 | 
			
		||||
                            .textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
			
		||||
                      ],
 | 
			
		||||
                    ).opacity(0.75).padding(horizontal: 16),
 | 
			
		||||
                    Row(
 | 
			
		||||
                      spacing: 2,
 | 
			
		||||
                      children: [
 | 
			
		||||
                        Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
			
		||||
                        Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
 | 
			
		||||
                        Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
			
		||||
                      ],
 | 
			
		||||
                    ).opacity(0.75).padding(horizontal: 16),
 | 
			
		||||
                    const Gap(16),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,6 +14,7 @@ import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/markdown_content.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_item.dart';
 | 
			
		||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
			
		||||
 | 
			
		||||
@@ -137,7 +138,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
			
		||||
    final ua = context.read<UserProvider>();
 | 
			
		||||
 | 
			
		||||
    if (!ua.isAuthorized) {
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
      return AppScaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          leading: AutoAppBarLeading(),
 | 
			
		||||
          title: Text('screenNotification').tr(),
 | 
			
		||||
@@ -148,7 +149,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        title: Text('screenNotification').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_background.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_comment_list.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_item.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_mini_editor.dart';
 | 
			
		||||
@@ -20,12 +22,9 @@ import 'package:surface/widgets/post/post_mini_editor.dart';
 | 
			
		||||
class PostDetailScreen extends StatefulWidget {
 | 
			
		||||
  final String slug;
 | 
			
		||||
  final SnPost? preload;
 | 
			
		||||
  final Function? onBack;
 | 
			
		||||
 | 
			
		||||
  const PostDetailScreen({
 | 
			
		||||
    super.key,
 | 
			
		||||
    required this.slug,
 | 
			
		||||
    this.preload,
 | 
			
		||||
  });
 | 
			
		||||
  const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<PostDetailScreen> createState() => _PostDetailScreenState();
 | 
			
		||||
@@ -67,10 +66,15 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
 | 
			
		||||
    final ua = context.watch<UserProvider>();
 | 
			
		||||
    final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppBackground(
 | 
			
		||||
      isRoot: widget.onBack != null,
 | 
			
		||||
      child: AppScaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          leading: BackButton(
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              if (widget.onBack != null) {
 | 
			
		||||
                widget.onBack!.call();
 | 
			
		||||
              }
 | 
			
		||||
              if (GoRouter.of(context).canPop()) {
 | 
			
		||||
                GoRouter.of(context).pop(context);
 | 
			
		||||
                return;
 | 
			
		||||
@@ -185,6 +189,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
 | 
			
		||||
            SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_item.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_media_pending_list.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_meta_editor.dart';
 | 
			
		||||
@@ -128,7 +129,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
    return ListenableBuilder(
 | 
			
		||||
      listenable: _writeController,
 | 
			
		||||
      builder: (context, _) {
 | 
			
		||||
        return Scaffold(
 | 
			
		||||
        return AppScaffold(
 | 
			
		||||
          appBar: AppBar(
 | 
			
		||||
            leading: BackButton(
 | 
			
		||||
              onPressed: () {
 | 
			
		||||
@@ -303,7 +304,9 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                      // Content Input Area
 | 
			
		||||
                      TextField(
 | 
			
		||||
                      Container(
 | 
			
		||||
                        constraints: const BoxConstraints(maxWidth: 640),
 | 
			
		||||
                        child: TextField(
 | 
			
		||||
                          controller: _writeController.contentController,
 | 
			
		||||
                          maxLines: null,
 | 
			
		||||
                          decoration: InputDecoration(
 | 
			
		||||
@@ -317,6 +320,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
                          ),
 | 
			
		||||
                          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ]
 | 
			
		||||
                        .expandIndexed(
 | 
			
		||||
                          (idx, ele) => [
 | 
			
		||||
@@ -366,6 +370,15 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
                child: Column(
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    LoadingIndicator(isActive: _isLoading),
 | 
			
		||||
                    if (_writeController.isBusy && _writeController.progress != null)
 | 
			
		||||
                      TweenAnimationBuilder<double>(
 | 
			
		||||
                        tween: Tween(begin: 0, end: _writeController.progress),
 | 
			
		||||
                        duration: Duration(milliseconds: 300),
 | 
			
		||||
                        builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
 | 
			
		||||
                      )
 | 
			
		||||
                    else if (_writeController.isBusy)
 | 
			
		||||
                      const LinearProgressIndicator(value: null, minHeight: 2),
 | 
			
		||||
                    Container(
 | 
			
		||||
                      child: _writeController.temporaryRestored
 | 
			
		||||
                          ? Container(
 | 
			
		||||
@@ -396,15 +409,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
 | 
			
		||||
                    )
 | 
			
		||||
                        .height(_writeController.temporaryRestored ? 32 : 0, animate: true)
 | 
			
		||||
                        .animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
 | 
			
		||||
                    LoadingIndicator(isActive: _isLoading),
 | 
			
		||||
                    if (_writeController.isBusy && _writeController.progress != null)
 | 
			
		||||
                      TweenAnimationBuilder<double>(
 | 
			
		||||
                        tween: Tween(begin: 0, end: _writeController.progress),
 | 
			
		||||
                        duration: Duration(milliseconds: 300),
 | 
			
		||||
                        builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
 | 
			
		||||
                      )
 | 
			
		||||
                    else if (_writeController.isBusy)
 | 
			
		||||
                      const LinearProgressIndicator(value: null, minHeight: 2),
 | 
			
		||||
                    Row(
 | 
			
		||||
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
			
		||||
                      children: [
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/post.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_item.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_tags_field.dart';
 | 
			
		||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
			
		||||
@@ -119,7 +120,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
 | 
			
		||||
      ),
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text('screenPostSearch').tr(),
 | 
			
		||||
        actions: [
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_item.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
			
		||||
@@ -274,7 +275,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      body: NestedScrollView(
 | 
			
		||||
        controller: _scrollController,
 | 
			
		||||
        headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/unauthorized_hint.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
 | 
			
		||||
@@ -83,7 +84,7 @@ class _RealmScreenState extends State<RealmScreen> {
 | 
			
		||||
    final ua = context.read<UserProvider>();
 | 
			
		||||
 | 
			
		||||
    if (!ua.isAuthorized) {
 | 
			
		||||
      return Scaffold(
 | 
			
		||||
      return AppScaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          leading: AutoAppBarLeading(),
 | 
			
		||||
          title: Text('screenRealm').tr(),
 | 
			
		||||
@@ -94,7 +95,7 @@ class _RealmScreenState extends State<RealmScreen> {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        title: Text('screenRealm').tr(),
 | 
			
		||||
@@ -118,6 +119,9 @@ class _RealmScreenState extends State<RealmScreen> {
 | 
			
		||||
        children: [
 | 
			
		||||
          LoadingIndicator(isActive: _isBusy),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: MediaQuery.removePadding(
 | 
			
		||||
              context: context,
 | 
			
		||||
              removeTop: true,
 | 
			
		||||
              child: RefreshIndicator(
 | 
			
		||||
                onRefresh: _fetchRealms,
 | 
			
		||||
                child: ListView.builder(
 | 
			
		||||
@@ -196,7 +200,9 @@ class _RealmScreenState extends State<RealmScreen> {
 | 
			
		||||
                                  clipBehavior: Clip.none,
 | 
			
		||||
                                  fit: StackFit.expand,
 | 
			
		||||
                                  children: [
 | 
			
		||||
                                  Container(
 | 
			
		||||
                                    ClipRRect(
 | 
			
		||||
                                      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                                      child: Container(
 | 
			
		||||
                                        color: Theme.of(context).colorScheme.surfaceContainer,
 | 
			
		||||
                                        child: (realm.banner?.isEmpty ?? true)
 | 
			
		||||
                                            ? const SizedBox.shrink()
 | 
			
		||||
@@ -205,6 +211,7 @@ class _RealmScreenState extends State<RealmScreen> {
 | 
			
		||||
                                                fit: BoxFit.cover,
 | 
			
		||||
                                              ),
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    Positioned(
 | 
			
		||||
                                      bottom: -30,
 | 
			
		||||
                                      left: 18,
 | 
			
		||||
@@ -240,6 +247,7 @@ class _RealmScreenState extends State<RealmScreen> {
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import 'package:surface/types/realm.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
import 'package:uuid/uuid.dart';
 | 
			
		||||
 | 
			
		||||
@@ -179,7 +180,7 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: widget.editingRealmAlias != null
 | 
			
		||||
            ? Text('screenRealmManage').tr()
 | 
			
		||||
 
 | 
			
		||||
@@ -8,13 +8,13 @@ import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/user_directory.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
			
		||||
 | 
			
		||||
import '../../types/post.dart';
 | 
			
		||||
 | 
			
		||||
class RealmDetailScreen extends StatefulWidget {
 | 
			
		||||
  final String alias;
 | 
			
		||||
 | 
			
		||||
@@ -70,19 +70,11 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return DefaultTabController(
 | 
			
		||||
      length: 3,
 | 
			
		||||
      child: Scaffold(
 | 
			
		||||
      child: AppScaffold(
 | 
			
		||||
        body: NestedScrollView(
 | 
			
		||||
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
 | 
			
		||||
            // These are the slivers that show up in the "outer" scroll view.
 | 
			
		||||
            return <Widget>[
 | 
			
		||||
              SliverOverlapAbsorber(
 | 
			
		||||
                // This widget takes the overlapping behavior of the SliverAppBar,
 | 
			
		||||
                // and redirects it to the SliverOverlapInjector below. If it is
 | 
			
		||||
                // missing, then it is possible for the nested "inner" scroll view
 | 
			
		||||
                // below to end up under the SliverAppBar even when the inner
 | 
			
		||||
                // scroll view thinks it has not been scrolled.
 | 
			
		||||
                // This is not necessary if the "headerSliverBuilder" only builds
 | 
			
		||||
                // widgets that do not overlap the next sliver.
 | 
			
		||||
                handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
 | 
			
		||||
                sliver: SliverAppBar(
 | 
			
		||||
                  title: Text(_realm?.name ?? 'loading'.tr()),
 | 
			
		||||
@@ -428,7 +420,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
 | 
			
		||||
 | 
			
		||||
    return Column(
 | 
			
		||||
      children: [
 | 
			
		||||
        const Gap(16),
 | 
			
		||||
        const Gap(8),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          leading: const Icon(Symbols.edit),
 | 
			
		||||
          trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/theme.dart';
 | 
			
		||||
import 'package:surface/theme.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
const Map<String, Color> kColorSchemes = {
 | 
			
		||||
  'colorSchemeIndigo': Colors.indigo,
 | 
			
		||||
@@ -67,7 +68,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('screenSettings').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: SingleChildScrollView(
 | 
			
		||||
        child: Column(
 | 
			
		||||
          spacing: 16,
 | 
			
		||||
@@ -120,7 +125,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
			
		||||
                  subtitle: Text('settingsThemeMaterial3Description').tr(),
 | 
			
		||||
                  contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
			
		||||
                  secondary: const Icon(Symbols.new_releases),
 | 
			
		||||
                  value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false,
 | 
			
		||||
                  value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
 | 
			
		||||
                  onChanged: (value) {
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      _prefs.setBool(
 | 
			
		||||
@@ -255,6 +260,48 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
                Text('settingsFeatures').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
 | 
			
		||||
                CheckboxListTile(
 | 
			
		||||
                  secondary: const Icon(Symbols.vibration),
 | 
			
		||||
                  contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
			
		||||
                  title: Text('settingsNotifyWithHaptic').tr(),
 | 
			
		||||
                  subtitle: Text('settingsNotifyWithHapticDescription').tr(),
 | 
			
		||||
                  value: _prefs.getBool(kAppNotifyWithHaptic) ?? true,
 | 
			
		||||
                  onChanged: (value) {
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      _prefs.setBool(kAppNotifyWithHaptic, value ?? false);
 | 
			
		||||
                    });
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                CheckboxListTile(
 | 
			
		||||
                  secondary: const Icon(Symbols.link),
 | 
			
		||||
                  title: Text('settingsExpandPostLink').tr(),
 | 
			
		||||
                  subtitle: Text('settingsExpandPostLinkDescription').tr(),
 | 
			
		||||
                  contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
			
		||||
                  value: _prefs.getBool(kAppExpandPostLink) ?? true,
 | 
			
		||||
                  onChanged: (value) {
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      _prefs.setBool(kAppExpandPostLink, value ?? false);
 | 
			
		||||
                    });
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                CheckboxListTile(
 | 
			
		||||
                  secondary: const Icon(Symbols.chat),
 | 
			
		||||
                  title: Text('settingsExpandChatLink').tr(),
 | 
			
		||||
                  subtitle: Text('settingsExpandChatLinkDescription').tr(),
 | 
			
		||||
                  contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
			
		||||
                  value: _prefs.getBool(kAppExpandChatLink) ?? true,
 | 
			
		||||
                  onChanged: (value) {
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      _prefs.setBool(kAppExpandChatLink, value ?? false);
 | 
			
		||||
                    });
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
 
 | 
			
		||||
@@ -34,9 +34,10 @@ Future<ThemeData> createAppTheme(
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
 | 
			
		||||
  final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
 | 
			
		||||
 | 
			
		||||
  return ThemeData(
 | 
			
		||||
    useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false),
 | 
			
		||||
    useMaterial3: useM3,
 | 
			
		||||
    colorScheme: colorScheme,
 | 
			
		||||
    brightness: brightness,
 | 
			
		||||
    iconTheme: IconThemeData(
 | 
			
		||||
@@ -45,12 +46,24 @@ Future<ThemeData> createAppTheme(
 | 
			
		||||
      opticalSize: 20,
 | 
			
		||||
      color: colorScheme.onSurface,
 | 
			
		||||
    ),
 | 
			
		||||
    snackBarTheme: SnackBarThemeData(
 | 
			
		||||
      behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed,
 | 
			
		||||
    ),
 | 
			
		||||
    appBarTheme: AppBarTheme(
 | 
			
		||||
      centerTitle: true,
 | 
			
		||||
      elevation: hasAppBarBlurry ? 0 : null,
 | 
			
		||||
      backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
 | 
			
		||||
      foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
 | 
			
		||||
    ),
 | 
			
		||||
    scaffoldBackgroundColor: Colors.transparent,
 | 
			
		||||
    pageTransitionsTheme: PageTransitionsTheme(
 | 
			
		||||
      builders: {
 | 
			
		||||
        TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
 | 
			
		||||
        TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
 | 
			
		||||
        TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
 | 
			
		||||
        TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
 | 
			
		||||
        TargetPlatform.linux: ZoomPageTransitionsBuilder(),
 | 
			
		||||
        TargetPlatform.windows: ZoomPageTransitionsBuilder(),
 | 
			
		||||
      },
 | 
			
		||||
    ),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								lib/types/news.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								lib/types/news.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
			
		||||
 | 
			
		||||
part 'news.freezed.dart';
 | 
			
		||||
part 'news.g.dart';
 | 
			
		||||
 | 
			
		||||
@freezed
 | 
			
		||||
class SnNewsSource with _$SnNewsSource {
 | 
			
		||||
  const factory SnNewsSource({
 | 
			
		||||
    required String id,
 | 
			
		||||
    required String label,
 | 
			
		||||
    required String type,
 | 
			
		||||
    required String source,
 | 
			
		||||
    required int depth,
 | 
			
		||||
    required bool enabled,
 | 
			
		||||
  }) = _SnNewsSource;
 | 
			
		||||
 | 
			
		||||
  factory SnNewsSource.fromJson(Map<String, dynamic> json) => _$SnNewsSourceFromJson(json);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@freezed
 | 
			
		||||
class SnNewsArticle with _$SnNewsArticle {
 | 
			
		||||
  const factory SnNewsArticle({
 | 
			
		||||
    required int id,
 | 
			
		||||
    required DateTime createdAt,
 | 
			
		||||
    required DateTime updatedAt,
 | 
			
		||||
    required dynamic deletedAt,
 | 
			
		||||
    required String thumbnail,
 | 
			
		||||
    required String title,
 | 
			
		||||
    required String description,
 | 
			
		||||
    required String content,
 | 
			
		||||
    required String url,
 | 
			
		||||
    required String hash,
 | 
			
		||||
    required String source,
 | 
			
		||||
    required DateTime? publishedAt,
 | 
			
		||||
  }) = _SnNewsArticle;
 | 
			
		||||
 | 
			
		||||
  factory SnNewsArticle.fromJson(Map<String, dynamic> json) => _$SnNewsArticleFromJson(json);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										660
									
								
								lib/types/news.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										660
									
								
								lib/types/news.freezed.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,660 @@
 | 
			
		||||
// coverage:ignore-file
 | 
			
		||||
// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
			
		||||
// ignore_for_file: type=lint
 | 
			
		||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
 | 
			
		||||
 | 
			
		||||
part of 'news.dart';
 | 
			
		||||
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
// FreezedGenerator
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
 | 
			
		||||
T _$identity<T>(T value) => value;
 | 
			
		||||
 | 
			
		||||
final _privateConstructorUsedError = UnsupportedError(
 | 
			
		||||
    'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
 | 
			
		||||
 | 
			
		||||
SnNewsSource _$SnNewsSourceFromJson(Map<String, dynamic> json) {
 | 
			
		||||
  return _SnNewsSource.fromJson(json);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
mixin _$SnNewsSource {
 | 
			
		||||
  String get id => throw _privateConstructorUsedError;
 | 
			
		||||
  String get label => throw _privateConstructorUsedError;
 | 
			
		||||
  String get type => throw _privateConstructorUsedError;
 | 
			
		||||
  String get source => throw _privateConstructorUsedError;
 | 
			
		||||
  int get depth => throw _privateConstructorUsedError;
 | 
			
		||||
  bool get enabled => throw _privateConstructorUsedError;
 | 
			
		||||
 | 
			
		||||
  /// Serializes this SnNewsSource to a JSON map.
 | 
			
		||||
  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnNewsSource
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  $SnNewsSourceCopyWith<SnNewsSource> get copyWith =>
 | 
			
		||||
      throw _privateConstructorUsedError;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
abstract class $SnNewsSourceCopyWith<$Res> {
 | 
			
		||||
  factory $SnNewsSourceCopyWith(
 | 
			
		||||
          SnNewsSource value, $Res Function(SnNewsSource) then) =
 | 
			
		||||
      _$SnNewsSourceCopyWithImpl<$Res, SnNewsSource>;
 | 
			
		||||
  @useResult
 | 
			
		||||
  $Res call(
 | 
			
		||||
      {String id,
 | 
			
		||||
      String label,
 | 
			
		||||
      String type,
 | 
			
		||||
      String source,
 | 
			
		||||
      int depth,
 | 
			
		||||
      bool enabled});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
class _$SnNewsSourceCopyWithImpl<$Res, $Val extends SnNewsSource>
 | 
			
		||||
    implements $SnNewsSourceCopyWith<$Res> {
 | 
			
		||||
  _$SnNewsSourceCopyWithImpl(this._value, this._then);
 | 
			
		||||
 | 
			
		||||
  // ignore: unused_field
 | 
			
		||||
  final $Val _value;
 | 
			
		||||
  // ignore: unused_field
 | 
			
		||||
  final $Res Function($Val) _then;
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnNewsSource
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @pragma('vm:prefer-inline')
 | 
			
		||||
  @override
 | 
			
		||||
  $Res call({
 | 
			
		||||
    Object? id = null,
 | 
			
		||||
    Object? label = null,
 | 
			
		||||
    Object? type = null,
 | 
			
		||||
    Object? source = null,
 | 
			
		||||
    Object? depth = null,
 | 
			
		||||
    Object? enabled = null,
 | 
			
		||||
  }) {
 | 
			
		||||
    return _then(_value.copyWith(
 | 
			
		||||
      id: null == id
 | 
			
		||||
          ? _value.id
 | 
			
		||||
          : id // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      label: null == label
 | 
			
		||||
          ? _value.label
 | 
			
		||||
          : label // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      type: null == type
 | 
			
		||||
          ? _value.type
 | 
			
		||||
          : type // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      source: null == source
 | 
			
		||||
          ? _value.source
 | 
			
		||||
          : source // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      depth: null == depth
 | 
			
		||||
          ? _value.depth
 | 
			
		||||
          : depth // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as int,
 | 
			
		||||
      enabled: null == enabled
 | 
			
		||||
          ? _value.enabled
 | 
			
		||||
          : enabled // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as bool,
 | 
			
		||||
    ) as $Val);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
abstract class _$$SnNewsSourceImplCopyWith<$Res>
 | 
			
		||||
    implements $SnNewsSourceCopyWith<$Res> {
 | 
			
		||||
  factory _$$SnNewsSourceImplCopyWith(
 | 
			
		||||
          _$SnNewsSourceImpl value, $Res Function(_$SnNewsSourceImpl) then) =
 | 
			
		||||
      __$$SnNewsSourceImplCopyWithImpl<$Res>;
 | 
			
		||||
  @override
 | 
			
		||||
  @useResult
 | 
			
		||||
  $Res call(
 | 
			
		||||
      {String id,
 | 
			
		||||
      String label,
 | 
			
		||||
      String type,
 | 
			
		||||
      String source,
 | 
			
		||||
      int depth,
 | 
			
		||||
      bool enabled});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
class __$$SnNewsSourceImplCopyWithImpl<$Res>
 | 
			
		||||
    extends _$SnNewsSourceCopyWithImpl<$Res, _$SnNewsSourceImpl>
 | 
			
		||||
    implements _$$SnNewsSourceImplCopyWith<$Res> {
 | 
			
		||||
  __$$SnNewsSourceImplCopyWithImpl(
 | 
			
		||||
      _$SnNewsSourceImpl _value, $Res Function(_$SnNewsSourceImpl) _then)
 | 
			
		||||
      : super(_value, _then);
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnNewsSource
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @pragma('vm:prefer-inline')
 | 
			
		||||
  @override
 | 
			
		||||
  $Res call({
 | 
			
		||||
    Object? id = null,
 | 
			
		||||
    Object? label = null,
 | 
			
		||||
    Object? type = null,
 | 
			
		||||
    Object? source = null,
 | 
			
		||||
    Object? depth = null,
 | 
			
		||||
    Object? enabled = null,
 | 
			
		||||
  }) {
 | 
			
		||||
    return _then(_$SnNewsSourceImpl(
 | 
			
		||||
      id: null == id
 | 
			
		||||
          ? _value.id
 | 
			
		||||
          : id // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      label: null == label
 | 
			
		||||
          ? _value.label
 | 
			
		||||
          : label // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      type: null == type
 | 
			
		||||
          ? _value.type
 | 
			
		||||
          : type // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      source: null == source
 | 
			
		||||
          ? _value.source
 | 
			
		||||
          : source // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      depth: null == depth
 | 
			
		||||
          ? _value.depth
 | 
			
		||||
          : depth // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as int,
 | 
			
		||||
      enabled: null == enabled
 | 
			
		||||
          ? _value.enabled
 | 
			
		||||
          : enabled // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as bool,
 | 
			
		||||
    ));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
@JsonSerializable()
 | 
			
		||||
class _$SnNewsSourceImpl implements _SnNewsSource {
 | 
			
		||||
  const _$SnNewsSourceImpl(
 | 
			
		||||
      {required this.id,
 | 
			
		||||
      required this.label,
 | 
			
		||||
      required this.type,
 | 
			
		||||
      required this.source,
 | 
			
		||||
      required this.depth,
 | 
			
		||||
      required this.enabled});
 | 
			
		||||
 | 
			
		||||
  factory _$SnNewsSourceImpl.fromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      _$$SnNewsSourceImplFromJson(json);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  final String id;
 | 
			
		||||
  @override
 | 
			
		||||
  final String label;
 | 
			
		||||
  @override
 | 
			
		||||
  final String type;
 | 
			
		||||
  @override
 | 
			
		||||
  final String source;
 | 
			
		||||
  @override
 | 
			
		||||
  final int depth;
 | 
			
		||||
  @override
 | 
			
		||||
  final bool enabled;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(Object other) {
 | 
			
		||||
    return identical(this, other) ||
 | 
			
		||||
        (other.runtimeType == runtimeType &&
 | 
			
		||||
            other is _$SnNewsSourceImpl &&
 | 
			
		||||
            (identical(other.id, id) || other.id == id) &&
 | 
			
		||||
            (identical(other.label, label) || other.label == label) &&
 | 
			
		||||
            (identical(other.type, type) || other.type == type) &&
 | 
			
		||||
            (identical(other.source, source) || other.source == source) &&
 | 
			
		||||
            (identical(other.depth, depth) || other.depth == depth) &&
 | 
			
		||||
            (identical(other.enabled, enabled) || other.enabled == enabled));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode =>
 | 
			
		||||
      Object.hash(runtimeType, id, label, type, source, depth, enabled);
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnNewsSource
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  @override
 | 
			
		||||
  @pragma('vm:prefer-inline')
 | 
			
		||||
  _$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith =>
 | 
			
		||||
      __$$SnNewsSourceImplCopyWithImpl<_$SnNewsSourceImpl>(this, _$identity);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Map<String, dynamic> toJson() {
 | 
			
		||||
    return _$$SnNewsSourceImplToJson(
 | 
			
		||||
      this,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
abstract class _SnNewsSource implements SnNewsSource {
 | 
			
		||||
  const factory _SnNewsSource(
 | 
			
		||||
      {required final String id,
 | 
			
		||||
      required final String label,
 | 
			
		||||
      required final String type,
 | 
			
		||||
      required final String source,
 | 
			
		||||
      required final int depth,
 | 
			
		||||
      required final bool enabled}) = _$SnNewsSourceImpl;
 | 
			
		||||
 | 
			
		||||
  factory _SnNewsSource.fromJson(Map<String, dynamic> json) =
 | 
			
		||||
      _$SnNewsSourceImpl.fromJson;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String get id;
 | 
			
		||||
  @override
 | 
			
		||||
  String get label;
 | 
			
		||||
  @override
 | 
			
		||||
  String get type;
 | 
			
		||||
  @override
 | 
			
		||||
  String get source;
 | 
			
		||||
  @override
 | 
			
		||||
  int get depth;
 | 
			
		||||
  @override
 | 
			
		||||
  bool get enabled;
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnNewsSource
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @override
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  _$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith =>
 | 
			
		||||
      throw _privateConstructorUsedError;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SnNewsArticle _$SnNewsArticleFromJson(Map<String, dynamic> json) {
 | 
			
		||||
  return _SnNewsArticle.fromJson(json);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
mixin _$SnNewsArticle {
 | 
			
		||||
  int get id => throw _privateConstructorUsedError;
 | 
			
		||||
  DateTime get createdAt => throw _privateConstructorUsedError;
 | 
			
		||||
  DateTime get updatedAt => throw _privateConstructorUsedError;
 | 
			
		||||
  dynamic get deletedAt => throw _privateConstructorUsedError;
 | 
			
		||||
  String get thumbnail => throw _privateConstructorUsedError;
 | 
			
		||||
  String get title => throw _privateConstructorUsedError;
 | 
			
		||||
  String get description => throw _privateConstructorUsedError;
 | 
			
		||||
  String get content => throw _privateConstructorUsedError;
 | 
			
		||||
  String get url => throw _privateConstructorUsedError;
 | 
			
		||||
  String get hash => throw _privateConstructorUsedError;
 | 
			
		||||
  String get source => throw _privateConstructorUsedError;
 | 
			
		||||
  DateTime? get publishedAt => throw _privateConstructorUsedError;
 | 
			
		||||
 | 
			
		||||
  /// Serializes this SnNewsArticle to a JSON map.
 | 
			
		||||
  Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnNewsArticle
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  $SnNewsArticleCopyWith<SnNewsArticle> get copyWith =>
 | 
			
		||||
      throw _privateConstructorUsedError;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
abstract class $SnNewsArticleCopyWith<$Res> {
 | 
			
		||||
  factory $SnNewsArticleCopyWith(
 | 
			
		||||
          SnNewsArticle value, $Res Function(SnNewsArticle) then) =
 | 
			
		||||
      _$SnNewsArticleCopyWithImpl<$Res, SnNewsArticle>;
 | 
			
		||||
  @useResult
 | 
			
		||||
  $Res call(
 | 
			
		||||
      {int id,
 | 
			
		||||
      DateTime createdAt,
 | 
			
		||||
      DateTime updatedAt,
 | 
			
		||||
      dynamic deletedAt,
 | 
			
		||||
      String thumbnail,
 | 
			
		||||
      String title,
 | 
			
		||||
      String description,
 | 
			
		||||
      String content,
 | 
			
		||||
      String url,
 | 
			
		||||
      String hash,
 | 
			
		||||
      String source,
 | 
			
		||||
      DateTime? publishedAt});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
class _$SnNewsArticleCopyWithImpl<$Res, $Val extends SnNewsArticle>
 | 
			
		||||
    implements $SnNewsArticleCopyWith<$Res> {
 | 
			
		||||
  _$SnNewsArticleCopyWithImpl(this._value, this._then);
 | 
			
		||||
 | 
			
		||||
  // ignore: unused_field
 | 
			
		||||
  final $Val _value;
 | 
			
		||||
  // ignore: unused_field
 | 
			
		||||
  final $Res Function($Val) _then;
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnNewsArticle
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @pragma('vm:prefer-inline')
 | 
			
		||||
  @override
 | 
			
		||||
  $Res call({
 | 
			
		||||
    Object? id = null,
 | 
			
		||||
    Object? createdAt = null,
 | 
			
		||||
    Object? updatedAt = null,
 | 
			
		||||
    Object? deletedAt = freezed,
 | 
			
		||||
    Object? thumbnail = null,
 | 
			
		||||
    Object? title = null,
 | 
			
		||||
    Object? description = null,
 | 
			
		||||
    Object? content = null,
 | 
			
		||||
    Object? url = null,
 | 
			
		||||
    Object? hash = null,
 | 
			
		||||
    Object? source = null,
 | 
			
		||||
    Object? publishedAt = freezed,
 | 
			
		||||
  }) {
 | 
			
		||||
    return _then(_value.copyWith(
 | 
			
		||||
      id: null == id
 | 
			
		||||
          ? _value.id
 | 
			
		||||
          : id // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as int,
 | 
			
		||||
      createdAt: null == createdAt
 | 
			
		||||
          ? _value.createdAt
 | 
			
		||||
          : createdAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime,
 | 
			
		||||
      updatedAt: null == updatedAt
 | 
			
		||||
          ? _value.updatedAt
 | 
			
		||||
          : updatedAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime,
 | 
			
		||||
      deletedAt: freezed == deletedAt
 | 
			
		||||
          ? _value.deletedAt
 | 
			
		||||
          : deletedAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as dynamic,
 | 
			
		||||
      thumbnail: null == thumbnail
 | 
			
		||||
          ? _value.thumbnail
 | 
			
		||||
          : thumbnail // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      title: null == title
 | 
			
		||||
          ? _value.title
 | 
			
		||||
          : title // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      description: null == description
 | 
			
		||||
          ? _value.description
 | 
			
		||||
          : description // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      content: null == content
 | 
			
		||||
          ? _value.content
 | 
			
		||||
          : content // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      url: null == url
 | 
			
		||||
          ? _value.url
 | 
			
		||||
          : url // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      hash: null == hash
 | 
			
		||||
          ? _value.hash
 | 
			
		||||
          : hash // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      source: null == source
 | 
			
		||||
          ? _value.source
 | 
			
		||||
          : source // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      publishedAt: freezed == publishedAt
 | 
			
		||||
          ? _value.publishedAt
 | 
			
		||||
          : publishedAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime?,
 | 
			
		||||
    ) as $Val);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
abstract class _$$SnNewsArticleImplCopyWith<$Res>
 | 
			
		||||
    implements $SnNewsArticleCopyWith<$Res> {
 | 
			
		||||
  factory _$$SnNewsArticleImplCopyWith(
 | 
			
		||||
          _$SnNewsArticleImpl value, $Res Function(_$SnNewsArticleImpl) then) =
 | 
			
		||||
      __$$SnNewsArticleImplCopyWithImpl<$Res>;
 | 
			
		||||
  @override
 | 
			
		||||
  @useResult
 | 
			
		||||
  $Res call(
 | 
			
		||||
      {int id,
 | 
			
		||||
      DateTime createdAt,
 | 
			
		||||
      DateTime updatedAt,
 | 
			
		||||
      dynamic deletedAt,
 | 
			
		||||
      String thumbnail,
 | 
			
		||||
      String title,
 | 
			
		||||
      String description,
 | 
			
		||||
      String content,
 | 
			
		||||
      String url,
 | 
			
		||||
      String hash,
 | 
			
		||||
      String source,
 | 
			
		||||
      DateTime? publishedAt});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
class __$$SnNewsArticleImplCopyWithImpl<$Res>
 | 
			
		||||
    extends _$SnNewsArticleCopyWithImpl<$Res, _$SnNewsArticleImpl>
 | 
			
		||||
    implements _$$SnNewsArticleImplCopyWith<$Res> {
 | 
			
		||||
  __$$SnNewsArticleImplCopyWithImpl(
 | 
			
		||||
      _$SnNewsArticleImpl _value, $Res Function(_$SnNewsArticleImpl) _then)
 | 
			
		||||
      : super(_value, _then);
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnNewsArticle
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @pragma('vm:prefer-inline')
 | 
			
		||||
  @override
 | 
			
		||||
  $Res call({
 | 
			
		||||
    Object? id = null,
 | 
			
		||||
    Object? createdAt = null,
 | 
			
		||||
    Object? updatedAt = null,
 | 
			
		||||
    Object? deletedAt = freezed,
 | 
			
		||||
    Object? thumbnail = null,
 | 
			
		||||
    Object? title = null,
 | 
			
		||||
    Object? description = null,
 | 
			
		||||
    Object? content = null,
 | 
			
		||||
    Object? url = null,
 | 
			
		||||
    Object? hash = null,
 | 
			
		||||
    Object? source = null,
 | 
			
		||||
    Object? publishedAt = freezed,
 | 
			
		||||
  }) {
 | 
			
		||||
    return _then(_$SnNewsArticleImpl(
 | 
			
		||||
      id: null == id
 | 
			
		||||
          ? _value.id
 | 
			
		||||
          : id // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as int,
 | 
			
		||||
      createdAt: null == createdAt
 | 
			
		||||
          ? _value.createdAt
 | 
			
		||||
          : createdAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime,
 | 
			
		||||
      updatedAt: null == updatedAt
 | 
			
		||||
          ? _value.updatedAt
 | 
			
		||||
          : updatedAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime,
 | 
			
		||||
      deletedAt: freezed == deletedAt
 | 
			
		||||
          ? _value.deletedAt
 | 
			
		||||
          : deletedAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as dynamic,
 | 
			
		||||
      thumbnail: null == thumbnail
 | 
			
		||||
          ? _value.thumbnail
 | 
			
		||||
          : thumbnail // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      title: null == title
 | 
			
		||||
          ? _value.title
 | 
			
		||||
          : title // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      description: null == description
 | 
			
		||||
          ? _value.description
 | 
			
		||||
          : description // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      content: null == content
 | 
			
		||||
          ? _value.content
 | 
			
		||||
          : content // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      url: null == url
 | 
			
		||||
          ? _value.url
 | 
			
		||||
          : url // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      hash: null == hash
 | 
			
		||||
          ? _value.hash
 | 
			
		||||
          : hash // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      source: null == source
 | 
			
		||||
          ? _value.source
 | 
			
		||||
          : source // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as String,
 | 
			
		||||
      publishedAt: freezed == publishedAt
 | 
			
		||||
          ? _value.publishedAt
 | 
			
		||||
          : publishedAt // ignore: cast_nullable_to_non_nullable
 | 
			
		||||
              as DateTime?,
 | 
			
		||||
    ));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// @nodoc
 | 
			
		||||
@JsonSerializable()
 | 
			
		||||
class _$SnNewsArticleImpl implements _SnNewsArticle {
 | 
			
		||||
  const _$SnNewsArticleImpl(
 | 
			
		||||
      {required this.id,
 | 
			
		||||
      required this.createdAt,
 | 
			
		||||
      required this.updatedAt,
 | 
			
		||||
      required this.deletedAt,
 | 
			
		||||
      required this.thumbnail,
 | 
			
		||||
      required this.title,
 | 
			
		||||
      required this.description,
 | 
			
		||||
      required this.content,
 | 
			
		||||
      required this.url,
 | 
			
		||||
      required this.hash,
 | 
			
		||||
      required this.source,
 | 
			
		||||
      required this.publishedAt});
 | 
			
		||||
 | 
			
		||||
  factory _$SnNewsArticleImpl.fromJson(Map<String, dynamic> json) =>
 | 
			
		||||
      _$$SnNewsArticleImplFromJson(json);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  final int id;
 | 
			
		||||
  @override
 | 
			
		||||
  final DateTime createdAt;
 | 
			
		||||
  @override
 | 
			
		||||
  final DateTime updatedAt;
 | 
			
		||||
  @override
 | 
			
		||||
  final dynamic deletedAt;
 | 
			
		||||
  @override
 | 
			
		||||
  final String thumbnail;
 | 
			
		||||
  @override
 | 
			
		||||
  final String title;
 | 
			
		||||
  @override
 | 
			
		||||
  final String description;
 | 
			
		||||
  @override
 | 
			
		||||
  final String content;
 | 
			
		||||
  @override
 | 
			
		||||
  final String url;
 | 
			
		||||
  @override
 | 
			
		||||
  final String hash;
 | 
			
		||||
  @override
 | 
			
		||||
  final String source;
 | 
			
		||||
  @override
 | 
			
		||||
  final DateTime? publishedAt;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return 'SnNewsArticle(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, source: $source, publishedAt: $publishedAt)';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(Object other) {
 | 
			
		||||
    return identical(this, other) ||
 | 
			
		||||
        (other.runtimeType == runtimeType &&
 | 
			
		||||
            other is _$SnNewsArticleImpl &&
 | 
			
		||||
            (identical(other.id, id) || other.id == id) &&
 | 
			
		||||
            (identical(other.createdAt, createdAt) ||
 | 
			
		||||
                other.createdAt == createdAt) &&
 | 
			
		||||
            (identical(other.updatedAt, updatedAt) ||
 | 
			
		||||
                other.updatedAt == updatedAt) &&
 | 
			
		||||
            const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
 | 
			
		||||
            (identical(other.thumbnail, thumbnail) ||
 | 
			
		||||
                other.thumbnail == thumbnail) &&
 | 
			
		||||
            (identical(other.title, title) || other.title == title) &&
 | 
			
		||||
            (identical(other.description, description) ||
 | 
			
		||||
                other.description == description) &&
 | 
			
		||||
            (identical(other.content, content) || other.content == content) &&
 | 
			
		||||
            (identical(other.url, url) || other.url == url) &&
 | 
			
		||||
            (identical(other.hash, hash) || other.hash == hash) &&
 | 
			
		||||
            (identical(other.source, source) || other.source == source) &&
 | 
			
		||||
            (identical(other.publishedAt, publishedAt) ||
 | 
			
		||||
                other.publishedAt == publishedAt));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode => Object.hash(
 | 
			
		||||
      runtimeType,
 | 
			
		||||
      id,
 | 
			
		||||
      createdAt,
 | 
			
		||||
      updatedAt,
 | 
			
		||||
      const DeepCollectionEquality().hash(deletedAt),
 | 
			
		||||
      thumbnail,
 | 
			
		||||
      title,
 | 
			
		||||
      description,
 | 
			
		||||
      content,
 | 
			
		||||
      url,
 | 
			
		||||
      hash,
 | 
			
		||||
      source,
 | 
			
		||||
      publishedAt);
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnNewsArticle
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  @override
 | 
			
		||||
  @pragma('vm:prefer-inline')
 | 
			
		||||
  _$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith =>
 | 
			
		||||
      __$$SnNewsArticleImplCopyWithImpl<_$SnNewsArticleImpl>(this, _$identity);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Map<String, dynamic> toJson() {
 | 
			
		||||
    return _$$SnNewsArticleImplToJson(
 | 
			
		||||
      this,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
abstract class _SnNewsArticle implements SnNewsArticle {
 | 
			
		||||
  const factory _SnNewsArticle(
 | 
			
		||||
      {required final int id,
 | 
			
		||||
      required final DateTime createdAt,
 | 
			
		||||
      required final DateTime updatedAt,
 | 
			
		||||
      required final dynamic deletedAt,
 | 
			
		||||
      required final String thumbnail,
 | 
			
		||||
      required final String title,
 | 
			
		||||
      required final String description,
 | 
			
		||||
      required final String content,
 | 
			
		||||
      required final String url,
 | 
			
		||||
      required final String hash,
 | 
			
		||||
      required final String source,
 | 
			
		||||
      required final DateTime? publishedAt}) = _$SnNewsArticleImpl;
 | 
			
		||||
 | 
			
		||||
  factory _SnNewsArticle.fromJson(Map<String, dynamic> json) =
 | 
			
		||||
      _$SnNewsArticleImpl.fromJson;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get id;
 | 
			
		||||
  @override
 | 
			
		||||
  DateTime get createdAt;
 | 
			
		||||
  @override
 | 
			
		||||
  DateTime get updatedAt;
 | 
			
		||||
  @override
 | 
			
		||||
  dynamic get deletedAt;
 | 
			
		||||
  @override
 | 
			
		||||
  String get thumbnail;
 | 
			
		||||
  @override
 | 
			
		||||
  String get title;
 | 
			
		||||
  @override
 | 
			
		||||
  String get description;
 | 
			
		||||
  @override
 | 
			
		||||
  String get content;
 | 
			
		||||
  @override
 | 
			
		||||
  String get url;
 | 
			
		||||
  @override
 | 
			
		||||
  String get hash;
 | 
			
		||||
  @override
 | 
			
		||||
  String get source;
 | 
			
		||||
  @override
 | 
			
		||||
  DateTime? get publishedAt;
 | 
			
		||||
 | 
			
		||||
  /// Create a copy of SnNewsArticle
 | 
			
		||||
  /// with the given fields replaced by the non-null parameter values.
 | 
			
		||||
  @override
 | 
			
		||||
  @JsonKey(includeFromJson: false, includeToJson: false)
 | 
			
		||||
  _$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith =>
 | 
			
		||||
      throw _privateConstructorUsedError;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								lib/types/news.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								lib/types/news.g.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
			
		||||
 | 
			
		||||
part of 'news.dart';
 | 
			
		||||
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
// JsonSerializableGenerator
 | 
			
		||||
// **************************************************************************
 | 
			
		||||
 | 
			
		||||
_$SnNewsSourceImpl _$$SnNewsSourceImplFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
    _$SnNewsSourceImpl(
 | 
			
		||||
      id: json['id'] as String,
 | 
			
		||||
      label: json['label'] as String,
 | 
			
		||||
      type: json['type'] as String,
 | 
			
		||||
      source: json['source'] as String,
 | 
			
		||||
      depth: (json['depth'] as num).toInt(),
 | 
			
		||||
      enabled: json['enabled'] as bool,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
Map<String, dynamic> _$$SnNewsSourceImplToJson(_$SnNewsSourceImpl instance) =>
 | 
			
		||||
    <String, dynamic>{
 | 
			
		||||
      'id': instance.id,
 | 
			
		||||
      'label': instance.label,
 | 
			
		||||
      'type': instance.type,
 | 
			
		||||
      'source': instance.source,
 | 
			
		||||
      'depth': instance.depth,
 | 
			
		||||
      'enabled': instance.enabled,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
_$SnNewsArticleImpl _$$SnNewsArticleImplFromJson(Map<String, dynamic> json) =>
 | 
			
		||||
    _$SnNewsArticleImpl(
 | 
			
		||||
      id: (json['id'] as num).toInt(),
 | 
			
		||||
      createdAt: DateTime.parse(json['created_at'] as String),
 | 
			
		||||
      updatedAt: DateTime.parse(json['updated_at'] as String),
 | 
			
		||||
      deletedAt: json['deleted_at'],
 | 
			
		||||
      thumbnail: json['thumbnail'] as String,
 | 
			
		||||
      title: json['title'] as String,
 | 
			
		||||
      description: json['description'] as String,
 | 
			
		||||
      content: json['content'] as String,
 | 
			
		||||
      url: json['url'] as String,
 | 
			
		||||
      hash: json['hash'] as String,
 | 
			
		||||
      source: json['source'] as String,
 | 
			
		||||
      publishedAt: json['published_at'] == null
 | 
			
		||||
          ? null
 | 
			
		||||
          : DateTime.parse(json['published_at'] as String),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
Map<String, dynamic> _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) =>
 | 
			
		||||
    <String, dynamic>{
 | 
			
		||||
      'id': instance.id,
 | 
			
		||||
      'created_at': instance.createdAt.toIso8601String(),
 | 
			
		||||
      'updated_at': instance.updatedAt.toIso8601String(),
 | 
			
		||||
      'deleted_at': instance.deletedAt,
 | 
			
		||||
      'thumbnail': instance.thumbnail,
 | 
			
		||||
      'title': instance.title,
 | 
			
		||||
      'description': instance.description,
 | 
			
		||||
      'content': instance.content,
 | 
			
		||||
      'url': instance.url,
 | 
			
		||||
      'hash': instance.hash,
 | 
			
		||||
      'source': instance.source,
 | 
			
		||||
      'published_at': instance.publishedAt?.toIso8601String(),
 | 
			
		||||
    };
 | 
			
		||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:package_info_plus/package_info_plus.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
			
		||||
 | 
			
		||||
class AboutScreen extends StatelessWidget {
 | 
			
		||||
@@ -12,7 +13,12 @@ class AboutScreen extends StatelessWidget {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4));
 | 
			
		||||
 | 
			
		||||
    return SizedBox(
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('screenAbout').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: SizedBox(
 | 
			
		||||
        width: double.infinity,
 | 
			
		||||
        child: Column(
 | 
			
		||||
          mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
@@ -104,6 +110,7 @@ class AboutScreen extends StatelessWidget {
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										164
									
								
								lib/widgets/account/account_popover.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								lib/widgets/account/account_popover.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,164 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:relative_time/relative_time.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/experience.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/screens/account/profile_page.dart';
 | 
			
		||||
import 'package:surface/types/account.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
 | 
			
		||||
class AccountPopoverCard extends StatelessWidget {
 | 
			
		||||
  final SnAccount data;
 | 
			
		||||
 | 
			
		||||
  const AccountPopoverCard({super.key, required this.data});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    return Column(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
      mainAxisSize: MainAxisSize.min,
 | 
			
		||||
      children: [
 | 
			
		||||
        if (data.banner.isNotEmpty)
 | 
			
		||||
          Container(
 | 
			
		||||
            color: Theme.of(context).colorScheme.surfaceContainer,
 | 
			
		||||
            child: AspectRatio(
 | 
			
		||||
              aspectRatio: 16 / 7,
 | 
			
		||||
              child: AutoResizeUniversalImage(
 | 
			
		||||
                sn.getAttachmentUrl(data.banner),
 | 
			
		||||
                fit: BoxFit.cover,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        // Top padding
 | 
			
		||||
        Gap(16),
 | 
			
		||||
        Row(
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
          children: [
 | 
			
		||||
            AccountImage(
 | 
			
		||||
              content: data.avatar,
 | 
			
		||||
              radius: 20,
 | 
			
		||||
            ),
 | 
			
		||||
            Gap(16),
 | 
			
		||||
            Expanded(
 | 
			
		||||
              child: Column(
 | 
			
		||||
                crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                children: [
 | 
			
		||||
                  Text(data.nick).bold(),
 | 
			
		||||
                  Text('@${data.name}').fontSize(13).opacity(0.75),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            IconButton(
 | 
			
		||||
              onPressed: () {
 | 
			
		||||
                Navigator.pop(context);
 | 
			
		||||
                GoRouter.of(context).pushNamed(
 | 
			
		||||
                  'accountProfilePage',
 | 
			
		||||
                  pathParameters: {'name': data.name},
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
              icon: const Icon(Symbols.chevron_right),
 | 
			
		||||
              padding: EdgeInsets.zero,
 | 
			
		||||
              visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
 | 
			
		||||
            ),
 | 
			
		||||
            const Gap(8)
 | 
			
		||||
          ],
 | 
			
		||||
        ).padding(horizontal: 16),
 | 
			
		||||
        const Gap(16),
 | 
			
		||||
        Wrap(
 | 
			
		||||
          children: data.badges
 | 
			
		||||
              .map(
 | 
			
		||||
                (ele) => Tooltip(
 | 
			
		||||
              richMessage: TextSpan(
 | 
			
		||||
                children: [
 | 
			
		||||
                  TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
 | 
			
		||||
                  if (ele.metadata['title'] != null)
 | 
			
		||||
                    TextSpan(
 | 
			
		||||
                      text: '\n${ele.metadata['title']}',
 | 
			
		||||
                      style: const TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
                    ),
 | 
			
		||||
                  TextSpan(text: '\n'),
 | 
			
		||||
                  TextSpan(
 | 
			
		||||
                    text: DateFormat.yMEd().format(ele.createdAt),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
              child: Icon(
 | 
			
		||||
                kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
 | 
			
		||||
                color: kBadgesMeta[ele.type]?.$3,
 | 
			
		||||
                fill: 1,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          )
 | 
			
		||||
              .toList(),
 | 
			
		||||
        ).padding(horizontal: 24),
 | 
			
		||||
        const Gap(8),
 | 
			
		||||
        Row(
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
          children: [
 | 
			
		||||
            const Icon(Symbols.star),
 | 
			
		||||
            const Gap(8),
 | 
			
		||||
            Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
 | 
			
		||||
            const Gap(8),
 | 
			
		||||
            Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
 | 
			
		||||
            const Gap(8),
 | 
			
		||||
            Container(
 | 
			
		||||
              width: double.infinity,
 | 
			
		||||
              constraints: const BoxConstraints(maxWidth: 160),
 | 
			
		||||
              child: LinearProgressIndicator(
 | 
			
		||||
                value: calcLevelUpProgress(data.profile?.experience ?? 0),
 | 
			
		||||
                borderRadius: BorderRadius.circular(8),
 | 
			
		||||
                backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
 | 
			
		||||
              ).alignment(Alignment.centerLeft),
 | 
			
		||||
            ),
 | 
			
		||||
          ],
 | 
			
		||||
        ).padding(horizontal: 24),
 | 
			
		||||
        FutureBuilder(
 | 
			
		||||
          future: sn.client.get('/cgi/id/users/${data.name}/status'),
 | 
			
		||||
          builder: (context, snapshot) {
 | 
			
		||||
            final SnAccountStatusInfo? status =
 | 
			
		||||
                snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null;
 | 
			
		||||
            return Row(
 | 
			
		||||
                children: [
 | 
			
		||||
                  Icon(
 | 
			
		||||
                    Symbols.circle,
 | 
			
		||||
                    fill: 1,
 | 
			
		||||
                    size: 16,
 | 
			
		||||
                    color: (status?.isOnline ?? false) ? Colors.green : Colors.grey,
 | 
			
		||||
                  ).padding(all: 4),
 | 
			
		||||
                  const Gap(8),
 | 
			
		||||
                  Text(
 | 
			
		||||
                    status != null
 | 
			
		||||
                        ? status.isOnline
 | 
			
		||||
                            ? 'accountStatusOnline'.tr()
 | 
			
		||||
                            : 'accountStatusOffline'.tr()
 | 
			
		||||
                        : 'loading'.tr(),
 | 
			
		||||
                  ),
 | 
			
		||||
                  if (status != null && !status.isOnline && status.lastSeenAt != null)
 | 
			
		||||
                    Text(
 | 
			
		||||
                      'accountStatusLastSeen'.tr(args: [
 | 
			
		||||
                        status.lastSeenAt != null
 | 
			
		||||
                            ? RelativeTime(context).format(
 | 
			
		||||
                                status.lastSeenAt!.toLocal(),
 | 
			
		||||
                              )
 | 
			
		||||
                            : 'unknown',
 | 
			
		||||
                      ]),
 | 
			
		||||
                    ).padding(left: 6).opacity(0.75),
 | 
			
		||||
                ],
 | 
			
		||||
              ).padding(horizontal: 24);
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        // Bottom padding
 | 
			
		||||
        const Gap(16),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -15,6 +15,7 @@ class AttachmentList extends StatefulWidget {
 | 
			
		||||
  final List<SnAttachment?> data;
 | 
			
		||||
  final bool bordered;
 | 
			
		||||
  final bool gridded;
 | 
			
		||||
  final bool columned;
 | 
			
		||||
  final BoxFit fit;
 | 
			
		||||
  final double? maxHeight;
 | 
			
		||||
  final double? minWidth;
 | 
			
		||||
@@ -26,6 +27,7 @@ class AttachmentList extends StatefulWidget {
 | 
			
		||||
    required this.data,
 | 
			
		||||
    this.bordered = false,
 | 
			
		||||
    this.gridded = false,
 | 
			
		||||
    this.columned = false,
 | 
			
		||||
    this.fit = BoxFit.cover,
 | 
			
		||||
    this.maxHeight,
 | 
			
		||||
    this.minWidth,
 | 
			
		||||
@@ -105,7 +107,10 @@ class _AttachmentListState extends State<AttachmentList> {
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (widget.gridded) {
 | 
			
		||||
        final fullOfImage =
 | 
			
		||||
            widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
 | 
			
		||||
 | 
			
		||||
        if (widget.gridded && fullOfImage) {
 | 
			
		||||
          return Container(
 | 
			
		||||
            margin: widget.padding ?? EdgeInsets.zero,
 | 
			
		||||
            decoration: BoxDecoration(
 | 
			
		||||
@@ -153,6 +158,44 @@ class _AttachmentListState extends State<AttachmentList> {
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ((!fullOfImage && widget.gridded) || widget.columned) {
 | 
			
		||||
          return Container(
 | 
			
		||||
            margin: widget.padding ?? EdgeInsets.zero,
 | 
			
		||||
            decoration: BoxDecoration(
 | 
			
		||||
              color: backgroundColor,
 | 
			
		||||
              border: Border(
 | 
			
		||||
                top: borderSide,
 | 
			
		||||
                bottom: borderSide,
 | 
			
		||||
              ),
 | 
			
		||||
              borderRadius: AttachmentList.kDefaultRadius,
 | 
			
		||||
            ),
 | 
			
		||||
            child: ClipRRect(
 | 
			
		||||
              borderRadius: AttachmentList.kDefaultRadius,
 | 
			
		||||
              child: Column(
 | 
			
		||||
                children: widget.data
 | 
			
		||||
                    .mapIndexed(
 | 
			
		||||
                      (idx, ele) => GestureDetector(
 | 
			
		||||
                        child: AspectRatio(
 | 
			
		||||
                          aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
 | 
			
		||||
                          child: Container(
 | 
			
		||||
                            constraints: constraints,
 | 
			
		||||
                            child: AttachmentItem(
 | 
			
		||||
                              data: ele,
 | 
			
		||||
                              heroTag: heroTags[idx],
 | 
			
		||||
                              fit: BoxFit.cover,
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    )
 | 
			
		||||
                    .expand((ele) => [ele, const Divider(height: 1)])
 | 
			
		||||
                    .toList()
 | 
			
		||||
                  ..removeLast(),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Container(
 | 
			
		||||
          constraints: BoxConstraints(maxHeight: constraints.maxHeight),
 | 
			
		||||
          child: ScrollConfiguration(
 | 
			
		||||
 
 | 
			
		||||
@@ -129,6 +129,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
 | 
			
		||||
 | 
			
		||||
  Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
 | 
			
		||||
 | 
			
		||||
  bool _showDetail = false;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
@@ -144,9 +146,11 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
 | 
			
		||||
      onDismissed: () {
 | 
			
		||||
        Navigator.of(context).pop();
 | 
			
		||||
      },
 | 
			
		||||
      direction: DismissiblePageDismissDirection.down,
 | 
			
		||||
      direction: DismissiblePageDismissDirection.none,
 | 
			
		||||
      backgroundColor: Colors.transparent,
 | 
			
		||||
      isFullScreen: true,
 | 
			
		||||
      child: GestureDetector(
 | 
			
		||||
        behavior: HitTestBehavior.translucent,
 | 
			
		||||
        child: Scaffold(
 | 
			
		||||
          body: Stack(
 | 
			
		||||
            children: [
 | 
			
		||||
@@ -264,7 +268,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
 | 
			
		||||
                                borderRadius: const BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
                                onTap: _isDownloading
 | 
			
		||||
                                    ? null
 | 
			
		||||
                                  : () => _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
 | 
			
		||||
                                    : () =>
 | 
			
		||||
                                        _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
 | 
			
		||||
                                child: Container(
 | 
			
		||||
                                  padding: const EdgeInsets.all(6),
 | 
			
		||||
                                  child: !_isDownloading
 | 
			
		||||
@@ -322,7 +327,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
 | 
			
		||||
                                  'f/${item.metadata['exif']?['Aperture']}',
 | 
			
		||||
                                  style: metaTextStyle,
 | 
			
		||||
                                ).padding(right: 2),
 | 
			
		||||
                            if (item.metadata['exif']?['Megapixels'] != null && item.metadata['exif']?['Model'] != null)
 | 
			
		||||
                              if (item.metadata['exif']?['Megapixels'] != null &&
 | 
			
		||||
                                  item.metadata['exif']?['Model'] != null)
 | 
			
		||||
                                Text(
 | 
			
		||||
                                  '${item.metadata['exif']?['Megapixels']}MP',
 | 
			
		||||
                                  style: metaTextStyle,
 | 
			
		||||
@@ -357,6 +363,134 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        onVerticalDragUpdate: (details) {
 | 
			
		||||
          if (_showDetail) return;
 | 
			
		||||
          if (details.delta.dy <= -40) {
 | 
			
		||||
            _showDetail = true;
 | 
			
		||||
            showModalBottomSheet(
 | 
			
		||||
              context: context,
 | 
			
		||||
              builder: (context) => _AttachmentZoomDetailPopup(
 | 
			
		||||
                data: widget.data.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
 | 
			
		||||
              ),
 | 
			
		||||
            ).then((_) {
 | 
			
		||||
              _showDetail = false;
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        onTap: () {
 | 
			
		||||
          Navigator.of(context).pop();
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AttachmentZoomDetailPopup extends StatelessWidget {
 | 
			
		||||
  final SnAttachment data;
 | 
			
		||||
 | 
			
		||||
  const _AttachmentZoomDetailPopup({required this.data});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final ud = context.read<UserDirectoryProvider>();
 | 
			
		||||
    final account = ud.getAccountFromCache(data.accountId);
 | 
			
		||||
 | 
			
		||||
    const tableGap = TableRow(
 | 
			
		||||
      children: [
 | 
			
		||||
        TableCell(child: SizedBox(height: 16)),
 | 
			
		||||
        TableCell(child: SizedBox(height: 16)),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return SizedBox(
 | 
			
		||||
      child: Column(
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
        children: [
 | 
			
		||||
          Row(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
            children: [
 | 
			
		||||
              const Icon(Symbols.info, size: 24),
 | 
			
		||||
              const Gap(16),
 | 
			
		||||
              Text('attachmentDetailInfo').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
			
		||||
            ],
 | 
			
		||||
          ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: SingleChildScrollView(
 | 
			
		||||
              child: Table(
 | 
			
		||||
                columnWidths: {
 | 
			
		||||
                  0: IntrinsicColumnWidth(),
 | 
			
		||||
                  1: FlexColumnWidth(),
 | 
			
		||||
                },
 | 
			
		||||
                children: [
 | 
			
		||||
                  TableRow(
 | 
			
		||||
                    children: [
 | 
			
		||||
                      TableCell(
 | 
			
		||||
                        child: Text('attachmentUploadBy').tr().padding(right: 16),
 | 
			
		||||
                      ),
 | 
			
		||||
                      TableCell(
 | 
			
		||||
                        child: Row(
 | 
			
		||||
                          children: [
 | 
			
		||||
                            if (data.accountId > 0)
 | 
			
		||||
                              AccountImage(
 | 
			
		||||
                                content: account?.avatar,
 | 
			
		||||
                                radius: 8,
 | 
			
		||||
                              ),
 | 
			
		||||
                            const Gap(8),
 | 
			
		||||
                            Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()),
 | 
			
		||||
                            const Gap(8),
 | 
			
		||||
                            Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                  tableGap,
 | 
			
		||||
                  TableRow(
 | 
			
		||||
                    children: [
 | 
			
		||||
                      TableCell(child: Text('Mimetype').padding(right: 16)),
 | 
			
		||||
                      TableCell(child: Text(data.mimetype)),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                  TableRow(
 | 
			
		||||
                    children: [
 | 
			
		||||
                      TableCell(child: Text('Size').padding(right: 16)),
 | 
			
		||||
                      TableCell(
 | 
			
		||||
                          child: Row(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Text(data.size.formatBytes()),
 | 
			
		||||
                          const Gap(12),
 | 
			
		||||
                          Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75),
 | 
			
		||||
                        ],
 | 
			
		||||
                      )),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                  TableRow(
 | 
			
		||||
                    children: [
 | 
			
		||||
                      TableCell(child: Text('Name').padding(right: 16)),
 | 
			
		||||
                      TableCell(child: Text(data.name)),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                  if (data.hash.isNotEmpty)
 | 
			
		||||
                    TableRow(
 | 
			
		||||
                      children: [
 | 
			
		||||
                        TableCell(child: Text('Hash').padding(right: 16)),
 | 
			
		||||
                        TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)),
 | 
			
		||||
                      ],
 | 
			
		||||
                    ),
 | 
			
		||||
                  tableGap,
 | 
			
		||||
                  ...(data.metadata['exif']?.keys.map((k) => TableRow(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          TableCell(child: Text(k).padding(right: 16)),
 | 
			
		||||
                          TableCell(child: Text(data.metadata['exif'][k].toString())),
 | 
			
		||||
                        ],
 | 
			
		||||
                      )) ??
 | 
			
		||||
                      []),
 | 
			
		||||
                ],
 | 
			
		||||
              ).padding(horizontal: 20, vertical: 8),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,19 @@
 | 
			
		||||
import 'dart:math' as math;
 | 
			
		||||
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:popover/popover.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/config.dart';
 | 
			
		||||
import 'package:surface/providers/user_directory.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/types/chat.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_popover.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_list.dart';
 | 
			
		||||
import 'package:surface/widgets/context_menu.dart';
 | 
			
		||||
import 'package:surface/widgets/link_preview.dart';
 | 
			
		||||
@@ -49,6 +54,8 @@ class ChatMessage extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
    final dateFormatter = DateFormat('MM/dd HH:mm');
 | 
			
		||||
 | 
			
		||||
    final cfg = context.read<ConfigProvider>();
 | 
			
		||||
 | 
			
		||||
    return SwipeTo(
 | 
			
		||||
      key: Key('chat-message-${data.id}'),
 | 
			
		||||
      iconOnLeftSwipe: Symbols.reply,
 | 
			
		||||
@@ -95,8 +102,28 @@ class ChatMessage extends StatelessWidget {
 | 
			
		||||
                crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                children: [
 | 
			
		||||
                  if (!isMerged && !isCompact)
 | 
			
		||||
                    AccountImage(
 | 
			
		||||
                    GestureDetector(
 | 
			
		||||
                      child: AccountImage(
 | 
			
		||||
                        content: user?.avatar,
 | 
			
		||||
                      ),
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        if (user == null) return;
 | 
			
		||||
                        showPopover(
 | 
			
		||||
                          backgroundColor: Theme.of(context).colorScheme.surface,
 | 
			
		||||
                          context: context,
 | 
			
		||||
                          transition: PopoverTransition.other,
 | 
			
		||||
                          bodyBuilder: (context) => SizedBox(
 | 
			
		||||
                            width: math.min(400, MediaQuery.of(context).size.width - 10),
 | 
			
		||||
                            child: AccountPopoverCard(
 | 
			
		||||
                              data: user,
 | 
			
		||||
                            ),
 | 
			
		||||
                          ),
 | 
			
		||||
                          direction: PopoverDirection.bottom,
 | 
			
		||||
                          arrowHeight: 5,
 | 
			
		||||
                          arrowWidth: 15,
 | 
			
		||||
                          arrowDxOffset: -190,
 | 
			
		||||
                        );
 | 
			
		||||
                      },
 | 
			
		||||
                    )
 | 
			
		||||
                  else if (isMerged)
 | 
			
		||||
                    const Gap(40),
 | 
			
		||||
@@ -168,7 +195,10 @@ class ChatMessage extends StatelessWidget {
 | 
			
		||||
                ],
 | 
			
		||||
              ).opacity(isPending ? 0.5 : 1),
 | 
			
		||||
            ),
 | 
			
		||||
            if (data.body['text'] != null && data.type == 'messages.new' && (data.body['text']?.isNotEmpty ?? false))
 | 
			
		||||
            if (data.body['text'] != null &&
 | 
			
		||||
                data.type == 'messages.new' &&
 | 
			
		||||
                (data.body['text']?.isNotEmpty ?? false) &&
 | 
			
		||||
                (cfg.prefs.getBool(kAppExpandChatLink) ?? true))
 | 
			
		||||
              LinkPreviewWidget(text: data.body['text']!),
 | 
			
		||||
            if (data.preload?.attachments?.isNotEmpty ?? false)
 | 
			
		||||
              AttachmentList(
 | 
			
		||||
@@ -251,11 +281,14 @@ class _ChatMessageText extends StatelessWidget {
 | 
			
		||||
                buttonItems: items,
 | 
			
		||||
              );
 | 
			
		||||
            },
 | 
			
		||||
            child: Container(
 | 
			
		||||
              constraints: const BoxConstraints(maxWidth: 480),
 | 
			
		||||
              child: MarkdownTextContent(
 | 
			
		||||
                content: data.body['text'],
 | 
			
		||||
                isAutoWarp: true,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
          if (data.updatedAt != data.createdAt)
 | 
			
		||||
            Text(
 | 
			
		||||
              'messageEditedHint'.tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -117,7 +117,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
 | 
			
		||||
    // Send the message
 | 
			
		||||
    // NOTICE This future should not be awaited, so that the message can be sent in the background and the user can continue to type
 | 
			
		||||
    widget.controller.sendMessage(
 | 
			
		||||
      'messages.new',
 | 
			
		||||
      _editingMessage != null ? 'messages.edit' : 'messages.new',
 | 
			
		||||
      _contentController.text,
 | 
			
		||||
      attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
 | 
			
		||||
      relatedId: _editingMessage?.id,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
@@ -16,15 +18,14 @@ class ConnectionIndicator extends StatelessWidget {
 | 
			
		||||
      listenable: ws,
 | 
			
		||||
      builder: (context, _) {
 | 
			
		||||
        final ua = context.read<UserProvider>();
 | 
			
		||||
        final show = (ws.isBusy || !ws.isConnected) && ua.isAuthorized;
 | 
			
		||||
 | 
			
		||||
        return GestureDetector(
 | 
			
		||||
          child: Container(
 | 
			
		||||
            padding: EdgeInsets.only(
 | 
			
		||||
              bottom: 8,
 | 
			
		||||
              top: MediaQuery.of(context).padding.top + 8,
 | 
			
		||||
              left: 24,
 | 
			
		||||
              right: 24,
 | 
			
		||||
            ),
 | 
			
		||||
        return IgnorePointer(
 | 
			
		||||
          ignoring: !show,
 | 
			
		||||
          child: GestureDetector(
 | 
			
		||||
            child: Material(
 | 
			
		||||
              elevation: 2,
 | 
			
		||||
              shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
 | 
			
		||||
              color: Theme.of(context).colorScheme.secondaryContainer,
 | 
			
		||||
              child: ua.isAuthorized
 | 
			
		||||
                  ? Row(
 | 
			
		||||
@@ -32,21 +33,25 @@ class ConnectionIndicator extends StatelessWidget {
 | 
			
		||||
                      crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                      children: [
 | 
			
		||||
                        if (ws.isBusy)
 | 
			
		||||
                        Text('serverConnecting').tr().textColor(
 | 
			
		||||
                            Theme.of(context).colorScheme.onSecondaryContainer)
 | 
			
		||||
                          Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
 | 
			
		||||
                        else if (!ws.isConnected)
 | 
			
		||||
                        Text('serverDisconnected').tr().textColor(
 | 
			
		||||
                            Theme.of(context).colorScheme.onSecondaryContainer),
 | 
			
		||||
                          Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
 | 
			
		||||
                        else
 | 
			
		||||
                          Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
 | 
			
		||||
                        const Gap(8),
 | 
			
		||||
                        if (ws.isBusy)
 | 
			
		||||
                          const CircularProgressIndicator(strokeWidth: 2.5)
 | 
			
		||||
                              .width(12)
 | 
			
		||||
                              .height(12)
 | 
			
		||||
                              .padding(horizontal: 4, right: 4)
 | 
			
		||||
                        else if (!ws.isConnected)
 | 
			
		||||
                          const Icon(Symbols.power_off, size: 18)
 | 
			
		||||
                        else
 | 
			
		||||
                          const Icon(Symbols.power, size: 18),
 | 
			
		||||
                      ],
 | 
			
		||||
                  )
 | 
			
		||||
                    ).padding(horizontal: 8, vertical: 4)
 | 
			
		||||
                  : const SizedBox.shrink(),
 | 
			
		||||
          )
 | 
			
		||||
              .height(
 | 
			
		||||
                  (ws.isBusy || !ws.isConnected) && ua.isAuthorized
 | 
			
		||||
                      ? MediaQuery.of(context).padding.top + 36
 | 
			
		||||
                      : 0,
 | 
			
		||||
                  animate: true)
 | 
			
		||||
              .animate(
 | 
			
		||||
            ).opacity(show ? 1 : 0, animate: true).animate(
 | 
			
		||||
                  const Duration(milliseconds: 300),
 | 
			
		||||
                  Curves.easeInOut,
 | 
			
		||||
                ),
 | 
			
		||||
@@ -55,6 +60,7 @@ class ConnectionIndicator extends StatelessWidget {
 | 
			
		||||
                ws.connect();
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -7,12 +7,11 @@ import 'package:marquee/marquee.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:responsive_framework/responsive_framework.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/link_preview.dart';
 | 
			
		||||
import 'package:surface/types/link.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
			
		||||
 | 
			
		||||
import '../providers/link_preview.dart';
 | 
			
		||||
 | 
			
		||||
class LinkPreviewWidget extends StatefulWidget {
 | 
			
		||||
  final String text;
 | 
			
		||||
 | 
			
		||||
@@ -81,8 +80,9 @@ class _LinkPreviewEntry extends StatelessWidget {
 | 
			
		||||
                  child: AspectRatio(
 | 
			
		||||
                    aspectRatio: 16 / 9,
 | 
			
		||||
                    child: ClipRRect(
 | 
			
		||||
                      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                      child: AutoResizeUniversalImage(
 | 
			
		||||
                        meta.image!,
 | 
			
		||||
                        meta.image!.startsWith('//') ? 'https:${meta.image}' : meta.image!,
 | 
			
		||||
                        fit: BoxFit.contain,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
@@ -94,11 +94,14 @@ class _LinkPreviewEntry extends StatelessWidget {
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    if (meta.icon?.isNotEmpty ?? false)
 | 
			
		||||
                      StyledWidget(
 | 
			
		||||
                        meta.icon!.endsWith('.svg')
 | 
			
		||||
                            ? SvgPicture.network(meta.icon!)
 | 
			
		||||
                      SizedBox(
 | 
			
		||||
                        width: 36,
 | 
			
		||||
                        height: 36,
 | 
			
		||||
                        child: meta.icon!.endsWith('.svg')
 | 
			
		||||
                            ? SvgPicture.network(meta.icon!, width: 36, height: 36)
 | 
			
		||||
                            : UniversalImage(
 | 
			
		||||
                                meta.icon!,
 | 
			
		||||
                                noErrorWidget: true,
 | 
			
		||||
                                width: 36,
 | 
			
		||||
                                height: 36,
 | 
			
		||||
                                cacheHeight: 36,
 | 
			
		||||
 
 | 
			
		||||
@@ -18,9 +18,7 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
      context
 | 
			
		||||
          .read<NavigationProvider>()
 | 
			
		||||
          .autoDetectIndex(GoRouter.maybeOf(context));
 | 
			
		||||
      context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -31,11 +29,11 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
 | 
			
		||||
    return ListenableBuilder(
 | 
			
		||||
      listenable: nav,
 | 
			
		||||
      builder: (context, _) {
 | 
			
		||||
        final destinations =
 | 
			
		||||
            nav.destinations.where((ele) => ele.isPinned).toList();
 | 
			
		||||
        final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
 | 
			
		||||
 | 
			
		||||
        return NavigationRail(
 | 
			
		||||
          selectedIndex: nav.currentIndex,
 | 
			
		||||
          selectedIndex:
 | 
			
		||||
              nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
 | 
			
		||||
          destinations: [
 | 
			
		||||
            ...destinations.where((ele) => ele.isPinned).map((ele) {
 | 
			
		||||
              return NavigationRailDestination(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
@@ -12,42 +11,84 @@ import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/config.dart';
 | 
			
		||||
import 'package:surface/providers/navigation.dart';
 | 
			
		||||
import 'package:surface/widgets/connection_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_background.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_rail_navigation.dart';
 | 
			
		||||
import 'package:surface/widgets/notify_indicator.dart';
 | 
			
		||||
 | 
			
		||||
final globalRootScaffoldKey = GlobalKey<ScaffoldState>();
 | 
			
		||||
 | 
			
		||||
class AppPageScaffold extends StatelessWidget {
 | 
			
		||||
  final String? title;
 | 
			
		||||
class AppScaffold extends StatelessWidget {
 | 
			
		||||
  final Widget? body;
 | 
			
		||||
  final bool showAppBar;
 | 
			
		||||
  final bool showBottomNavigation;
 | 
			
		||||
  final PreferredSizeWidget? bottomNavigationBar;
 | 
			
		||||
  final PreferredSizeWidget? bottomSheet;
 | 
			
		||||
  final Drawer? drawer;
 | 
			
		||||
  final Widget? endDrawer;
 | 
			
		||||
  final FloatingActionButtonAnimator? floatingActionButtonAnimator;
 | 
			
		||||
  final FloatingActionButtonLocation? floatingActionButtonLocation;
 | 
			
		||||
  final Widget? floatingActionButton;
 | 
			
		||||
  final AppBar? appBar;
 | 
			
		||||
  final DrawerCallback? onDrawerChanged;
 | 
			
		||||
  final DrawerCallback? onEndDrawerChanged;
 | 
			
		||||
 | 
			
		||||
  const AppPageScaffold({
 | 
			
		||||
  const AppScaffold({
 | 
			
		||||
    super.key,
 | 
			
		||||
    this.title,
 | 
			
		||||
    this.appBar,
 | 
			
		||||
    this.body,
 | 
			
		||||
    this.showAppBar = true,
 | 
			
		||||
    this.showBottomNavigation = false,
 | 
			
		||||
    this.floatingActionButton,
 | 
			
		||||
    this.floatingActionButtonLocation,
 | 
			
		||||
    this.floatingActionButtonAnimator,
 | 
			
		||||
    this.bottomNavigationBar,
 | 
			
		||||
    this.bottomSheet,
 | 
			
		||||
    this.drawer,
 | 
			
		||||
    this.endDrawer,
 | 
			
		||||
    this.onDrawerChanged,
 | 
			
		||||
    this.onEndDrawerChanged,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final state = GoRouter.maybeOf(context);
 | 
			
		||||
    final routeName = state?.routerDelegate.currentConfiguration.last.route.name;
 | 
			
		||||
 | 
			
		||||
    final autoTitle = state != null ? 'screen${routeName?.capitalize()}' : 'screen';
 | 
			
		||||
    final appBarHeight = appBar?.preferredSize.height ?? 0;
 | 
			
		||||
    final safeTop = MediaQuery.of(context).padding.top;
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: showAppBar
 | 
			
		||||
          ? AppBar(
 | 
			
		||||
              title: Text(title ?? autoTitle.tr()),
 | 
			
		||||
            )
 | 
			
		||||
          : null,
 | 
			
		||||
      body: body,
 | 
			
		||||
      extendBody: true,
 | 
			
		||||
      extendBodyBehindAppBar: true,
 | 
			
		||||
      backgroundColor: Theme.of(context).scaffoldBackgroundColor,
 | 
			
		||||
      body: SizedBox.expand(
 | 
			
		||||
        child: AppBackground(
 | 
			
		||||
          child: Column(
 | 
			
		||||
            children: [
 | 
			
		||||
              IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)),
 | 
			
		||||
              if (body != null) Expanded(child: body!),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      appBar: appBar,
 | 
			
		||||
      bottomNavigationBar: bottomNavigationBar,
 | 
			
		||||
      bottomSheet: bottomSheet,
 | 
			
		||||
      drawer: drawer,
 | 
			
		||||
      endDrawer: endDrawer,
 | 
			
		||||
      floatingActionButton: floatingActionButton,
 | 
			
		||||
      floatingActionButtonAnimator: floatingActionButtonAnimator,
 | 
			
		||||
      floatingActionButtonLocation: floatingActionButtonLocation,
 | 
			
		||||
      onDrawerChanged: onDrawerChanged,
 | 
			
		||||
      onEndDrawerChanged: onEndDrawerChanged,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class PageBackButton extends StatelessWidget {
 | 
			
		||||
  const PageBackButton({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return BackButton(
 | 
			
		||||
      onPressed: () {
 | 
			
		||||
        GoRouter.of(context).pop();
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -98,14 +139,18 @@ class AppRootScaffold extends StatelessWidget {
 | 
			
		||||
      iconMouseDown: Theme.of(context).colorScheme.primary,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return AppBackground(
 | 
			
		||||
      isRoot: true,
 | 
			
		||||
      child: Scaffold(
 | 
			
		||||
    final safeTop = MediaQuery.of(context).padding.top;
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      key: globalRootScaffoldKey,
 | 
			
		||||
        body: Column(
 | 
			
		||||
      backgroundColor: Theme.of(context).colorScheme.surface,
 | 
			
		||||
      body: Stack(
 | 
			
		||||
        children: [
 | 
			
		||||
          Column(
 | 
			
		||||
            children: [
 | 
			
		||||
              if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
 | 
			
		||||
              Container(
 | 
			
		||||
                WindowTitleBarBox(
 | 
			
		||||
                  child: Container(
 | 
			
		||||
                    decoration: BoxDecoration(
 | 
			
		||||
                      border: Border(
 | 
			
		||||
                        bottom: BorderSide(
 | 
			
		||||
@@ -114,22 +159,18 @@ class AppRootScaffold extends StatelessWidget {
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    child: MoveWindow(
 | 
			
		||||
                      child: Row(
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                        mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
 | 
			
		||||
                        children: [
 | 
			
		||||
                    WindowTitleBarBox(
 | 
			
		||||
                      child: MoveWindow(
 | 
			
		||||
                        child: Text(
 | 
			
		||||
                          Text(
 | 
			
		||||
                            'Solar Network',
 | 
			
		||||
                            style: GoogleFonts.spaceGrotesk(),
 | 
			
		||||
                          ).padding(horizontal: 12, vertical: 5),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                          if (!Platform.isMacOS)
 | 
			
		||||
                      Expanded(
 | 
			
		||||
                        child: WindowTitleBarBox(
 | 
			
		||||
                          child: Row(
 | 
			
		||||
                            Row(
 | 
			
		||||
                              mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                              children: [
 | 
			
		||||
                                Expanded(child: MoveWindow()),
 | 
			
		||||
                                Row(
 | 
			
		||||
@@ -141,19 +182,21 @@ class AppRootScaffold extends StatelessWidget {
 | 
			
		||||
                                ),
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
            ConnectionIndicator(),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              Expanded(child: innerWidget),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
          Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
 | 
			
		||||
          Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
 | 
			
		||||
      drawerEdgeDragWidth: isPopable ? 0 : null,
 | 
			
		||||
      bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										63
									
								
								lib/widgets/notify_indicator.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								lib/widgets/notify_indicator.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/notification.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
 | 
			
		||||
class NotifyIndicator extends StatelessWidget {
 | 
			
		||||
  const NotifyIndicator({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final ua = context.read<UserProvider>();
 | 
			
		||||
    final nty = context.watch<NotificationProvider>();
 | 
			
		||||
 | 
			
		||||
    final show = nty.notifications.isNotEmpty && ua.isAuthorized;
 | 
			
		||||
 | 
			
		||||
    return ListenableBuilder(
 | 
			
		||||
        listenable: nty,
 | 
			
		||||
        builder: (context, _) {
 | 
			
		||||
          return IgnorePointer(
 | 
			
		||||
            ignoring: !show,
 | 
			
		||||
            child: GestureDetector(
 | 
			
		||||
              child: Material(
 | 
			
		||||
                elevation: 2,
 | 
			
		||||
                shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
 | 
			
		||||
                color: Theme.of(context).colorScheme.secondaryContainer,
 | 
			
		||||
                child: ua.isAuthorized
 | 
			
		||||
                    ? Row(
 | 
			
		||||
                        mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Text(
 | 
			
		||||
                            nty.notifications.lastOrNull?.title ??
 | 
			
		||||
                                'notificationUnreadCount'.plural(nty.notifications.length),
 | 
			
		||||
                            maxLines: 1,
 | 
			
		||||
                            overflow: TextOverflow.ellipsis,
 | 
			
		||||
                          ),
 | 
			
		||||
                          if (nty.notifications.lastOrNull?.body != null)
 | 
			
		||||
                            Text(
 | 
			
		||||
                              nty.notifications.lastOrNull!.body,
 | 
			
		||||
                              maxLines: 1,
 | 
			
		||||
                              overflow: TextOverflow.ellipsis,
 | 
			
		||||
                            ).padding(left: 4),
 | 
			
		||||
                          const Gap(8),
 | 
			
		||||
                          const Icon(Symbols.notifications_unread, size: 18),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ).padding(horizontal: 8, vertical: 4)
 | 
			
		||||
                    : const SizedBox.shrink(),
 | 
			
		||||
              ).opacity(show ? 1 : 0, animate: true).animate(
 | 
			
		||||
                    const Duration(milliseconds: 300),
 | 
			
		||||
                    Curves.easeInOut,
 | 
			
		||||
                  ),
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                nty.clear();
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -203,6 +203,8 @@ class PostItem extends StatelessWidget {
 | 
			
		||||
        ?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
 | 
			
		||||
        .toList();
 | 
			
		||||
 | 
			
		||||
    final cfg = context.read<ConfigProvider>();
 | 
			
		||||
 | 
			
		||||
    return Column(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
      children: [
 | 
			
		||||
@@ -256,13 +258,12 @@ class PostItem extends StatelessWidget {
 | 
			
		||||
          AttachmentList(
 | 
			
		||||
            data: displayableAttachments!,
 | 
			
		||||
            bordered: true,
 | 
			
		||||
            gridded: true,
 | 
			
		||||
            maxHeight: showFullPost ? null : 480,
 | 
			
		||||
            minWidth: 640,
 | 
			
		||||
            maxWidth: MediaQuery.of(context).size.width - 20,
 | 
			
		||||
            fit: showFullPost ? BoxFit.cover : BoxFit.contain,
 | 
			
		||||
            padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
          ),
 | 
			
		||||
        if (data.body['content'] != null)
 | 
			
		||||
        if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
 | 
			
		||||
          LinkPreviewWidget(
 | 
			
		||||
            text: data.body['content'],
 | 
			
		||||
          ).padding(horizontal: 4),
 | 
			
		||||
@@ -344,7 +345,7 @@ class PostShareImageWidget extends StatelessWidget {
 | 
			
		||||
          if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
 | 
			
		||||
            StyledWidget(AttachmentList(
 | 
			
		||||
              data: data.preload!.attachments!,
 | 
			
		||||
              gridded: true,
 | 
			
		||||
              columned: true,
 | 
			
		||||
            )).padding(horizontal: 16, bottom: 8),
 | 
			
		||||
          Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
@@ -880,6 +881,7 @@ class _PostContentBody extends StatelessWidget {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    if (data.body['content'] == null) return const SizedBox.shrink();
 | 
			
		||||
    final content = MarkdownTextContent(
 | 
			
		||||
      isAutoWarp: data.type == 'story',
 | 
			
		||||
      isEnlargeSticker: true,
 | 
			
		||||
      textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
 | 
			
		||||
      content: data.body['content'],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
@@ -12,6 +13,7 @@ import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
class PostReactionPopup extends StatefulWidget {
 | 
			
		||||
  final SnPost data;
 | 
			
		||||
  final Function(Map<String, int> value, int attr, int delta)? onChanged;
 | 
			
		||||
 | 
			
		||||
  const PostReactionPopup({super.key, required this.data, this.onChanged});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -59,6 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      HapticFeedback.mediumImpact();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      // ignore: use_build_context_synchronously
 | 
			
		||||
      if (context.mounted) context.showErrorDialog(err);
 | 
			
		||||
@@ -84,9 +87,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
 | 
			
		||||
            children: [
 | 
			
		||||
              const Icon(Symbols.mood, size: 24),
 | 
			
		||||
              const Gap(16),
 | 
			
		||||
              Text('postReactions')
 | 
			
		||||
                  .tr()
 | 
			
		||||
                  .textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
			
		||||
              Text('postReactions').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
 | 
			
		||||
            ],
 | 
			
		||||
          ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
			
		||||
          Container(
 | 
			
		||||
@@ -102,9 +103,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
 | 
			
		||||
                Text('postReactionDownvote').plural(widget.data.totalDownvote),
 | 
			
		||||
                const Gap(24),
 | 
			
		||||
                Icon(
 | 
			
		||||
                  widget.data.totalUpvote >= widget.data.totalDownvote
 | 
			
		||||
                      ? Symbols.trending_up
 | 
			
		||||
                      : Symbols.trending_down,
 | 
			
		||||
                  widget.data.totalUpvote >= widget.data.totalDownvote ? Symbols.trending_up : Symbols.trending_down,
 | 
			
		||||
                  size: 16,
 | 
			
		||||
                ),
 | 
			
		||||
                const Gap(8),
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ import file_selector_macos
 | 
			
		||||
import firebase_analytics
 | 
			
		||||
import firebase_core
 | 
			
		||||
import firebase_messaging
 | 
			
		||||
import flutter_inappwebview_macos
 | 
			
		||||
import flutter_udid
 | 
			
		||||
import flutter_webrtc
 | 
			
		||||
import gal
 | 
			
		||||
@@ -40,6 +41,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
			
		||||
  FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
 | 
			
		||||
  FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
 | 
			
		||||
  FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
 | 
			
		||||
  InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
 | 
			
		||||
  FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
 | 
			
		||||
  FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
 | 
			
		||||
  GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
 | 
			
		||||
 
 | 
			
		||||
@@ -12,59 +12,59 @@ PODS:
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - file_selector_macos (0.0.1):
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - Firebase/Analytics (11.4.0):
 | 
			
		||||
  - Firebase/Analytics (11.6.0):
 | 
			
		||||
    - Firebase/Core
 | 
			
		||||
  - Firebase/Core (11.4.0):
 | 
			
		||||
  - Firebase/Core (11.6.0):
 | 
			
		||||
    - Firebase/CoreOnly
 | 
			
		||||
    - FirebaseAnalytics (~> 11.4.0)
 | 
			
		||||
  - Firebase/CoreOnly (11.4.0):
 | 
			
		||||
    - FirebaseCore (= 11.4.0)
 | 
			
		||||
  - Firebase/Messaging (11.4.0):
 | 
			
		||||
    - FirebaseAnalytics (~> 11.6.0)
 | 
			
		||||
  - Firebase/CoreOnly (11.6.0):
 | 
			
		||||
    - FirebaseCore (~> 11.6.0)
 | 
			
		||||
  - Firebase/Messaging (11.6.0):
 | 
			
		||||
    - Firebase/CoreOnly
 | 
			
		||||
    - FirebaseMessaging (~> 11.4.0)
 | 
			
		||||
  - firebase_analytics (11.3.6):
 | 
			
		||||
    - Firebase/Analytics (= 11.4.0)
 | 
			
		||||
    - FirebaseMessaging (~> 11.6.0)
 | 
			
		||||
  - firebase_analytics (11.4.0):
 | 
			
		||||
    - Firebase/Analytics (= 11.6.0)
 | 
			
		||||
    - firebase_core
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - firebase_core (3.9.0):
 | 
			
		||||
    - Firebase/CoreOnly (~> 11.4.0)
 | 
			
		||||
  - firebase_core (3.10.0):
 | 
			
		||||
    - Firebase/CoreOnly (~> 11.6.0)
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - firebase_messaging (15.1.6):
 | 
			
		||||
    - Firebase/CoreOnly (~> 11.4.0)
 | 
			
		||||
    - Firebase/Messaging (~> 11.4.0)
 | 
			
		||||
  - firebase_messaging (15.2.0):
 | 
			
		||||
    - Firebase/CoreOnly (~> 11.6.0)
 | 
			
		||||
    - Firebase/Messaging (~> 11.6.0)
 | 
			
		||||
    - firebase_core
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - FirebaseAnalytics (11.4.0):
 | 
			
		||||
    - FirebaseAnalytics/AdIdSupport (= 11.4.0)
 | 
			
		||||
    - FirebaseCore (~> 11.0)
 | 
			
		||||
  - FirebaseAnalytics (11.6.0):
 | 
			
		||||
    - FirebaseAnalytics/AdIdSupport (= 11.6.0)
 | 
			
		||||
    - FirebaseCore (~> 11.6.0)
 | 
			
		||||
    - FirebaseInstallations (~> 11.0)
 | 
			
		||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/Network (~> 8.0)
 | 
			
		||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
			
		||||
    - nanopb (~> 3.30910.0)
 | 
			
		||||
  - FirebaseAnalytics/AdIdSupport (11.4.0):
 | 
			
		||||
    - FirebaseCore (~> 11.0)
 | 
			
		||||
  - FirebaseAnalytics/AdIdSupport (11.6.0):
 | 
			
		||||
    - FirebaseCore (~> 11.6.0)
 | 
			
		||||
    - FirebaseInstallations (~> 11.0)
 | 
			
		||||
    - GoogleAppMeasurement (= 11.4.0)
 | 
			
		||||
    - GoogleAppMeasurement (= 11.6.0)
 | 
			
		||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/Network (~> 8.0)
 | 
			
		||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
			
		||||
    - nanopb (~> 3.30910.0)
 | 
			
		||||
  - FirebaseCore (11.4.0):
 | 
			
		||||
    - FirebaseCoreInternal (~> 11.0)
 | 
			
		||||
  - FirebaseCore (11.6.0):
 | 
			
		||||
    - FirebaseCoreInternal (~> 11.6.0)
 | 
			
		||||
    - GoogleUtilities/Environment (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/Logger (~> 8.0)
 | 
			
		||||
  - FirebaseCoreInternal (11.6.0):
 | 
			
		||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
			
		||||
  - FirebaseInstallations (11.4.0):
 | 
			
		||||
    - FirebaseCore (~> 11.0)
 | 
			
		||||
  - FirebaseInstallations (11.6.0):
 | 
			
		||||
    - FirebaseCore (~> 11.6.0)
 | 
			
		||||
    - GoogleUtilities/Environment (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/UserDefaults (~> 8.0)
 | 
			
		||||
    - PromisesObjC (~> 2.4)
 | 
			
		||||
  - FirebaseMessaging (11.4.0):
 | 
			
		||||
    - FirebaseCore (~> 11.0)
 | 
			
		||||
  - FirebaseMessaging (11.6.0):
 | 
			
		||||
    - FirebaseCore (~> 11.6.0)
 | 
			
		||||
    - FirebaseInstallations (~> 11.0)
 | 
			
		||||
    - GoogleDataTransport (~> 10.0)
 | 
			
		||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
			
		||||
@@ -75,28 +75,28 @@ PODS:
 | 
			
		||||
  - flutter_udid (0.0.1):
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
    - SAMKeychain
 | 
			
		||||
  - flutter_webrtc (0.12.2):
 | 
			
		||||
  - flutter_webrtc (0.12.6):
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
    - WebRTC-SDK (= 125.6422.06)
 | 
			
		||||
  - FlutterMacOS (1.0.0)
 | 
			
		||||
  - gal (1.0.0):
 | 
			
		||||
    - Flutter
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - GoogleAppMeasurement (11.4.0):
 | 
			
		||||
    - GoogleAppMeasurement/AdIdSupport (= 11.4.0)
 | 
			
		||||
  - GoogleAppMeasurement (11.6.0):
 | 
			
		||||
    - GoogleAppMeasurement/AdIdSupport (= 11.6.0)
 | 
			
		||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/Network (~> 8.0)
 | 
			
		||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
			
		||||
    - nanopb (~> 3.30910.0)
 | 
			
		||||
  - GoogleAppMeasurement/AdIdSupport (11.4.0):
 | 
			
		||||
    - GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0)
 | 
			
		||||
  - GoogleAppMeasurement/AdIdSupport (11.6.0):
 | 
			
		||||
    - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
 | 
			
		||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/Network (~> 8.0)
 | 
			
		||||
    - "GoogleUtilities/NSData+zlib (~> 8.0)"
 | 
			
		||||
    - nanopb (~> 3.30910.0)
 | 
			
		||||
  - GoogleAppMeasurement/WithoutAdIdSupport (11.4.0):
 | 
			
		||||
  - GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
 | 
			
		||||
    - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/MethodSwizzler (~> 8.0)
 | 
			
		||||
    - GoogleUtilities/Network (~> 8.0)
 | 
			
		||||
@@ -134,7 +134,7 @@ PODS:
 | 
			
		||||
    - GoogleUtilities/Privacy
 | 
			
		||||
  - in_app_review (2.0.0):
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
  - livekit_client (2.3.4):
 | 
			
		||||
  - livekit_client (2.3.5):
 | 
			
		||||
    - flutter_webrtc
 | 
			
		||||
    - FlutterMacOS
 | 
			
		||||
    - WebRTC-SDK (= 125.6422.06)
 | 
			
		||||
@@ -287,24 +287,24 @@ SPEC CHECKSUMS:
 | 
			
		||||
  device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
 | 
			
		||||
  file_saver: 44e6fbf666677faf097302460e214e977fdd977b
 | 
			
		||||
  file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
 | 
			
		||||
  Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99
 | 
			
		||||
  firebase_analytics: a80b3d6645f2f12d626fde928b61dae12e5ea2ef
 | 
			
		||||
  firebase_core: 1dfe1f4d02ad78be0277e320aa3d8384cf46231f
 | 
			
		||||
  firebase_messaging: 61f678060b69a7ae1013e3a939ec8e1c56ef6fcf
 | 
			
		||||
  FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49
 | 
			
		||||
  FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771
 | 
			
		||||
  Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
 | 
			
		||||
  firebase_analytics: 5249f87da6fed852901581aab2602e0280ec2fdb
 | 
			
		||||
  firebase_core: 6d9bb8b0ea817e8fe0d928177d42275b45fdba6f
 | 
			
		||||
  firebase_messaging: ae8e88b586e4d50abc7cac5bacf74d21967fd226
 | 
			
		||||
  FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
 | 
			
		||||
  FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
 | 
			
		||||
  FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
 | 
			
		||||
  FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414
 | 
			
		||||
  FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
 | 
			
		||||
  FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
 | 
			
		||||
  FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
 | 
			
		||||
  flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
 | 
			
		||||
  flutter_webrtc: 53c9e1285ab32dfb58afb1e1471416a877e23d7a
 | 
			
		||||
  flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
 | 
			
		||||
  FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
 | 
			
		||||
  gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
 | 
			
		||||
  GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e
 | 
			
		||||
  GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
 | 
			
		||||
  GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
 | 
			
		||||
  GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
 | 
			
		||||
  in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
 | 
			
		||||
  livekit_client: b7ab91e79e657d7d40da16cb2f90d517cb72d406
 | 
			
		||||
  livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1
 | 
			
		||||
  media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
 | 
			
		||||
  media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
 | 
			
		||||
  media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										176
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										176
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -13,10 +13,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: _flutterfire_internals
 | 
			
		||||
      sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe
 | 
			
		||||
      sha256: "27899c95f9e7ec06c8310e6e0eac967707714b9f1450c4a58fa00ca011a4a8ae"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.3.48"
 | 
			
		||||
    version: "1.3.49"
 | 
			
		||||
  _macros:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description: dart
 | 
			
		||||
@@ -266,10 +266,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: connectivity_plus
 | 
			
		||||
      sha256: e0817759ec6d2d8e57eb234e6e57d2173931367a865850c7acea40d4b4f9c27d
 | 
			
		||||
      sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.1.1"
 | 
			
		||||
    version: "6.1.2"
 | 
			
		||||
  connectivity_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -290,10 +290,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: croppy
 | 
			
		||||
      sha256: "14bb40fd6c1771b093a907ddbf24df9aa49a4e6e379dd630602eb446e30ec629"
 | 
			
		||||
      sha256: bf99b00023df0d7d047e04d27d496d87cbefd968f578d0bd30f342ff75570a12
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.3.1"
 | 
			
		||||
    version: "1.3.3"
 | 
			
		||||
  cross_file:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -354,18 +354,18 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: dbus
 | 
			
		||||
      sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
 | 
			
		||||
      sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.7.10"
 | 
			
		||||
    version: "0.7.11"
 | 
			
		||||
  device_info_plus:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: device_info_plus
 | 
			
		||||
      sha256: "4fa68e53e26ab17b70ca39f072c285562cfc1589df5bb1e9295db90f6645f431"
 | 
			
		||||
      sha256: b37d37c2f912ad4e8ec694187de87d05de2a3cb82b465ff1f65f65a2d05de544
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "11.2.0"
 | 
			
		||||
    version: "11.2.1"
 | 
			
		||||
  device_info_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -538,34 +538,34 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: firebase_analytics
 | 
			
		||||
      sha256: "366140abb55418ea23060b779893fa997c2d8e1974a4d1cc4d9590933b65c5fd"
 | 
			
		||||
      sha256: "498c6cb8468e348a556709c745d92a52173ab3a9b906aa0593393f0787f201ea"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "11.3.6"
 | 
			
		||||
    version: "11.4.0"
 | 
			
		||||
  firebase_analytics_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: firebase_analytics_platform_interface
 | 
			
		||||
      sha256: "8e987cf977c0c8f4ad02d9950a9b25b1a9606899f37b66a322a43af05be0246b"
 | 
			
		||||
      sha256: ccbb350554e98afdb4b59852689292d194d31232a2647b5012a66622b3711df9
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.2.8"
 | 
			
		||||
    version: "4.3.0"
 | 
			
		||||
  firebase_analytics_web:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: firebase_analytics_web
 | 
			
		||||
      sha256: "0b64ef9060d394bba3d3b4777f49ee098efeeea7b0afb04663c956de6a3da170"
 | 
			
		||||
      sha256: "68e1f18fc16482c211c658e739c25f015b202a260d9ad8249c6d3d7963b8105f"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.5.10+5"
 | 
			
		||||
    version: "0.5.10+6"
 | 
			
		||||
  firebase_core:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: firebase_core
 | 
			
		||||
      sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde"
 | 
			
		||||
      sha256: "0307c1fde82e2b8b97e0be2dab93612aff9a72f31ebe9bfac66ed8b37ef7c568"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.9.0"
 | 
			
		||||
    version: "3.10.0"
 | 
			
		||||
  firebase_core_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -586,26 +586,26 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: firebase_messaging
 | 
			
		||||
      sha256: "151a3ee68736abf293aab66d1317ade53c88abe1db09c75a0460aebf7767bbdf"
 | 
			
		||||
      sha256: "48a8a59197c1c5174060ba9aa1e0036e9b5a0d28a0cc22d19c1fcabc67fafe3c"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "15.1.6"
 | 
			
		||||
    version: "15.2.0"
 | 
			
		||||
  firebase_messaging_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: firebase_messaging_platform_interface
 | 
			
		||||
      sha256: f331ee51e40c243f90cc7bc059222dfec4e5df53125b08d31fb28961b00d2a9d
 | 
			
		||||
      sha256: "9770a8e91f54296829dcaa61ce9b7c2f9ae9abbf99976dd3103a60470d5264dd"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.5.49"
 | 
			
		||||
    version: "4.6.0"
 | 
			
		||||
  firebase_messaging_web:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: firebase_messaging_web
 | 
			
		||||
      sha256: efaf3fdc54cd77e0eedb8e75f7f01c808828c64d052ddbf94d3009974e47d30f
 | 
			
		||||
      sha256: "329ca4ef45ec616abe6f1d5e58feed0934a50840a65aa327052354ad3c64ed77"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.9.5"
 | 
			
		||||
    version: "3.10.0"
 | 
			
		||||
  fixnum:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -618,10 +618,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: fl_chart
 | 
			
		||||
      sha256: "10ddaf334fe84d59333a12d153043e366f243e0bdfff2df0313e1e249f5bf926"
 | 
			
		||||
      sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.70.1"
 | 
			
		||||
    version: "0.70.2"
 | 
			
		||||
  flutter:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description: flutter
 | 
			
		||||
@@ -675,14 +675,78 @@ packages:
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.0"
 | 
			
		||||
  flutter_inappwebview:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_inappwebview
 | 
			
		||||
      sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "6.1.5"
 | 
			
		||||
  flutter_inappwebview_android:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_inappwebview_android
 | 
			
		||||
      sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.1.3"
 | 
			
		||||
  flutter_inappwebview_internal_annotations:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_inappwebview_internal_annotations
 | 
			
		||||
      sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.2.0"
 | 
			
		||||
  flutter_inappwebview_ios:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_inappwebview_ios
 | 
			
		||||
      sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.1.2"
 | 
			
		||||
  flutter_inappwebview_macos:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_inappwebview_macos
 | 
			
		||||
      sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.1.2"
 | 
			
		||||
  flutter_inappwebview_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_inappwebview_platform_interface
 | 
			
		||||
      sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.3.0+1"
 | 
			
		||||
  flutter_inappwebview_web:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_inappwebview_web
 | 
			
		||||
      sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.1.2"
 | 
			
		||||
  flutter_inappwebview_windows:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_inappwebview_windows
 | 
			
		||||
      sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.6.0"
 | 
			
		||||
  flutter_launcher_icons:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_launcher_icons
 | 
			
		||||
      sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5"
 | 
			
		||||
      sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.14.2"
 | 
			
		||||
    version: "0.14.3"
 | 
			
		||||
  flutter_lints:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -740,10 +804,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_svg
 | 
			
		||||
      sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123"
 | 
			
		||||
      sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.16"
 | 
			
		||||
    version: "2.0.17"
 | 
			
		||||
  flutter_test:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description: flutter
 | 
			
		||||
@@ -766,10 +830,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_webrtc
 | 
			
		||||
      sha256: e82ffd0d0b79621c5554eed73509d7f5bd286d57fef29a573846785c65237fb1
 | 
			
		||||
      sha256: "188401cc3275bc4f1f965babdff6cac612a4b46572f1e49f49db8af5361d5712"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.12.5+hotfix.2"
 | 
			
		||||
    version: "0.12.6"
 | 
			
		||||
  freezed:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -875,7 +939,7 @@ packages:
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.7.0"
 | 
			
		||||
  html:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: html
 | 
			
		||||
      sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
 | 
			
		||||
@@ -934,10 +998,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: image_picker_android
 | 
			
		||||
      sha256: aa6f1280b670861ac45220cc95adc59bb6ae130259d36f980ccb62220dc5e59f
 | 
			
		||||
      sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.8.12+19"
 | 
			
		||||
    version: "0.8.12+20"
 | 
			
		||||
  image_picker_for_web:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -974,10 +1038,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: image_picker_platform_interface
 | 
			
		||||
      sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80"
 | 
			
		||||
      sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.10.0"
 | 
			
		||||
    version: "2.10.1"
 | 
			
		||||
  image_picker_windows:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1086,10 +1150,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: livekit_client
 | 
			
		||||
      sha256: a19bcf8640b45e0730b1e3e3e78be7882dad680c6ebe8ae75294fd8d4612450d
 | 
			
		||||
      sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.4+hotfix.2"
 | 
			
		||||
    version: "2.3.5"
 | 
			
		||||
  logging:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1110,10 +1174,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: markdown
 | 
			
		||||
      sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
 | 
			
		||||
      sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "7.2.2"
 | 
			
		||||
    version: "7.3.0"
 | 
			
		||||
  marquee:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1270,10 +1334,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: package_info_plus
 | 
			
		||||
      sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d"
 | 
			
		||||
      sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "8.1.2"
 | 
			
		||||
    version: "8.1.3"
 | 
			
		||||
  package_info_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1502,10 +1566,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: pubspec_parse
 | 
			
		||||
      sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
 | 
			
		||||
      sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.4.0"
 | 
			
		||||
    version: "1.5.0"
 | 
			
		||||
  qr:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1630,10 +1694,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: share_plus
 | 
			
		||||
      sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400"
 | 
			
		||||
      sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "10.1.3"
 | 
			
		||||
    version: "10.1.4"
 | 
			
		||||
  share_plus_platform_interface:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1654,10 +1718,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: shared_preferences_android
 | 
			
		||||
      sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d"
 | 
			
		||||
      sha256: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.4.0"
 | 
			
		||||
    version: "2.4.2"
 | 
			
		||||
  shared_preferences_foundation:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1971,18 +2035,18 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: url_launcher_web
 | 
			
		||||
      sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
 | 
			
		||||
      sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.3"
 | 
			
		||||
    version: "2.4.0"
 | 
			
		||||
  url_launcher_windows:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: url_launcher_windows
 | 
			
		||||
      sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
 | 
			
		||||
      sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.1.3"
 | 
			
		||||
    version: "3.1.4"
 | 
			
		||||
  uuid:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -2003,10 +2067,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: vector_graphics_codec
 | 
			
		||||
      sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb"
 | 
			
		||||
      sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.1.12"
 | 
			
		||||
    version: "1.1.13"
 | 
			
		||||
  vector_graphics_compiler:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -2169,4 +2233,4 @@ packages:
 | 
			
		||||
    version: "3.1.3"
 | 
			
		||||
sdks:
 | 
			
		||||
  dart: ">=3.6.0 <4.0.0"
 | 
			
		||||
  flutter: ">=3.24.0"
 | 
			
		||||
  flutter: ">=3.27.0"
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
 | 
			
		||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 2.2.2+52
 | 
			
		||||
version: 2.2.2+56
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: ^3.5.4
 | 
			
		||||
@@ -115,6 +115,8 @@ dependencies:
 | 
			
		||||
  slide_countdown: ^2.0.2
 | 
			
		||||
  video_compress: ^3.1.3
 | 
			
		||||
  cached_network_image: ^3.4.1
 | 
			
		||||
  flutter_inappwebview: ^6.1.5
 | 
			
		||||
  html: ^0.15.5
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
id = "solian-next"
 | 
			
		||||
id = "solian"
 | 
			
		||||
 | 
			
		||||
[[locations]]
 | 
			
		||||
id = "solian-next"
 | 
			
		||||
host = ["sn-next.solsynth.dev"]
 | 
			
		||||
path = ["/"]
 | 
			
		||||
id = "solian"
 | 
			
		||||
hosts = ["sn.solsynth.dev"]
 | 
			
		||||
paths = ["/"]
 | 
			
		||||
[[locations.destinations]]
 | 
			
		||||
id = "solian-next-web"
 | 
			
		||||
uri = "files:///workdir/solian-next?fallback=index.html&index=index.html"
 | 
			
		||||
id = "solian-web"
 | 
			
		||||
uri = "files:///workdir/solian?fallback=index.html&index=index.html"
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,8 @@
 | 
			
		||||
    -->
 | 
			
		||||
    <base href="$FLUTTER_BASE_HREF">
 | 
			
		||||
 | 
			
		||||
    <script type="application/javascript" src="/assets/packages/flutter_inappwebview_web/assets/web/web_support.js" defer></script>
 | 
			
		||||
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta content="IE=Edge" http-equiv="X-UA-Compatible">
 | 
			
		||||
    <meta name="description" content="A new Flutter project.">
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@
 | 
			
		||||
#include <file_saver/file_saver_plugin.h>
 | 
			
		||||
#include <file_selector_windows/file_selector_windows.h>
 | 
			
		||||
#include <firebase_core/firebase_core_plugin_c_api.h>
 | 
			
		||||
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
 | 
			
		||||
#include <flutter_udid/flutter_udid_plugin_c_api.h>
 | 
			
		||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
 | 
			
		||||
#include <gal/gal_plugin_c_api.h>
 | 
			
		||||
@@ -34,6 +35,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
 | 
			
		||||
      registry->GetRegistrarForPlugin("FileSelectorWindows"));
 | 
			
		||||
  FirebaseCorePluginCApiRegisterWithRegistrar(
 | 
			
		||||
      registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
 | 
			
		||||
  FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
 | 
			
		||||
      registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
 | 
			
		||||
  FlutterUdidPluginCApiRegisterWithRegistrar(
 | 
			
		||||
      registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
 | 
			
		||||
  FlutterWebRTCPluginRegisterWithRegistrar(
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
 | 
			
		||||
  file_saver
 | 
			
		||||
  file_selector_windows
 | 
			
		||||
  firebase_core
 | 
			
		||||
  flutter_inappwebview_windows
 | 
			
		||||
  flutter_udid
 | 
			
		||||
  flutter_webrtc
 | 
			
		||||
  gal
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user