Compare commits
	
		
			12 Commits
		
	
	
		
			2.2.2+55
			...
			0dcfcaad56
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0dcfcaad56 | |||
| 687e720956 | |||
| 180876949e | |||
| 9718965809 | |||
| 5377161fb0 | |||
| 963e538ae5 | |||
| a355e3bf90 | |||
| cb4a2598c8 | |||
| 950612dc07 | |||
| cbd1eaf1af | |||
| ac41cbd99f | |||
| 9f9c90abc4 | 
@@ -19,6 +19,10 @@
 | 
				
			|||||||
        android:icon="@mipmap/ic_launcher"
 | 
					        android:icon="@mipmap/ic_launcher"
 | 
				
			||||||
        android:enableOnBackInvokedCallback="true"
 | 
					        android:enableOnBackInvokedCallback="true"
 | 
				
			||||||
        android:requestLegacyExternalStorage="true">
 | 
					        android:requestLegacyExternalStorage="true">
 | 
				
			||||||
 | 
					        <meta-data
 | 
				
			||||||
 | 
					            android:name="flutterEmbedding"
 | 
				
			||||||
 | 
					            android:value="2" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <activity
 | 
					        <activity
 | 
				
			||||||
            android:name=".MainActivity"
 | 
					            android:name=".MainActivity"
 | 
				
			||||||
            android:exported="true"
 | 
					            android:exported="true"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								api/Reader/List News Sources.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								api/Reader/List News Sources.bru
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: List News Sources
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 3
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					get {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/re/well-known/sources
 | 
				
			||||||
 | 
					  body: none
 | 
				
			||||||
 | 
					  auth: none
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										17
									
								
								api/Reader/List News.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								api/Reader/List News.bru
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: List News
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 2
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					get {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/re/news?take=10&offset=0&source=shadiao
 | 
				
			||||||
 | 
					  body: none
 | 
				
			||||||
 | 
					  auth: none
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					params:query {
 | 
				
			||||||
 | 
					  take: 10
 | 
				
			||||||
 | 
					  offset: 0
 | 
				
			||||||
 | 
					  source: shadiao
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								api/Reader/Trigger Scan News.bru
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/Reader/Trigger Scan News.bru
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					meta {
 | 
				
			||||||
 | 
					  name: Trigger Scan News
 | 
				
			||||||
 | 
					  type: http
 | 
				
			||||||
 | 
					  seq: 1
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					post {
 | 
				
			||||||
 | 
					  url: {{endpoint}}/cgi/re/admin/scan
 | 
				
			||||||
 | 
					  body: json
 | 
				
			||||||
 | 
					  auth: inherit
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					body:json {
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    "sources": ["taiwan-ltn"],
 | 
				
			||||||
 | 
					    "eager": true
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -17,6 +17,7 @@
 | 
				
			|||||||
  "screenAccountProfileEdit": "Edit Profile",
 | 
					  "screenAccountProfileEdit": "Edit Profile",
 | 
				
			||||||
  "screenAbuseReport": "Abuse Reports",
 | 
					  "screenAbuseReport": "Abuse Reports",
 | 
				
			||||||
  "screenSettings": "Settings",
 | 
					  "screenSettings": "Settings",
 | 
				
			||||||
 | 
					  "screenNews": "News",
 | 
				
			||||||
  "screenAlbum": "Album",
 | 
					  "screenAlbum": "Album",
 | 
				
			||||||
  "screenChat": "Chat",
 | 
					  "screenChat": "Chat",
 | 
				
			||||||
  "screenChatManage": "Edit Channel",
 | 
					  "screenChatManage": "Edit Channel",
 | 
				
			||||||
@@ -196,6 +197,10 @@
 | 
				
			|||||||
  "settingsFeatures": "Features",
 | 
					  "settingsFeatures": "Features",
 | 
				
			||||||
  "settingsNotifyWithHaptic": "Haptic when Notified",
 | 
					  "settingsNotifyWithHaptic": "Haptic when Notified",
 | 
				
			||||||
  "settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.",
 | 
					  "settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.",
 | 
				
			||||||
 | 
					  "settingsExpandPostLink": "Expand Post Link",
 | 
				
			||||||
 | 
					  "settingsExpandPostLinkDescription": "Expand the post link in the post list.",
 | 
				
			||||||
 | 
					  "settingsExpandChatLink": "Expand Chat Link",
 | 
				
			||||||
 | 
					  "settingsExpandChatLinkDescription": "Expand the chat link in the chat list.",
 | 
				
			||||||
  "settingsNetwork": "Network",
 | 
					  "settingsNetwork": "Network",
 | 
				
			||||||
  "settingsNetworkServer": "HyperNet Server",
 | 
					  "settingsNetworkServer": "HyperNet Server",
 | 
				
			||||||
  "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
 | 
					  "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
 | 
				
			||||||
@@ -554,5 +559,11 @@
 | 
				
			|||||||
  "postCategoryKnowledge": "Knowledge",
 | 
					  "postCategoryKnowledge": "Knowledge",
 | 
				
			||||||
  "postCategoryLiterature": "Literature",
 | 
					  "postCategoryLiterature": "Literature",
 | 
				
			||||||
  "postCategoryFunny": "Funny",
 | 
					  "postCategoryFunny": "Funny",
 | 
				
			||||||
  "postCategoryUncategorized": "Uncategorized"
 | 
					  "postCategoryUncategorized": "Uncategorized",
 | 
				
			||||||
 | 
					  "newsAllSources": "All News",
 | 
				
			||||||
 | 
					  "newsReadingProviderSwap": "Swap",
 | 
				
			||||||
 | 
					  "newsReadingFromReader": "You're reading from HyperNet.Reader",
 | 
				
			||||||
 | 
					  "newsReadingFromOriginal": "You're reading the original article",
 | 
				
			||||||
 | 
					  "newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.",
 | 
				
			||||||
 | 
					  "newsToday": "Today's News"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@
 | 
				
			|||||||
  "screenAccountProfileEdit": "编辑资料",
 | 
					  "screenAccountProfileEdit": "编辑资料",
 | 
				
			||||||
  "screenAbuseReport": "滥用检举",
 | 
					  "screenAbuseReport": "滥用检举",
 | 
				
			||||||
  "screenSettings": "设置",
 | 
					  "screenSettings": "设置",
 | 
				
			||||||
 | 
					  "screenNews": "新闻",
 | 
				
			||||||
  "screenAlbum": "相册",
 | 
					  "screenAlbum": "相册",
 | 
				
			||||||
  "screenChat": "聊天",
 | 
					  "screenChat": "聊天",
 | 
				
			||||||
  "screenChatManage": "编辑聊天频道",
 | 
					  "screenChatManage": "编辑聊天频道",
 | 
				
			||||||
@@ -194,6 +195,10 @@
 | 
				
			|||||||
  "settingsFeatures": "功能",
 | 
					  "settingsFeatures": "功能",
 | 
				
			||||||
  "settingsNotifyWithHaptic": "新通知时振动",
 | 
					  "settingsNotifyWithHaptic": "新通知时振动",
 | 
				
			||||||
  "settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
 | 
					  "settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
 | 
				
			||||||
 | 
					  "settingsExpandPostLink": "展开帖子链接",
 | 
				
			||||||
 | 
					  "settingsExpandPostLinkDescription": "在帖子列表中展开显示帖子中的链接。",
 | 
				
			||||||
 | 
					  "settingsExpandChatLink": "展开聊天链接",
 | 
				
			||||||
 | 
					  "settingsExpandChatLinkDescription": "在聊天信息中展开显示内容中的链接。",
 | 
				
			||||||
  "settingsNetwork": "网络",
 | 
					  "settingsNetwork": "网络",
 | 
				
			||||||
  "settingsNetworkServer": "HyperNet 服务器",
 | 
					  "settingsNetworkServer": "HyperNet 服务器",
 | 
				
			||||||
  "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
 | 
					  "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
 | 
				
			||||||
@@ -552,5 +557,11 @@
 | 
				
			|||||||
  "postCategoryKnowledge": "知识",
 | 
					  "postCategoryKnowledge": "知识",
 | 
				
			||||||
  "postCategoryLiterature": "文学",
 | 
					  "postCategoryLiterature": "文学",
 | 
				
			||||||
  "postCategoryFunny": "搞笑",
 | 
					  "postCategoryFunny": "搞笑",
 | 
				
			||||||
  "postCategoryUncategorized": "未分类"
 | 
					  "postCategoryUncategorized": "未分类",
 | 
				
			||||||
 | 
					  "newsAllSources": "所有新闻",
 | 
				
			||||||
 | 
					  "newsReadingProviderSwap": "切换",
 | 
				
			||||||
 | 
					  "newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
 | 
				
			||||||
 | 
					  "newsReadingFromOriginal": "你正在阅读原始文章",
 | 
				
			||||||
 | 
					  "newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。",
 | 
				
			||||||
 | 
					  "newsToday": "快讯"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@
 | 
				
			|||||||
  "screenAccountProfileEdit": "編輯資料",
 | 
					  "screenAccountProfileEdit": "編輯資料",
 | 
				
			||||||
  "screenAbuseReport": "濫用檢舉",
 | 
					  "screenAbuseReport": "濫用檢舉",
 | 
				
			||||||
  "screenSettings": "設置",
 | 
					  "screenSettings": "設置",
 | 
				
			||||||
 | 
					  "screenNews": "新聞",
 | 
				
			||||||
  "screenAlbum": "相冊",
 | 
					  "screenAlbum": "相冊",
 | 
				
			||||||
  "screenChat": "聊天",
 | 
					  "screenChat": "聊天",
 | 
				
			||||||
  "screenChatManage": "編輯聊天頻道",
 | 
					  "screenChatManage": "編輯聊天頻道",
 | 
				
			||||||
@@ -194,6 +195,10 @@
 | 
				
			|||||||
  "settingsFeatures": "功能",
 | 
					  "settingsFeatures": "功能",
 | 
				
			||||||
  "settingsNotifyWithHaptic": "新通知時振動",
 | 
					  "settingsNotifyWithHaptic": "新通知時振動",
 | 
				
			||||||
  "settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
 | 
					  "settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
 | 
				
			||||||
 | 
					  "settingsExpandPostLink": "展開帖子鏈接",
 | 
				
			||||||
 | 
					  "settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
 | 
				
			||||||
 | 
					  "settingsExpandChatLink": "展開聊天鏈接",
 | 
				
			||||||
 | 
					  "settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
 | 
				
			||||||
  "settingsNetwork": "網絡",
 | 
					  "settingsNetwork": "網絡",
 | 
				
			||||||
  "settingsNetworkServer": "HyperNet 服務器",
 | 
					  "settingsNetworkServer": "HyperNet 服務器",
 | 
				
			||||||
  "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
 | 
					  "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
 | 
				
			||||||
@@ -552,5 +557,11 @@
 | 
				
			|||||||
  "postCategoryKnowledge": "知識",
 | 
					  "postCategoryKnowledge": "知識",
 | 
				
			||||||
  "postCategoryLiterature": "文學",
 | 
					  "postCategoryLiterature": "文學",
 | 
				
			||||||
  "postCategoryFunny": "搞笑",
 | 
					  "postCategoryFunny": "搞笑",
 | 
				
			||||||
  "postCategoryUncategorized": "未分類"
 | 
					  "postCategoryUncategorized": "未分類",
 | 
				
			||||||
 | 
					  "newsAllSources": "所有新聞",
 | 
				
			||||||
 | 
					  "newsReadingProviderSwap": "切換",
 | 
				
			||||||
 | 
					  "newsReadingFromReader": "你正在從 HyperNet.Reader 閲讀文章",
 | 
				
			||||||
 | 
					  "newsReadingFromOriginal": "你正在閲讀原始文章",
 | 
				
			||||||
 | 
					  "newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
 | 
				
			||||||
 | 
					  "newsToday": "快訊"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,7 @@
 | 
				
			|||||||
  "screenAccountProfileEdit": "編輯資料",
 | 
					  "screenAccountProfileEdit": "編輯資料",
 | 
				
			||||||
  "screenAbuseReport": "濫用檢舉",
 | 
					  "screenAbuseReport": "濫用檢舉",
 | 
				
			||||||
  "screenSettings": "設置",
 | 
					  "screenSettings": "設置",
 | 
				
			||||||
 | 
					  "screenNews": "新聞",
 | 
				
			||||||
  "screenAlbum": "相冊",
 | 
					  "screenAlbum": "相冊",
 | 
				
			||||||
  "screenChat": "聊天",
 | 
					  "screenChat": "聊天",
 | 
				
			||||||
  "screenChatManage": "編輯聊天頻道",
 | 
					  "screenChatManage": "編輯聊天頻道",
 | 
				
			||||||
@@ -194,6 +195,10 @@
 | 
				
			|||||||
  "settingsFeatures": "功能",
 | 
					  "settingsFeatures": "功能",
 | 
				
			||||||
  "settingsNotifyWithHaptic": "新通知時振動",
 | 
					  "settingsNotifyWithHaptic": "新通知時振動",
 | 
				
			||||||
  "settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
 | 
					  "settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
 | 
				
			||||||
 | 
					  "settingsExpandPostLink": "展開帖子鏈接",
 | 
				
			||||||
 | 
					  "settingsExpandPostLinkDescription": "在帖子列表中展開顯示帖子中的鏈接。",
 | 
				
			||||||
 | 
					  "settingsExpandChatLink": "展開聊天鏈接",
 | 
				
			||||||
 | 
					  "settingsExpandChatLinkDescription": "在聊天信息中展開顯示內容中的鏈接。",
 | 
				
			||||||
  "settingsNetwork": "網絡",
 | 
					  "settingsNetwork": "網絡",
 | 
				
			||||||
  "settingsNetworkServer": "HyperNet 服務器",
 | 
					  "settingsNetworkServer": "HyperNet 服務器",
 | 
				
			||||||
  "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
 | 
					  "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
 | 
				
			||||||
@@ -552,5 +557,11 @@
 | 
				
			|||||||
  "postCategoryKnowledge": "知識",
 | 
					  "postCategoryKnowledge": "知識",
 | 
				
			||||||
  "postCategoryLiterature": "文學",
 | 
					  "postCategoryLiterature": "文學",
 | 
				
			||||||
  "postCategoryFunny": "搞笑",
 | 
					  "postCategoryFunny": "搞笑",
 | 
				
			||||||
  "postCategoryUncategorized": "未分類"
 | 
					  "postCategoryUncategorized": "未分類",
 | 
				
			||||||
 | 
					  "newsAllSources": "所有新聞",
 | 
				
			||||||
 | 
					  "newsReadingProviderSwap": "切換",
 | 
				
			||||||
 | 
					  "newsReadingFromReader": "你正在從 HyperNet.Reader 閱讀文章",
 | 
				
			||||||
 | 
					  "newsReadingFromOriginal": "你正在閱讀原始文章",
 | 
				
			||||||
 | 
					  "newsDisclaimer": "本文由 HyperNet.Reader 從互聯網上獲取,我們不擔保其內容的真實性,請自行判斷。本文章的所有內容版權歸原作者所有。",
 | 
				
			||||||
 | 
					  "newsToday": "快訊"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -105,6 +105,13 @@ PODS:
 | 
				
			|||||||
  - Flutter (1.0.0)
 | 
					  - Flutter (1.0.0)
 | 
				
			||||||
  - flutter_app_update (0.0.1):
 | 
					  - flutter_app_update (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
 | 
					  - flutter_inappwebview_ios (0.0.1):
 | 
				
			||||||
 | 
					    - Flutter
 | 
				
			||||||
 | 
					    - flutter_inappwebview_ios/Core (= 0.0.1)
 | 
				
			||||||
 | 
					    - OrderedSet (~> 6.0.3)
 | 
				
			||||||
 | 
					  - flutter_inappwebview_ios/Core (0.0.1):
 | 
				
			||||||
 | 
					    - Flutter
 | 
				
			||||||
 | 
					    - OrderedSet (~> 6.0.3)
 | 
				
			||||||
  - flutter_native_splash (2.4.3):
 | 
					  - flutter_native_splash (2.4.3):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - flutter_udid (0.0.1):
 | 
					  - flutter_udid (0.0.1):
 | 
				
			||||||
@@ -188,6 +195,7 @@ PODS:
 | 
				
			|||||||
    - nanopb/encode (= 3.30910.0)
 | 
					    - nanopb/encode (= 3.30910.0)
 | 
				
			||||||
  - nanopb/decode (3.30910.0)
 | 
					  - nanopb/decode (3.30910.0)
 | 
				
			||||||
  - nanopb/encode (3.30910.0)
 | 
					  - nanopb/encode (3.30910.0)
 | 
				
			||||||
 | 
					  - OrderedSet (6.0.3)
 | 
				
			||||||
  - package_info_plus (0.4.5):
 | 
					  - package_info_plus (0.4.5):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - pasteboard (0.0.1):
 | 
					  - pasteboard (0.0.1):
 | 
				
			||||||
@@ -239,6 +247,7 @@ DEPENDENCIES:
 | 
				
			|||||||
  - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
 | 
					  - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
 | 
				
			||||||
  - Flutter (from `Flutter`)
 | 
					  - Flutter (from `Flutter`)
 | 
				
			||||||
  - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
 | 
					  - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
 | 
				
			||||||
 | 
					  - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
 | 
				
			||||||
  - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
 | 
					  - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
 | 
				
			||||||
  - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
 | 
					  - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
 | 
				
			||||||
  - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
 | 
					  - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
 | 
				
			||||||
@@ -282,6 +291,7 @@ SPEC REPOS:
 | 
				
			|||||||
    - GoogleUtilities
 | 
					    - GoogleUtilities
 | 
				
			||||||
    - Kingfisher
 | 
					    - Kingfisher
 | 
				
			||||||
    - nanopb
 | 
					    - nanopb
 | 
				
			||||||
 | 
					    - OrderedSet
 | 
				
			||||||
    - PromisesObjC
 | 
					    - PromisesObjC
 | 
				
			||||||
    - SAMKeychain
 | 
					    - SAMKeychain
 | 
				
			||||||
    - SDWebImage
 | 
					    - SDWebImage
 | 
				
			||||||
@@ -309,6 +319,8 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
    :path: Flutter
 | 
					    :path: Flutter
 | 
				
			||||||
  flutter_app_update:
 | 
					  flutter_app_update:
 | 
				
			||||||
    :path: ".symlinks/plugins/flutter_app_update/ios"
 | 
					    :path: ".symlinks/plugins/flutter_app_update/ios"
 | 
				
			||||||
 | 
					  flutter_inappwebview_ios:
 | 
				
			||||||
 | 
					    :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
 | 
				
			||||||
  flutter_native_splash:
 | 
					  flutter_native_splash:
 | 
				
			||||||
    :path: ".symlinks/plugins/flutter_native_splash/ios"
 | 
					    :path: ".symlinks/plugins/flutter_native_splash/ios"
 | 
				
			||||||
  flutter_udid:
 | 
					  flutter_udid:
 | 
				
			||||||
@@ -380,6 +392,7 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
 | 
					  FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
 | 
				
			||||||
  Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
 | 
					  Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
 | 
				
			||||||
  flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
 | 
					  flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
 | 
				
			||||||
 | 
					  flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
 | 
				
			||||||
  flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
 | 
					  flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
 | 
				
			||||||
  flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
 | 
					  flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
 | 
				
			||||||
  flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
 | 
					  flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
 | 
				
			||||||
@@ -396,6 +409,7 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
 | 
					  media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
 | 
				
			||||||
  media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
 | 
					  media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
 | 
				
			||||||
  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
					  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
				
			||||||
 | 
					  OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
 | 
				
			||||||
  package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
 | 
					  package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
 | 
				
			||||||
  pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
 | 
					  pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
 | 
				
			||||||
  path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
 | 
					  path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,8 @@ const kAppBackgroundStoreKey = 'app_has_background';
 | 
				
			|||||||
const kAppColorSchemeStoreKey = 'app_color_scheme';
 | 
					const kAppColorSchemeStoreKey = 'app_color_scheme';
 | 
				
			||||||
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
 | 
					const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
 | 
				
			||||||
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
 | 
					const kAppNotifyWithHaptic = 'app_notify_with_haptic';
 | 
				
			||||||
 | 
					const kAppExpandPostLink = 'app_expand_post_link';
 | 
				
			||||||
 | 
					const kAppExpandChatLink = 'app_expand_chat_link';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Map<String, FilterQuality> kImageQualityLevel = {
 | 
					const Map<String, FilterQuality> kImageQualityLevel = {
 | 
				
			||||||
  'settingsImageQualityLowest': FilterQuality.none,
 | 
					  'settingsImageQualityLowest': FilterQuality.none,
 | 
				
			||||||
@@ -39,14 +41,22 @@ class ConfigProvider extends ChangeNotifier {
 | 
				
			|||||||
  bool drawerIsCollapsed = false;
 | 
					  bool drawerIsCollapsed = false;
 | 
				
			||||||
  bool drawerIsExpanded = false;
 | 
					  bool drawerIsExpanded = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void calcDrawerSize(BuildContext context) {
 | 
					  void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
 | 
				
			||||||
    final rpb = ResponsiveBreakpoints.of(context);
 | 
					    bool newDrawerIsCollapsed = false;
 | 
				
			||||||
    final newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
 | 
					    bool newDrawerIsExpanded = false;
 | 
				
			||||||
    final newDrawerIsExpanded = rpb.largerThan(TABLET)
 | 
					    if (withMediaQuery) {
 | 
				
			||||||
        ? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
 | 
					      newDrawerIsCollapsed = MediaQuery.of(context).size.width < 450;
 | 
				
			||||||
            ? false
 | 
					      newDrawerIsExpanded = MediaQuery.of(context).size.width >= 451;
 | 
				
			||||||
            : true
 | 
					    } else {
 | 
				
			||||||
        : false;
 | 
					      final rpb = ResponsiveBreakpoints.of(context);
 | 
				
			||||||
 | 
					      newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
 | 
				
			||||||
 | 
					      newDrawerIsExpanded = rpb.largerThan(TABLET)
 | 
				
			||||||
 | 
					          ? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
 | 
				
			||||||
 | 
					              ? false
 | 
				
			||||||
 | 
					              : true
 | 
				
			||||||
 | 
					          : false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) {
 | 
					    if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) {
 | 
				
			||||||
      drawerIsExpanded = newDrawerIsExpanded;
 | 
					      drawerIsExpanded = newDrawerIsExpanded;
 | 
				
			||||||
      drawerIsCollapsed = newDrawerIsCollapsed;
 | 
					      drawerIsCollapsed = newDrawerIsCollapsed;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -58,6 +58,11 @@ class NavigationProvider extends ChangeNotifier {
 | 
				
			|||||||
      screen: 'realm',
 | 
					      screen: 'realm',
 | 
				
			||||||
      label: 'screenRealm',
 | 
					      label: 'screenRealm',
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    AppNavDestination(
 | 
				
			||||||
 | 
					      icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20),
 | 
				
			||||||
 | 
					      screen: 'news',
 | 
				
			||||||
 | 
					      label: 'screenNews',
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
    AppNavDestination(
 | 
					    AppNavDestination(
 | 
				
			||||||
      icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
 | 
					      icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
 | 
				
			||||||
      screen: 'album',
 | 
					      screen: 'album',
 | 
				
			||||||
@@ -83,8 +88,7 @@ class NavigationProvider extends ChangeNotifier {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  List<AppNavDestination> destinations = [];
 | 
					  List<AppNavDestination> destinations = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  int get pinnedDestinationCount =>
 | 
					  int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length;
 | 
				
			||||||
      destinations.where((ele) => ele.isPinned).length;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  NavigationProvider() {
 | 
					  NavigationProvider() {
 | 
				
			||||||
    buildDestinations(kDefaultPinnedDestination);
 | 
					    buildDestinations(kDefaultPinnedDestination);
 | 
				
			||||||
@@ -113,17 +117,13 @@ class NavigationProvider extends ChangeNotifier {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool isIndexInRange(int min, int max) {
 | 
					  bool isIndexInRange(int min, int max) {
 | 
				
			||||||
    return _currentIndex != null &&
 | 
					    return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max;
 | 
				
			||||||
        _currentIndex! >= min &&
 | 
					 | 
				
			||||||
        _currentIndex! < max;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void autoDetectIndex(GoRouter? state) {
 | 
					  void autoDetectIndex(GoRouter? state) {
 | 
				
			||||||
    if (state == null) return;
 | 
					    if (state == null) return;
 | 
				
			||||||
    final idx = destinations.indexWhere(
 | 
					    final idx = destinations.indexWhere(
 | 
				
			||||||
      (ele) =>
 | 
					      (ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name,
 | 
				
			||||||
          ele.screen ==
 | 
					 | 
				
			||||||
          state.routerDelegate.currentConfiguration.last.route.name,
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    _currentIndex = idx == -1 ? null : idx;
 | 
					    _currentIndex = idx == -1 ? null : idx;
 | 
				
			||||||
    notifyListeners();
 | 
					    notifyListeners();
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										407
									
								
								lib/router.dart
									
									
									
									
									
								
							
							
						
						
									
										407
									
								
								lib/router.dart
									
									
									
									
									
								
							@@ -19,6 +19,8 @@ import 'package:surface/screens/chat/room.dart';
 | 
				
			|||||||
import 'package:surface/screens/explore.dart';
 | 
					import 'package:surface/screens/explore.dart';
 | 
				
			||||||
import 'package:surface/screens/friend.dart';
 | 
					import 'package:surface/screens/friend.dart';
 | 
				
			||||||
import 'package:surface/screens/home.dart';
 | 
					import 'package:surface/screens/home.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/news/news_detail.dart';
 | 
				
			||||||
 | 
					import 'package:surface/screens/news/news_list.dart';
 | 
				
			||||||
import 'package:surface/screens/notification.dart';
 | 
					import 'package:surface/screens/notification.dart';
 | 
				
			||||||
import 'package:surface/screens/post/post_detail.dart';
 | 
					import 'package:surface/screens/post/post_detail.dart';
 | 
				
			||||||
import 'package:surface/screens/post/post_editor.dart';
 | 
					import 'package:surface/screens/post/post_editor.dart';
 | 
				
			||||||
@@ -31,249 +33,200 @@ import 'package:surface/screens/settings.dart';
 | 
				
			|||||||
import 'package:surface/screens/sharing.dart';
 | 
					import 'package:surface/screens/sharing.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/about.dart';
 | 
					import 'package:surface/widgets/about.dart';
 | 
				
			||||||
import 'package:surface/widgets/navigation/app_background.dart';
 | 
					 | 
				
			||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Widget _fadeThroughTransition(
 | 
				
			||||||
 | 
					    BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
 | 
				
			||||||
 | 
					  return FadeThroughTransition(
 | 
				
			||||||
 | 
					    animation: animation,
 | 
				
			||||||
 | 
					    secondaryAnimation: secondaryAnimation,
 | 
				
			||||||
 | 
					    fillColor: Colors.transparent,
 | 
				
			||||||
 | 
					    child: child,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
final _appRoutes = [
 | 
					final _appRoutes = [
 | 
				
			||||||
  ShellRoute(
 | 
					  GoRoute(
 | 
				
			||||||
    builder: (context, state, child) => child,
 | 
					    path: '/',
 | 
				
			||||||
 | 
					    name: 'home',
 | 
				
			||||||
 | 
					    builder: (context, state) => const HomeScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/posts',
 | 
				
			||||||
 | 
					    name: 'explore',
 | 
				
			||||||
 | 
					    builder: (context, state) => const ExploreScreen(),
 | 
				
			||||||
    routes: [
 | 
					    routes: [
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/',
 | 
					        path: '/write/:mode',
 | 
				
			||||||
        name: 'home',
 | 
					        name: 'postEditor',
 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					        builder: (context, state) => PostEditorScreen(
 | 
				
			||||||
          child: const HomeScreen(),
 | 
					          mode: state.pathParameters['mode']!,
 | 
				
			||||||
 | 
					          postEditId: int.tryParse(
 | 
				
			||||||
 | 
					            state.uri.queryParameters['editing'] ?? '',
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          postReplyId: int.tryParse(
 | 
				
			||||||
 | 
					            state.uri.queryParameters['replying'] ?? '',
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          postRepostId: int.tryParse(
 | 
				
			||||||
 | 
					            state.uri.queryParameters['reposting'] ?? '',
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          extraProps: state.extra as PostEditorExtraProps?,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/posts',
 | 
					        path: '/search',
 | 
				
			||||||
        name: 'explore',
 | 
					        name: 'postSearch',
 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					        builder: (context, state) => PostSearchScreen(
 | 
				
			||||||
          child: const ExploreScreen(),
 | 
					          initialTags: state.uri.queryParameters['tags']?.split(','),
 | 
				
			||||||
        ),
 | 
					          initialCategories: state.uri.queryParameters['categories']?.split(','),
 | 
				
			||||||
        routes: [
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/write/:mode',
 | 
					 | 
				
			||||||
            name: 'postEditor',
 | 
					 | 
				
			||||||
            builder: (context, state) => PostEditorScreen(
 | 
					 | 
				
			||||||
              mode: state.pathParameters['mode']!,
 | 
					 | 
				
			||||||
              postEditId: int.tryParse(
 | 
					 | 
				
			||||||
                state.uri.queryParameters['editing'] ?? '',
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              postReplyId: int.tryParse(
 | 
					 | 
				
			||||||
                state.uri.queryParameters['replying'] ?? '',
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              postRepostId: int.tryParse(
 | 
					 | 
				
			||||||
                state.uri.queryParameters['reposting'] ?? '',
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              extraProps: state.extra as PostEditorExtraProps?,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/search',
 | 
					 | 
				
			||||||
            name: 'postSearch',
 | 
					 | 
				
			||||||
            builder: (context, state) => PostSearchScreen(
 | 
					 | 
				
			||||||
              initialTags: state.uri.queryParameters['tags']?.split(','),
 | 
					 | 
				
			||||||
              initialCategories: state.uri.queryParameters['categories']?.split(','),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/publishers/:name',
 | 
					 | 
				
			||||||
            name: 'postPublisher',
 | 
					 | 
				
			||||||
            builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/:slug',
 | 
					 | 
				
			||||||
            name: 'postDetail',
 | 
					 | 
				
			||||||
            builder: (context, state) => PostDetailScreen(
 | 
					 | 
				
			||||||
              slug: state.pathParameters['slug']!,
 | 
					 | 
				
			||||||
              preload: state.extra as SnPost?,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/account',
 | 
					 | 
				
			||||||
        name: 'account',
 | 
					 | 
				
			||||||
        pageBuilder: (context, state) => CustomTransitionPage(
 | 
					 | 
				
			||||||
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
 | 
					 | 
				
			||||||
            return FadeThroughTransition(
 | 
					 | 
				
			||||||
              animation: animation,
 | 
					 | 
				
			||||||
              secondaryAnimation: secondaryAnimation,
 | 
					 | 
				
			||||||
              fillColor: Colors.transparent,
 | 
					 | 
				
			||||||
              child: child,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          child: const AccountScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        routes: [],
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/chat',
 | 
					 | 
				
			||||||
        name: 'chat',
 | 
					 | 
				
			||||||
        pageBuilder: (context, state) => CustomTransitionPage(
 | 
					 | 
				
			||||||
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
 | 
					 | 
				
			||||||
            return FadeThroughTransition(
 | 
					 | 
				
			||||||
              animation: animation,
 | 
					 | 
				
			||||||
              secondaryAnimation: secondaryAnimation,
 | 
					 | 
				
			||||||
              fillColor: Colors.transparent,
 | 
					 | 
				
			||||||
              child: child,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          child: const ChatScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        routes: [
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/:scope/:alias',
 | 
					 | 
				
			||||||
            name: 'chatRoom',
 | 
					 | 
				
			||||||
            builder: (context, state) => AppBackground(
 | 
					 | 
				
			||||||
              child: ChatRoomScreen(
 | 
					 | 
				
			||||||
                scope: state.pathParameters['scope']!,
 | 
					 | 
				
			||||||
                alias: state.pathParameters['alias']!,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/:scope/:alias/call',
 | 
					 | 
				
			||||||
            name: 'chatCallRoom',
 | 
					 | 
				
			||||||
            builder: (context, state) => AppBackground(
 | 
					 | 
				
			||||||
              child: CallRoomScreen(
 | 
					 | 
				
			||||||
                scope: state.pathParameters['scope']!,
 | 
					 | 
				
			||||||
                alias: state.pathParameters['alias']!,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/:scope/:alias/detail',
 | 
					 | 
				
			||||||
            name: 'channelDetail',
 | 
					 | 
				
			||||||
            builder: (context, state) => AppBackground(
 | 
					 | 
				
			||||||
              child: ChannelDetailScreen(
 | 
					 | 
				
			||||||
                scope: state.pathParameters['scope']!,
 | 
					 | 
				
			||||||
                alias: state.pathParameters['alias']!,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/manage',
 | 
					 | 
				
			||||||
            name: 'chatManage',
 | 
					 | 
				
			||||||
            pageBuilder: (context, state) => CustomTransitionPage(
 | 
					 | 
				
			||||||
              child: ChatManageScreen(
 | 
					 | 
				
			||||||
                editingChannelAlias: state.uri.queryParameters['editing'],
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              transitionsBuilder: (context, animation, secondaryAnimation, child) {
 | 
					 | 
				
			||||||
                return FadeThroughTransition(
 | 
					 | 
				
			||||||
                  animation: animation,
 | 
					 | 
				
			||||||
                  secondaryAnimation: secondaryAnimation,
 | 
					 | 
				
			||||||
                  fillColor: Colors.transparent,
 | 
					 | 
				
			||||||
                  child: AppBackground(
 | 
					 | 
				
			||||||
                    child: child,
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/:alias',
 | 
					 | 
				
			||||||
            name: 'realmDetail',
 | 
					 | 
				
			||||||
            builder: (context, state) => AppBackground(
 | 
					 | 
				
			||||||
              child: RealmDetailScreen(alias: state.pathParameters['alias']!),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/realm',
 | 
					 | 
				
			||||||
        name: 'realm',
 | 
					 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					 | 
				
			||||||
          child: const RealmScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
        routes: [
 | 
					 | 
				
			||||||
          GoRoute(
 | 
					 | 
				
			||||||
            path: '/manage',
 | 
					 | 
				
			||||||
            name: 'realmManage',
 | 
					 | 
				
			||||||
            pageBuilder: (context, state) => CustomTransitionPage(
 | 
					 | 
				
			||||||
              child: RealmManageScreen(
 | 
					 | 
				
			||||||
                editingRealmAlias: state.uri.queryParameters['editing'],
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              transitionsBuilder: (context, animation, secondaryAnimation, child) {
 | 
					 | 
				
			||||||
                return FadeThroughTransition(
 | 
					 | 
				
			||||||
                  animation: animation,
 | 
					 | 
				
			||||||
                  secondaryAnimation: secondaryAnimation,
 | 
					 | 
				
			||||||
                  fillColor: Colors.transparent,
 | 
					 | 
				
			||||||
                  child: AppBackground(
 | 
					 | 
				
			||||||
                    child: child,
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/album',
 | 
					 | 
				
			||||||
        name: 'album',
 | 
					 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					 | 
				
			||||||
          child: const AlbumScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/friend',
 | 
					        path: '/publishers/:name',
 | 
				
			||||||
        name: 'friend',
 | 
					        name: 'postPublisher',
 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					        builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
 | 
				
			||||||
          child: const FriendScreen(),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/notification',
 | 
					        path: '/:slug',
 | 
				
			||||||
        name: 'notification',
 | 
					        name: 'postDetail',
 | 
				
			||||||
        pageBuilder: (context, state) => NoTransitionPage(
 | 
					        builder: (context, state) => PostDetailScreen(
 | 
				
			||||||
          child: const NotificationScreen(),
 | 
					          slug: state.pathParameters['slug']!,
 | 
				
			||||||
 | 
					          preload: state.extra as SnPost?,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  ShellRoute(
 | 
					  GoRoute(
 | 
				
			||||||
    builder: (context, state, child) => child,
 | 
					    path: '/account',
 | 
				
			||||||
 | 
					    name: 'account',
 | 
				
			||||||
 | 
					    builder: (context, state) => const AccountScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/chat',
 | 
				
			||||||
 | 
					    name: 'chat',
 | 
				
			||||||
 | 
					    builder: (context, state) => const ChatScreen(),
 | 
				
			||||||
    routes: [
 | 
					    routes: [
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/auth/login',
 | 
					        path: '/:scope/:alias',
 | 
				
			||||||
        name: 'authLogin',
 | 
					        name: 'chatRoom',
 | 
				
			||||||
        builder: (context, state) => LoginScreen(),
 | 
					        builder: (context, state) => ChatRoomScreen(
 | 
				
			||||||
 | 
					          scope: state.pathParameters['scope']!,
 | 
				
			||||||
 | 
					          alias: state.pathParameters['alias']!,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/auth/register',
 | 
					        path: '/:scope/:alias/call',
 | 
				
			||||||
        name: 'authRegister',
 | 
					        name: 'chatCallRoom',
 | 
				
			||||||
        builder: (context, state) => RegisterScreen(),
 | 
					        builder: (context, state) => CallRoomScreen(
 | 
				
			||||||
 | 
					          scope: state.pathParameters['scope']!,
 | 
				
			||||||
 | 
					          alias: state.pathParameters['alias']!,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/reports',
 | 
					        path: '/:scope/:alias/detail',
 | 
				
			||||||
        name: 'abuseReport',
 | 
					        name: 'channelDetail',
 | 
				
			||||||
        builder: (context, state) => AbuseReportScreen(),
 | 
					        builder: (context, state) => ChannelDetailScreen(
 | 
				
			||||||
 | 
					          scope: state.pathParameters['scope']!,
 | 
				
			||||||
 | 
					          alias: state.pathParameters['alias']!,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      GoRoute(
 | 
					      GoRoute(
 | 
				
			||||||
        path: '/account/profile/edit',
 | 
					        path: '/manage',
 | 
				
			||||||
        name: 'accountProfileEdit',
 | 
					        name: 'chatManage',
 | 
				
			||||||
        builder: (context, state) => ProfileEditScreen(),
 | 
					        builder: (context, state) => ChatManageScreen(
 | 
				
			||||||
      ),
 | 
					          editingChannelAlias: state.uri.queryParameters['editing'],
 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/account/publishers',
 | 
					 | 
				
			||||||
        name: 'accountPublishers',
 | 
					 | 
				
			||||||
        builder: (context, state) => PublisherScreen(),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/account/publishers/new',
 | 
					 | 
				
			||||||
        name: 'accountPublisherNew',
 | 
					 | 
				
			||||||
        builder: (context, state) => AccountPublisherNewScreen(),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
      GoRoute(
 | 
					 | 
				
			||||||
        path: '/account/publishers/edit/:name',
 | 
					 | 
				
			||||||
        name: 'accountPublisherEdit',
 | 
					 | 
				
			||||||
        builder: (context, state) => AccountPublisherEditScreen(
 | 
					 | 
				
			||||||
          name: state.pathParameters['name']!,
 | 
					 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/realm',
 | 
				
			||||||
 | 
					    name: 'realm',
 | 
				
			||||||
 | 
					    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',
 | 
				
			||||||
 | 
					        builder: (context, state) => RealmManageScreen(
 | 
				
			||||||
 | 
					          editingRealmAlias: state.uri.queryParameters['editing'],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/news',
 | 
				
			||||||
 | 
					    name: 'news',
 | 
				
			||||||
 | 
					    builder: (context, state) => const NewsScreen(),
 | 
				
			||||||
 | 
					    routes: [
 | 
				
			||||||
 | 
					      GoRoute(
 | 
				
			||||||
 | 
					        path: '/:hash',
 | 
				
			||||||
 | 
					        name: 'newsDetail',
 | 
				
			||||||
 | 
					        builder: (context, state) => NewsDetailScreen(
 | 
				
			||||||
 | 
					          hash: state.pathParameters['hash']!,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/album',
 | 
				
			||||||
 | 
					    name: 'album',
 | 
				
			||||||
 | 
					    builder: (context, state) => const AlbumScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/friend',
 | 
				
			||||||
 | 
					    name: 'friend',
 | 
				
			||||||
 | 
					    builder: (context, state) => const FriendScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/notification',
 | 
				
			||||||
 | 
					    name: 'notification',
 | 
				
			||||||
 | 
					    builder: (context, state) => const NotificationScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/auth/login',
 | 
				
			||||||
 | 
					    name: 'authLogin',
 | 
				
			||||||
 | 
					    builder: (context, state) => LoginScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/auth/register',
 | 
				
			||||||
 | 
					    name: 'authRegister',
 | 
				
			||||||
 | 
					    builder: (context, state) => RegisterScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/reports',
 | 
				
			||||||
 | 
					    name: 'abuseReport',
 | 
				
			||||||
 | 
					    builder: (context, state) => AbuseReportScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/account/profile/edit',
 | 
				
			||||||
 | 
					    name: 'accountProfileEdit',
 | 
				
			||||||
 | 
					    builder: (context, state) => ProfileEditScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/account/publishers',
 | 
				
			||||||
 | 
					    name: 'accountPublishers',
 | 
				
			||||||
 | 
					    builder: (context, state) => PublisherScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/account/publishers/new',
 | 
				
			||||||
 | 
					    name: 'accountPublisherNew',
 | 
				
			||||||
 | 
					    builder: (context, state) => AccountPublisherNewScreen(),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					  GoRoute(
 | 
				
			||||||
 | 
					    path: '/account/publishers/edit/:name',
 | 
				
			||||||
 | 
					    name: 'accountPublisherEdit',
 | 
				
			||||||
 | 
					    builder: (context, state) => AccountPublisherEditScreen(
 | 
				
			||||||
 | 
					      name: state.pathParameters['name']!,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
  GoRoute(
 | 
					  GoRoute(
 | 
				
			||||||
    path: '/account/:name',
 | 
					    path: '/account/:name',
 | 
				
			||||||
    name: 'accountProfilePage',
 | 
					    name: 'accountProfilePage',
 | 
				
			||||||
@@ -281,25 +234,15 @@ final _appRoutes = [
 | 
				
			|||||||
      child: UserScreen(name: state.pathParameters['name']!),
 | 
					      child: UserScreen(name: state.pathParameters['name']!),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  ShellRoute(
 | 
					  GoRoute(
 | 
				
			||||||
    builder: (context, state, child) => child,
 | 
					    path: '/settings',
 | 
				
			||||||
    routes: [
 | 
					    name: 'settings',
 | 
				
			||||||
      GoRoute(
 | 
					    builder: (context, state) => SettingsScreen(),
 | 
				
			||||||
        path: '/settings',
 | 
					 | 
				
			||||||
        name: 'settings',
 | 
					 | 
				
			||||||
        builder: (context, state) => SettingsScreen(),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  ShellRoute(
 | 
					  GoRoute(
 | 
				
			||||||
    builder: (context, state, child) => child,
 | 
					    path: '/about',
 | 
				
			||||||
    routes: [
 | 
					    name: 'about',
 | 
				
			||||||
      GoRoute(
 | 
					    builder: (context, state) => AboutScreen(),
 | 
				
			||||||
        path: '/about',
 | 
					 | 
				
			||||||
        name: 'about',
 | 
					 | 
				
			||||||
        builder: (context, state) => AboutScreen(),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,6 @@ import 'package:surface/types/check_in.dart';
 | 
				
			|||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/account/account_image.dart';
 | 
					import 'package:surface/widgets/account/account_image.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
					 | 
				
			||||||
import 'package:surface/widgets/universal_image.dart';
 | 
					import 'package:surface/widgets/universal_image.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Map<String, (String, IconData, Color)> kBadgesMeta = {
 | 
					const Map<String, (String, IconData, Color)> kBadgesMeta = {
 | 
				
			||||||
@@ -596,7 +595,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
 | 
				
			|||||||
                subtitle: Text('@${ele.name}'),
 | 
					                subtitle: Text('@${ele.name}'),
 | 
				
			||||||
                trailing: const Icon(Symbols.chevron_right),
 | 
					                trailing: const Icon(Symbols.chevron_right),
 | 
				
			||||||
                onTap: () {
 | 
					                onTap: () {
 | 
				
			||||||
                  GoRouter.of(context).pushNamed(
 | 
					                  GoRouter.of(context).goNamed(
 | 
				
			||||||
                    'postPublisher',
 | 
					                    'postPublisher',
 | 
				
			||||||
                    pathParameters: {'name': ele.name},
 | 
					                    pathParameters: {'name': ele.name},
 | 
				
			||||||
                  );
 | 
					                  );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,7 @@ import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			|||||||
class CallRoomScreen extends StatefulWidget {
 | 
					class CallRoomScreen extends StatefulWidget {
 | 
				
			||||||
  final String scope;
 | 
					  final String scope;
 | 
				
			||||||
  final String alias;
 | 
					  final String alias;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const CallRoomScreen({super.key, required this.scope, required this.alias});
 | 
					  const CallRoomScreen({super.key, required this.scope, required this.alias});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -36,8 +37,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
    return Stack(
 | 
					    return Stack(
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
        Container(
 | 
					        Container(
 | 
				
			||||||
          color:
 | 
					          color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
 | 
				
			||||||
              Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
 | 
					 | 
				
			||||||
          child: call.focusTrack != null
 | 
					          child: call.focusTrack != null
 | 
				
			||||||
              ? InteractiveParticipantWidget(
 | 
					              ? InteractiveParticipantWidget(
 | 
				
			||||||
                  isFixedAvatar: false,
 | 
					                  isFixedAvatar: false,
 | 
				
			||||||
@@ -72,8 +72,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
                      color: Theme.of(context).cardColor,
 | 
					                      color: Theme.of(context).cardColor,
 | 
				
			||||||
                      participant: track,
 | 
					                      participant: track,
 | 
				
			||||||
                      onTap: () {
 | 
					                      onTap: () {
 | 
				
			||||||
                        if (track.participant.sid !=
 | 
					                        if (track.participant.sid != call.focusTrack?.participant.sid) {
 | 
				
			||||||
                            call.focusTrack?.participant.sid) {
 | 
					 | 
				
			||||||
                          call.setFocusTrack(track);
 | 
					                          call.setFocusTrack(track);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                      },
 | 
					                      },
 | 
				
			||||||
@@ -115,14 +114,10 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
            child: ClipRRect(
 | 
					            child: ClipRRect(
 | 
				
			||||||
              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					              borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
              child: InteractiveParticipantWidget(
 | 
					              child: InteractiveParticipantWidget(
 | 
				
			||||||
                color: Theme.of(context)
 | 
					                color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
 | 
				
			||||||
                    .colorScheme
 | 
					 | 
				
			||||||
                    .surfaceContainerHigh
 | 
					 | 
				
			||||||
                    .withOpacity(0.75),
 | 
					 | 
				
			||||||
                participant: track,
 | 
					                participant: track,
 | 
				
			||||||
                onTap: () {
 | 
					                onTap: () {
 | 
				
			||||||
                  if (track.participant.sid !=
 | 
					                  if (track.participant.sid != call.focusTrack?.participant.sid) {
 | 
				
			||||||
                      call.focusTrack?.participant.sid) {
 | 
					 | 
				
			||||||
                    call.setFocusTrack(track);
 | 
					                    call.setFocusTrack(track);
 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
@@ -160,150 +155,127 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
 | 
				
			|||||||
                text: TextSpan(children: [
 | 
					                text: TextSpan(children: [
 | 
				
			||||||
                  TextSpan(
 | 
					                  TextSpan(
 | 
				
			||||||
                    text: 'call'.tr(),
 | 
					                    text: 'call'.tr(),
 | 
				
			||||||
                    style: Theme.of(context)
 | 
					                    style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Colors.white),
 | 
				
			||||||
                        .textTheme
 | 
					 | 
				
			||||||
                        .titleLarge!
 | 
					 | 
				
			||||||
                        .copyWith(color: Colors.white),
 | 
					 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  const TextSpan(text: '\n'),
 | 
					                  const TextSpan(text: '\n'),
 | 
				
			||||||
                  TextSpan(
 | 
					                  TextSpan(
 | 
				
			||||||
                    text: call.lastDuration.toString(),
 | 
					                    text: call.lastDuration.toString(),
 | 
				
			||||||
                    style: Theme.of(context)
 | 
					                    style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Colors.white),
 | 
				
			||||||
                        .textTheme
 | 
					 | 
				
			||||||
                        .bodySmall!
 | 
					 | 
				
			||||||
                        .copyWith(color: Colors.white),
 | 
					 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ]),
 | 
					                ]),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            body: SafeArea(
 | 
					            body: GestureDetector(
 | 
				
			||||||
              child: GestureDetector(
 | 
					              behavior: HitTestBehavior.translucent,
 | 
				
			||||||
                behavior: HitTestBehavior.translucent,
 | 
					              child: Column(
 | 
				
			||||||
                child: Column(
 | 
					                children: [
 | 
				
			||||||
                  children: [
 | 
					                  SizedBox(
 | 
				
			||||||
 | 
					                    width: MediaQuery.of(context).size.width,
 | 
				
			||||||
 | 
					                    height: 64,
 | 
				
			||||||
 | 
					                    child: Row(
 | 
				
			||||||
 | 
					                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					                      crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        Builder(builder: (context) {
 | 
				
			||||||
 | 
					                          final call = context.read<ChatCallProvider>();
 | 
				
			||||||
 | 
					                          final connectionQuality =
 | 
				
			||||||
 | 
					                              call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown;
 | 
				
			||||||
 | 
					                          return Expanded(
 | 
				
			||||||
 | 
					                            child: Column(
 | 
				
			||||||
 | 
					                              mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
 | 
					                              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                              children: [
 | 
				
			||||||
 | 
					                                Row(
 | 
				
			||||||
 | 
					                                  children: [
 | 
				
			||||||
 | 
					                                    Text(
 | 
				
			||||||
 | 
					                                      call.channel?.name ?? 'unknown'.tr(),
 | 
				
			||||||
 | 
					                                      style: const TextStyle(
 | 
				
			||||||
 | 
					                                        fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                                      ),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    const Gap(6),
 | 
				
			||||||
 | 
					                                    Text(call.lastDuration.toString())
 | 
				
			||||||
 | 
					                                  ],
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                                Row(
 | 
				
			||||||
 | 
					                                  children: [
 | 
				
			||||||
 | 
					                                    Text(
 | 
				
			||||||
 | 
					                                      {
 | 
				
			||||||
 | 
					                                        livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(),
 | 
				
			||||||
 | 
					                                        livekit.ConnectionState.connected: 'callStatusConnected'.tr(),
 | 
				
			||||||
 | 
					                                        livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(),
 | 
				
			||||||
 | 
					                                        livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(),
 | 
				
			||||||
 | 
					                                      }[call.room.connectionState]!,
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    const Gap(6),
 | 
				
			||||||
 | 
					                                    if (connectionQuality != livekit.ConnectionQuality.unknown)
 | 
				
			||||||
 | 
					                                      Icon(
 | 
				
			||||||
 | 
					                                        {
 | 
				
			||||||
 | 
					                                          livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt,
 | 
				
			||||||
 | 
					                                          livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar,
 | 
				
			||||||
 | 
					                                          livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar,
 | 
				
			||||||
 | 
					                                        }[connectionQuality],
 | 
				
			||||||
 | 
					                                        color: {
 | 
				
			||||||
 | 
					                                          livekit.ConnectionQuality.excellent: Colors.green,
 | 
				
			||||||
 | 
					                                          livekit.ConnectionQuality.good: Colors.orange,
 | 
				
			||||||
 | 
					                                          livekit.ConnectionQuality.poor: Colors.red,
 | 
				
			||||||
 | 
					                                        }[connectionQuality],
 | 
				
			||||||
 | 
					                                        size: 16,
 | 
				
			||||||
 | 
					                                      )
 | 
				
			||||||
 | 
					                                    else
 | 
				
			||||||
 | 
					                                      const SizedBox(
 | 
				
			||||||
 | 
					                                        width: 12,
 | 
				
			||||||
 | 
					                                        height: 12,
 | 
				
			||||||
 | 
					                                        child: CircularProgressIndicator(
 | 
				
			||||||
 | 
					                                          color: Colors.white,
 | 
				
			||||||
 | 
					                                          strokeWidth: 2,
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                      ).padding(all: 3),
 | 
				
			||||||
 | 
					                                  ],
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              ],
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          );
 | 
				
			||||||
 | 
					                        }),
 | 
				
			||||||
 | 
					                        Row(
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            IconButton(
 | 
				
			||||||
 | 
					                              icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view),
 | 
				
			||||||
 | 
					                              onPressed: () {
 | 
				
			||||||
 | 
					                                _switchLayout();
 | 
				
			||||||
 | 
					                              },
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ).padding(left: 20, right: 16),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  Expanded(
 | 
				
			||||||
 | 
					                    child: Material(
 | 
				
			||||||
 | 
					                      color: Theme.of(context).colorScheme.surfaceContainerLow,
 | 
				
			||||||
 | 
					                      child: Builder(
 | 
				
			||||||
 | 
					                        builder: (context) {
 | 
				
			||||||
 | 
					                          switch (_layoutMode) {
 | 
				
			||||||
 | 
					                            case 1:
 | 
				
			||||||
 | 
					                              return _buildGridLayout();
 | 
				
			||||||
 | 
					                            default:
 | 
				
			||||||
 | 
					                              return _buildListLayout();
 | 
				
			||||||
 | 
					                          }
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  if (call.room.localParticipant != null)
 | 
				
			||||||
                    SizedBox(
 | 
					                    SizedBox(
 | 
				
			||||||
                      width: MediaQuery.of(context).size.width,
 | 
					                      width: MediaQuery.of(context).size.width,
 | 
				
			||||||
                      height: 64,
 | 
					                      child: ControlsWidget(
 | 
				
			||||||
                      child: Row(
 | 
					                        call.room,
 | 
				
			||||||
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
					                        call.room.localParticipant!,
 | 
				
			||||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
					 | 
				
			||||||
                        children: [
 | 
					 | 
				
			||||||
                          Builder(builder: (context) {
 | 
					 | 
				
			||||||
                            final call = context.read<ChatCallProvider>();
 | 
					 | 
				
			||||||
                            final connectionQuality =
 | 
					 | 
				
			||||||
                                call.room.localParticipant?.connectionQuality ??
 | 
					 | 
				
			||||||
                                    livekit.ConnectionQuality.unknown;
 | 
					 | 
				
			||||||
                            return Expanded(
 | 
					 | 
				
			||||||
                              child: Column(
 | 
					 | 
				
			||||||
                                mainAxisSize: MainAxisSize.min,
 | 
					 | 
				
			||||||
                                crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
                                children: [
 | 
					 | 
				
			||||||
                                  Row(
 | 
					 | 
				
			||||||
                                    children: [
 | 
					 | 
				
			||||||
                                      Text(
 | 
					 | 
				
			||||||
                                        call.channel?.name ?? 'unknown'.tr(),
 | 
					 | 
				
			||||||
                                        style: const TextStyle(
 | 
					 | 
				
			||||||
                                          fontWeight: FontWeight.bold,
 | 
					 | 
				
			||||||
                                        ),
 | 
					 | 
				
			||||||
                                      ),
 | 
					 | 
				
			||||||
                                      const Gap(6),
 | 
					 | 
				
			||||||
                                      Text(call.lastDuration.toString())
 | 
					 | 
				
			||||||
                                    ],
 | 
					 | 
				
			||||||
                                  ),
 | 
					 | 
				
			||||||
                                  Row(
 | 
					 | 
				
			||||||
                                    children: [
 | 
					 | 
				
			||||||
                                      Text(
 | 
					 | 
				
			||||||
                                        {
 | 
					 | 
				
			||||||
                                          livekit.ConnectionState.disconnected:
 | 
					 | 
				
			||||||
                                              'callStatusDisconnected'.tr(),
 | 
					 | 
				
			||||||
                                          livekit.ConnectionState.connected:
 | 
					 | 
				
			||||||
                                              'callStatusConnected'.tr(),
 | 
					 | 
				
			||||||
                                          livekit.ConnectionState.connecting:
 | 
					 | 
				
			||||||
                                              'callStatusConnecting'.tr(),
 | 
					 | 
				
			||||||
                                          livekit.ConnectionState.reconnecting:
 | 
					 | 
				
			||||||
                                              'callStatusReconnecting'.tr(),
 | 
					 | 
				
			||||||
                                        }[call.room.connectionState]!,
 | 
					 | 
				
			||||||
                                      ),
 | 
					 | 
				
			||||||
                                      const Gap(6),
 | 
					 | 
				
			||||||
                                      if (connectionQuality !=
 | 
					 | 
				
			||||||
                                          livekit.ConnectionQuality.unknown)
 | 
					 | 
				
			||||||
                                        Icon(
 | 
					 | 
				
			||||||
                                          {
 | 
					 | 
				
			||||||
                                            livekit.ConnectionQuality.excellent:
 | 
					 | 
				
			||||||
                                                Icons.signal_cellular_alt,
 | 
					 | 
				
			||||||
                                            livekit.ConnectionQuality.good:
 | 
					 | 
				
			||||||
                                                Icons.signal_cellular_alt_2_bar,
 | 
					 | 
				
			||||||
                                            livekit.ConnectionQuality.poor:
 | 
					 | 
				
			||||||
                                                Icons.signal_cellular_alt_1_bar,
 | 
					 | 
				
			||||||
                                          }[connectionQuality],
 | 
					 | 
				
			||||||
                                          color: {
 | 
					 | 
				
			||||||
                                            livekit.ConnectionQuality.excellent:
 | 
					 | 
				
			||||||
                                                Colors.green,
 | 
					 | 
				
			||||||
                                            livekit.ConnectionQuality.good:
 | 
					 | 
				
			||||||
                                                Colors.orange,
 | 
					 | 
				
			||||||
                                            livekit.ConnectionQuality.poor:
 | 
					 | 
				
			||||||
                                                Colors.red,
 | 
					 | 
				
			||||||
                                          }[connectionQuality],
 | 
					 | 
				
			||||||
                                          size: 16,
 | 
					 | 
				
			||||||
                                        )
 | 
					 | 
				
			||||||
                                      else
 | 
					 | 
				
			||||||
                                        const SizedBox(
 | 
					 | 
				
			||||||
                                          width: 12,
 | 
					 | 
				
			||||||
                                          height: 12,
 | 
					 | 
				
			||||||
                                          child: CircularProgressIndicator(
 | 
					 | 
				
			||||||
                                            color: Colors.white,
 | 
					 | 
				
			||||||
                                            strokeWidth: 2,
 | 
					 | 
				
			||||||
                                          ),
 | 
					 | 
				
			||||||
                                        ).padding(all: 3),
 | 
					 | 
				
			||||||
                                    ],
 | 
					 | 
				
			||||||
                                  ),
 | 
					 | 
				
			||||||
                                ],
 | 
					 | 
				
			||||||
                              ),
 | 
					 | 
				
			||||||
                            );
 | 
					 | 
				
			||||||
                          }),
 | 
					 | 
				
			||||||
                          Row(
 | 
					 | 
				
			||||||
                            children: [
 | 
					 | 
				
			||||||
                              IconButton(
 | 
					 | 
				
			||||||
                                icon: _layoutMode == 0
 | 
					 | 
				
			||||||
                                    ? const Icon(Icons.view_list)
 | 
					 | 
				
			||||||
                                    : const Icon(Icons.grid_view),
 | 
					 | 
				
			||||||
                                onPressed: () {
 | 
					 | 
				
			||||||
                                  _switchLayout();
 | 
					 | 
				
			||||||
                                },
 | 
					 | 
				
			||||||
                              ),
 | 
					 | 
				
			||||||
                            ],
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ],
 | 
					 | 
				
			||||||
                      ).padding(left: 20, right: 16),
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                    Expanded(
 | 
					 | 
				
			||||||
                      child: Material(
 | 
					 | 
				
			||||||
                        color:
 | 
					 | 
				
			||||||
                            Theme.of(context).colorScheme.surfaceContainerLow,
 | 
					 | 
				
			||||||
                        child: Builder(
 | 
					 | 
				
			||||||
                          builder: (context) {
 | 
					 | 
				
			||||||
                            switch (_layoutMode) {
 | 
					 | 
				
			||||||
                              case 1:
 | 
					 | 
				
			||||||
                                return _buildGridLayout();
 | 
					 | 
				
			||||||
                              default:
 | 
					 | 
				
			||||||
                                return _buildListLayout();
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                          },
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    if (call.room.localParticipant != null)
 | 
					                ],
 | 
				
			||||||
                      SizedBox(
 | 
					 | 
				
			||||||
                        width: MediaQuery.of(context).size.width,
 | 
					 | 
				
			||||||
                        child: ControlsWidget(
 | 
					 | 
				
			||||||
                          call.room,
 | 
					 | 
				
			||||||
                          call.room.localParticipant!,
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                  ],
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                onTap: () {},
 | 
					 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
 | 
					              onTap: () {},
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
 | 
				
			|||||||
import 'package:gap/gap.dart';
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
import 'package:google_fonts/google_fonts.dart';
 | 
					import 'package:google_fonts/google_fonts.dart';
 | 
				
			||||||
 | 
					import 'package:html/parser.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:relative_time/relative_time.dart';
 | 
					import 'package:relative_time/relative_time.dart';
 | 
				
			||||||
@@ -22,6 +23,7 @@ import 'package:surface/providers/special_day.dart';
 | 
				
			|||||||
import 'package:surface/providers/userinfo.dart';
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
import 'package:surface/providers/widget.dart';
 | 
					import 'package:surface/providers/widget.dart';
 | 
				
			||||||
import 'package:surface/types/check_in.dart';
 | 
					import 'package:surface/types/check_in.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/news.dart';
 | 
				
			||||||
import 'package:surface/types/post.dart';
 | 
					import 'package:surface/types/post.dart';
 | 
				
			||||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
					import 'package:surface/widgets/app_bar_leading.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
@@ -52,9 +54,9 @@ class _HomeScreenState extends State<HomeScreen> {
 | 
				
			|||||||
  static const List<HomeScreenDashEntry> kCards = [
 | 
					  static const List<HomeScreenDashEntry> kCards = [
 | 
				
			||||||
    HomeScreenDashEntry(
 | 
					    HomeScreenDashEntry(
 | 
				
			||||||
      name: 'dashEntryRecommendation',
 | 
					      name: 'dashEntryRecommendation',
 | 
				
			||||||
      cols: 2,
 | 
					 | 
				
			||||||
      rows: 2,
 | 
					 | 
				
			||||||
      child: _HomeDashRecommendationPostWidget(),
 | 
					      child: _HomeDashRecommendationPostWidget(),
 | 
				
			||||||
 | 
					      rows: 2,
 | 
				
			||||||
 | 
					      cols: 2,
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    HomeScreenDashEntry(
 | 
					    HomeScreenDashEntry(
 | 
				
			||||||
      name: 'dashEntryCheckIn',
 | 
					      name: 'dashEntryCheckIn',
 | 
				
			||||||
@@ -64,6 +66,11 @@ class _HomeScreenState extends State<HomeScreen> {
 | 
				
			|||||||
      name: 'dashEntryNotification',
 | 
					      name: 'dashEntryNotification',
 | 
				
			||||||
      child: _HomeDashNotificationWidget(),
 | 
					      child: _HomeDashNotificationWidget(),
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
 | 
					    HomeScreenDashEntry(
 | 
				
			||||||
 | 
					      name: 'dashEntryTodayNews',
 | 
				
			||||||
 | 
					      child: _HomeDashTodayNews(),
 | 
				
			||||||
 | 
					      cols: 2,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@@ -230,6 +237,107 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _HomeDashTodayNews extends StatefulWidget {
 | 
				
			||||||
 | 
					  const _HomeDashTodayNews();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<_HomeDashTodayNews> createState() => _HomeDashTodayNewsState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
 | 
				
			||||||
 | 
					  SnNewsArticle? _article;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _fetchArticle() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					      final resp = await sn.client.get('/cgi/re/news/today');
 | 
				
			||||||
 | 
					      _article = SnNewsArticle.fromJson(resp.data['data']);
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      context.showErrorDialog(err);
 | 
				
			||||||
 | 
					      rethrow;
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setState(() {});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					    _fetchArticle();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return Card(
 | 
				
			||||||
 | 
					      child: Column(
 | 
				
			||||||
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Row(
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              const Icon(Symbols.newspaper),
 | 
				
			||||||
 | 
					              const Gap(8),
 | 
				
			||||||
 | 
					              Text(
 | 
				
			||||||
 | 
					                'newsToday',
 | 
				
			||||||
 | 
					                style: Theme.of(context).textTheme.titleLarge,
 | 
				
			||||||
 | 
					              ).tr()
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ).padding(horizontal: 18, top: 12, bottom: 8),
 | 
				
			||||||
 | 
					          if (_article != null)
 | 
				
			||||||
 | 
					            Expanded(
 | 
				
			||||||
 | 
					              child: InkWell(
 | 
				
			||||||
 | 
					                borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					                child: Column(
 | 
				
			||||||
 | 
					                  spacing: 4,
 | 
				
			||||||
 | 
					                  children: [
 | 
				
			||||||
 | 
					                    Text(
 | 
				
			||||||
 | 
					                      _article!.title,
 | 
				
			||||||
 | 
					                      style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18),
 | 
				
			||||||
 | 
					                      maxLines: 2,
 | 
				
			||||||
 | 
					                      overflow: TextOverflow.ellipsis,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    Text(
 | 
				
			||||||
 | 
					                      parse(_article!.description).children.map((e) => e.text.trim()).join(),
 | 
				
			||||||
 | 
					                      maxLines: 3,
 | 
				
			||||||
 | 
					                      overflow: TextOverflow.ellipsis,
 | 
				
			||||||
 | 
					                      style: Theme.of(context).textTheme.bodyMedium,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    Builder(
 | 
				
			||||||
 | 
					                      builder: (context) {
 | 
				
			||||||
 | 
					                        final date = _article!.publishedAt ?? _article!.createdAt;
 | 
				
			||||||
 | 
					                        return Row(
 | 
				
			||||||
 | 
					                          crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					                          spacing: 2,
 | 
				
			||||||
 | 
					                          children: [
 | 
				
			||||||
 | 
					                            Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
				
			||||||
 | 
					                            Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
 | 
				
			||||||
 | 
					                            Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ).opacity(0.75);
 | 
				
			||||||
 | 
					                      }
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ).padding(horizontal: 16),
 | 
				
			||||||
 | 
					                onTap: () {
 | 
				
			||||||
 | 
					                  GoRouter.of(context).pushNamed(
 | 
				
			||||||
 | 
					                    'newsDetail',
 | 
				
			||||||
 | 
					                    pathParameters: {'hash': _article!.hash},
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          else
 | 
				
			||||||
 | 
					            Expanded(
 | 
				
			||||||
 | 
					              child: Center(
 | 
				
			||||||
 | 
					                child: CircularProgressIndicator(),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _HomeDashCheckInWidget extends StatefulWidget {
 | 
					class _HomeDashCheckInWidget extends StatefulWidget {
 | 
				
			||||||
  const _HomeDashCheckInWidget();
 | 
					  const _HomeDashCheckInWidget();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										238
									
								
								lib/screens/news/news_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								lib/screens/news/news_detail.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,238 @@
 | 
				
			|||||||
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/gestures.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
 | 
					import 'package:html/dom.dart' as dom;
 | 
				
			||||||
 | 
					import 'package:html/parser.dart';
 | 
				
			||||||
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
 | 
					import 'package:relative_time/relative_time.dart';
 | 
				
			||||||
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/sn_network.dart';
 | 
				
			||||||
 | 
					import 'package:surface/types/news.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/dialog.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_inappwebview/flutter_inappwebview.dart';
 | 
				
			||||||
 | 
					import 'package:surface/widgets/universal_image.dart';
 | 
				
			||||||
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NewsDetailScreen extends StatefulWidget {
 | 
				
			||||||
 | 
					  final String hash;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const NewsDetailScreen({super.key, required this.hash});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  State<NewsDetailScreen> createState() => _NewsDetailScreenState();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _NewsDetailScreenState extends State<NewsDetailScreen> {
 | 
				
			||||||
 | 
					  SnNewsArticle? _article;
 | 
				
			||||||
 | 
					  dom.Document? _articleFragment;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _fetchArticle() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final sn = context.read<SnNetworkProvider>();
 | 
				
			||||||
 | 
					      final resp = await sn.client.get('/cgi/re/news/${widget.hash}');
 | 
				
			||||||
 | 
					      _article = SnNewsArticle.fromJson(resp.data);
 | 
				
			||||||
 | 
					      _articleFragment = parse(_article!.content);
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      if (!mounted) return;
 | 
				
			||||||
 | 
					      context.showErrorDialog(err).then((_) {
 | 
				
			||||||
 | 
					        if (!mounted) return;
 | 
				
			||||||
 | 
					        Navigator.pop(context);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } finally {
 | 
				
			||||||
 | 
					      setState(() {});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<Widget> _parseHtmlToWidgets(Iterable<dom.Element>? elements) {
 | 
				
			||||||
 | 
					    if (elements == null) return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final List<Widget> widgets = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (final node in elements) {
 | 
				
			||||||
 | 
					      switch (node.localName) {
 | 
				
			||||||
 | 
					        case 'h1':
 | 
				
			||||||
 | 
					        case 'h2':
 | 
				
			||||||
 | 
					        case 'h3':
 | 
				
			||||||
 | 
					        case 'h4':
 | 
				
			||||||
 | 
					        case 'h5':
 | 
				
			||||||
 | 
					        case 'h6':
 | 
				
			||||||
 | 
					          widgets.add(Text(node.text.trim(), style: Theme.of(context).textTheme.titleMedium));
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case 'p':
 | 
				
			||||||
 | 
					          if (node.text.trim().isEmpty) continue;
 | 
				
			||||||
 | 
					          widgets.add(
 | 
				
			||||||
 | 
					            Text.rich(
 | 
				
			||||||
 | 
					              TextSpan(
 | 
				
			||||||
 | 
					                text: node.text.trim(),
 | 
				
			||||||
 | 
					                children: [
 | 
				
			||||||
 | 
					                  for (final child in node.children)
 | 
				
			||||||
 | 
					                    switch (child.localName) {
 | 
				
			||||||
 | 
					                      'a' => TextSpan(
 | 
				
			||||||
 | 
					                          text: child.text.trim(),
 | 
				
			||||||
 | 
					                          style: const TextStyle(decoration: TextDecoration.underline),
 | 
				
			||||||
 | 
					                          recognizer: TapGestureRecognizer()
 | 
				
			||||||
 | 
					                            ..onTap = () {
 | 
				
			||||||
 | 
					                              launchUrlString(child.attributes['href']!);
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      _ => TextSpan(text: child.text.trim()),
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              style: Theme.of(context).textTheme.bodyLarge,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case 'a':
 | 
				
			||||||
 | 
					          // drop single link
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case 'div':
 | 
				
			||||||
 | 
					          // ignore div text, normally it is not meaningful
 | 
				
			||||||
 | 
					          widgets.addAll(_parseHtmlToWidgets(node.children));
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case 'hr':
 | 
				
			||||||
 | 
					          widgets.add(const Divider());
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case 'img':
 | 
				
			||||||
 | 
					          var src = node.attributes['src'];
 | 
				
			||||||
 | 
					          if (src == null) break;
 | 
				
			||||||
 | 
					          final width = double.tryParse(node.attributes['width'] ?? 'null');
 | 
				
			||||||
 | 
					          final height = double.tryParse(node.attributes['height'] ?? 'null');
 | 
				
			||||||
 | 
					          final ratio = width != null && height != null ? width / height : 1.0;
 | 
				
			||||||
 | 
					          if (src.startsWith('//')) {
 | 
				
			||||||
 | 
					            src = 'https:$src';
 | 
				
			||||||
 | 
					          } else if (!src.startsWith('http')) {
 | 
				
			||||||
 | 
					            final baseUri = Uri.parse(_article!.url);
 | 
				
			||||||
 | 
					            final baseUrl = '${baseUri.scheme}://${baseUri.host}';
 | 
				
			||||||
 | 
					            src = '$baseUrl/$src';
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          widgets.add(
 | 
				
			||||||
 | 
					            AspectRatio(
 | 
				
			||||||
 | 
					              aspectRatio: ratio,
 | 
				
			||||||
 | 
					              child: Container(
 | 
				
			||||||
 | 
					                decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                  borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					                  border: Border.all(
 | 
				
			||||||
 | 
					                    color: Theme.of(context).dividerColor,
 | 
				
			||||||
 | 
					                    width: 1,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                height: height ?? double.infinity,
 | 
				
			||||||
 | 
					                child: ClipRRect(
 | 
				
			||||||
 | 
					                  borderRadius: BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
 | 
					                  child: Container(
 | 
				
			||||||
 | 
					                    color: Theme.of(context).colorScheme.surfaceContainer,
 | 
				
			||||||
 | 
					                    child: AutoResizeUniversalImage(
 | 
				
			||||||
 | 
					                      src,
 | 
				
			||||||
 | 
					                      fit: width != null && height != null ? BoxFit.cover : BoxFit.contain,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          widgets.addAll(_parseHtmlToWidgets(node.children));
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return widgets;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					    _fetchArticle();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool _isReadingFromReader = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    return AppScaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(
 | 
				
			||||||
 | 
					        leading: const PageBackButton(),
 | 
				
			||||||
 | 
					        title: Text(_article?.title ?? 'loading'.tr()),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      body: Column(
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          MaterialBanner(
 | 
				
			||||||
 | 
					            dividerColor: Colors.transparent,
 | 
				
			||||||
 | 
					            leading: const Icon(Icons.info),
 | 
				
			||||||
 | 
					            content: Text(_isReadingFromReader ? 'newsReadingFromReader'.tr() : 'newsReadingFromOriginal'.tr()),
 | 
				
			||||||
 | 
					            actions: [
 | 
				
			||||||
 | 
					              TextButton(
 | 
				
			||||||
 | 
					                child: Text('newsReadingProviderSwap').tr(),
 | 
				
			||||||
 | 
					                onPressed: () {
 | 
				
			||||||
 | 
					                  setState(() => _isReadingFromReader = !_isReadingFromReader);
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          if (_articleFragment != null && _isReadingFromReader)
 | 
				
			||||||
 | 
					            Expanded(
 | 
				
			||||||
 | 
					              child: 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)),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										227
									
								
								lib/screens/news/news_list.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								lib/screens/news/news_list.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,227 @@
 | 
				
			|||||||
 | 
					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.only(
 | 
				
			||||||
 | 
					                          topRight: Radius.circular(8),
 | 
				
			||||||
 | 
					                          topLeft: Radius.circular(8),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        child: AspectRatio(
 | 
				
			||||||
 | 
					                          aspectRatio: 16 / 9,
 | 
				
			||||||
 | 
					                          child: Container(
 | 
				
			||||||
 | 
					                            color: Theme.of(context).colorScheme.surfaceContainer,
 | 
				
			||||||
 | 
					                            child: AutoResizeUniversalImage(
 | 
				
			||||||
 | 
					                              article.thumbnail.startsWith('http') ? article.thumbnail : '$baseUrl/${article.thumbnail}',
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    const Gap(16),
 | 
				
			||||||
 | 
					                    Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
 | 
				
			||||||
 | 
					                    const Gap(8),
 | 
				
			||||||
 | 
					                    Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
 | 
				
			||||||
 | 
					                        .textStyle(Theme.of(context).textTheme.bodyMedium!)
 | 
				
			||||||
 | 
					                        .padding(horizontal: 16),
 | 
				
			||||||
 | 
					                    const Gap(8),
 | 
				
			||||||
 | 
					                    Row(
 | 
				
			||||||
 | 
					                      spacing: 2,
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        Text(widget.allSources.where((x) => x.id == article.source).first.label)
 | 
				
			||||||
 | 
					                            .textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ).opacity(0.75).padding(horizontal: 16),
 | 
				
			||||||
 | 
					                    Row(
 | 
				
			||||||
 | 
					                      spacing: 2,
 | 
				
			||||||
 | 
					                      children: [
 | 
				
			||||||
 | 
					                        Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
				
			||||||
 | 
					                        Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
 | 
				
			||||||
 | 
					                        Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
 | 
				
			||||||
 | 
					                      ],
 | 
				
			||||||
 | 
					                    ).opacity(0.75).padding(horizontal: 16),
 | 
				
			||||||
 | 
					                    const Gap(16),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -276,6 +276,30 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
				
			|||||||
                    });
 | 
					                    });
 | 
				
			||||||
                  },
 | 
					                  },
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
 | 
					                CheckboxListTile(
 | 
				
			||||||
 | 
					                  secondary: const Icon(Symbols.link),
 | 
				
			||||||
 | 
					                  title: Text('settingsExpandPostLink').tr(),
 | 
				
			||||||
 | 
					                  subtitle: Text('settingsExpandPostLinkDescription').tr(),
 | 
				
			||||||
 | 
					                  contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
				
			||||||
 | 
					                  value: _prefs.getBool(kAppExpandPostLink) ?? true,
 | 
				
			||||||
 | 
					                  onChanged: (value) {
 | 
				
			||||||
 | 
					                    setState(() {
 | 
				
			||||||
 | 
					                      _prefs.setBool(kAppExpandPostLink, value ?? false);
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                CheckboxListTile(
 | 
				
			||||||
 | 
					                  secondary: const Icon(Symbols.chat),
 | 
				
			||||||
 | 
					                  title: Text('settingsExpandChatLink').tr(),
 | 
				
			||||||
 | 
					                  subtitle: Text('settingsExpandChatLinkDescription').tr(),
 | 
				
			||||||
 | 
					                  contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
				
			||||||
 | 
					                  value: _prefs.getBool(kAppExpandChatLink) ?? true,
 | 
				
			||||||
 | 
					                  onChanged: (value) {
 | 
				
			||||||
 | 
					                    setState(() {
 | 
				
			||||||
 | 
					                      _prefs.setBool(kAppExpandChatLink, value ?? false);
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                  },
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            Column(
 | 
					            Column(
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
@@ -8,6 +8,7 @@ import 'package:material_symbols_icons/symbols.dart';
 | 
				
			|||||||
import 'package:popover/popover.dart';
 | 
					import 'package:popover/popover.dart';
 | 
				
			||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/config.dart';
 | 
				
			||||||
import 'package:surface/providers/user_directory.dart';
 | 
					import 'package:surface/providers/user_directory.dart';
 | 
				
			||||||
import 'package:surface/providers/userinfo.dart';
 | 
					import 'package:surface/providers/userinfo.dart';
 | 
				
			||||||
import 'package:surface/types/chat.dart';
 | 
					import 'package:surface/types/chat.dart';
 | 
				
			||||||
@@ -53,6 +54,8 @@ class ChatMessage extends StatelessWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final dateFormatter = DateFormat('MM/dd HH:mm');
 | 
					    final dateFormatter = DateFormat('MM/dd HH:mm');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final cfg = context.read<ConfigProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return SwipeTo(
 | 
					    return SwipeTo(
 | 
				
			||||||
      key: Key('chat-message-${data.id}'),
 | 
					      key: Key('chat-message-${data.id}'),
 | 
				
			||||||
      iconOnLeftSwipe: Symbols.reply,
 | 
					      iconOnLeftSwipe: Symbols.reply,
 | 
				
			||||||
@@ -192,7 +195,10 @@ class ChatMessage extends StatelessWidget {
 | 
				
			|||||||
                ],
 | 
					                ],
 | 
				
			||||||
              ).opacity(isPending ? 0.5 : 1),
 | 
					              ).opacity(isPending ? 0.5 : 1),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            if (data.body['text'] != null && data.type == 'messages.new' && (data.body['text']?.isNotEmpty ?? false))
 | 
					            if (data.body['text'] != null &&
 | 
				
			||||||
 | 
					                data.type == 'messages.new' &&
 | 
				
			||||||
 | 
					                (data.body['text']?.isNotEmpty ?? false) &&
 | 
				
			||||||
 | 
					                (cfg.prefs.getBool(kAppExpandChatLink) ?? true))
 | 
				
			||||||
              LinkPreviewWidget(text: data.body['text']!),
 | 
					              LinkPreviewWidget(text: data.body['text']!),
 | 
				
			||||||
            if (data.preload?.attachments?.isNotEmpty ?? false)
 | 
					            if (data.preload?.attachments?.isNotEmpty ?? false)
 | 
				
			||||||
              AttachmentList(
 | 
					              AttachmentList(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,12 +7,11 @@ import 'package:marquee/marquee.dart';
 | 
				
			|||||||
import 'package:provider/provider.dart';
 | 
					import 'package:provider/provider.dart';
 | 
				
			||||||
import 'package:responsive_framework/responsive_framework.dart';
 | 
					import 'package:responsive_framework/responsive_framework.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					import 'package:surface/providers/link_preview.dart';
 | 
				
			||||||
import 'package:surface/types/link.dart';
 | 
					import 'package:surface/types/link.dart';
 | 
				
			||||||
import 'package:surface/widgets/universal_image.dart';
 | 
					import 'package:surface/widgets/universal_image.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import '../providers/link_preview.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class LinkPreviewWidget extends StatefulWidget {
 | 
					class LinkPreviewWidget extends StatefulWidget {
 | 
				
			||||||
  final String text;
 | 
					  final String text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -81,8 +80,9 @@ class _LinkPreviewEntry extends StatelessWidget {
 | 
				
			|||||||
                  child: AspectRatio(
 | 
					                  child: AspectRatio(
 | 
				
			||||||
                    aspectRatio: 16 / 9,
 | 
					                    aspectRatio: 16 / 9,
 | 
				
			||||||
                    child: ClipRRect(
 | 
					                    child: ClipRRect(
 | 
				
			||||||
 | 
					                      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
				
			||||||
                      child: AutoResizeUniversalImage(
 | 
					                      child: AutoResizeUniversalImage(
 | 
				
			||||||
                        meta.image!,
 | 
					                        meta.image!.startsWith('//') ? 'https:${meta.image}' : meta.image!,
 | 
				
			||||||
                        fit: BoxFit.contain,
 | 
					                        fit: BoxFit.contain,
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
 | 
					import 'package:bitsdojo_window/bitsdojo_window.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:go_router/go_router.dart';
 | 
					import 'package:go_router/go_router.dart';
 | 
				
			||||||
@@ -12,7 +11,6 @@ import 'package:styled_widget/styled_widget.dart';
 | 
				
			|||||||
import 'package:surface/providers/config.dart';
 | 
					import 'package:surface/providers/config.dart';
 | 
				
			||||||
import 'package:surface/providers/navigation.dart';
 | 
					import 'package:surface/providers/navigation.dart';
 | 
				
			||||||
import 'package:surface/widgets/connection_indicator.dart';
 | 
					import 'package:surface/widgets/connection_indicator.dart';
 | 
				
			||||||
import 'package:surface/widgets/dialog.dart';
 | 
					 | 
				
			||||||
import 'package:surface/widgets/navigation/app_background.dart';
 | 
					import 'package:surface/widgets/navigation/app_background.dart';
 | 
				
			||||||
import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
 | 
					import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
 | 
				
			||||||
import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
 | 
					import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -203,6 +203,8 @@ class PostItem extends StatelessWidget {
 | 
				
			|||||||
        ?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
 | 
					        ?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
 | 
				
			||||||
        .toList();
 | 
					        .toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final cfg = context.read<ConfigProvider>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Column(
 | 
					    return Column(
 | 
				
			||||||
      crossAxisAlignment: CrossAxisAlignment.center,
 | 
					      crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
      children: [
 | 
					      children: [
 | 
				
			||||||
@@ -261,7 +263,7 @@ class PostItem extends StatelessWidget {
 | 
				
			|||||||
            fit: showFullPost ? BoxFit.cover : BoxFit.contain,
 | 
					            fit: showFullPost ? BoxFit.cover : BoxFit.contain,
 | 
				
			||||||
            padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
					            padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        if (data.body['content'] != null)
 | 
					        if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
 | 
				
			||||||
          LinkPreviewWidget(
 | 
					          LinkPreviewWidget(
 | 
				
			||||||
            text: data.body['content'],
 | 
					            text: data.body['content'],
 | 
				
			||||||
          ).padding(horizontal: 4),
 | 
					          ).padding(horizontal: 4),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,6 +13,7 @@ import file_selector_macos
 | 
				
			|||||||
import firebase_analytics
 | 
					import firebase_analytics
 | 
				
			||||||
import firebase_core
 | 
					import firebase_core
 | 
				
			||||||
import firebase_messaging
 | 
					import firebase_messaging
 | 
				
			||||||
 | 
					import flutter_inappwebview_macos
 | 
				
			||||||
import flutter_udid
 | 
					import flutter_udid
 | 
				
			||||||
import flutter_webrtc
 | 
					import flutter_webrtc
 | 
				
			||||||
import gal
 | 
					import gal
 | 
				
			||||||
@@ -40,6 +41,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
				
			|||||||
  FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
 | 
					  FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin"))
 | 
				
			||||||
  FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
 | 
					  FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
 | 
				
			||||||
  FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
 | 
					  FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
 | 
				
			||||||
 | 
					  InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
 | 
				
			||||||
  FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
 | 
					  FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
 | 
				
			||||||
  FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
 | 
					  FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
 | 
				
			||||||
  GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
 | 
					  GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,6 +72,9 @@ PODS:
 | 
				
			|||||||
    - GoogleUtilities/Reachability (~> 8.0)
 | 
					    - GoogleUtilities/Reachability (~> 8.0)
 | 
				
			||||||
    - GoogleUtilities/UserDefaults (~> 8.0)
 | 
					    - GoogleUtilities/UserDefaults (~> 8.0)
 | 
				
			||||||
    - nanopb (~> 3.30910.0)
 | 
					    - nanopb (~> 3.30910.0)
 | 
				
			||||||
 | 
					  - flutter_inappwebview_macos (0.0.1):
 | 
				
			||||||
 | 
					    - FlutterMacOS
 | 
				
			||||||
 | 
					    - OrderedSet (~> 6.0.3)
 | 
				
			||||||
  - flutter_udid (0.0.1):
 | 
					  - flutter_udid (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
    - SAMKeychain
 | 
					    - SAMKeychain
 | 
				
			||||||
@@ -149,6 +152,7 @@ PODS:
 | 
				
			|||||||
    - nanopb/encode (= 3.30910.0)
 | 
					    - nanopb/encode (= 3.30910.0)
 | 
				
			||||||
  - nanopb/decode (3.30910.0)
 | 
					  - nanopb/decode (3.30910.0)
 | 
				
			||||||
  - nanopb/encode (3.30910.0)
 | 
					  - nanopb/encode (3.30910.0)
 | 
				
			||||||
 | 
					  - OrderedSet (6.0.3)
 | 
				
			||||||
  - package_info_plus (0.0.1):
 | 
					  - package_info_plus (0.0.1):
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
  - pasteboard (0.0.1):
 | 
					  - pasteboard (0.0.1):
 | 
				
			||||||
@@ -186,6 +190,7 @@ DEPENDENCIES:
 | 
				
			|||||||
  - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
 | 
					  - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`)
 | 
				
			||||||
  - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
 | 
					  - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`)
 | 
				
			||||||
  - firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
 | 
					  - firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`)
 | 
				
			||||||
 | 
					  - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`)
 | 
				
			||||||
  - flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
 | 
					  - flutter_udid (from `Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos`)
 | 
				
			||||||
  - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
 | 
					  - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
 | 
				
			||||||
  - FlutterMacOS (from `Flutter/ephemeral`)
 | 
					  - FlutterMacOS (from `Flutter/ephemeral`)
 | 
				
			||||||
@@ -218,6 +223,7 @@ SPEC REPOS:
 | 
				
			|||||||
    - GoogleDataTransport
 | 
					    - GoogleDataTransport
 | 
				
			||||||
    - GoogleUtilities
 | 
					    - GoogleUtilities
 | 
				
			||||||
    - nanopb
 | 
					    - nanopb
 | 
				
			||||||
 | 
					    - OrderedSet
 | 
				
			||||||
    - PromisesObjC
 | 
					    - PromisesObjC
 | 
				
			||||||
    - SAMKeychain
 | 
					    - SAMKeychain
 | 
				
			||||||
    - WebRTC-SDK
 | 
					    - WebRTC-SDK
 | 
				
			||||||
@@ -241,6 +247,8 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
    :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos
 | 
				
			||||||
  firebase_messaging:
 | 
					  firebase_messaging:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos
 | 
				
			||||||
 | 
					  flutter_inappwebview_macos:
 | 
				
			||||||
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos
 | 
				
			||||||
  flutter_udid:
 | 
					  flutter_udid:
 | 
				
			||||||
    :path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
 | 
					    :path: Flutter/ephemeral/.symlinks/plugins/flutter_udid/macos
 | 
				
			||||||
  flutter_webrtc:
 | 
					  flutter_webrtc:
 | 
				
			||||||
@@ -296,6 +304,7 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
 | 
					  FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
 | 
				
			||||||
  FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
 | 
					  FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
 | 
				
			||||||
  FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
 | 
					  FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
 | 
				
			||||||
 | 
					  flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
 | 
				
			||||||
  flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
 | 
					  flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
 | 
				
			||||||
  flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
 | 
					  flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
 | 
				
			||||||
  FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
 | 
					  FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
 | 
				
			||||||
@@ -309,6 +318,7 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
 | 
					  media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
 | 
				
			||||||
  media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
 | 
					  media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
 | 
				
			||||||
  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
					  nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
 | 
				
			||||||
 | 
					  OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
 | 
				
			||||||
  package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
 | 
					  package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
 | 
				
			||||||
  pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
 | 
					  pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99
 | 
				
			||||||
  path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
 | 
					  path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										66
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -675,6 +675,70 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.3.0"
 | 
					    version: "2.3.0"
 | 
				
			||||||
 | 
					  flutter_inappwebview:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_inappwebview
 | 
				
			||||||
 | 
					      sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "6.1.5"
 | 
				
			||||||
 | 
					  flutter_inappwebview_android:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_inappwebview_android
 | 
				
			||||||
 | 
					      sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.1.3"
 | 
				
			||||||
 | 
					  flutter_inappwebview_internal_annotations:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_inappwebview_internal_annotations
 | 
				
			||||||
 | 
					      sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.2.0"
 | 
				
			||||||
 | 
					  flutter_inappwebview_ios:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_inappwebview_ios
 | 
				
			||||||
 | 
					      sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.1.2"
 | 
				
			||||||
 | 
					  flutter_inappwebview_macos:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_inappwebview_macos
 | 
				
			||||||
 | 
					      sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.1.2"
 | 
				
			||||||
 | 
					  flutter_inappwebview_platform_interface:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_inappwebview_platform_interface
 | 
				
			||||||
 | 
					      sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.3.0+1"
 | 
				
			||||||
 | 
					  flutter_inappwebview_web:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_inappwebview_web
 | 
				
			||||||
 | 
					      sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.1.2"
 | 
				
			||||||
 | 
					  flutter_inappwebview_windows:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_inappwebview_windows
 | 
				
			||||||
 | 
					      sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "0.6.0"
 | 
				
			||||||
  flutter_launcher_icons:
 | 
					  flutter_launcher_icons:
 | 
				
			||||||
    dependency: "direct dev"
 | 
					    dependency: "direct dev"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -875,7 +939,7 @@ packages:
 | 
				
			|||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.7.0"
 | 
					    version: "0.7.0"
 | 
				
			||||||
  html:
 | 
					  html:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: html
 | 
					      name: html
 | 
				
			||||||
      sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
 | 
					      sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
 | 
				
			|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
					# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
				
			||||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
					# In Windows, build-name is used as the major, minor, and patch parts
 | 
				
			||||||
# of the product and file versions while build-number is used as the build suffix.
 | 
					# of the product and file versions while build-number is used as the build suffix.
 | 
				
			||||||
version: 2.2.2+55
 | 
					version: 2.2.2+57
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ^3.5.4
 | 
					  sdk: ^3.5.4
 | 
				
			||||||
@@ -115,6 +115,8 @@ dependencies:
 | 
				
			|||||||
  slide_countdown: ^2.0.2
 | 
					  slide_countdown: ^2.0.2
 | 
				
			||||||
  video_compress: ^3.1.3
 | 
					  video_compress: ^3.1.3
 | 
				
			||||||
  cached_network_image: ^3.4.1
 | 
					  cached_network_image: ^3.4.1
 | 
				
			||||||
 | 
					  flutter_inappwebview: ^6.1.5
 | 
				
			||||||
 | 
					  html: ^0.15.5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,8 @@
 | 
				
			|||||||
    -->
 | 
					    -->
 | 
				
			||||||
    <base href="$FLUTTER_BASE_HREF">
 | 
					    <base href="$FLUTTER_BASE_HREF">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <script type="application/javascript" src="/assets/packages/flutter_inappwebview_web/assets/web/web_support.js" defer></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <meta charset="UTF-8">
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
    <meta content="IE=Edge" http-equiv="X-UA-Compatible">
 | 
					    <meta content="IE=Edge" http-equiv="X-UA-Compatible">
 | 
				
			||||||
    <meta name="description" content="A new Flutter project.">
 | 
					    <meta name="description" content="A new Flutter project.">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@
 | 
				
			|||||||
#include <file_saver/file_saver_plugin.h>
 | 
					#include <file_saver/file_saver_plugin.h>
 | 
				
			||||||
#include <file_selector_windows/file_selector_windows.h>
 | 
					#include <file_selector_windows/file_selector_windows.h>
 | 
				
			||||||
#include <firebase_core/firebase_core_plugin_c_api.h>
 | 
					#include <firebase_core/firebase_core_plugin_c_api.h>
 | 
				
			||||||
 | 
					#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
 | 
				
			||||||
#include <flutter_udid/flutter_udid_plugin_c_api.h>
 | 
					#include <flutter_udid/flutter_udid_plugin_c_api.h>
 | 
				
			||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
 | 
					#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
 | 
				
			||||||
#include <gal/gal_plugin_c_api.h>
 | 
					#include <gal/gal_plugin_c_api.h>
 | 
				
			||||||
@@ -34,6 +35,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
 | 
				
			|||||||
      registry->GetRegistrarForPlugin("FileSelectorWindows"));
 | 
					      registry->GetRegistrarForPlugin("FileSelectorWindows"));
 | 
				
			||||||
  FirebaseCorePluginCApiRegisterWithRegistrar(
 | 
					  FirebaseCorePluginCApiRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
 | 
					      registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
 | 
				
			||||||
 | 
					  FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
 | 
				
			||||||
 | 
					      registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
 | 
				
			||||||
  FlutterUdidPluginCApiRegisterWithRegistrar(
 | 
					  FlutterUdidPluginCApiRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
 | 
					      registry->GetRegistrarForPlugin("FlutterUdidPluginCApi"));
 | 
				
			||||||
  FlutterWebRTCPluginRegisterWithRegistrar(
 | 
					  FlutterWebRTCPluginRegisterWithRegistrar(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
 | 
				
			|||||||
  file_saver
 | 
					  file_saver
 | 
				
			||||||
  file_selector_windows
 | 
					  file_selector_windows
 | 
				
			||||||
  firebase_core
 | 
					  firebase_core
 | 
				
			||||||
 | 
					  flutter_inappwebview_windows
 | 
				
			||||||
  flutter_udid
 | 
					  flutter_udid
 | 
				
			||||||
  flutter_webrtc
 | 
					  flutter_webrtc
 | 
				
			||||||
  gal
 | 
					  gal
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user