Compare commits
	
		
			26 Commits
		
	
	
		
			2.4.2+80
			...
			9311bfc3b5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9311bfc3b5 | |||
| 8dd6435a30 | |||
| 21a1d4a2ad | |||
| 603875b1af | |||
| 4209a13c84 | |||
| 55b79bfd8f | |||
| 6e6c3f42f6 | |||
| dc38b46b2c | |||
| b4990308e9 | |||
| 237abe564d | |||
| 71b41d470a | |||
| 7052b5b635 | |||
| f356e08f79 | |||
| 152872db65 | |||
| dfe117d04f | |||
| caf63f0cbe | |||
| b8f5cc82f9 | |||
| 360bc50f21 | |||
| 2de93a0486 | |||
| 02227852f8 | |||
| ad16de595b | |||
| 9f8c8923d9 | |||
| 060bfa4887 | |||
| e68ada2d04 | |||
| d6013078bd | |||
| 5976d61997 | 
							
								
								
									
										
											BIN
										
									
								
								assets/icon/kanban-1st.jpg
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/icon/kanban-1st.jpg
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 509 KiB  | 
@@ -130,7 +130,7 @@
 | 
			
		||||
  "accountPublishersSubtitle": "Manage your publish identities.",
 | 
			
		||||
  "accountSettings": "Account Settings",
 | 
			
		||||
  "accountSettingsSubtitle": "Manage your account and make it yours.",
 | 
			
		||||
  "accountProfileEdit": "Edit your profile",
 | 
			
		||||
  "accountProfileEdit": "Edit Profile",
 | 
			
		||||
  "accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
 | 
			
		||||
  "accountWallet": "Wallet",
 | 
			
		||||
  "accountWalletSubtitle": "View your balance and transactions.",
 | 
			
		||||
@@ -338,6 +338,7 @@
 | 
			
		||||
  "fieldAttachmentRandomId": "Random ID",
 | 
			
		||||
  "fieldAttachmentAlt": "Alternative text",
 | 
			
		||||
  "addAttachmentFromAlbum": "Add from album",
 | 
			
		||||
  "addAttachmentFromFiles": "Add from files",
 | 
			
		||||
  "addAttachmentFromClipboard": "Paste file",
 | 
			
		||||
  "addAttachmentFromCameraPhoto": "Take photo",
 | 
			
		||||
  "addAttachmentFromCameraVideo": "Take video",
 | 
			
		||||
@@ -846,5 +847,55 @@
 | 
			
		||||
  "translating": "Translating…",
 | 
			
		||||
  "translated": "Translated",
 | 
			
		||||
  "settingsAutoTranslate": "Auto Translate",
 | 
			
		||||
  "settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages."
 | 
			
		||||
  "settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages.",
 | 
			
		||||
  "trayMenuHide": "Hide",
 | 
			
		||||
  "accountSettingsNotify": "Notify Settings",
 | 
			
		||||
  "accountSettingsNotifyDescription": "Adjust the types of notifications you receive.",
 | 
			
		||||
  "accountSettingsSecurity": "Security Settings",
 | 
			
		||||
  "accountSettingsSecurityDescription": "Adjust your account security settings.",
 | 
			
		||||
  "save": "Save",
 | 
			
		||||
  "notificationTopicPostFeedback": "Post Feedback",
 | 
			
		||||
  "notificationTopicPostReply": "Post Replies",
 | 
			
		||||
  "notificationTopicPostSubscription": "Post Subscriptions",
 | 
			
		||||
  "notificationTopicMessaging": "New Messages",
 | 
			
		||||
  "notificationTopicMessagingCall": "Incoming Calls",
 | 
			
		||||
  "notificationTopicGeneral": "General",
 | 
			
		||||
  "authMaximumAuthSteps": "Maximum Authenticate Steps",
 | 
			
		||||
  "authMaximumAuthStepsDescription": {
 | 
			
		||||
    "one": "Maximum ask for {} step authenticate",
 | 
			
		||||
    "other": "Maximum ask for {} steps authenticate"
 | 
			
		||||
  },
 | 
			
		||||
  "authAlwaysRisky": "Always Risky",
 | 
			
		||||
  "authAlwaysRiskyDescription": "Always ask for the highest steps count of authentication when logging in.",
 | 
			
		||||
  "chatUnjoined": "Unjoined Channel",
 | 
			
		||||
  "chatUnjoinedDescription": "You haven't joined this channel, so you can't send messages either view messages in it.",
 | 
			
		||||
  "chatUnjoinedPublicDescription": "Fortunately, this is a public channel, so you can join it as you want.",
 | 
			
		||||
  "chatJoin": "Join the Channel",
 | 
			
		||||
  "appInitStarting": "Starting",
 | 
			
		||||
  "appInitNetwork": "Initializing Network",
 | 
			
		||||
  "appInitUserdata": "Initializing User Data",
 | 
			
		||||
  "appInitWebsocket": "Establishing Solar Link",
 | 
			
		||||
  "appInitNotification": "Initializing Push Notifications", 
 | 
			
		||||
  "appInitKeyPair": "Initializing Key Pairs",
 | 
			
		||||
  "appInitStickers": "Initializing Stickers",
 | 
			
		||||
  "appInitUserDirectory": "Initializing User Directory",
 | 
			
		||||
  "appInitRealm": "Initializing Realms",
 | 
			
		||||
  "appInitChat": "Initializing Chat",
 | 
			
		||||
  "appInitDone": "Completed",
 | 
			
		||||
  "community": "Community",
 | 
			
		||||
  "realmCommunity": "{}'s Community",
 | 
			
		||||
  "postTotalCount": {
 | 
			
		||||
    "one": "Total {} post",
 | 
			
		||||
    "other": "Total {} posts"
 | 
			
		||||
  },
 | 
			
		||||
  "settingsHideBottomNav": "Hide Bottom Navigation",
 | 
			
		||||
  "settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.",
 | 
			
		||||
  "reCaptcha": "reCaptcha",
 | 
			
		||||
  "friends": "Friends",
 | 
			
		||||
  "friendsDescription": "Manage your friendships.",
 | 
			
		||||
  "album": "Album",
 | 
			
		||||
  "albumDescription": "View albums and manage attachments.",
 | 
			
		||||
  "stickers": "Stickers",
 | 
			
		||||
  "stickersDescription": "View sticker packs and manage stickers.",
 | 
			
		||||
  "navBottomUnauthorizedCaption": "Or create an account"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -336,6 +336,7 @@
 | 
			
		||||
  "fieldAttachmentRandomId": "访问 ID",
 | 
			
		||||
  "fieldAttachmentAlt": "概述文字",
 | 
			
		||||
  "addAttachmentFromAlbum": "从相册中添加附件",
 | 
			
		||||
  "addAttachmentFromFiles": "从文件中添加附件",
 | 
			
		||||
  "addAttachmentFromClipboard": "粘贴附件",
 | 
			
		||||
  "addAttachmentFromCameraPhoto": "拍摄照片",
 | 
			
		||||
  "addAttachmentFromCameraVideo": "拍摄视频",
 | 
			
		||||
@@ -844,5 +845,55 @@
 | 
			
		||||
  "translating": "正在翻译……",
 | 
			
		||||
  "translated": "已翻译",
 | 
			
		||||
  "settingsAutoTranslate": "自动翻译",
 | 
			
		||||
  "settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。"
 | 
			
		||||
  "settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。",
 | 
			
		||||
  "trayMenuHide": "隐藏",
 | 
			
		||||
  "accountSettingsNotify": "通知设置",
 | 
			
		||||
  "accountSettingsNotifyDescription": "调整你所收到的通知种类。",
 | 
			
		||||
  "accountSettingsSecurity": "安全设置",
 | 
			
		||||
  "accountSettingsSecurityDescription": "调整你的帐户安全设置。",
 | 
			
		||||
  "save": "保存",
 | 
			
		||||
  "notificationTopicPostFeedback": "帖子数据反馈",
 | 
			
		||||
  "notificationTopicPostReply": "帖子回复",
 | 
			
		||||
  "notificationTopicPostSubscription": "帖子订阅",
 | 
			
		||||
  "notificationTopicMessaging": "消息",
 | 
			
		||||
  "notificationTopicMessagingCall": "通话",
 | 
			
		||||
  "notificationTopicGeneral": "杂项",
 | 
			
		||||
  "authMaximumAuthSteps": "最大验证步骤",
 | 
			
		||||
  "authMaximumAuthStepsDescription": {
 | 
			
		||||
    "one": "登入时最多要求 {} 步验证",
 | 
			
		||||
    "other": "登入时最多要求 {} 步验证"
 | 
			
		||||
  },
 | 
			
		||||
  "authAlwaysRisky": "总是风险",
 | 
			
		||||
  "authAlwaysRiskyDescription": "在登入时始终按最高标准要求验证。",
 | 
			
		||||
  "chatUnjoined": "未加入频道",
 | 
			
		||||
  "chatUnjoinedDescription": "你没有加入这个频道,所以你也无法发送消息或者查看这个频道中的消息。",
 | 
			
		||||
  "chatUnjoinedPublicDescription": "但幸运的是,这是一个公开频道,所以你可以主动加入。",
 | 
			
		||||
  "chatJoin": "加入频道",
 | 
			
		||||
  "appInitStarting": "启动中",
 | 
			
		||||
  "appInitNetwork": "正在初始化网络",
 | 
			
		||||
  "appInitUserdata": "正在初始化用户数据",
 | 
			
		||||
  "appInitWebsocket": "正在建立 Solar Link",
 | 
			
		||||
  "appInitNotification": "正在初始化推送通知", 
 | 
			
		||||
  "appInitKeyPair": "正在初始化密钥对",
 | 
			
		||||
  "appInitStickers": "正在初始化贴图包",
 | 
			
		||||
  "appInitUserDirectory": "正在初始化用户目录",
 | 
			
		||||
  "appInitRealm": "正在初始化领域信息",
 | 
			
		||||
  "appInitChat": "正在初始化聊天",
 | 
			
		||||
  "appInitDone": "完成",
 | 
			
		||||
  "community": "社区",
 | 
			
		||||
  "realmCommunity": "{}的社区",
 | 
			
		||||
  "postTotalCount": {
 | 
			
		||||
    "zero": "没有帖子",
 | 
			
		||||
    "one": "共 {} 条帖子"
 | 
			
		||||
  },
 | 
			
		||||
  "settingsHideBottomNav": "隐藏底部导航栏",
 | 
			
		||||
  "settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。",
 | 
			
		||||
  "reCaptcha": "人机验证",
 | 
			
		||||
  "friends": "好友",
 | 
			
		||||
  "friendsDescription": "管理好友关系。",
 | 
			
		||||
  "album": "相册",
 | 
			
		||||
  "albumDescription": "查看相册与管理上传附件。",
 | 
			
		||||
  "stickers": "贴图",
 | 
			
		||||
  "stickersDescription": "查看贴图包与管理贴图。",
 | 
			
		||||
  "navBottomUnauthorizedCaption": "或者注册一个账号"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -336,6 +336,7 @@
 | 
			
		||||
  "fieldAttachmentRandomId": "訪問 ID",
 | 
			
		||||
  "fieldAttachmentAlt": "概述文字",
 | 
			
		||||
  "addAttachmentFromAlbum": "從相冊中添加附件",
 | 
			
		||||
  "addAttachmentFromFiles": "從文件中添加附件",
 | 
			
		||||
  "addAttachmentFromClipboard": "粘貼附件",
 | 
			
		||||
  "addAttachmentFromCameraPhoto": "拍攝照片",
 | 
			
		||||
  "addAttachmentFromCameraVideo": "拍攝視頻",
 | 
			
		||||
@@ -844,5 +845,55 @@
 | 
			
		||||
  "translating": "正在翻譯……",
 | 
			
		||||
  "translated": "已翻譯",
 | 
			
		||||
  "settingsAutoTranslate": "自動翻譯",
 | 
			
		||||
  "settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。"
 | 
			
		||||
  "settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。",
 | 
			
		||||
  "trayMenuHide": "隱藏",
 | 
			
		||||
  "accountSettingsNotify": "通知設置",
 | 
			
		||||
  "accountSettingsNotifyDescription": "調整你所收到的通知種類。",
 | 
			
		||||
  "accountSettingsSecurity": "安全設置",
 | 
			
		||||
  "accountSettingsSecurityDescription": "調整你的帳户安全設置。",
 | 
			
		||||
  "save": "保存",
 | 
			
		||||
  "notificationTopicPostFeedback": "帖子數據反饋",
 | 
			
		||||
  "notificationTopicPostReply": "帖子回覆",
 | 
			
		||||
  "notificationTopicPostSubscription": "帖子訂閲",
 | 
			
		||||
  "notificationTopicMessaging": "消息",
 | 
			
		||||
  "notificationTopicMessagingCall": "通話",
 | 
			
		||||
  "notificationTopicGeneral": "雜項",
 | 
			
		||||
  "authMaximumAuthSteps": "最大驗證步驟",
 | 
			
		||||
  "authMaximumAuthStepsDescription": {
 | 
			
		||||
    "one": "登入時最多要求 {} 步驗證",
 | 
			
		||||
    "other": "登入時最多要求 {} 步驗證"
 | 
			
		||||
  },
 | 
			
		||||
  "authAlwaysRisky": "總是風險",
 | 
			
		||||
  "authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。",
 | 
			
		||||
  "chatUnjoined": "未加入頻道",
 | 
			
		||||
  "chatUnjoinedDescription": "你沒有加入這個頻道,所以你也無法發送消息或者查看這個頻道中的消息。",
 | 
			
		||||
  "chatUnjoinedPublicDescription": "但幸運的是,這是一個公開頻道,所以你可以主動加入。",
 | 
			
		||||
  "chatJoin": "加入頻道",
 | 
			
		||||
  "appInitStarting": "啓動中",
 | 
			
		||||
  "appInitNetwork": "正在初始化網絡",
 | 
			
		||||
  "appInitUserdata": "正在初始化用户數據",
 | 
			
		||||
  "appInitWebsocket": "正在建立 Solar Link",
 | 
			
		||||
  "appInitNotification": "正在初始化推送通知", 
 | 
			
		||||
  "appInitKeyPair": "正在初始化密鑰對",
 | 
			
		||||
  "appInitStickers": "正在初始化貼圖包",
 | 
			
		||||
  "appInitUserDirectory": "正在初始化用户目錄",
 | 
			
		||||
  "appInitRealm": "正在初始化領域信息",
 | 
			
		||||
  "appInitChat": "正在初始化聊天",
 | 
			
		||||
  "appInitDone": "完成",
 | 
			
		||||
  "community": "社區",
 | 
			
		||||
  "realmCommunity": "{}的社區",
 | 
			
		||||
  "postTotalCount": {
 | 
			
		||||
    "zero": "沒有帖子",
 | 
			
		||||
    "one": "共 {} 條帖子"
 | 
			
		||||
  },
 | 
			
		||||
  "settingsHideBottomNav": "隱藏底部導航欄",
 | 
			
		||||
  "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
 | 
			
		||||
  "reCaptcha": "人機驗證",
 | 
			
		||||
  "friends": "好友",
 | 
			
		||||
  "friendsDescription": "管理好友關係。",
 | 
			
		||||
  "album": "相冊",
 | 
			
		||||
  "albumDescription": "查看相冊與管理上傳附件。",
 | 
			
		||||
  "stickers": "貼圖",
 | 
			
		||||
  "stickersDescription": "查看貼圖包與管理貼圖。",
 | 
			
		||||
  "navBottomUnauthorizedCaption": "或者註冊一個賬號"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -336,6 +336,7 @@
 | 
			
		||||
  "fieldAttachmentRandomId": "訪問 ID",
 | 
			
		||||
  "fieldAttachmentAlt": "概述文字",
 | 
			
		||||
  "addAttachmentFromAlbum": "從相冊中添加附件",
 | 
			
		||||
  "addAttachmentFromFiles": "從文件中添加附件",
 | 
			
		||||
  "addAttachmentFromClipboard": "粘貼附件",
 | 
			
		||||
  "addAttachmentFromCameraPhoto": "拍攝照片",
 | 
			
		||||
  "addAttachmentFromCameraVideo": "拍攝視頻",
 | 
			
		||||
@@ -844,5 +845,55 @@
 | 
			
		||||
  "translating": "正在翻譯……",
 | 
			
		||||
  "translated": "已翻譯",
 | 
			
		||||
  "settingsAutoTranslate": "自動翻譯",
 | 
			
		||||
  "settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。"
 | 
			
		||||
  "settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。",
 | 
			
		||||
  "trayMenuHide": "隱藏",
 | 
			
		||||
  "accountSettingsNotify": "通知設置",
 | 
			
		||||
  "accountSettingsNotifyDescription": "調整你所收到的通知種類。",
 | 
			
		||||
  "accountSettingsSecurity": "安全設置",
 | 
			
		||||
  "accountSettingsSecurityDescription": "調整你的帳戶安全設置。",
 | 
			
		||||
  "save": "保存",
 | 
			
		||||
  "notificationTopicPostFeedback": "帖子數據反饋",
 | 
			
		||||
  "notificationTopicPostReply": "帖子回覆",
 | 
			
		||||
  "notificationTopicPostSubscription": "帖子訂閱",
 | 
			
		||||
  "notificationTopicMessaging": "消息",
 | 
			
		||||
  "notificationTopicMessagingCall": "通話",
 | 
			
		||||
  "notificationTopicGeneral": "雜項",
 | 
			
		||||
  "authMaximumAuthSteps": "最大驗證步驟",
 | 
			
		||||
  "authMaximumAuthStepsDescription": {
 | 
			
		||||
    "one": "登入時最多要求 {} 步驗證",
 | 
			
		||||
    "other": "登入時最多要求 {} 步驗證"
 | 
			
		||||
  },
 | 
			
		||||
  "authAlwaysRisky": "總是風險",
 | 
			
		||||
  "authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。",
 | 
			
		||||
  "chatUnjoined": "未加入頻道",
 | 
			
		||||
  "chatUnjoinedDescription": "你沒有加入這個頻道,所以你也無法發送消息或者查看這個頻道中的消息。",
 | 
			
		||||
  "chatUnjoinedPublicDescription": "但幸運的是,這是一個公開頻道,所以你可以主動加入。",
 | 
			
		||||
  "chatJoin": "加入頻道",
 | 
			
		||||
  "appInitStarting": "啟動中",
 | 
			
		||||
  "appInitNetwork": "正在初始化網絡",
 | 
			
		||||
  "appInitUserdata": "正在初始化用戶數據",
 | 
			
		||||
  "appInitWebsocket": "正在建立 Solar Link",
 | 
			
		||||
  "appInitNotification": "正在初始化推送通知", 
 | 
			
		||||
  "appInitKeyPair": "正在初始化密鑰對",
 | 
			
		||||
  "appInitStickers": "正在初始化貼圖包",
 | 
			
		||||
  "appInitUserDirectory": "正在初始化用戶目錄",
 | 
			
		||||
  "appInitRealm": "正在初始化領域信息",
 | 
			
		||||
  "appInitChat": "正在初始化聊天",
 | 
			
		||||
  "appInitDone": "完成",
 | 
			
		||||
  "community": "社區",
 | 
			
		||||
  "realmCommunity": "{}的社區",
 | 
			
		||||
  "postTotalCount": {
 | 
			
		||||
    "zero": "沒有帖子",
 | 
			
		||||
    "one": "共 {} 條帖子"
 | 
			
		||||
  },
 | 
			
		||||
  "settingsHideBottomNav": "隱藏底部導航欄",
 | 
			
		||||
  "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
 | 
			
		||||
  "reCaptcha": "人機驗證",
 | 
			
		||||
  "friends": "好友",
 | 
			
		||||
  "friendsDescription": "管理好友關係。",
 | 
			
		||||
  "album": "相冊",
 | 
			
		||||
  "albumDescription": "查看相冊與管理上傳附件。",
 | 
			
		||||
  "stickers": "貼圖",
 | 
			
		||||
  "stickersDescription": "查看貼圖包與管理貼圖。",
 | 
			
		||||
  "navBottomUnauthorizedCaption": "或者註冊一個賬號"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								drift_schemas/my_database/drift_schema_v4.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								drift_schemas/my_database/drift_schema_v4.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -232,6 +232,8 @@ PODS:
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/fts5 (3.49.1):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/math (3.49.1):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/perf-threadsafe (3.49.1):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/rtree (3.49.1):
 | 
			
		||||
@@ -242,6 +244,7 @@ PODS:
 | 
			
		||||
    - sqlite3 (~> 3.49.1)
 | 
			
		||||
    - sqlite3/dbstatvtab
 | 
			
		||||
    - sqlite3/fts5
 | 
			
		||||
    - sqlite3/math
 | 
			
		||||
    - sqlite3/perf-threadsafe
 | 
			
		||||
    - sqlite3/rtree
 | 
			
		||||
  - SwiftyGif (5.4.5)
 | 
			
		||||
@@ -457,7 +460,7 @@ SPEC CHECKSUMS:
 | 
			
		||||
  shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
 | 
			
		||||
  sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
 | 
			
		||||
  sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
 | 
			
		||||
  sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
 | 
			
		||||
  sqlite3_flutter_libs: 487032b9008b28de37c72a3aa66849ef3745f3e6
 | 
			
		||||
  SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
 | 
			
		||||
  url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
 | 
			
		||||
  video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
 | 
			
		||||
 
 | 
			
		||||
@@ -79,6 +79,8 @@
 | 
			
		||||
		<string>UIInterfaceOrientationLandscapeLeft</string>
 | 
			
		||||
		<string>UIInterfaceOrientationLandscapeRight</string>
 | 
			
		||||
	</array>
 | 
			
		||||
	<key>LSSupportsOpeningDocumentsInPlace</key>
 | 
			
		||||
	<true/>
 | 
			
		||||
	<key>UISupportedInterfaceOrientations~ipad</key>
 | 
			
		||||
	<array>
 | 
			
		||||
		<string>UIInterfaceOrientationPortrait</string>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,10 +6,12 @@ import 'package:surface/database/attachment.dart';
 | 
			
		||||
import 'package:surface/database/chat.dart';
 | 
			
		||||
import 'package:surface/database/database.steps.dart';
 | 
			
		||||
import 'package:surface/database/keypair.dart';
 | 
			
		||||
import 'package:surface/database/realm.dart';
 | 
			
		||||
import 'package:surface/database/sticker.dart';
 | 
			
		||||
import 'package:surface/types/chat.dart';
 | 
			
		||||
import 'package:surface/types/attachment.dart';
 | 
			
		||||
import 'package:surface/types/account.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
 | 
			
		||||
part 'database.g.dart';
 | 
			
		||||
 | 
			
		||||
@@ -22,12 +24,13 @@ part 'database.g.dart';
 | 
			
		||||
  SnLocalAttachment,
 | 
			
		||||
  SnLocalSticker,
 | 
			
		||||
  SnLocalStickerPack,
 | 
			
		||||
  SnLocalRealm,
 | 
			
		||||
])
 | 
			
		||||
class AppDatabase extends _$AppDatabase {
 | 
			
		||||
  AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get schemaVersion => 3;
 | 
			
		||||
  int get schemaVersion => 4;
 | 
			
		||||
 | 
			
		||||
  static QueryExecutor _openConnection() {
 | 
			
		||||
    return driftDatabase(
 | 
			
		||||
@@ -49,6 +52,10 @@ class AppDatabase extends _$AppDatabase {
 | 
			
		||||
        // Nothing else to do here
 | 
			
		||||
      }, from2To3: (m, schema) async {
 | 
			
		||||
        // Nothing else to do here, too
 | 
			
		||||
      }, from3To4: (m, schema) async {
 | 
			
		||||
        m.createTable(schema.snLocalRealm);
 | 
			
		||||
        m.createIndex(schema.idxRealmAccount);
 | 
			
		||||
        m.createIndex(schema.idxRealmAlias);
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -2454,6 +2454,351 @@ class SnLocalStickerPackCompanion
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class $SnLocalRealmTable extends SnLocalRealm
 | 
			
		||||
    with TableInfo<$SnLocalRealmTable, SnLocalRealmData> {
 | 
			
		||||
  @override
 | 
			
		||||
  final GeneratedDatabase attachedDatabase;
 | 
			
		||||
  final String? _alias;
 | 
			
		||||
  $SnLocalRealmTable(this.attachedDatabase, [this._alias]);
 | 
			
		||||
  static const VerificationMeta _idMeta = const VerificationMeta('id');
 | 
			
		||||
  @override
 | 
			
		||||
  late final GeneratedColumn<int> id = GeneratedColumn<int>(
 | 
			
		||||
      'id', aliasedName, false,
 | 
			
		||||
      hasAutoIncrement: true,
 | 
			
		||||
      type: DriftSqlType.int,
 | 
			
		||||
      requiredDuringInsert: false,
 | 
			
		||||
      defaultConstraints:
 | 
			
		||||
          GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
 | 
			
		||||
  static const VerificationMeta _aliasMeta = const VerificationMeta('alias');
 | 
			
		||||
  @override
 | 
			
		||||
  late final GeneratedColumn<String> alias = GeneratedColumn<String>(
 | 
			
		||||
      'alias', aliasedName, false,
 | 
			
		||||
      type: DriftSqlType.string,
 | 
			
		||||
      requiredDuringInsert: true,
 | 
			
		||||
      defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'));
 | 
			
		||||
  @override
 | 
			
		||||
  late final GeneratedColumnWithTypeConverter<SnRealm, String> content =
 | 
			
		||||
      GeneratedColumn<String>('content', aliasedName, false,
 | 
			
		||||
              type: DriftSqlType.string, requiredDuringInsert: true)
 | 
			
		||||
          .withConverter<SnRealm>($SnLocalRealmTable.$convertercontent);
 | 
			
		||||
  static const VerificationMeta _accountIdMeta =
 | 
			
		||||
      const VerificationMeta('accountId');
 | 
			
		||||
  @override
 | 
			
		||||
  late final GeneratedColumn<int> accountId = GeneratedColumn<int>(
 | 
			
		||||
      'account_id', aliasedName, false,
 | 
			
		||||
      type: DriftSqlType.int, requiredDuringInsert: true);
 | 
			
		||||
  static const VerificationMeta _createdAtMeta =
 | 
			
		||||
      const VerificationMeta('createdAt');
 | 
			
		||||
  @override
 | 
			
		||||
  late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
 | 
			
		||||
      'created_at', aliasedName, false,
 | 
			
		||||
      type: DriftSqlType.dateTime,
 | 
			
		||||
      requiredDuringInsert: false,
 | 
			
		||||
      defaultValue: currentDateAndTime);
 | 
			
		||||
  static const VerificationMeta _cacheExpiredAtMeta =
 | 
			
		||||
      const VerificationMeta('cacheExpiredAt');
 | 
			
		||||
  @override
 | 
			
		||||
  late final GeneratedColumn<DateTime> cacheExpiredAt =
 | 
			
		||||
      GeneratedColumn<DateTime>('cache_expired_at', aliasedName, false,
 | 
			
		||||
          type: DriftSqlType.dateTime, requiredDuringInsert: true);
 | 
			
		||||
  @override
 | 
			
		||||
  List<GeneratedColumn> get $columns =>
 | 
			
		||||
      [id, alias, content, accountId, createdAt, cacheExpiredAt];
 | 
			
		||||
  @override
 | 
			
		||||
  String get aliasedName => _alias ?? actualTableName;
 | 
			
		||||
  @override
 | 
			
		||||
  String get actualTableName => $name;
 | 
			
		||||
  static const String $name = 'sn_local_realm';
 | 
			
		||||
  @override
 | 
			
		||||
  VerificationContext validateIntegrity(Insertable<SnLocalRealmData> instance,
 | 
			
		||||
      {bool isInserting = false}) {
 | 
			
		||||
    final context = VerificationContext();
 | 
			
		||||
    final data = instance.toColumns(true);
 | 
			
		||||
    if (data.containsKey('id')) {
 | 
			
		||||
      context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
 | 
			
		||||
    }
 | 
			
		||||
    if (data.containsKey('alias')) {
 | 
			
		||||
      context.handle(
 | 
			
		||||
          _aliasMeta, alias.isAcceptableOrUnknown(data['alias']!, _aliasMeta));
 | 
			
		||||
    } else if (isInserting) {
 | 
			
		||||
      context.missing(_aliasMeta);
 | 
			
		||||
    }
 | 
			
		||||
    if (data.containsKey('account_id')) {
 | 
			
		||||
      context.handle(_accountIdMeta,
 | 
			
		||||
          accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta));
 | 
			
		||||
    } else if (isInserting) {
 | 
			
		||||
      context.missing(_accountIdMeta);
 | 
			
		||||
    }
 | 
			
		||||
    if (data.containsKey('created_at')) {
 | 
			
		||||
      context.handle(_createdAtMeta,
 | 
			
		||||
          createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
 | 
			
		||||
    }
 | 
			
		||||
    if (data.containsKey('cache_expired_at')) {
 | 
			
		||||
      context.handle(
 | 
			
		||||
          _cacheExpiredAtMeta,
 | 
			
		||||
          cacheExpiredAt.isAcceptableOrUnknown(
 | 
			
		||||
              data['cache_expired_at']!, _cacheExpiredAtMeta));
 | 
			
		||||
    } else if (isInserting) {
 | 
			
		||||
      context.missing(_cacheExpiredAtMeta);
 | 
			
		||||
    }
 | 
			
		||||
    return context;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Set<GeneratedColumn> get $primaryKey => {id};
 | 
			
		||||
  @override
 | 
			
		||||
  SnLocalRealmData map(Map<String, dynamic> data, {String? tablePrefix}) {
 | 
			
		||||
    final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
 | 
			
		||||
    return SnLocalRealmData(
 | 
			
		||||
      id: attachedDatabase.typeMapping
 | 
			
		||||
          .read(DriftSqlType.int, data['${effectivePrefix}id'])!,
 | 
			
		||||
      alias: attachedDatabase.typeMapping
 | 
			
		||||
          .read(DriftSqlType.string, data['${effectivePrefix}alias'])!,
 | 
			
		||||
      content: $SnLocalRealmTable.$convertercontent.fromSql(attachedDatabase
 | 
			
		||||
          .typeMapping
 | 
			
		||||
          .read(DriftSqlType.string, data['${effectivePrefix}content'])!),
 | 
			
		||||
      accountId: attachedDatabase.typeMapping
 | 
			
		||||
          .read(DriftSqlType.int, data['${effectivePrefix}account_id'])!,
 | 
			
		||||
      createdAt: attachedDatabase.typeMapping
 | 
			
		||||
          .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
 | 
			
		||||
      cacheExpiredAt: attachedDatabase.typeMapping.read(
 | 
			
		||||
          DriftSqlType.dateTime, data['${effectivePrefix}cache_expired_at'])!,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  $SnLocalRealmTable createAlias(String alias) {
 | 
			
		||||
    return $SnLocalRealmTable(attachedDatabase, alias);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static JsonTypeConverter2<SnRealm, String, Map<String, Object?>>
 | 
			
		||||
      $convertercontent = const SnRealmConverter();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SnLocalRealmData extends DataClass
 | 
			
		||||
    implements Insertable<SnLocalRealmData> {
 | 
			
		||||
  final int id;
 | 
			
		||||
  final String alias;
 | 
			
		||||
  final SnRealm content;
 | 
			
		||||
  final int accountId;
 | 
			
		||||
  final DateTime createdAt;
 | 
			
		||||
  final DateTime cacheExpiredAt;
 | 
			
		||||
  const SnLocalRealmData(
 | 
			
		||||
      {required this.id,
 | 
			
		||||
      required this.alias,
 | 
			
		||||
      required this.content,
 | 
			
		||||
      required this.accountId,
 | 
			
		||||
      required this.createdAt,
 | 
			
		||||
      required this.cacheExpiredAt});
 | 
			
		||||
  @override
 | 
			
		||||
  Map<String, Expression> toColumns(bool nullToAbsent) {
 | 
			
		||||
    final map = <String, Expression>{};
 | 
			
		||||
    map['id'] = Variable<int>(id);
 | 
			
		||||
    map['alias'] = Variable<String>(alias);
 | 
			
		||||
    {
 | 
			
		||||
      map['content'] =
 | 
			
		||||
          Variable<String>($SnLocalRealmTable.$convertercontent.toSql(content));
 | 
			
		||||
    }
 | 
			
		||||
    map['account_id'] = Variable<int>(accountId);
 | 
			
		||||
    map['created_at'] = Variable<DateTime>(createdAt);
 | 
			
		||||
    map['cache_expired_at'] = Variable<DateTime>(cacheExpiredAt);
 | 
			
		||||
    return map;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SnLocalRealmCompanion toCompanion(bool nullToAbsent) {
 | 
			
		||||
    return SnLocalRealmCompanion(
 | 
			
		||||
      id: Value(id),
 | 
			
		||||
      alias: Value(alias),
 | 
			
		||||
      content: Value(content),
 | 
			
		||||
      accountId: Value(accountId),
 | 
			
		||||
      createdAt: Value(createdAt),
 | 
			
		||||
      cacheExpiredAt: Value(cacheExpiredAt),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  factory SnLocalRealmData.fromJson(Map<String, dynamic> json,
 | 
			
		||||
      {ValueSerializer? serializer}) {
 | 
			
		||||
    serializer ??= driftRuntimeOptions.defaultSerializer;
 | 
			
		||||
    return SnLocalRealmData(
 | 
			
		||||
      id: serializer.fromJson<int>(json['id']),
 | 
			
		||||
      alias: serializer.fromJson<String>(json['alias']),
 | 
			
		||||
      content: $SnLocalRealmTable.$convertercontent
 | 
			
		||||
          .fromJson(serializer.fromJson<Map<String, Object?>>(json['content'])),
 | 
			
		||||
      accountId: serializer.fromJson<int>(json['accountId']),
 | 
			
		||||
      createdAt: serializer.fromJson<DateTime>(json['createdAt']),
 | 
			
		||||
      cacheExpiredAt: serializer.fromJson<DateTime>(json['cacheExpiredAt']),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  @override
 | 
			
		||||
  Map<String, dynamic> toJson({ValueSerializer? serializer}) {
 | 
			
		||||
    serializer ??= driftRuntimeOptions.defaultSerializer;
 | 
			
		||||
    return <String, dynamic>{
 | 
			
		||||
      'id': serializer.toJson<int>(id),
 | 
			
		||||
      'alias': serializer.toJson<String>(alias),
 | 
			
		||||
      'content': serializer.toJson<Map<String, Object?>>(
 | 
			
		||||
          $SnLocalRealmTable.$convertercontent.toJson(content)),
 | 
			
		||||
      'accountId': serializer.toJson<int>(accountId),
 | 
			
		||||
      'createdAt': serializer.toJson<DateTime>(createdAt),
 | 
			
		||||
      'cacheExpiredAt': serializer.toJson<DateTime>(cacheExpiredAt),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SnLocalRealmData copyWith(
 | 
			
		||||
          {int? id,
 | 
			
		||||
          String? alias,
 | 
			
		||||
          SnRealm? content,
 | 
			
		||||
          int? accountId,
 | 
			
		||||
          DateTime? createdAt,
 | 
			
		||||
          DateTime? cacheExpiredAt}) =>
 | 
			
		||||
      SnLocalRealmData(
 | 
			
		||||
        id: id ?? this.id,
 | 
			
		||||
        alias: alias ?? this.alias,
 | 
			
		||||
        content: content ?? this.content,
 | 
			
		||||
        accountId: accountId ?? this.accountId,
 | 
			
		||||
        createdAt: createdAt ?? this.createdAt,
 | 
			
		||||
        cacheExpiredAt: cacheExpiredAt ?? this.cacheExpiredAt,
 | 
			
		||||
      );
 | 
			
		||||
  SnLocalRealmData copyWithCompanion(SnLocalRealmCompanion data) {
 | 
			
		||||
    return SnLocalRealmData(
 | 
			
		||||
      id: data.id.present ? data.id.value : this.id,
 | 
			
		||||
      alias: data.alias.present ? data.alias.value : this.alias,
 | 
			
		||||
      content: data.content.present ? data.content.value : this.content,
 | 
			
		||||
      accountId: data.accountId.present ? data.accountId.value : this.accountId,
 | 
			
		||||
      createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
 | 
			
		||||
      cacheExpiredAt: data.cacheExpiredAt.present
 | 
			
		||||
          ? data.cacheExpiredAt.value
 | 
			
		||||
          : this.cacheExpiredAt,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return (StringBuffer('SnLocalRealmData(')
 | 
			
		||||
          ..write('id: $id, ')
 | 
			
		||||
          ..write('alias: $alias, ')
 | 
			
		||||
          ..write('content: $content, ')
 | 
			
		||||
          ..write('accountId: $accountId, ')
 | 
			
		||||
          ..write('createdAt: $createdAt, ')
 | 
			
		||||
          ..write('cacheExpiredAt: $cacheExpiredAt')
 | 
			
		||||
          ..write(')'))
 | 
			
		||||
        .toString();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  int get hashCode =>
 | 
			
		||||
      Object.hash(id, alias, content, accountId, createdAt, cacheExpiredAt);
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(Object other) =>
 | 
			
		||||
      identical(this, other) ||
 | 
			
		||||
      (other is SnLocalRealmData &&
 | 
			
		||||
          other.id == this.id &&
 | 
			
		||||
          other.alias == this.alias &&
 | 
			
		||||
          other.content == this.content &&
 | 
			
		||||
          other.accountId == this.accountId &&
 | 
			
		||||
          other.createdAt == this.createdAt &&
 | 
			
		||||
          other.cacheExpiredAt == this.cacheExpiredAt);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SnLocalRealmCompanion extends UpdateCompanion<SnLocalRealmData> {
 | 
			
		||||
  final Value<int> id;
 | 
			
		||||
  final Value<String> alias;
 | 
			
		||||
  final Value<SnRealm> content;
 | 
			
		||||
  final Value<int> accountId;
 | 
			
		||||
  final Value<DateTime> createdAt;
 | 
			
		||||
  final Value<DateTime> cacheExpiredAt;
 | 
			
		||||
  const SnLocalRealmCompanion({
 | 
			
		||||
    this.id = const Value.absent(),
 | 
			
		||||
    this.alias = const Value.absent(),
 | 
			
		||||
    this.content = const Value.absent(),
 | 
			
		||||
    this.accountId = const Value.absent(),
 | 
			
		||||
    this.createdAt = const Value.absent(),
 | 
			
		||||
    this.cacheExpiredAt = const Value.absent(),
 | 
			
		||||
  });
 | 
			
		||||
  SnLocalRealmCompanion.insert({
 | 
			
		||||
    this.id = const Value.absent(),
 | 
			
		||||
    required String alias,
 | 
			
		||||
    required SnRealm content,
 | 
			
		||||
    required int accountId,
 | 
			
		||||
    this.createdAt = const Value.absent(),
 | 
			
		||||
    required DateTime cacheExpiredAt,
 | 
			
		||||
  })  : alias = Value(alias),
 | 
			
		||||
        content = Value(content),
 | 
			
		||||
        accountId = Value(accountId),
 | 
			
		||||
        cacheExpiredAt = Value(cacheExpiredAt);
 | 
			
		||||
  static Insertable<SnLocalRealmData> custom({
 | 
			
		||||
    Expression<int>? id,
 | 
			
		||||
    Expression<String>? alias,
 | 
			
		||||
    Expression<String>? content,
 | 
			
		||||
    Expression<int>? accountId,
 | 
			
		||||
    Expression<DateTime>? createdAt,
 | 
			
		||||
    Expression<DateTime>? cacheExpiredAt,
 | 
			
		||||
  }) {
 | 
			
		||||
    return RawValuesInsertable({
 | 
			
		||||
      if (id != null) 'id': id,
 | 
			
		||||
      if (alias != null) 'alias': alias,
 | 
			
		||||
      if (content != null) 'content': content,
 | 
			
		||||
      if (accountId != null) 'account_id': accountId,
 | 
			
		||||
      if (createdAt != null) 'created_at': createdAt,
 | 
			
		||||
      if (cacheExpiredAt != null) 'cache_expired_at': cacheExpiredAt,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SnLocalRealmCompanion copyWith(
 | 
			
		||||
      {Value<int>? id,
 | 
			
		||||
      Value<String>? alias,
 | 
			
		||||
      Value<SnRealm>? content,
 | 
			
		||||
      Value<int>? accountId,
 | 
			
		||||
      Value<DateTime>? createdAt,
 | 
			
		||||
      Value<DateTime>? cacheExpiredAt}) {
 | 
			
		||||
    return SnLocalRealmCompanion(
 | 
			
		||||
      id: id ?? this.id,
 | 
			
		||||
      alias: alias ?? this.alias,
 | 
			
		||||
      content: content ?? this.content,
 | 
			
		||||
      accountId: accountId ?? this.accountId,
 | 
			
		||||
      createdAt: createdAt ?? this.createdAt,
 | 
			
		||||
      cacheExpiredAt: cacheExpiredAt ?? this.cacheExpiredAt,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Map<String, Expression> toColumns(bool nullToAbsent) {
 | 
			
		||||
    final map = <String, Expression>{};
 | 
			
		||||
    if (id.present) {
 | 
			
		||||
      map['id'] = Variable<int>(id.value);
 | 
			
		||||
    }
 | 
			
		||||
    if (alias.present) {
 | 
			
		||||
      map['alias'] = Variable<String>(alias.value);
 | 
			
		||||
    }
 | 
			
		||||
    if (content.present) {
 | 
			
		||||
      map['content'] = Variable<String>(
 | 
			
		||||
          $SnLocalRealmTable.$convertercontent.toSql(content.value));
 | 
			
		||||
    }
 | 
			
		||||
    if (accountId.present) {
 | 
			
		||||
      map['account_id'] = Variable<int>(accountId.value);
 | 
			
		||||
    }
 | 
			
		||||
    if (createdAt.present) {
 | 
			
		||||
      map['created_at'] = Variable<DateTime>(createdAt.value);
 | 
			
		||||
    }
 | 
			
		||||
    if (cacheExpiredAt.present) {
 | 
			
		||||
      map['cache_expired_at'] = Variable<DateTime>(cacheExpiredAt.value);
 | 
			
		||||
    }
 | 
			
		||||
    return map;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
    return (StringBuffer('SnLocalRealmCompanion(')
 | 
			
		||||
          ..write('id: $id, ')
 | 
			
		||||
          ..write('alias: $alias, ')
 | 
			
		||||
          ..write('content: $content, ')
 | 
			
		||||
          ..write('accountId: $accountId, ')
 | 
			
		||||
          ..write('createdAt: $createdAt, ')
 | 
			
		||||
          ..write('cacheExpiredAt: $cacheExpiredAt')
 | 
			
		||||
          ..write(')'))
 | 
			
		||||
        .toString();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
abstract class _$AppDatabase extends GeneratedDatabase {
 | 
			
		||||
  _$AppDatabase(QueryExecutor e) : super(e);
 | 
			
		||||
  $AppDatabaseManager get managers => $AppDatabaseManager(this);
 | 
			
		||||
@@ -2470,6 +2815,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
 | 
			
		||||
  late final $SnLocalStickerTable snLocalSticker = $SnLocalStickerTable(this);
 | 
			
		||||
  late final $SnLocalStickerPackTable snLocalStickerPack =
 | 
			
		||||
      $SnLocalStickerPackTable(this);
 | 
			
		||||
  late final $SnLocalRealmTable snLocalRealm = $SnLocalRealmTable(this);
 | 
			
		||||
  late final Index idxChannelAlias = Index('idx_channel_alias',
 | 
			
		||||
      'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
 | 
			
		||||
  late final Index idxChatChannel = Index('idx_chat_channel',
 | 
			
		||||
@@ -2480,6 +2826,10 @@ abstract class _$AppDatabase extends GeneratedDatabase {
 | 
			
		||||
      'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
 | 
			
		||||
  late final Index idxAttachmentAccount = Index('idx_attachment_account',
 | 
			
		||||
      'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
 | 
			
		||||
  late final Index idxRealmAlias = Index('idx_realm_alias',
 | 
			
		||||
      'CREATE INDEX idx_realm_alias ON sn_local_realm (alias)');
 | 
			
		||||
  late final Index idxRealmAccount = Index('idx_realm_account',
 | 
			
		||||
      'CREATE INDEX idx_realm_account ON sn_local_realm (account_id)');
 | 
			
		||||
  @override
 | 
			
		||||
  Iterable<TableInfo<Table, Object?>> get allTables =>
 | 
			
		||||
      allSchemaEntities.whereType<TableInfo<Table, Object?>>();
 | 
			
		||||
@@ -2493,11 +2843,14 @@ abstract class _$AppDatabase extends GeneratedDatabase {
 | 
			
		||||
        snLocalAttachment,
 | 
			
		||||
        snLocalSticker,
 | 
			
		||||
        snLocalStickerPack,
 | 
			
		||||
        snLocalRealm,
 | 
			
		||||
        idxChannelAlias,
 | 
			
		||||
        idxChatChannel,
 | 
			
		||||
        idxAccountName,
 | 
			
		||||
        idxAttachmentRid,
 | 
			
		||||
        idxAttachmentAccount
 | 
			
		||||
        idxAttachmentAccount,
 | 
			
		||||
        idxRealmAlias,
 | 
			
		||||
        idxRealmAccount
 | 
			
		||||
      ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -3888,6 +4241,192 @@ typedef $$SnLocalStickerPackTableProcessedTableManager = ProcessedTableManager<
 | 
			
		||||
    ),
 | 
			
		||||
    SnLocalStickerPackData,
 | 
			
		||||
    PrefetchHooks Function()>;
 | 
			
		||||
typedef $$SnLocalRealmTableCreateCompanionBuilder = SnLocalRealmCompanion
 | 
			
		||||
    Function({
 | 
			
		||||
  Value<int> id,
 | 
			
		||||
  required String alias,
 | 
			
		||||
  required SnRealm content,
 | 
			
		||||
  required int accountId,
 | 
			
		||||
  Value<DateTime> createdAt,
 | 
			
		||||
  required DateTime cacheExpiredAt,
 | 
			
		||||
});
 | 
			
		||||
typedef $$SnLocalRealmTableUpdateCompanionBuilder = SnLocalRealmCompanion
 | 
			
		||||
    Function({
 | 
			
		||||
  Value<int> id,
 | 
			
		||||
  Value<String> alias,
 | 
			
		||||
  Value<SnRealm> content,
 | 
			
		||||
  Value<int> accountId,
 | 
			
		||||
  Value<DateTime> createdAt,
 | 
			
		||||
  Value<DateTime> cacheExpiredAt,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
class $$SnLocalRealmTableFilterComposer
 | 
			
		||||
    extends Composer<_$AppDatabase, $SnLocalRealmTable> {
 | 
			
		||||
  $$SnLocalRealmTableFilterComposer({
 | 
			
		||||
    required super.$db,
 | 
			
		||||
    required super.$table,
 | 
			
		||||
    super.joinBuilder,
 | 
			
		||||
    super.$addJoinBuilderToRootComposer,
 | 
			
		||||
    super.$removeJoinBuilderFromRootComposer,
 | 
			
		||||
  });
 | 
			
		||||
  ColumnFilters<int> get id => $composableBuilder(
 | 
			
		||||
      column: $table.id, builder: (column) => ColumnFilters(column));
 | 
			
		||||
 | 
			
		||||
  ColumnFilters<String> get alias => $composableBuilder(
 | 
			
		||||
      column: $table.alias, builder: (column) => ColumnFilters(column));
 | 
			
		||||
 | 
			
		||||
  ColumnWithTypeConverterFilters<SnRealm, SnRealm, String> get content =>
 | 
			
		||||
      $composableBuilder(
 | 
			
		||||
          column: $table.content,
 | 
			
		||||
          builder: (column) => ColumnWithTypeConverterFilters(column));
 | 
			
		||||
 | 
			
		||||
  ColumnFilters<int> get accountId => $composableBuilder(
 | 
			
		||||
      column: $table.accountId, builder: (column) => ColumnFilters(column));
 | 
			
		||||
 | 
			
		||||
  ColumnFilters<DateTime> get createdAt => $composableBuilder(
 | 
			
		||||
      column: $table.createdAt, builder: (column) => ColumnFilters(column));
 | 
			
		||||
 | 
			
		||||
  ColumnFilters<DateTime> get cacheExpiredAt => $composableBuilder(
 | 
			
		||||
      column: $table.cacheExpiredAt,
 | 
			
		||||
      builder: (column) => ColumnFilters(column));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class $$SnLocalRealmTableOrderingComposer
 | 
			
		||||
    extends Composer<_$AppDatabase, $SnLocalRealmTable> {
 | 
			
		||||
  $$SnLocalRealmTableOrderingComposer({
 | 
			
		||||
    required super.$db,
 | 
			
		||||
    required super.$table,
 | 
			
		||||
    super.joinBuilder,
 | 
			
		||||
    super.$addJoinBuilderToRootComposer,
 | 
			
		||||
    super.$removeJoinBuilderFromRootComposer,
 | 
			
		||||
  });
 | 
			
		||||
  ColumnOrderings<int> get id => $composableBuilder(
 | 
			
		||||
      column: $table.id, builder: (column) => ColumnOrderings(column));
 | 
			
		||||
 | 
			
		||||
  ColumnOrderings<String> get alias => $composableBuilder(
 | 
			
		||||
      column: $table.alias, builder: (column) => ColumnOrderings(column));
 | 
			
		||||
 | 
			
		||||
  ColumnOrderings<String> get content => $composableBuilder(
 | 
			
		||||
      column: $table.content, builder: (column) => ColumnOrderings(column));
 | 
			
		||||
 | 
			
		||||
  ColumnOrderings<int> get accountId => $composableBuilder(
 | 
			
		||||
      column: $table.accountId, builder: (column) => ColumnOrderings(column));
 | 
			
		||||
 | 
			
		||||
  ColumnOrderings<DateTime> get createdAt => $composableBuilder(
 | 
			
		||||
      column: $table.createdAt, builder: (column) => ColumnOrderings(column));
 | 
			
		||||
 | 
			
		||||
  ColumnOrderings<DateTime> get cacheExpiredAt => $composableBuilder(
 | 
			
		||||
      column: $table.cacheExpiredAt,
 | 
			
		||||
      builder: (column) => ColumnOrderings(column));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class $$SnLocalRealmTableAnnotationComposer
 | 
			
		||||
    extends Composer<_$AppDatabase, $SnLocalRealmTable> {
 | 
			
		||||
  $$SnLocalRealmTableAnnotationComposer({
 | 
			
		||||
    required super.$db,
 | 
			
		||||
    required super.$table,
 | 
			
		||||
    super.joinBuilder,
 | 
			
		||||
    super.$addJoinBuilderToRootComposer,
 | 
			
		||||
    super.$removeJoinBuilderFromRootComposer,
 | 
			
		||||
  });
 | 
			
		||||
  GeneratedColumn<int> get id =>
 | 
			
		||||
      $composableBuilder(column: $table.id, builder: (column) => column);
 | 
			
		||||
 | 
			
		||||
  GeneratedColumn<String> get alias =>
 | 
			
		||||
      $composableBuilder(column: $table.alias, builder: (column) => column);
 | 
			
		||||
 | 
			
		||||
  GeneratedColumnWithTypeConverter<SnRealm, String> get content =>
 | 
			
		||||
      $composableBuilder(column: $table.content, builder: (column) => column);
 | 
			
		||||
 | 
			
		||||
  GeneratedColumn<int> get accountId =>
 | 
			
		||||
      $composableBuilder(column: $table.accountId, builder: (column) => column);
 | 
			
		||||
 | 
			
		||||
  GeneratedColumn<DateTime> get createdAt =>
 | 
			
		||||
      $composableBuilder(column: $table.createdAt, builder: (column) => column);
 | 
			
		||||
 | 
			
		||||
  GeneratedColumn<DateTime> get cacheExpiredAt => $composableBuilder(
 | 
			
		||||
      column: $table.cacheExpiredAt, builder: (column) => column);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class $$SnLocalRealmTableTableManager extends RootTableManager<
 | 
			
		||||
    _$AppDatabase,
 | 
			
		||||
    $SnLocalRealmTable,
 | 
			
		||||
    SnLocalRealmData,
 | 
			
		||||
    $$SnLocalRealmTableFilterComposer,
 | 
			
		||||
    $$SnLocalRealmTableOrderingComposer,
 | 
			
		||||
    $$SnLocalRealmTableAnnotationComposer,
 | 
			
		||||
    $$SnLocalRealmTableCreateCompanionBuilder,
 | 
			
		||||
    $$SnLocalRealmTableUpdateCompanionBuilder,
 | 
			
		||||
    (
 | 
			
		||||
      SnLocalRealmData,
 | 
			
		||||
      BaseReferences<_$AppDatabase, $SnLocalRealmTable, SnLocalRealmData>
 | 
			
		||||
    ),
 | 
			
		||||
    SnLocalRealmData,
 | 
			
		||||
    PrefetchHooks Function()> {
 | 
			
		||||
  $$SnLocalRealmTableTableManager(_$AppDatabase db, $SnLocalRealmTable table)
 | 
			
		||||
      : super(TableManagerState(
 | 
			
		||||
          db: db,
 | 
			
		||||
          table: table,
 | 
			
		||||
          createFilteringComposer: () =>
 | 
			
		||||
              $$SnLocalRealmTableFilterComposer($db: db, $table: table),
 | 
			
		||||
          createOrderingComposer: () =>
 | 
			
		||||
              $$SnLocalRealmTableOrderingComposer($db: db, $table: table),
 | 
			
		||||
          createComputedFieldComposer: () =>
 | 
			
		||||
              $$SnLocalRealmTableAnnotationComposer($db: db, $table: table),
 | 
			
		||||
          updateCompanionCallback: ({
 | 
			
		||||
            Value<int> id = const Value.absent(),
 | 
			
		||||
            Value<String> alias = const Value.absent(),
 | 
			
		||||
            Value<SnRealm> content = const Value.absent(),
 | 
			
		||||
            Value<int> accountId = const Value.absent(),
 | 
			
		||||
            Value<DateTime> createdAt = const Value.absent(),
 | 
			
		||||
            Value<DateTime> cacheExpiredAt = const Value.absent(),
 | 
			
		||||
          }) =>
 | 
			
		||||
              SnLocalRealmCompanion(
 | 
			
		||||
            id: id,
 | 
			
		||||
            alias: alias,
 | 
			
		||||
            content: content,
 | 
			
		||||
            accountId: accountId,
 | 
			
		||||
            createdAt: createdAt,
 | 
			
		||||
            cacheExpiredAt: cacheExpiredAt,
 | 
			
		||||
          ),
 | 
			
		||||
          createCompanionCallback: ({
 | 
			
		||||
            Value<int> id = const Value.absent(),
 | 
			
		||||
            required String alias,
 | 
			
		||||
            required SnRealm content,
 | 
			
		||||
            required int accountId,
 | 
			
		||||
            Value<DateTime> createdAt = const Value.absent(),
 | 
			
		||||
            required DateTime cacheExpiredAt,
 | 
			
		||||
          }) =>
 | 
			
		||||
              SnLocalRealmCompanion.insert(
 | 
			
		||||
            id: id,
 | 
			
		||||
            alias: alias,
 | 
			
		||||
            content: content,
 | 
			
		||||
            accountId: accountId,
 | 
			
		||||
            createdAt: createdAt,
 | 
			
		||||
            cacheExpiredAt: cacheExpiredAt,
 | 
			
		||||
          ),
 | 
			
		||||
          withReferenceMapper: (p0) => p0
 | 
			
		||||
              .map((e) => (e.readTable(table), BaseReferences(db, table, e)))
 | 
			
		||||
              .toList(),
 | 
			
		||||
          prefetchHooksCallback: null,
 | 
			
		||||
        ));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
typedef $$SnLocalRealmTableProcessedTableManager = ProcessedTableManager<
 | 
			
		||||
    _$AppDatabase,
 | 
			
		||||
    $SnLocalRealmTable,
 | 
			
		||||
    SnLocalRealmData,
 | 
			
		||||
    $$SnLocalRealmTableFilterComposer,
 | 
			
		||||
    $$SnLocalRealmTableOrderingComposer,
 | 
			
		||||
    $$SnLocalRealmTableAnnotationComposer,
 | 
			
		||||
    $$SnLocalRealmTableCreateCompanionBuilder,
 | 
			
		||||
    $$SnLocalRealmTableUpdateCompanionBuilder,
 | 
			
		||||
    (
 | 
			
		||||
      SnLocalRealmData,
 | 
			
		||||
      BaseReferences<_$AppDatabase, $SnLocalRealmTable, SnLocalRealmData>
 | 
			
		||||
    ),
 | 
			
		||||
    SnLocalRealmData,
 | 
			
		||||
    PrefetchHooks Function()>;
 | 
			
		||||
 | 
			
		||||
class $AppDatabaseManager {
 | 
			
		||||
  final _$AppDatabase _db;
 | 
			
		||||
@@ -3908,4 +4447,6 @@ class $AppDatabaseManager {
 | 
			
		||||
      $$SnLocalStickerTableTableManager(_db, _db.snLocalSticker);
 | 
			
		||||
  $$SnLocalStickerPackTableTableManager get snLocalStickerPack =>
 | 
			
		||||
      $$SnLocalStickerPackTableTableManager(_db, _db.snLocalStickerPack);
 | 
			
		||||
  $$SnLocalRealmTableTableManager get snLocalRealm =>
 | 
			
		||||
      $$SnLocalRealmTableTableManager(_db, _db.snLocalRealm);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -412,9 +412,214 @@ class Shape8 extends i0.VersionedTable {
 | 
			
		||||
      columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final class Schema4 extends i0.VersionedSchema {
 | 
			
		||||
  Schema4({required super.database}) : super(version: 4);
 | 
			
		||||
  @override
 | 
			
		||||
  late final List<i1.DatabaseSchemaEntity> entities = [
 | 
			
		||||
    snLocalChatChannel,
 | 
			
		||||
    snLocalChatMessage,
 | 
			
		||||
    snLocalChannelMember,
 | 
			
		||||
    snLocalKeyPair,
 | 
			
		||||
    snLocalAccount,
 | 
			
		||||
    snLocalAttachment,
 | 
			
		||||
    snLocalSticker,
 | 
			
		||||
    snLocalStickerPack,
 | 
			
		||||
    snLocalRealm,
 | 
			
		||||
    idxChannelAlias,
 | 
			
		||||
    idxChatChannel,
 | 
			
		||||
    idxAccountName,
 | 
			
		||||
    idxAttachmentRid,
 | 
			
		||||
    idxAttachmentAccount,
 | 
			
		||||
    idxRealmAlias,
 | 
			
		||||
    idxRealmAccount,
 | 
			
		||||
  ];
 | 
			
		||||
  late final Shape0 snLocalChatChannel = Shape0(
 | 
			
		||||
      source: i0.VersionedTable(
 | 
			
		||||
        entityName: 'sn_local_chat_channel',
 | 
			
		||||
        withoutRowId: false,
 | 
			
		||||
        isStrict: false,
 | 
			
		||||
        tableConstraints: [],
 | 
			
		||||
        columns: [
 | 
			
		||||
          _column_0,
 | 
			
		||||
          _column_1,
 | 
			
		||||
          _column_2,
 | 
			
		||||
          _column_3,
 | 
			
		||||
        ],
 | 
			
		||||
        attachedDatabase: database,
 | 
			
		||||
      ),
 | 
			
		||||
      alias: null);
 | 
			
		||||
  late final Shape3 snLocalChatMessage = Shape3(
 | 
			
		||||
      source: i0.VersionedTable(
 | 
			
		||||
        entityName: 'sn_local_chat_message',
 | 
			
		||||
        withoutRowId: false,
 | 
			
		||||
        isStrict: false,
 | 
			
		||||
        tableConstraints: [],
 | 
			
		||||
        columns: [
 | 
			
		||||
          _column_0,
 | 
			
		||||
          _column_4,
 | 
			
		||||
          _column_10,
 | 
			
		||||
          _column_2,
 | 
			
		||||
          _column_3,
 | 
			
		||||
        ],
 | 
			
		||||
        attachedDatabase: database,
 | 
			
		||||
      ),
 | 
			
		||||
      alias: null);
 | 
			
		||||
  late final Shape4 snLocalChannelMember = Shape4(
 | 
			
		||||
      source: i0.VersionedTable(
 | 
			
		||||
        entityName: 'sn_local_channel_member',
 | 
			
		||||
        withoutRowId: false,
 | 
			
		||||
        isStrict: false,
 | 
			
		||||
        tableConstraints: [],
 | 
			
		||||
        columns: [
 | 
			
		||||
          _column_0,
 | 
			
		||||
          _column_4,
 | 
			
		||||
          _column_6,
 | 
			
		||||
          _column_2,
 | 
			
		||||
          _column_3,
 | 
			
		||||
          _column_11,
 | 
			
		||||
        ],
 | 
			
		||||
        attachedDatabase: database,
 | 
			
		||||
      ),
 | 
			
		||||
      alias: null);
 | 
			
		||||
  late final Shape2 snLocalKeyPair = Shape2(
 | 
			
		||||
      source: i0.VersionedTable(
 | 
			
		||||
        entityName: 'sn_local_key_pair',
 | 
			
		||||
        withoutRowId: false,
 | 
			
		||||
        isStrict: false,
 | 
			
		||||
        tableConstraints: [
 | 
			
		||||
          'PRIMARY KEY(id)',
 | 
			
		||||
        ],
 | 
			
		||||
        columns: [
 | 
			
		||||
          _column_5,
 | 
			
		||||
          _column_6,
 | 
			
		||||
          _column_7,
 | 
			
		||||
          _column_8,
 | 
			
		||||
          _column_9,
 | 
			
		||||
        ],
 | 
			
		||||
        attachedDatabase: database,
 | 
			
		||||
      ),
 | 
			
		||||
      alias: null);
 | 
			
		||||
  late final Shape5 snLocalAccount = Shape5(
 | 
			
		||||
      source: i0.VersionedTable(
 | 
			
		||||
        entityName: 'sn_local_account',
 | 
			
		||||
        withoutRowId: false,
 | 
			
		||||
        isStrict: false,
 | 
			
		||||
        tableConstraints: [],
 | 
			
		||||
        columns: [
 | 
			
		||||
          _column_0,
 | 
			
		||||
          _column_12,
 | 
			
		||||
          _column_2,
 | 
			
		||||
          _column_3,
 | 
			
		||||
          _column_11,
 | 
			
		||||
        ],
 | 
			
		||||
        attachedDatabase: database,
 | 
			
		||||
      ),
 | 
			
		||||
      alias: null);
 | 
			
		||||
  late final Shape6 snLocalAttachment = Shape6(
 | 
			
		||||
      source: i0.VersionedTable(
 | 
			
		||||
        entityName: 'sn_local_attachment',
 | 
			
		||||
        withoutRowId: false,
 | 
			
		||||
        isStrict: false,
 | 
			
		||||
        tableConstraints: [],
 | 
			
		||||
        columns: [
 | 
			
		||||
          _column_0,
 | 
			
		||||
          _column_13,
 | 
			
		||||
          _column_14,
 | 
			
		||||
          _column_2,
 | 
			
		||||
          _column_6,
 | 
			
		||||
          _column_3,
 | 
			
		||||
          _column_11,
 | 
			
		||||
        ],
 | 
			
		||||
        attachedDatabase: database,
 | 
			
		||||
      ),
 | 
			
		||||
      alias: null);
 | 
			
		||||
  late final Shape7 snLocalSticker = Shape7(
 | 
			
		||||
      source: i0.VersionedTable(
 | 
			
		||||
        entityName: 'sn_local_sticker',
 | 
			
		||||
        withoutRowId: false,
 | 
			
		||||
        isStrict: false,
 | 
			
		||||
        tableConstraints: [],
 | 
			
		||||
        columns: [
 | 
			
		||||
          _column_0,
 | 
			
		||||
          _column_1,
 | 
			
		||||
          _column_15,
 | 
			
		||||
          _column_2,
 | 
			
		||||
          _column_3,
 | 
			
		||||
        ],
 | 
			
		||||
        attachedDatabase: database,
 | 
			
		||||
      ),
 | 
			
		||||
      alias: null);
 | 
			
		||||
  late final Shape8 snLocalStickerPack = Shape8(
 | 
			
		||||
      source: i0.VersionedTable(
 | 
			
		||||
        entityName: 'sn_local_sticker_pack',
 | 
			
		||||
        withoutRowId: false,
 | 
			
		||||
        isStrict: false,
 | 
			
		||||
        tableConstraints: [],
 | 
			
		||||
        columns: [
 | 
			
		||||
          _column_0,
 | 
			
		||||
          _column_2,
 | 
			
		||||
          _column_3,
 | 
			
		||||
        ],
 | 
			
		||||
        attachedDatabase: database,
 | 
			
		||||
      ),
 | 
			
		||||
      alias: null);
 | 
			
		||||
  late final Shape9 snLocalRealm = Shape9(
 | 
			
		||||
      source: i0.VersionedTable(
 | 
			
		||||
        entityName: 'sn_local_realm',
 | 
			
		||||
        withoutRowId: false,
 | 
			
		||||
        isStrict: false,
 | 
			
		||||
        tableConstraints: [],
 | 
			
		||||
        columns: [
 | 
			
		||||
          _column_0,
 | 
			
		||||
          _column_16,
 | 
			
		||||
          _column_2,
 | 
			
		||||
          _column_6,
 | 
			
		||||
          _column_3,
 | 
			
		||||
          _column_11,
 | 
			
		||||
        ],
 | 
			
		||||
        attachedDatabase: database,
 | 
			
		||||
      ),
 | 
			
		||||
      alias: null);
 | 
			
		||||
  final i1.Index idxChannelAlias = i1.Index('idx_channel_alias',
 | 
			
		||||
      'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
 | 
			
		||||
  final i1.Index idxChatChannel = i1.Index('idx_chat_channel',
 | 
			
		||||
      'CREATE INDEX idx_chat_channel ON sn_local_chat_message (channel_id)');
 | 
			
		||||
  final i1.Index idxAccountName = i1.Index('idx_account_name',
 | 
			
		||||
      'CREATE INDEX idx_account_name ON sn_local_account (name)');
 | 
			
		||||
  final i1.Index idxAttachmentRid = i1.Index('idx_attachment_rid',
 | 
			
		||||
      'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
 | 
			
		||||
  final i1.Index idxAttachmentAccount = i1.Index('idx_attachment_account',
 | 
			
		||||
      'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
 | 
			
		||||
  final i1.Index idxRealmAlias = i1.Index('idx_realm_alias',
 | 
			
		||||
      'CREATE INDEX idx_realm_alias ON sn_local_realm (alias)');
 | 
			
		||||
  final i1.Index idxRealmAccount = i1.Index('idx_realm_account',
 | 
			
		||||
      'CREATE INDEX idx_realm_account ON sn_local_realm (account_id)');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Shape9 extends i0.VersionedTable {
 | 
			
		||||
  Shape9({required super.source, required super.alias}) : super.aliased();
 | 
			
		||||
  i1.GeneratedColumn<int> get id =>
 | 
			
		||||
      columnsByName['id']! as i1.GeneratedColumn<int>;
 | 
			
		||||
  i1.GeneratedColumn<String> get alias =>
 | 
			
		||||
      columnsByName['alias']! as i1.GeneratedColumn<String>;
 | 
			
		||||
  i1.GeneratedColumn<String> get content =>
 | 
			
		||||
      columnsByName['content']! as i1.GeneratedColumn<String>;
 | 
			
		||||
  i1.GeneratedColumn<int> get accountId =>
 | 
			
		||||
      columnsByName['account_id']! as i1.GeneratedColumn<int>;
 | 
			
		||||
  i1.GeneratedColumn<DateTime> get createdAt =>
 | 
			
		||||
      columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
 | 
			
		||||
  i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
 | 
			
		||||
      columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
i1.GeneratedColumn<String> _column_16(String aliasedName) =>
 | 
			
		||||
    i1.GeneratedColumn<String>('alias', aliasedName, false,
 | 
			
		||||
        type: i1.DriftSqlType.string,
 | 
			
		||||
        defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
 | 
			
		||||
i0.MigrationStepWithVersion migrationSteps({
 | 
			
		||||
  required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
 | 
			
		||||
  required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
 | 
			
		||||
  required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
 | 
			
		||||
}) {
 | 
			
		||||
  return (currentVersion, database) async {
 | 
			
		||||
    switch (currentVersion) {
 | 
			
		||||
@@ -428,6 +633,11 @@ i0.MigrationStepWithVersion migrationSteps({
 | 
			
		||||
        final migrator = i1.Migrator(database, schema);
 | 
			
		||||
        await from2To3(migrator, schema);
 | 
			
		||||
        return 3;
 | 
			
		||||
      case 3:
 | 
			
		||||
        final schema = Schema4(database: database);
 | 
			
		||||
        final migrator = i1.Migrator(database, schema);
 | 
			
		||||
        await from3To4(migrator, schema);
 | 
			
		||||
        return 4;
 | 
			
		||||
      default:
 | 
			
		||||
        throw ArgumentError.value('Unknown migration from $currentVersion');
 | 
			
		||||
    }
 | 
			
		||||
@@ -437,9 +647,11 @@ i0.MigrationStepWithVersion migrationSteps({
 | 
			
		||||
i1.OnUpgrade stepByStep({
 | 
			
		||||
  required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
 | 
			
		||||
  required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
 | 
			
		||||
  required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
 | 
			
		||||
}) =>
 | 
			
		||||
    i0.VersionedSchema.stepByStepHelper(
 | 
			
		||||
        step: migrationSteps(
 | 
			
		||||
      from1To2: from1To2,
 | 
			
		||||
      from2To3: from2To3,
 | 
			
		||||
      from3To4: from3To4,
 | 
			
		||||
    ));
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								lib/database/realm.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								lib/database/realm.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:drift/drift.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
 | 
			
		||||
class SnRealmConverter extends TypeConverter<SnRealm, String>
 | 
			
		||||
    with JsonTypeConverter2<SnRealm, String, Map<String, Object?>> {
 | 
			
		||||
  const SnRealmConverter();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  SnRealm fromSql(String fromDb) {
 | 
			
		||||
    return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toSql(SnRealm value) {
 | 
			
		||||
    return jsonEncode(toJson(value));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  SnRealm fromJson(Map<String, Object?> json) {
 | 
			
		||||
    return SnRealm.fromJson(json);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Map<String, Object?> toJson(SnRealm value) {
 | 
			
		||||
    return value.toJson();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@TableIndex(name: 'idx_realm_alias', columns: {#alias})
 | 
			
		||||
@TableIndex(name: 'idx_realm_account', columns: {#accountId})
 | 
			
		||||
class SnLocalRealm extends Table {
 | 
			
		||||
  IntColumn get id => integer().autoIncrement()();
 | 
			
		||||
 | 
			
		||||
  TextColumn get alias => text().unique()();
 | 
			
		||||
 | 
			
		||||
  TextColumn get content => text().map(const SnRealmConverter())();
 | 
			
		||||
 | 
			
		||||
  IntColumn get accountId => integer()();
 | 
			
		||||
 | 
			
		||||
  DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
 | 
			
		||||
 | 
			
		||||
  DateTimeColumn get cacheExpiredAt => dateTime()();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										200
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										200
									
								
								lib/main.dart
									
									
									
									
									
								
							@@ -12,6 +12,7 @@ import 'package:firebase_core/firebase_core.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:hotkey_manager/hotkey_manager.dart';
 | 
			
		||||
import 'package:package_info_plus/package_info_plus.dart';
 | 
			
		||||
@@ -19,6 +20,7 @@ import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:relative_time/relative_time.dart';
 | 
			
		||||
import 'package:responsive_framework/responsive_framework.dart';
 | 
			
		||||
import 'package:shared_preferences/shared_preferences.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/firebase_options.dart';
 | 
			
		||||
import 'package:surface/logger.dart';
 | 
			
		||||
import 'package:surface/providers/channel.dart';
 | 
			
		||||
@@ -46,6 +48,7 @@ import 'package:surface/router.dart';
 | 
			
		||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/menu_bar.dart';
 | 
			
		||||
import 'package:surface/widgets/version_label.dart';
 | 
			
		||||
import 'package:tray_manager/tray_manager.dart';
 | 
			
		||||
import 'package:version/version.dart';
 | 
			
		||||
import 'package:workmanager/workmanager.dart';
 | 
			
		||||
@@ -86,19 +89,14 @@ void main() async {
 | 
			
		||||
  await EasyLocalization.ensureInitialized();
 | 
			
		||||
 | 
			
		||||
  if (!kIsWeb && !Platform.isLinux) {
 | 
			
		||||
    await Firebase.initializeApp(
 | 
			
		||||
      options: DefaultFirebaseOptions.currentPlatform,
 | 
			
		||||
    );
 | 
			
		||||
    await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  GoRouter.optionURLReflectsImperativeAPIs = true;
 | 
			
		||||
  usePathUrlStrategy();
 | 
			
		||||
 | 
			
		||||
  if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
 | 
			
		||||
    Workmanager().initialize(
 | 
			
		||||
      appBackgroundDispatcher,
 | 
			
		||||
      isInDebugMode: kDebugMode,
 | 
			
		||||
    );
 | 
			
		||||
    Workmanager().initialize(appBackgroundDispatcher, isInDebugMode: kDebugMode);
 | 
			
		||||
    if (Platform.isAndroid) {
 | 
			
		||||
      Workmanager().registerPeriodicTask(
 | 
			
		||||
        "widget-update-random-post",
 | 
			
		||||
@@ -111,8 +109,7 @@ void main() async {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!kIsWeb && Platform.isAndroid) {
 | 
			
		||||
    final ImagePickerPlatform imagePickerImplementation =
 | 
			
		||||
        ImagePickerPlatform.instance;
 | 
			
		||||
    final ImagePickerPlatform imagePickerImplementation = ImagePickerPlatform.instance;
 | 
			
		||||
    if (imagePickerImplementation is ImagePickerAndroid) {
 | 
			
		||||
      imagePickerImplementation.useAndroidPhotoPicker = true;
 | 
			
		||||
    }
 | 
			
		||||
@@ -129,12 +126,7 @@ class SolianApp extends StatelessWidget {
 | 
			
		||||
    return ResponsiveBreakpoints.builder(
 | 
			
		||||
      child: EasyLocalization(
 | 
			
		||||
        path: 'assets/translations',
 | 
			
		||||
        supportedLocales: [
 | 
			
		||||
          Locale('en', 'US'),
 | 
			
		||||
          Locale('zh', 'CN'),
 | 
			
		||||
          Locale('zh', 'TW'),
 | 
			
		||||
          Locale('zh', 'HK'),
 | 
			
		||||
        ],
 | 
			
		||||
        supportedLocales: [Locale('en', 'US'), Locale('zh', 'CN'), Locale('zh', 'TW'), Locale('zh', 'HK')],
 | 
			
		||||
        fallbackLocale: Locale('en', 'US'),
 | 
			
		||||
        useFallbackTranslations: true,
 | 
			
		||||
        assetLoader: JsonAssetLoader(),
 | 
			
		||||
@@ -157,7 +149,7 @@ class SolianApp extends StatelessWidget {
 | 
			
		||||
            Provider(create: (ctx) => SnNetworkProvider(ctx)),
 | 
			
		||||
            Provider(create: (ctx) => UserDirectoryProvider(ctx)),
 | 
			
		||||
            Provider(create: (ctx) => SnAttachmentProvider(ctx)),
 | 
			
		||||
            Provider(create: (ctx) => SnRealmProvider(ctx)),
 | 
			
		||||
            ChangeNotifierProvider(create: (ctx) => SnRealmProvider(ctx)),
 | 
			
		||||
            Provider(create: (ctx) => SnPostContentProvider(ctx)),
 | 
			
		||||
            Provider(create: (ctx) => SnRelationshipProvider(ctx)),
 | 
			
		||||
            Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
 | 
			
		||||
@@ -209,10 +201,7 @@ class _AppDelegate extends StatelessWidget {
 | 
			
		||||
      ],
 | 
			
		||||
      routerConfig: appRouter,
 | 
			
		||||
      builder: (context, child) {
 | 
			
		||||
        return _AppSplashScreen(
 | 
			
		||||
          key: const Key('global-splash-screen'),
 | 
			
		||||
          child: child!,
 | 
			
		||||
        );
 | 
			
		||||
        return _AppSplashScreen(key: const Key('global-splash-screen'), child: child!);
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
@@ -228,13 +217,15 @@ class _AppSplashScreen extends StatefulWidget {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
  bool _isBusy = false;
 | 
			
		||||
  String _phaseText = 'appInitStarting';
 | 
			
		||||
 | 
			
		||||
  void _tryRequestRating() async {
 | 
			
		||||
    final prefs = await SharedPreferences.getInstance();
 | 
			
		||||
    if (prefs.containsKey('first_boot_time')) {
 | 
			
		||||
      final rawTime = prefs.getString('first_boot_time');
 | 
			
		||||
      final time = DateTime.tryParse(rawTime ?? '');
 | 
			
		||||
      if (time != null &&
 | 
			
		||||
          time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
 | 
			
		||||
      if (time != null && time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
 | 
			
		||||
        final inAppReview = InAppReview.instance;
 | 
			
		||||
        if (prefs.getBool('rating_requested') == true) return;
 | 
			
		||||
        if (await inAppReview.isAvailable()) {
 | 
			
		||||
@@ -255,30 +246,17 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
      final info = await PackageInfo.fromPlatform();
 | 
			
		||||
      final localVersionString = '${info.version}+${info.buildNumber}';
 | 
			
		||||
      final resp = await Dio(
 | 
			
		||||
        BaseOptions(
 | 
			
		||||
          sendTimeout: const Duration(seconds: 60),
 | 
			
		||||
          receiveTimeout: const Duration(seconds: 60),
 | 
			
		||||
        ),
 | 
			
		||||
      ).get(
 | 
			
		||||
        'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest',
 | 
			
		||||
      );
 | 
			
		||||
        BaseOptions(sendTimeout: const Duration(seconds: 60), receiveTimeout: const Duration(seconds: 60)),
 | 
			
		||||
      ).get('https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest');
 | 
			
		||||
      final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0';
 | 
			
		||||
      final remoteVersion = Version.parse(remoteVersionString.split('+').first);
 | 
			
		||||
      final localVersion = Version.parse(localVersionString.split('+').first);
 | 
			
		||||
      final remoteBuildNumber =
 | 
			
		||||
          int.tryParse(remoteVersionString.split('+').last) ?? 0;
 | 
			
		||||
      final localBuildNumber =
 | 
			
		||||
          int.tryParse(localVersionString.split('+').last) ?? 0;
 | 
			
		||||
      logging.info(
 | 
			
		||||
          "[Update] Local: $localVersionString, Remote: $remoteVersionString");
 | 
			
		||||
      if ((remoteVersion > localVersion ||
 | 
			
		||||
              remoteBuildNumber > localBuildNumber) &&
 | 
			
		||||
          mounted) {
 | 
			
		||||
      final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0;
 | 
			
		||||
      final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0;
 | 
			
		||||
      logging.info("[Update] Local: $localVersionString, Remote: $remoteVersionString");
 | 
			
		||||
      if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) {
 | 
			
		||||
        final config = context.read<ConfigProvider>();
 | 
			
		||||
        config.setUpdate(
 | 
			
		||||
          remoteVersionString,
 | 
			
		||||
          resp.data?['body'] ?? 'No changelog',
 | 
			
		||||
        );
 | 
			
		||||
        config.setUpdate(remoteVersionString, resp.data?['body'] ?? 'No changelog');
 | 
			
		||||
        logging.info("[Update] Update available: $remoteVersionString");
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
@@ -287,6 +265,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _setPhaseText(String text) {
 | 
			
		||||
    _phaseText = 'appInit${text.capitalize()}'.tr();
 | 
			
		||||
    if (mounted) setState(() {});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _initialize() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final cfg = context.read<ConfigProvider>();
 | 
			
		||||
@@ -299,31 +282,51 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
      // The Network initialization must be done after the HomeWidget initialization
 | 
			
		||||
      // The Network initialization will save the server url to the HomeWidget
 | 
			
		||||
      // The Network initialization will also save initialize the Config, so it not need to be initialized again
 | 
			
		||||
      _setPhaseText('network');
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      await sn.initializeUserAgent();
 | 
			
		||||
      await sn.setConfigWithNative();
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      _setPhaseText('userdata');
 | 
			
		||||
      final ua = context.read<UserProvider>();
 | 
			
		||||
      await ua.initialize();
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      _setPhaseText('websocket');
 | 
			
		||||
      final ws = context.read<WebSocketProvider>();
 | 
			
		||||
      await ws.tryConnect();
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      final notify = context.read<NotificationProvider>();
 | 
			
		||||
      notify.listen();
 | 
			
		||||
      await notify.registerPushNotifications();
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      final kp = context.read<KeyPairProvider>();
 | 
			
		||||
      await kp.reloadActive();
 | 
			
		||||
      kp.listen();
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      final sticker = context.read<SnStickerProvider>();
 | 
			
		||||
      await sticker.listSticker();
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      final ud = context.read<UserDirectoryProvider>();
 | 
			
		||||
      final userCacheSize = await ud.loadAccountCache();
 | 
			
		||||
      logging.info('[Users] Loaded local user cache, size: $userCacheSize');
 | 
			
		||||
      logging.info('[Bootstrap] Everything initialized!');
 | 
			
		||||
      try {
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        _setPhaseText('keyPair');
 | 
			
		||||
        final kp = context.read<KeyPairProvider>();
 | 
			
		||||
        await kp.reloadActive();
 | 
			
		||||
        kp.listen();
 | 
			
		||||
      } catch (_) {}
 | 
			
		||||
      if (ua.isAuthorized) {
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        _setPhaseText('notification');
 | 
			
		||||
        final notify = context.read<NotificationProvider>();
 | 
			
		||||
        notify.listen();
 | 
			
		||||
        try {
 | 
			
		||||
          await notify.registerPushNotifications();
 | 
			
		||||
        } catch (_) {}
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        _setPhaseText('stickers');
 | 
			
		||||
        final sticker = context.read<SnStickerProvider>();
 | 
			
		||||
        await sticker.listSticker();
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        _setPhaseText('userDirectory');
 | 
			
		||||
        final ud = context.read<UserDirectoryProvider>();
 | 
			
		||||
        await ud.loadAccountCache();
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        _setPhaseText('realm');
 | 
			
		||||
        final rm = context.read<SnRealmProvider>();
 | 
			
		||||
        await rm.refreshAvailableRealms();
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        _setPhaseText('chat');
 | 
			
		||||
        final ct = context.read<ChatChannelProvider>();
 | 
			
		||||
        await ct.refreshAvailableChannels();
 | 
			
		||||
        _setPhaseText('done');
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      await context.showErrorDialog(err);
 | 
			
		||||
@@ -341,35 +344,19 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
 | 
			
		||||
  final Menu _appTrayMenu = Menu(
 | 
			
		||||
    items: [
 | 
			
		||||
      MenuItem(
 | 
			
		||||
        key: 'version_label',
 | 
			
		||||
        label: 'Solian',
 | 
			
		||||
        disabled: true,
 | 
			
		||||
      ),
 | 
			
		||||
      MenuItem(key: 'version_label', label: 'Solian', disabled: true),
 | 
			
		||||
      MenuItem.separator(),
 | 
			
		||||
      MenuItem.checkbox(
 | 
			
		||||
        checked: false,
 | 
			
		||||
        key: 'mute_notification',
 | 
			
		||||
        label: 'trayMenuMuteNotification'.tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      MenuItem.checkbox(checked: false, key: 'mute_notification', label: 'trayMenuMuteNotification'.tr()),
 | 
			
		||||
      MenuItem.separator(),
 | 
			
		||||
      MenuItem(
 | 
			
		||||
        key: 'window_show',
 | 
			
		||||
        label: 'trayMenuShow'.tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      MenuItem(
 | 
			
		||||
        key: 'exit',
 | 
			
		||||
        label: 'trayMenuExit'.tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      MenuItem(key: 'window_show', label: 'trayMenuShow'.tr()),
 | 
			
		||||
      MenuItem(key: 'exit', label: 'trayMenuExit'.tr()),
 | 
			
		||||
    ],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  Future<void> _trayInitialization() async {
 | 
			
		||||
    if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
 | 
			
		||||
 | 
			
		||||
    final icon = Platform.isWindows
 | 
			
		||||
        ? 'assets/icon/tray-icon.ico'
 | 
			
		||||
        : 'assets/icon/tray-icon.png';
 | 
			
		||||
    final icon = Platform.isWindows ? 'assets/icon/tray-icon.ico' : 'assets/icon/tray-icon.png';
 | 
			
		||||
    final appVersion = await PackageInfo.fromPlatform();
 | 
			
		||||
 | 
			
		||||
    trayManager.addListener(this);
 | 
			
		||||
@@ -387,10 +374,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
  Future<void> _notifyInitialization() async {
 | 
			
		||||
    if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
 | 
			
		||||
 | 
			
		||||
    await localNotifier.setup(
 | 
			
		||||
      appName: 'Solian',
 | 
			
		||||
      shortcutPolicy: ShortcutPolicy.requireCreate,
 | 
			
		||||
    );
 | 
			
		||||
    await localNotifier.setup(appName: 'Solian', shortcutPolicy: ShortcutPolicy.requireCreate);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  AppLifecycleListener? _appLifecycleListener;
 | 
			
		||||
@@ -399,10 +383,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
 | 
			
		||||
    _isBusy = true;
 | 
			
		||||
    if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
 | 
			
		||||
      _appLifecycleListener = AppLifecycleListener(
 | 
			
		||||
        onExitRequested: _onExitRequested,
 | 
			
		||||
      );
 | 
			
		||||
      _appLifecycleListener = AppLifecycleListener(onExitRequested: _onExitRequested);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _trayInitialization();
 | 
			
		||||
@@ -412,6 +395,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
      _postInitialization();
 | 
			
		||||
      _tryRequestRating();
 | 
			
		||||
      _checkForUpdate();
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -501,7 +485,49 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
            return SizeChangedLayoutNotifier(
 | 
			
		||||
              child: widget.child,
 | 
			
		||||
              child:
 | 
			
		||||
                  _isBusy
 | 
			
		||||
                      ? Material(
 | 
			
		||||
                        key: Key('app-splash-screen-$_isBusy'),
 | 
			
		||||
                        child: Stack(
 | 
			
		||||
                          children: [
 | 
			
		||||
                            Container(
 | 
			
		||||
                              decoration: BoxDecoration(
 | 
			
		||||
                                image: DecorationImage(
 | 
			
		||||
                                  image: AssetImage('assets/icon/kanban-1st.jpg'),
 | 
			
		||||
                                  fit: BoxFit.cover,
 | 
			
		||||
                                  opacity: 0.1,
 | 
			
		||||
                                ),
 | 
			
		||||
                                color: Theme.of(context).colorScheme.surface,
 | 
			
		||||
                                backgroundBlendMode: BlendMode.darken,
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                            Center(
 | 
			
		||||
                              child: Container(
 | 
			
		||||
                                constraints: const BoxConstraints(maxWidth: 240),
 | 
			
		||||
                                child: Column(
 | 
			
		||||
                                  mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                                  children: [
 | 
			
		||||
                                    Image.asset(
 | 
			
		||||
                                      'assets/icon/icon.png',
 | 
			
		||||
                                      width: 64,
 | 
			
		||||
                                      height: 64,
 | 
			
		||||
                                      color: Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    Text('Solar Network').bold(),
 | 
			
		||||
                                    AppVersionLabel(),
 | 
			
		||||
                                    Gap(8),
 | 
			
		||||
                                    Text(_phaseText, textAlign: TextAlign.center),
 | 
			
		||||
                                    Gap(16),
 | 
			
		||||
                                    const LinearProgressIndicator(),
 | 
			
		||||
                                  ],
 | 
			
		||||
                                ),
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                      )
 | 
			
		||||
                      : widget.child,
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,24 @@ class ChatChannelProvider extends ChangeNotifier {
 | 
			
		||||
    _rels = context.read<SnRealmProvider>();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  final List<SnChannel> _availableChannels = List.empty(growable: true);
 | 
			
		||||
 | 
			
		||||
  List<SnChannel> get availableChannels => _availableChannels;
 | 
			
		||||
 | 
			
		||||
  Future<void> refreshAvailableChannels() async {
 | 
			
		||||
    final stream = fetchChannels();
 | 
			
		||||
    stream.listen((ele) {
 | 
			
		||||
      _availableChannels.clear();
 | 
			
		||||
      _availableChannels.addAll(ele);
 | 
			
		||||
      notifyListeners();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void addAvailableChannel(SnChannel channel) {
 | 
			
		||||
    _availableChannels.add(channel);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
 | 
			
		||||
    await Future.wait(
 | 
			
		||||
      channels.map(
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ const kAppRealmCompactView = 'app_realm_compact_view';
 | 
			
		||||
const kAppCustomFonts = 'app_custom_fonts';
 | 
			
		||||
const kAppMixedFeed = 'app_mixed_feed';
 | 
			
		||||
const kAppAutoTranslate = 'app_auto_translate';
 | 
			
		||||
const kAppHideBottomNav = 'app_hide_bottom_nav';
 | 
			
		||||
 | 
			
		||||
const Map<String, FilterQuality> kImageQualityLevel = {
 | 
			
		||||
  'settingsImageQualityLowest': FilterQuality.none,
 | 
			
		||||
@@ -91,6 +92,15 @@ class ConfigProvider extends ChangeNotifier {
 | 
			
		||||
    return prefs.getBool(kAppAutoTranslate) ?? false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool get hideBottomNav {
 | 
			
		||||
    return prefs.getBool(kAppHideBottomNav) ?? false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set hideBottomNav(bool value) {
 | 
			
		||||
    prefs.setBool(kAppHideBottomNav, value);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set autoTranslate(bool value) {
 | 
			
		||||
    prefs.setBool(kAppAutoTranslate, value);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:shared_preferences/shared_preferences.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
 | 
			
		||||
class AppNavDestination {
 | 
			
		||||
  final String label;
 | 
			
		||||
@@ -24,13 +25,10 @@ class NavigationProvider extends ChangeNotifier {
 | 
			
		||||
 | 
			
		||||
  int? get currentIndex => _currentIndex;
 | 
			
		||||
 | 
			
		||||
  static const List<String> kShowBottomNavScreen = [
 | 
			
		||||
    'home',
 | 
			
		||||
    'explore',
 | 
			
		||||
    'account',
 | 
			
		||||
    'album',
 | 
			
		||||
    'chat',
 | 
			
		||||
  ];
 | 
			
		||||
  List<String> get showBottomNavScreen => destinations
 | 
			
		||||
      .where((ele) => ele.isPinned)
 | 
			
		||||
      .map((ele) => ele.screen)
 | 
			
		||||
      .toList();
 | 
			
		||||
 | 
			
		||||
  static const List<AppNavDestination> kAllDestination = [
 | 
			
		||||
    AppNavDestination(
 | 
			
		||||
@@ -63,32 +61,12 @@ class NavigationProvider extends ChangeNotifier {
 | 
			
		||||
      screen: 'news',
 | 
			
		||||
      label: 'screenNews',
 | 
			
		||||
    ),
 | 
			
		||||
    AppNavDestination(
 | 
			
		||||
      icon: Icon(Symbols.emoji_emotions, weight: 400, opticalSize: 20),
 | 
			
		||||
      screen: 'stickers',
 | 
			
		||||
      label: 'screenStickers',
 | 
			
		||||
    ),
 | 
			
		||||
    AppNavDestination(
 | 
			
		||||
      icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
 | 
			
		||||
      screen: 'album',
 | 
			
		||||
      label: 'screenAlbum',
 | 
			
		||||
    ),
 | 
			
		||||
    AppNavDestination(
 | 
			
		||||
      icon: Icon(Symbols.diversity_4, weight: 400, opticalSize: 20),
 | 
			
		||||
      screen: 'friend',
 | 
			
		||||
      label: 'screenFriend',
 | 
			
		||||
    ),
 | 
			
		||||
    AppNavDestination(
 | 
			
		||||
      icon: Icon(Symbols.notifications, weight: 400, opticalSize: 20),
 | 
			
		||||
      screen: 'notification',
 | 
			
		||||
      label: 'screenNotification',
 | 
			
		||||
    ),
 | 
			
		||||
  ];
 | 
			
		||||
  static const List<String> kDefaultPinnedDestination = [
 | 
			
		||||
    'home',
 | 
			
		||||
    'explore',
 | 
			
		||||
    'chat',
 | 
			
		||||
    'account',
 | 
			
		||||
    'realm',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  List<AppNavDestination> destinations = [];
 | 
			
		||||
@@ -143,4 +121,11 @@ class NavigationProvider extends ChangeNotifier {
 | 
			
		||||
    _currentIndex = idx;
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SnRealm? focusedRealm;
 | 
			
		||||
 | 
			
		||||
  void setFocusedRealm(SnRealm? realm) {
 | 
			
		||||
    focusedRealm = realm;
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -48,13 +48,11 @@ class NotificationProvider extends ChangeNotifier {
 | 
			
		||||
    var deviceUuid = await FlutterUdid.consistentUdid;
 | 
			
		||||
 | 
			
		||||
    if (deviceUuid.isEmpty) {
 | 
			
		||||
      logging.warning(
 | 
			
		||||
          '[Push Notification] Unable to active push notifications, couldn\'t get device uuid');
 | 
			
		||||
      logging.warning('[Push Notification] Unable to active push notifications, couldn\'t get device uuid');
 | 
			
		||||
      return;
 | 
			
		||||
    } else {
 | 
			
		||||
      logging.info('[Push Notification] Device UUID is $deviceUuid');
 | 
			
		||||
      logging
 | 
			
		||||
          .info('[Push Notification] Registering device push notifications...');
 | 
			
		||||
      logging.info('[Push Notification] Registering device push notifications...');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (Platform.isIOS || Platform.isMacOS) {
 | 
			
		||||
@@ -66,14 +64,14 @@ class NotificationProvider extends ChangeNotifier {
 | 
			
		||||
    }
 | 
			
		||||
    logging.info('[Push Notification] Device Push Token is $token');
 | 
			
		||||
 | 
			
		||||
    await _sn.client.post(
 | 
			
		||||
      '/cgi/id/notifications/subscription',
 | 
			
		||||
      data: {
 | 
			
		||||
        'provider': provider,
 | 
			
		||||
        'device_token': token,
 | 
			
		||||
        'device_id': deviceUuid,
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
    try {
 | 
			
		||||
      await _sn.client.post(
 | 
			
		||||
        '/cgi/id/notifications/subscription',
 | 
			
		||||
        data: {'provider': provider, 'device_token': token, 'device_id': deviceUuid},
 | 
			
		||||
      );
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      logging.error('[Push Notification] Unable to register push notifications: $err');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int showingCount = 0;
 | 
			
		||||
@@ -91,8 +89,7 @@ class NotificationProvider extends ChangeNotifier {
 | 
			
		||||
        final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
 | 
			
		||||
        if (doHaptic) HapticFeedback.mediumImpact();
 | 
			
		||||
 | 
			
		||||
        if (notification.topic == 'messaging.message' &&
 | 
			
		||||
            skippableNotifyChannel != null) {
 | 
			
		||||
        if (notification.topic == 'messaging.message' && skippableNotifyChannel != null) {
 | 
			
		||||
          if (notification.metadata['channel_id'] != null &&
 | 
			
		||||
              notification.metadata['channel_id'] == skippableNotifyChannel) {
 | 
			
		||||
            return;
 | 
			
		||||
 
 | 
			
		||||
@@ -321,13 +321,13 @@ class SnAttachmentProvider {
 | 
			
		||||
          uuid: ele.uuid,
 | 
			
		||||
          content: ele,
 | 
			
		||||
          accountId: ele.accountId,
 | 
			
		||||
          cacheExpiredAt: DateTime.now().add(const Duration(days: 7)),
 | 
			
		||||
          cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
 | 
			
		||||
        ),
 | 
			
		||||
        onConflict: DoUpdate(
 | 
			
		||||
          (_) => SnLocalAttachmentCompanion.custom(
 | 
			
		||||
            content: Constant(jsonEncode(ele.toJson())),
 | 
			
		||||
            cacheExpiredAt:
 | 
			
		||||
                Constant(DateTime.now().add(const Duration(days: 7))),
 | 
			
		||||
                Constant(DateTime.now().add(const Duration(hours: 1))),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,30 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
 | 
			
		||||
import 'package:drift/drift.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:surface/database/database.dart';
 | 
			
		||||
import 'package:surface/providers/database.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
 | 
			
		||||
class SnRealmProvider {
 | 
			
		||||
class SnRealmProvider extends ChangeNotifier {
 | 
			
		||||
  late final SnNetworkProvider _sn;
 | 
			
		||||
  late final DatabaseProvider _dt;
 | 
			
		||||
 | 
			
		||||
  SnRealmProvider(BuildContext context) {
 | 
			
		||||
    _sn = context.read<SnNetworkProvider>();
 | 
			
		||||
    _dt = context.read<DatabaseProvider>();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  final Map<String, SnRealm> _cache = {};
 | 
			
		||||
  List<SnRealm> _availableRealms = List.empty(growable: true);
 | 
			
		||||
 | 
			
		||||
  Future<void> refreshAvailableRealms() async {
 | 
			
		||||
    _availableRealms = await listAvailableRealms();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  List<SnRealm> get availableRealms => _availableRealms;
 | 
			
		||||
 | 
			
		||||
  Future<List<SnRealm>> listAvailableRealms() async {
 | 
			
		||||
    final resp = await _sn.client.get('/cgi/id/realms/me/available');
 | 
			
		||||
@@ -21,17 +35,56 @@ class SnRealmProvider {
 | 
			
		||||
      _cache[realm.alias] = realm;
 | 
			
		||||
      _cache[realm.id.toString()] = realm;
 | 
			
		||||
    }
 | 
			
		||||
    _saveToLocal(out);
 | 
			
		||||
    return out;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void addAvailableRealm(SnRealm realm) {
 | 
			
		||||
    _availableRealms.add(realm);
 | 
			
		||||
    notifyListeners();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<SnRealm> getRealm(dynamic aliasOrId) async {
 | 
			
		||||
    if (_cache.containsKey(aliasOrId.toString())) {
 | 
			
		||||
      return _cache[aliasOrId.toString()]!;
 | 
			
		||||
    }
 | 
			
		||||
    final localResp = await (_dt.db.snLocalRealm.select()
 | 
			
		||||
          ..where((e) =>
 | 
			
		||||
              e.id.equals(aliasOrId is int ? aliasOrId : 0) |
 | 
			
		||||
              e.alias.equals(aliasOrId.toString()))
 | 
			
		||||
          ..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now())))
 | 
			
		||||
        .getSingleOrNull();
 | 
			
		||||
    if (localResp != null) {
 | 
			
		||||
      _cache[localResp.content.id.toString()] = localResp.content;
 | 
			
		||||
      _cache[localResp.content.alias] = localResp.content;
 | 
			
		||||
      return localResp.content;
 | 
			
		||||
    }
 | 
			
		||||
    final resp = await _sn.client.get('/cgi/id/realms/$aliasOrId');
 | 
			
		||||
    final out = SnRealm.fromJson(resp.data);
 | 
			
		||||
    _cache[out.alias] = out;
 | 
			
		||||
    _cache[out.id.toString()] = out;
 | 
			
		||||
    _saveToLocal([out]);
 | 
			
		||||
    return out;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _saveToLocal(Iterable<SnRealm> out) async {
 | 
			
		||||
    for (final ele in out) {
 | 
			
		||||
      await _dt.db.snLocalRealm.insertOne(
 | 
			
		||||
        SnLocalRealmCompanion.insert(
 | 
			
		||||
          id: Value(ele.id),
 | 
			
		||||
          alias: ele.alias,
 | 
			
		||||
          content: ele,
 | 
			
		||||
          accountId: ele.accountId,
 | 
			
		||||
          cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
 | 
			
		||||
        ),
 | 
			
		||||
        onConflict: DoUpdate(
 | 
			
		||||
          (_) => SnLocalRealmCompanion.custom(
 | 
			
		||||
            content: Constant(jsonEncode(ele.toJson())),
 | 
			
		||||
            cacheExpiredAt:
 | 
			
		||||
                Constant(DateTime.now().add(const Duration(hours: 1))),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,7 @@ import 'package:crypto/crypto.dart';
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:surface/logger.dart';
 | 
			
		||||
 | 
			
		||||
// TODO self host translate api
 | 
			
		||||
const kTranslateApiBaseUrl = 'https://translate.disroot.org';
 | 
			
		||||
const kTranslateApiBaseUrl = 'https://translate.solsynth.dev';
 | 
			
		||||
 | 
			
		||||
class SnTranslator {
 | 
			
		||||
  final Dio client = Dio(
 | 
			
		||||
 
 | 
			
		||||
@@ -64,6 +64,7 @@ class UserProvider extends ChangeNotifier {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<SnAccount?> refreshUser() async {
 | 
			
		||||
    if (!isAuthorized) return null;
 | 
			
		||||
    final resp = await _sn.client.get('/cgi/id/users/me');
 | 
			
		||||
    final out = SnAccount.fromJson(resp.data);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,8 @@ import 'package:surface/screens/account/badges.dart';
 | 
			
		||||
import 'package:surface/screens/account/contact_methods.dart';
 | 
			
		||||
import 'package:surface/screens/account/factor_settings.dart';
 | 
			
		||||
import 'package:surface/screens/account/keypairs.dart';
 | 
			
		||||
import 'package:surface/screens/account/prefs/notify.dart';
 | 
			
		||||
import 'package:surface/screens/account/prefs/security.dart';
 | 
			
		||||
import 'package:surface/screens/account/profile_page.dart';
 | 
			
		||||
import 'package:surface/screens/account/profile_edit.dart';
 | 
			
		||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
 | 
			
		||||
@@ -37,6 +39,7 @@ import 'package:surface/screens/post/post_shuffle.dart';
 | 
			
		||||
import 'package:surface/screens/post/publisher_page.dart';
 | 
			
		||||
import 'package:surface/screens/post/post_search.dart';
 | 
			
		||||
import 'package:surface/screens/realm.dart';
 | 
			
		||||
import 'package:surface/screens/realm/community.dart';
 | 
			
		||||
import 'package:surface/screens/realm/manage.dart';
 | 
			
		||||
import 'package:surface/screens/realm/realm_detail.dart';
 | 
			
		||||
import 'package:surface/screens/realm/realm_discovery.dart';
 | 
			
		||||
@@ -161,6 +164,18 @@ final _appRoutes = [
 | 
			
		||||
        path: '/settings',
 | 
			
		||||
        name: 'accountSettings',
 | 
			
		||||
        builder: (context, state) => AccountSettingsScreen(),
 | 
			
		||||
        routes: [
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/notify',
 | 
			
		||||
            name: 'accountSettingsNotify',
 | 
			
		||||
            builder: (context, state) => const AccountNotifyPrefsScreen(),
 | 
			
		||||
          ),
 | 
			
		||||
          GoRoute(
 | 
			
		||||
            path: '/auth',
 | 
			
		||||
            name: 'accountSettingsSecurity',
 | 
			
		||||
            builder: (context, state) => const AccountSecurityPrefsScreen(),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/settings/factors',
 | 
			
		||||
@@ -245,6 +260,13 @@ final _appRoutes = [
 | 
			
		||||
      child: const RealmScreen(),
 | 
			
		||||
    ),
 | 
			
		||||
    routes: [
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/:alias/community',
 | 
			
		||||
        name: 'realmCommunity',
 | 
			
		||||
        builder: (context, state) => RealmCommunityScreen(
 | 
			
		||||
          alias: state.pathParameters['alias']!,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      GoRoute(
 | 
			
		||||
        path: '/manage',
 | 
			
		||||
        name: 'realmManage',
 | 
			
		||||
 
 | 
			
		||||
@@ -30,19 +30,7 @@ class AccountScreen extends StatelessWidget {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        title: Text(
 | 
			
		||||
          "screenAccount",
 | 
			
		||||
          style: TextStyle(
 | 
			
		||||
            color: Colors.white,
 | 
			
		||||
            shadows: [
 | 
			
		||||
              Shadow(
 | 
			
		||||
                offset: Offset(1, 1),
 | 
			
		||||
                blurRadius: 5.0,
 | 
			
		||||
                color: Color.fromARGB(255, 0, 0, 0),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        ).tr(),
 | 
			
		||||
        title: Text("screenAccount").tr(),
 | 
			
		||||
        flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
 | 
			
		||||
            ? Stack(
 | 
			
		||||
                fit: StackFit.expand,
 | 
			
		||||
@@ -158,23 +146,33 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('abuseReport').tr(),
 | 
			
		||||
          subtitle: Text('abuseReportActionDescription').tr(),
 | 
			
		||||
          title: Text('friends').tr(),
 | 
			
		||||
          subtitle: Text('friendsDescription').tr(),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          leading: const Icon(Symbols.flag),
 | 
			
		||||
          leading: const Icon(Symbols.person),
 | 
			
		||||
          trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            GoRouter.of(context).pushNamed('abuseReport');
 | 
			
		||||
            GoRouter.of(context).pushNamed('friend');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('factorSettings').tr(),
 | 
			
		||||
          subtitle: Text('factorSettingsSubtitle').tr(),
 | 
			
		||||
          title: Text('album').tr(),
 | 
			
		||||
          subtitle: Text('albumDescription').tr(),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          leading: const Icon(Symbols.lock),
 | 
			
		||||
          leading: const Icon(Symbols.photo_library),
 | 
			
		||||
          trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            GoRouter.of(context).pushNamed('factorSettings');
 | 
			
		||||
            GoRouter.of(context).pushNamed('album');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('stickers').tr(),
 | 
			
		||||
          subtitle: Text('stickersDescription').tr(),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          leading: const Icon(Symbols.emoji_emotions),
 | 
			
		||||
          trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            GoRouter.of(context).pushNamed('stickers');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
@@ -237,6 +235,16 @@ class _AuthorizedAccountScreen extends StatelessWidget {
 | 
			
		||||
            GoRouter.of(context).pushNamed('accountSettings');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('abuseReport').tr(),
 | 
			
		||||
          subtitle: Text('abuseReportActionDescription').tr(),
 | 
			
		||||
          contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
          leading: const Icon(Symbols.flag),
 | 
			
		||||
          trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            GoRouter.of(context).pushNamed('abuseReport');
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        ListTile(
 | 
			
		||||
          title: Text('accountLogout').tr(),
 | 
			
		||||
          subtitle: Text('accountLogoutSubtitle').tr(),
 | 
			
		||||
@@ -298,9 +306,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
 | 
			
		||||
            GoRouter.of(context).pushNamed('authLogin').then((value) {
 | 
			
		||||
              if (value == true && context.mounted) {
 | 
			
		||||
                final ua = context.read<UserProvider>();
 | 
			
		||||
                context.showSnackbar('loginSuccess'.tr(args: [
 | 
			
		||||
                  '@${ua.user?.name} (${ua.user?.nick})',
 | 
			
		||||
                ]));
 | 
			
		||||
                ua.refreshUser();
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
          },
 | 
			
		||||
 
 | 
			
		||||
@@ -97,6 +97,36 @@ class AccountSettingsScreen extends StatelessWidget {
 | 
			
		||||
                GoRouter.of(context).pushNamed('accountContactMethods');
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text('accountSettingsNotify').tr(),
 | 
			
		||||
              subtitle: Text('accountSettingsNotifyDescription').tr(),
 | 
			
		||||
              contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
              leading: const Icon(Symbols.notifications),
 | 
			
		||||
              trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                GoRouter.of(context).pushNamed('accountSettingsNotify');
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text('accountSettingsSecurity').tr(),
 | 
			
		||||
              subtitle: Text('accountSettingsSecurityDescription').tr(),
 | 
			
		||||
              contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
              leading: const Icon(Symbols.shield),
 | 
			
		||||
              trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                GoRouter.of(context).pushNamed('accountSettingsSecurity');
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text('factorSettings').tr(),
 | 
			
		||||
              subtitle: Text('factorSettingsSubtitle').tr(),
 | 
			
		||||
              contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
              leading: const Icon(Symbols.lock),
 | 
			
		||||
              trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                GoRouter.of(context).pushNamed('factorSettings');
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            ListTile(
 | 
			
		||||
              title: Text('accountProfileEdit').tr(),
 | 
			
		||||
              subtitle: Text('accountProfileEditSubtitle').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,122 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:google_fonts/google_fonts.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
class AccountNotifyPrefsScreen extends StatelessWidget {
 | 
			
		||||
final Map<String, String> kNotifyTopicMap = {
 | 
			
		||||
  'interactive.reply': 'notificationTopicPostReply'.tr(),
 | 
			
		||||
  'interactive.feedback': 'notificationTopicPostFeedback'.tr(),
 | 
			
		||||
  'interactive.subscription': 'notificationTopicPostSubscription'.tr(),
 | 
			
		||||
  'messaging.message': 'notificationTopicMessaging'.tr(),
 | 
			
		||||
  'messaging.call': 'notificationTopicMessagingCall'.tr(),
 | 
			
		||||
  'general': 'notificationTopicGeneral'.tr(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class AccountNotifyPrefsScreen extends StatefulWidget {
 | 
			
		||||
  const AccountNotifyPrefsScreen({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<AccountNotifyPrefsScreen> createState() =>
 | 
			
		||||
      _AccountNotifyPrefsScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AccountNotifyPrefsScreenState extends State<AccountNotifyPrefsScreen> {
 | 
			
		||||
  bool _isBusy = true;
 | 
			
		||||
 | 
			
		||||
  Map<String, bool> _config = {};
 | 
			
		||||
 | 
			
		||||
  Future<void> _getPreferences() async {
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final resp = await sn.client.get('/cgi/id/preferences/notifications');
 | 
			
		||||
      _config = resp.data['config']
 | 
			
		||||
          .map((k, v) => MapEntry(k, v as bool))
 | 
			
		||||
          .cast<String, bool>();
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _savePreferences() async {
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await sn.client.put(
 | 
			
		||||
        '/cgi/id/preferences/notifications',
 | 
			
		||||
        data: {
 | 
			
		||||
          'config': _config,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showSnackbar('accountSettingsApplied'.tr());
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _getPreferences();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold();
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('accountSettingsNotify').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          LoadingIndicator(isActive: _isBusy),
 | 
			
		||||
          ListTile(
 | 
			
		||||
            tileColor: Theme.of(context).colorScheme.surfaceContainer,
 | 
			
		||||
            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
            leading: const Icon(Icons.save),
 | 
			
		||||
            title: Text('save').tr(),
 | 
			
		||||
            enabled: !_isBusy,
 | 
			
		||||
            onTap: () {
 | 
			
		||||
              _savePreferences();
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: ListView.builder(
 | 
			
		||||
              padding: EdgeInsets.zero,
 | 
			
		||||
              itemCount: kNotifyTopicMap.length,
 | 
			
		||||
              itemBuilder: (context, index) {
 | 
			
		||||
                final element = kNotifyTopicMap.entries.elementAt(index);
 | 
			
		||||
                return CheckboxListTile(
 | 
			
		||||
                  title: Text(element.value),
 | 
			
		||||
                  subtitle: Text(
 | 
			
		||||
                    element.key,
 | 
			
		||||
                    style: GoogleFonts.robotoMono(fontSize: 12),
 | 
			
		||||
                  ),
 | 
			
		||||
                  value: _config[element.key] ?? true,
 | 
			
		||||
                  contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
                  onChanged: (value) {
 | 
			
		||||
                    setState(() {
 | 
			
		||||
                      _config[element.key] = value ?? false;
 | 
			
		||||
                    });
 | 
			
		||||
                  },
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										147
									
								
								lib/screens/account/prefs/security.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								lib/screens/account/prefs/security.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,147 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
class AccountSecurityPrefsScreen extends StatefulWidget {
 | 
			
		||||
  const AccountSecurityPrefsScreen({super.key});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<AccountSecurityPrefsScreen> createState() =>
 | 
			
		||||
      _AccountSecurityPrefsScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _AccountSecurityPrefsScreenState
 | 
			
		||||
    extends State<AccountSecurityPrefsScreen> {
 | 
			
		||||
  bool _isBusy = true;
 | 
			
		||||
 | 
			
		||||
  Map<String, dynamic> _config = {
 | 
			
		||||
    'maximum_auth_steps': 2,
 | 
			
		||||
    'always_risky': false,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  Future<void> _getPreferences() async {
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final resp = await sn.client.get('/cgi/id/preferences/auth');
 | 
			
		||||
      _config = resp.data['config']
 | 
			
		||||
          .map((k, v) => MapEntry(k, v as bool))
 | 
			
		||||
          .cast<String, bool>();
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _savePreferences() async {
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await sn.client.put(
 | 
			
		||||
        '/cgi/id/preferences/auth',
 | 
			
		||||
        data: {
 | 
			
		||||
          'config': _config,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showSnackbar('accountSettingsApplied'.tr());
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _getPreferences();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: const PageBackButton(),
 | 
			
		||||
        title: Text('accountSettingsSecurity').tr(),
 | 
			
		||||
      ),
 | 
			
		||||
      body: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          LoadingIndicator(isActive: _isBusy),
 | 
			
		||||
          ListTile(
 | 
			
		||||
            tileColor: Theme.of(context).colorScheme.surfaceContainer,
 | 
			
		||||
            contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
            leading: const Icon(Icons.save),
 | 
			
		||||
            title: Text('save').tr(),
 | 
			
		||||
            enabled: !_isBusy,
 | 
			
		||||
            onTap: () {
 | 
			
		||||
              _savePreferences();
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: ListView(
 | 
			
		||||
              padding: EdgeInsets.zero,
 | 
			
		||||
              children: [
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  title: Text('authMaximumAuthSteps').tr(),
 | 
			
		||||
                  subtitle: Text('authMaximumAuthStepsDescription')
 | 
			
		||||
                      .plural(_config['maximum_auth_steps'] ?? 2),
 | 
			
		||||
                  contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
                  trailing: Row(
 | 
			
		||||
                    mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        padding: EdgeInsets.zero,
 | 
			
		||||
                        visualDensity: const VisualDensity(
 | 
			
		||||
                          horizontal: -4,
 | 
			
		||||
                          vertical: -4,
 | 
			
		||||
                        ),
 | 
			
		||||
                        icon: const Icon(Symbols.remove),
 | 
			
		||||
                        onPressed: () {
 | 
			
		||||
                          if (_config['maximum_auth_steps'] > 1) {
 | 
			
		||||
                            setState(() => _config['maximum_auth_steps']--);
 | 
			
		||||
                          }
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        padding: EdgeInsets.zero,
 | 
			
		||||
                        visualDensity: const VisualDensity(
 | 
			
		||||
                          horizontal: -4,
 | 
			
		||||
                          vertical: -4,
 | 
			
		||||
                        ),
 | 
			
		||||
                        icon: const Icon(Symbols.add),
 | 
			
		||||
                        onPressed: () {
 | 
			
		||||
                          if (_config['maximum_auth_steps'] < 99) {
 | 
			
		||||
                            setState(() => _config['maximum_auth_steps']++);
 | 
			
		||||
                          }
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
                CheckboxListTile(
 | 
			
		||||
                  title: Text('authAlwaysRisky').tr(),
 | 
			
		||||
                  subtitle: Text('authAlwaysRiskyDescription').tr(),
 | 
			
		||||
                  contentPadding: const EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
                  value: _config['always_risky'] ?? false,
 | 
			
		||||
                  onChanged: (value) {
 | 
			
		||||
                    setState(() => _config['always_risky'] = value);
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -10,7 +10,6 @@ import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/user_directory.dart';
 | 
			
		||||
import 'package:surface/types/attachment.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
@@ -106,7 +105,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
 | 
			
		||||
        controller: _scrollController,
 | 
			
		||||
        slivers: [
 | 
			
		||||
          SliverAppBar(
 | 
			
		||||
            leading: AutoAppBarLeading(),
 | 
			
		||||
            leading: PageBackButton(),
 | 
			
		||||
            title: Text('screenAlbum').tr(),
 | 
			
		||||
          ),
 | 
			
		||||
          SliverToBoxAdapter(
 | 
			
		||||
@@ -119,7 +118,8 @@ class _AlbumScreenState extends State<AlbumScreen> {
 | 
			
		||||
                    child: CircularProgressIndicator(
 | 
			
		||||
                      value: _billing?.includedRatio ?? 0,
 | 
			
		||||
                      strokeWidth: 8,
 | 
			
		||||
                      backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
                      backgroundColor:
 | 
			
		||||
                          Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ).padding(all: 12),
 | 
			
		||||
                  const Gap(24),
 | 
			
		||||
@@ -129,7 +129,8 @@ class _AlbumScreenState extends State<AlbumScreen> {
 | 
			
		||||
                      children: [
 | 
			
		||||
                        Text('attachmentBillingUploaded').tr().bold(),
 | 
			
		||||
                        Text(
 | 
			
		||||
                          (_billing?.currentBytes ?? 0).formatBytes(decimals: 4),
 | 
			
		||||
                          (_billing?.currentBytes ?? 0)
 | 
			
		||||
                              .formatBytes(decimals: 4),
 | 
			
		||||
                          style: GoogleFonts.robotoMono(),
 | 
			
		||||
                        ),
 | 
			
		||||
                        Text('attachmentBillingDiscount').tr().bold(),
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/screens/captcha.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
			
		||||
@@ -33,10 +34,20 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
			
		||||
    final username = _usernameController.value.text;
 | 
			
		||||
    final nickname = _nicknameController.value.text;
 | 
			
		||||
    final password = _passwordController.value.text;
 | 
			
		||||
    if (email.isEmpty || username.isEmpty || nickname.isEmpty || password.isEmpty) {
 | 
			
		||||
    if (email.isEmpty ||
 | 
			
		||||
        username.isEmpty ||
 | 
			
		||||
        nickname.isEmpty ||
 | 
			
		||||
        password.isEmpty) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final captchaTk = await Navigator.of(context, rootNavigator: true).push(
 | 
			
		||||
      MaterialPageRoute(
 | 
			
		||||
        builder: (context) => CaptchaScreen(),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    if (captchaTk == null) return;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      await sn.client.post('/cgi/id/users', data: {
 | 
			
		||||
@@ -45,6 +56,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
			
		||||
        'email': email,
 | 
			
		||||
        'password': password,
 | 
			
		||||
        'language': EasyLocalization.of(context)!.currentLocale.toString(),
 | 
			
		||||
        'captcha_token': captchaTk,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      if (!context.mounted) return;
 | 
			
		||||
@@ -91,8 +103,11 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
			
		||||
                  children: [
 | 
			
		||||
                    TextFormField(
 | 
			
		||||
                      validator: (value) {
 | 
			
		||||
                        if (value == null || value.length < 4 || value.length > 32) {
 | 
			
		||||
                          return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
 | 
			
		||||
                        if (value == null ||
 | 
			
		||||
                            value.length < 4 ||
 | 
			
		||||
                            value.length > 32) {
 | 
			
		||||
                          return 'fieldUsernameLengthLimit'
 | 
			
		||||
                              .tr(args: [4.toString(), 32.toString()]);
 | 
			
		||||
                        }
 | 
			
		||||
                        if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
 | 
			
		||||
                          return 'fieldUsernameAlphanumOnly'.tr();
 | 
			
		||||
@@ -108,13 +123,17 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
			
		||||
                        border: const UnderlineInputBorder(),
 | 
			
		||||
                        labelText: 'fieldUsername'.tr(),
 | 
			
		||||
                      ),
 | 
			
		||||
                      onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                      onTapOutside: (_) =>
 | 
			
		||||
                          FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const Gap(12),
 | 
			
		||||
                    TextFormField(
 | 
			
		||||
                      validator: (value) {
 | 
			
		||||
                        if (value == null || value.length < 4 || value.length > 32) {
 | 
			
		||||
                          return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
 | 
			
		||||
                        if (value == null ||
 | 
			
		||||
                            value.length < 4 ||
 | 
			
		||||
                            value.length > 32) {
 | 
			
		||||
                          return 'fieldNicknameLengthLimit'
 | 
			
		||||
                              .tr(args: [4.toString(), 32.toString()]);
 | 
			
		||||
                        }
 | 
			
		||||
                        return null;
 | 
			
		||||
                      },
 | 
			
		||||
@@ -127,7 +146,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
			
		||||
                        border: const UnderlineInputBorder(),
 | 
			
		||||
                        labelText: 'fieldNickname'.tr(),
 | 
			
		||||
                      ),
 | 
			
		||||
                      onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                      onTapOutside: (_) =>
 | 
			
		||||
                          FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const Gap(12),
 | 
			
		||||
                    TextFormField(
 | 
			
		||||
@@ -149,7 +169,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
			
		||||
                        border: const UnderlineInputBorder(),
 | 
			
		||||
                        labelText: 'fieldEmail'.tr(),
 | 
			
		||||
                      ),
 | 
			
		||||
                      onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                      onTapOutside: (_) =>
 | 
			
		||||
                          FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const Gap(12),
 | 
			
		||||
                    TextFormField(
 | 
			
		||||
@@ -169,7 +190,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
			
		||||
                        border: const UnderlineInputBorder(),
 | 
			
		||||
                        labelText: 'fieldPassword'.tr(),
 | 
			
		||||
                      ),
 | 
			
		||||
                      onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                      onTapOutside: (_) =>
 | 
			
		||||
                          FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
                ).padding(horizontal: 7),
 | 
			
		||||
@@ -186,9 +208,13 @@ class _RegisterScreenState extends State<RegisterScreen> {
 | 
			
		||||
                        Text(
 | 
			
		||||
                          'termAcceptNextWithAgree'.tr(),
 | 
			
		||||
                          textAlign: TextAlign.end,
 | 
			
		||||
                          style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
			
		||||
                                color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
 | 
			
		||||
                              ),
 | 
			
		||||
                          style:
 | 
			
		||||
                              Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
			
		||||
                                    color: Theme.of(context)
 | 
			
		||||
                                        .colorScheme
 | 
			
		||||
                                        .onSurface
 | 
			
		||||
                                        .withAlpha((255 * 0.75).round()),
 | 
			
		||||
                                  ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        Material(
 | 
			
		||||
                          color: Colors.transparent,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										82
									
								
								lib/screens/captcha.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								lib/screens/captcha.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
import 'dart:html' as html;
 | 
			
		||||
import 'dart:ui_web' as ui;
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart' show kIsWeb;
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:surface/providers/config.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
 | 
			
		||||
class CaptchaScreen extends StatefulWidget {
 | 
			
		||||
  const CaptchaScreen({
 | 
			
		||||
    super.key,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<CaptchaScreen> createState() => _CaptchaScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _CaptchaScreenState extends State<CaptchaScreen> {
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    if (kIsWeb) {
 | 
			
		||||
      _setupWebListener();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _setupWebListener() {
 | 
			
		||||
    html.window.onMessage.listen((event) {
 | 
			
		||||
      if (event.data != null && event.data is String) {
 | 
			
		||||
        final message = event.data as String;
 | 
			
		||||
        if (message.startsWith("captcha_tk=")) {
 | 
			
		||||
          String token = message.replaceFirst("captcha_tk=", "");
 | 
			
		||||
          Navigator.pop(context, token);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Create an iframe for the captcha page
 | 
			
		||||
    final iframe = html.IFrameElement()
 | 
			
		||||
      ..src = '${context.read<ConfigProvider>().serverUrl}/captcha?redirect_uri=solink://captcha'
 | 
			
		||||
      ..style.border = 'none'
 | 
			
		||||
      ..width = '100%'
 | 
			
		||||
      ..height = '100%';
 | 
			
		||||
 | 
			
		||||
    html.document.body!.append(iframe);
 | 
			
		||||
    ui.platformViewRegistry.registerViewFactory(
 | 
			
		||||
      'captcha-iframe',
 | 
			
		||||
      (int viewId) => iframe,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final cfg = context.read<ConfigProvider>();
 | 
			
		||||
 | 
			
		||||
    if (kIsWeb) {
 | 
			
		||||
      return AppScaffold(
 | 
			
		||||
        appBar: AppBar(title: Text("reCaptcha").tr()),
 | 
			
		||||
        body: HtmlElementView(viewType: 'captcha-iframe'),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(title: Text("reCaptcha").tr()),
 | 
			
		||||
      body: InAppWebView(
 | 
			
		||||
        initialUrlRequest: URLRequest(
 | 
			
		||||
          url: WebUri('${cfg.serverUrl}/captcha?redirect_uri=solink://captcha'),
 | 
			
		||||
        ),
 | 
			
		||||
        shouldOverrideUrlLoading: (controller, navigationAction) async {
 | 
			
		||||
          Uri? url = navigationAction.request.url;
 | 
			
		||||
          if (url != null && url.queryParameters.containsKey('captcha_tk')) {
 | 
			
		||||
            Navigator.pop(context, url.queryParameters['captcha_tk']!);
 | 
			
		||||
            return NavigationActionPolicy.CANCEL;
 | 
			
		||||
          }
 | 
			
		||||
          return NavigationActionPolicy.ALLOW;
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -52,8 +52,10 @@ class ChatRoomScreen extends StatefulWidget {
 | 
			
		||||
class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
			
		||||
  bool _isBusy = false;
 | 
			
		||||
  bool _isCalling = false;
 | 
			
		||||
  bool _isJoining = false;
 | 
			
		||||
 | 
			
		||||
  SnChannel? _channel;
 | 
			
		||||
  SnChannelMember? _currentMember;
 | 
			
		||||
  SnChannelMember? _otherMember;
 | 
			
		||||
  SnChatCall? _ongoingCall;
 | 
			
		||||
 | 
			
		||||
@@ -67,7 +69,24 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
			
		||||
 | 
			
		||||
  StreamSubscription? _wsSubscription;
 | 
			
		||||
 | 
			
		||||
  // TODO fetch user identity and ask them to join the channel or not
 | 
			
		||||
  Future<void> _joinChannel() async {
 | 
			
		||||
    try {
 | 
			
		||||
      setState(() => _isJoining = true);
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final ua = context.read<UserProvider>();
 | 
			
		||||
      await sn.client
 | 
			
		||||
          .post('/cgi/im/channels/${_channel!.keyPath}/members', data: {
 | 
			
		||||
        'related': ua.user?.name,
 | 
			
		||||
      });
 | 
			
		||||
      _initializeChat();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isJoining = true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchChannel() async {
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
 | 
			
		||||
@@ -76,6 +95,12 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
			
		||||
      _channel = await chan.getChannel('${widget.scope}:${widget.alias}');
 | 
			
		||||
 | 
			
		||||
      if (!mounted || _channel == null) return;
 | 
			
		||||
      final ct = context.read<ChatChannelProvider>();
 | 
			
		||||
      try {
 | 
			
		||||
        _currentMember = await ct.getChannelProfile(_channel!);
 | 
			
		||||
      } catch (_) {}
 | 
			
		||||
 | 
			
		||||
      if (!mounted || _currentMember == null) return;
 | 
			
		||||
      final ud = context.read<UserDirectoryProvider>();
 | 
			
		||||
      final ua = context.read<UserProvider>();
 | 
			
		||||
      if (_channel!.type == 1) {
 | 
			
		||||
@@ -204,11 +229,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
			
		||||
    return a.createdAt.difference(b.createdAt).inMinutes <= 3;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _messageController = ChatMessageController(context);
 | 
			
		||||
  Future<void> _initializeChat() async {
 | 
			
		||||
    _fetchChannel().then((_) async {
 | 
			
		||||
      if (_currentMember == null) return;
 | 
			
		||||
      await _messageController.initialize(_channel!);
 | 
			
		||||
 | 
			
		||||
      if (widget.extra != null) {
 | 
			
		||||
@@ -230,6 +253,13 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
			
		||||
        _fetchOngoingCall(),
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _messageController = ChatMessageController(context);
 | 
			
		||||
    _initializeChat();
 | 
			
		||||
 | 
			
		||||
    _wsSubscription = _ws.pk.stream.listen((event) {
 | 
			
		||||
      switch (event.method) {
 | 
			
		||||
@@ -281,25 +311,27 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
			
		||||
              : _channel?.name ?? 'loading'.tr(),
 | 
			
		||||
        ),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              setState(() => _isEncrypted = !_isEncrypted);
 | 
			
		||||
              _inputGlobalKey.currentState?.setEncrypted(_isEncrypted);
 | 
			
		||||
            },
 | 
			
		||||
            icon: _isEncrypted
 | 
			
		||||
                ? const Icon(Symbols.lock)
 | 
			
		||||
                : const Icon(Symbols.no_encryption),
 | 
			
		||||
          ),
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: _ongoingCall == null
 | 
			
		||||
                ? const Icon(Symbols.call)
 | 
			
		||||
                : const Icon(Symbols.call_end),
 | 
			
		||||
            onPressed: _isCalling
 | 
			
		||||
                ? null
 | 
			
		||||
                : _ongoingCall == null
 | 
			
		||||
                    ? _makeCall
 | 
			
		||||
                    : _endCall,
 | 
			
		||||
          ),
 | 
			
		||||
          if (_currentMember != null)
 | 
			
		||||
            IconButton(
 | 
			
		||||
              onPressed: () {
 | 
			
		||||
                setState(() => _isEncrypted = !_isEncrypted);
 | 
			
		||||
                _inputGlobalKey.currentState?.setEncrypted(_isEncrypted);
 | 
			
		||||
              },
 | 
			
		||||
              icon: _isEncrypted
 | 
			
		||||
                  ? const Icon(Symbols.lock)
 | 
			
		||||
                  : const Icon(Symbols.no_encryption),
 | 
			
		||||
            ),
 | 
			
		||||
          if (_currentMember != null)
 | 
			
		||||
            IconButton(
 | 
			
		||||
              icon: _ongoingCall == null
 | 
			
		||||
                  ? const Icon(Symbols.call)
 | 
			
		||||
                  : const Icon(Symbols.call_end),
 | 
			
		||||
              onPressed: _isCalling
 | 
			
		||||
                  ? null
 | 
			
		||||
                  : _ongoingCall == null
 | 
			
		||||
                      ? _makeCall
 | 
			
		||||
                      : _endCall,
 | 
			
		||||
            ),
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: const Icon(Symbols.more_vert),
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
@@ -348,7 +380,41 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
			
		||||
              ).height(_ongoingCall != null ? 54 : 0, animate: true).animate(
 | 
			
		||||
                  const Duration(milliseconds: 300),
 | 
			
		||||
                  Curves.fastLinearToSlowEaseIn),
 | 
			
		||||
              if (_messageController.isPending)
 | 
			
		||||
              if (_currentMember == null && !_isBusy)
 | 
			
		||||
                Expanded(
 | 
			
		||||
                  child: Center(
 | 
			
		||||
                    child: Container(
 | 
			
		||||
                      constraints: const BoxConstraints(maxWidth: 280),
 | 
			
		||||
                      child: Column(
 | 
			
		||||
                        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          const Icon(Symbols.person_remove, size: 40, fill: 1),
 | 
			
		||||
                          const Gap(8),
 | 
			
		||||
                          Text('chatUnjoined'.tr(), textAlign: TextAlign.center)
 | 
			
		||||
                              .fontSize(16)
 | 
			
		||||
                              .bold(),
 | 
			
		||||
                          Text('chatUnjoinedDescription'.tr(),
 | 
			
		||||
                                  textAlign: TextAlign.center)
 | 
			
		||||
                              .fontSize(13),
 | 
			
		||||
                          if (_channel!.isPublic)
 | 
			
		||||
                            Text('chatUnjoinedPublicDescription'.tr(),
 | 
			
		||||
                                    textAlign: TextAlign.center)
 | 
			
		||||
                                .fontSize(13)
 | 
			
		||||
                                .padding(top: 8),
 | 
			
		||||
                          if (_channel!.isPublic)
 | 
			
		||||
                            TextButton(
 | 
			
		||||
                              style: ButtonStyle(
 | 
			
		||||
                                visualDensity: VisualDensity.compact,
 | 
			
		||||
                              ),
 | 
			
		||||
                              onPressed: _isJoining ? null : _joinChannel,
 | 
			
		||||
                              child: Text('chatJoin').tr(),
 | 
			
		||||
                            ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                )
 | 
			
		||||
              else if (_messageController.isPending)
 | 
			
		||||
                Expanded(
 | 
			
		||||
                  child: const CircularProgressIndicator().center(),
 | 
			
		||||
                )
 | 
			
		||||
@@ -403,7 +469,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              if (!_messageController.isPending)
 | 
			
		||||
              if (!_messageController.isPending && _currentMember != null)
 | 
			
		||||
                Material(
 | 
			
		||||
                  elevation: 2,
 | 
			
		||||
                  child: Column(
 | 
			
		||||
 
 | 
			
		||||
@@ -449,7 +449,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
 | 
			
		||||
          data: ele.toJson(),
 | 
			
		||||
          createdAt: ele.createdAt)),
 | 
			
		||||
    );
 | 
			
		||||
    _hasLoadedAll = postCount >= _feed.length;
 | 
			
		||||
    _hasLoadedAll = _feed.length >= postCount;
 | 
			
		||||
 | 
			
		||||
    if (mounted) setState(() => _isBusy = false);
 | 
			
		||||
  }
 | 
			
		||||
@@ -551,9 +551,11 @@ class _PostListWidgetState extends State<_PostListWidget> {
 | 
			
		||||
                  maxWidth: 640,
 | 
			
		||||
                );
 | 
			
		||||
              case 'reader.news':
 | 
			
		||||
                return Container(
 | 
			
		||||
                  constraints: BoxConstraints(maxWidth: 640),
 | 
			
		||||
                  child: NewsFeedEntry(data: ele),
 | 
			
		||||
                return Center(
 | 
			
		||||
                  child: Container(
 | 
			
		||||
                    constraints: BoxConstraints(maxWidth: 640),
 | 
			
		||||
                    child: NewsFeedEntry(data: ele),
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
              default:
 | 
			
		||||
                return Container(
 | 
			
		||||
 
 | 
			
		||||
@@ -201,7 +201,7 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
			
		||||
    if (!ua.isAuthorized) {
 | 
			
		||||
      return AppScaffold(
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          leading: AutoAppBarLeading(),
 | 
			
		||||
          leading: PageBackButton(),
 | 
			
		||||
          title: Text('screenFriend').tr(),
 | 
			
		||||
        ),
 | 
			
		||||
        body: Center(
 | 
			
		||||
@@ -254,7 +254,8 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
			
		||||
              trailing: const Icon(Symbols.chevron_right),
 | 
			
		||||
              onTap: _showBlocks,
 | 
			
		||||
            ),
 | 
			
		||||
          if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1),
 | 
			
		||||
          if (_requests.isNotEmpty || _blocks.isNotEmpty)
 | 
			
		||||
            const Divider(height: 1),
 | 
			
		||||
          Expanded(
 | 
			
		||||
            child: MediaQuery.removePadding(
 | 
			
		||||
              context: context,
 | 
			
		||||
@@ -270,7 +271,8 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
			
		||||
                    final relation = _relations[index];
 | 
			
		||||
                    final other = relation.related;
 | 
			
		||||
                    return ListTile(
 | 
			
		||||
                      contentPadding: const EdgeInsets.only(right: 24, left: 16),
 | 
			
		||||
                      contentPadding:
 | 
			
		||||
                          const EdgeInsets.only(right: 24, left: 16),
 | 
			
		||||
                      leading: AccountImage(content: other?.avatar),
 | 
			
		||||
                      title: Text(other?.nick ?? 'unknown'),
 | 
			
		||||
                      subtitle: Text(other?.nick ?? 'unknown'),
 | 
			
		||||
@@ -286,12 +288,16 @@ class _FriendScreenState extends State<FriendScreen> {
 | 
			
		||||
                              mainAxisAlignment: MainAxisAlignment.end,
 | 
			
		||||
                              children: [
 | 
			
		||||
                                InkWell(
 | 
			
		||||
                                  onTap: _isUpdating ? null : () => _changeRelation(relation, 2),
 | 
			
		||||
                                  onTap: _isUpdating
 | 
			
		||||
                                      ? null
 | 
			
		||||
                                      : () => _changeRelation(relation, 2),
 | 
			
		||||
                                  child: Text('friendBlock').tr(),
 | 
			
		||||
                                ),
 | 
			
		||||
                                const Gap(8),
 | 
			
		||||
                                InkWell(
 | 
			
		||||
                                  onTap: _isUpdating ? null : () => _deleteRelation(relation),
 | 
			
		||||
                                  onTap: _isUpdating
 | 
			
		||||
                                      ? null
 | 
			
		||||
                                      : () => _deleteRelation(relation),
 | 
			
		||||
                                  child: Text('friendDeleteAction').tr(),
 | 
			
		||||
                                ),
 | 
			
		||||
                              ],
 | 
			
		||||
@@ -420,7 +426,9 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
 | 
			
		||||
              mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.end,
 | 
			
		||||
              children: [
 | 
			
		||||
                Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75),
 | 
			
		||||
                Text(kFriendStatus[relation.status] ?? 'unknown')
 | 
			
		||||
                    .tr()
 | 
			
		||||
                    .opacity(0.75),
 | 
			
		||||
                if (relation.status == 0)
 | 
			
		||||
                  Row(
 | 
			
		||||
                    mainAxisAlignment: MainAxisAlignment.end,
 | 
			
		||||
@@ -441,7 +449,8 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
 | 
			
		||||
                    mainAxisAlignment: MainAxisAlignment.end,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      InkWell(
 | 
			
		||||
                        onTap: _isBusy ? null : () => _changeRelation(relation, 1),
 | 
			
		||||
                        onTap:
 | 
			
		||||
                            _isBusy ? null : () => _changeRelation(relation, 1),
 | 
			
		||||
                        child: Text('friendUnblock').tr(),
 | 
			
		||||
                      ),
 | 
			
		||||
                      const Gap(8),
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/special_day.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/providers/widget.dart';
 | 
			
		||||
import 'package:surface/screens/captcha.dart';
 | 
			
		||||
import 'package:surface/types/check_in.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
@@ -389,7 +390,7 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
 | 
			
		||||
                        size: 20,
 | 
			
		||||
                      ),
 | 
			
		||||
                      const Gap(10),
 | 
			
		||||
                      Text('serviceStatusOperational').tr(),
 | 
			
		||||
                      Text('loading').tr(),
 | 
			
		||||
                    ],
 | 
			
		||||
                  )
 | 
			
		||||
                : switch (_serviceStatus) {
 | 
			
		||||
@@ -434,6 +435,7 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
 | 
			
		||||
                padding: EdgeInsets.only(top: 6),
 | 
			
		||||
                child: Wrap(
 | 
			
		||||
                  spacing: 8,
 | 
			
		||||
                  runSpacing: 8,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    for (final entry in _statuses!.entries)
 | 
			
		||||
                      Tooltip(
 | 
			
		||||
@@ -441,6 +443,8 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
 | 
			
		||||
                            ? 'serviceName${kServicesName[entry.key]}'.tr()
 | 
			
		||||
                            : 'unknown'.tr(),
 | 
			
		||||
                        child: Chip(
 | 
			
		||||
                          visualDensity:
 | 
			
		||||
                              VisualDensity(horizontal: -4, vertical: -4),
 | 
			
		||||
                          avatar: entry.value
 | 
			
		||||
                              ? const Icon(
 | 
			
		||||
                                  Symbols.circle,
 | 
			
		||||
@@ -505,11 +509,20 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _doCheckIn() async {
 | 
			
		||||
    final captchaTk = await Navigator.of(context, rootNavigator: true).push(
 | 
			
		||||
      MaterialPageRoute(
 | 
			
		||||
        builder: (context) => CaptchaScreen(),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    if (captchaTk == null) return;
 | 
			
		||||
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
    try {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final home = context.read<HomeWidgetProvider>();
 | 
			
		||||
      final resp = await sn.client.post('/cgi/id/check-in');
 | 
			
		||||
      final resp = await sn.client.post('/cgi/id/check-in', data: {
 | 
			
		||||
        'captcha_token': captchaTk,
 | 
			
		||||
      });
 | 
			
		||||
      _todayRecord = SnCheckInRecord.fromJson(resp.data);
 | 
			
		||||
      await home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
@@ -793,7 +806,7 @@ class _HomeDashNotificationWidgetState
 | 
			
		||||
              child: IconButton(
 | 
			
		||||
                icon: const Icon(Symbols.arrow_right_alt),
 | 
			
		||||
                onPressed: () {
 | 
			
		||||
                  GoRouter.of(context).goNamed('notification');
 | 
			
		||||
                  GoRouter.of(context).pushNamed('notification');
 | 
			
		||||
                },
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
@@ -877,8 +890,10 @@ class _HomeDashRecommendationPostWidgetState
 | 
			
		||||
                  ).tr(),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
              Text('${_currentPage + 1}/${_posts?.length ?? 0}',
 | 
			
		||||
                  style: GoogleFonts.robotoMono())
 | 
			
		||||
              Text(
 | 
			
		||||
                '${_currentPage + 1}/${_posts?.length ?? 0}',
 | 
			
		||||
                style: GoogleFonts.robotoMono(),
 | 
			
		||||
              )
 | 
			
		||||
            ],
 | 
			
		||||
          ).padding(horizontal: 18, top: 12, bottom: 8),
 | 
			
		||||
          Expanded(
 | 
			
		||||
@@ -896,6 +911,7 @@ class _HomeDashRecommendationPostWidgetState
 | 
			
		||||
                    child: PostItem(
 | 
			
		||||
                      data: _posts![index],
 | 
			
		||||
                      showMenu: false,
 | 
			
		||||
                      showFullPost: true,
 | 
			
		||||
                    ).padding(bottom: 8),
 | 
			
		||||
                    onTap: () {
 | 
			
		||||
                      GoRouter.of(context)
 | 
			
		||||
 
 | 
			
		||||
@@ -63,7 +63,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
			
		||||
        queryParameters: {'take': 10, 'offset': _notifications.length},
 | 
			
		||||
      );
 | 
			
		||||
      _totalCount = resp.data['count'];
 | 
			
		||||
      _notifications.addAll(resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? []);
 | 
			
		||||
      _notifications.addAll(resp.data['data']
 | 
			
		||||
              ?.map((e) => SnNotification.fromJson(e))
 | 
			
		||||
              .cast<SnNotification>() ??
 | 
			
		||||
          []);
 | 
			
		||||
      nty.updateTray();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
@@ -98,7 +101,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
			
		||||
      nty.clear();
 | 
			
		||||
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showSnackbar('notificationMarkAllReadPrompt'.plural(resp.data['count']));
 | 
			
		||||
      context.showSnackbar(
 | 
			
		||||
          'notificationMarkAllReadPrompt'.plural(resp.data['count']));
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
@@ -122,7 +126,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
			
		||||
      _fetchNotifications();
 | 
			
		||||
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showSnackbar('notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']));
 | 
			
		||||
      context.showSnackbar(
 | 
			
		||||
          'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']));
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
@@ -143,7 +148,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
			
		||||
 | 
			
		||||
    if (!ua.isAuthorized) {
 | 
			
		||||
      return AppScaffold(
 | 
			
		||||
        appBar: AppBar(leading: AutoAppBarLeading(), title: Text('screenNotification').tr()),
 | 
			
		||||
        appBar: AppBar(
 | 
			
		||||
          leading: PageBackButton(),
 | 
			
		||||
          title: Text('screenNotification').tr(),
 | 
			
		||||
        ),
 | 
			
		||||
        body: Center(child: UnauthorizedHint()),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
@@ -153,7 +161,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        title: Text('screenNotification').tr(),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(icon: const Icon(Symbols.checklist), onPressed: _isSubmitting ? null : _markAllAsRead),
 | 
			
		||||
          IconButton(
 | 
			
		||||
              icon: const Icon(Symbols.checklist),
 | 
			
		||||
              onPressed: _isSubmitting ? null : _markAllAsRead),
 | 
			
		||||
          const Gap(8),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
@@ -167,13 +177,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
			
		||||
                return _fetchNotifications();
 | 
			
		||||
              },
 | 
			
		||||
              child: InfiniteList(
 | 
			
		||||
                padding: EdgeInsets.only(top: 16, bottom: math.max(MediaQuery.of(context).padding.bottom, 16)),
 | 
			
		||||
                padding: EdgeInsets.only(
 | 
			
		||||
                    top: 16,
 | 
			
		||||
                    bottom:
 | 
			
		||||
                        math.max(MediaQuery.of(context).padding.bottom, 16)),
 | 
			
		||||
                itemCount: _notifications.length,
 | 
			
		||||
                onFetchData: () {
 | 
			
		||||
                  _fetchNotifications();
 | 
			
		||||
                },
 | 
			
		||||
                isLoading: _isBusy,
 | 
			
		||||
                hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!,
 | 
			
		||||
                hasReachedMax: _totalCount != null &&
 | 
			
		||||
                    _notifications.length >= _totalCount!,
 | 
			
		||||
                itemBuilder: (context, idx) {
 | 
			
		||||
                  final nty = _notifications[idx];
 | 
			
		||||
                  return Row(
 | 
			
		||||
@@ -186,12 +200,19 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
			
		||||
                          crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                          children: [
 | 
			
		||||
                            if (nty.readAt == null)
 | 
			
		||||
                              StyledWidget(Badge(label: Text('notificationUnread').tr())).padding(bottom: 4),
 | 
			
		||||
                            Text(nty.title, style: Theme.of(context).textTheme.titleMedium),
 | 
			
		||||
                              StyledWidget(Badge(
 | 
			
		||||
                                      label: Text('notificationUnread').tr()))
 | 
			
		||||
                                  .padding(bottom: 4),
 | 
			
		||||
                            Text(nty.title,
 | 
			
		||||
                                style: Theme.of(context).textTheme.titleMedium),
 | 
			
		||||
                            if (nty.subtitle != null)
 | 
			
		||||
                              Text(nty.subtitle!, style: Theme.of(context).textTheme.titleSmall),
 | 
			
		||||
                              Text(nty.subtitle!,
 | 
			
		||||
                                  style:
 | 
			
		||||
                                      Theme.of(context).textTheme.titleSmall),
 | 
			
		||||
                            if (nty.subtitle != null) const Gap(4),
 | 
			
		||||
                            SelectionArea(child: MarkdownTextContent(content: nty.body, isAutoWarp: true)),
 | 
			
		||||
                            SelectionArea(
 | 
			
		||||
                                child: MarkdownTextContent(
 | 
			
		||||
                                    content: nty.body, isAutoWarp: true)),
 | 
			
		||||
                            if ([
 | 
			
		||||
                                  'interactive.reply',
 | 
			
		||||
                                  'interactive.feedback',
 | 
			
		||||
@@ -201,31 +222,43 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
			
		||||
                              GestureDetector(
 | 
			
		||||
                                child: Container(
 | 
			
		||||
                                  decoration: BoxDecoration(
 | 
			
		||||
                                    borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                                    border: Border.all(color: Theme.of(context).dividerColor, width: 1),
 | 
			
		||||
                                    borderRadius: const BorderRadius.all(
 | 
			
		||||
                                        Radius.circular(8)),
 | 
			
		||||
                                    border: Border.all(
 | 
			
		||||
                                        color: Theme.of(context).dividerColor,
 | 
			
		||||
                                        width: 1),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  child: PostItem(
 | 
			
		||||
                                    data: SnPost.fromJson(nty.metadata['related_post']!),
 | 
			
		||||
                                    data: SnPost.fromJson(
 | 
			
		||||
                                        nty.metadata['related_post']!),
 | 
			
		||||
                                    showComments: false,
 | 
			
		||||
                                    showReactions: false,
 | 
			
		||||
                                    showMenu: false,
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  ).padding(vertical: 4),
 | 
			
		||||
                                ),
 | 
			
		||||
                                onTap: () {
 | 
			
		||||
                                  GoRouter.of(context).pushNamed(
 | 
			
		||||
                                    'postDetail',
 | 
			
		||||
                                    pathParameters: {'slug': nty.metadata['related_post']!['id'].toString()},
 | 
			
		||||
                                    pathParameters: {
 | 
			
		||||
                                      'slug': nty
 | 
			
		||||
                                          .metadata['related_post']!['id']
 | 
			
		||||
                                          .toString()
 | 
			
		||||
                                    },
 | 
			
		||||
                                  );
 | 
			
		||||
                                },
 | 
			
		||||
                              ).padding(top: 8),
 | 
			
		||||
                            const Gap(8),
 | 
			
		||||
                            Row(
 | 
			
		||||
                              children: [
 | 
			
		||||
                                Text(DateFormat('yy/MM/dd').format(nty.createdAt)).fontSize(12),
 | 
			
		||||
                                Text(DateFormat('yy/MM/dd')
 | 
			
		||||
                                        .format(nty.createdAt))
 | 
			
		||||
                                    .fontSize(12),
 | 
			
		||||
                                const Gap(4),
 | 
			
		||||
                                Text('·', style: TextStyle(fontSize: 12)),
 | 
			
		||||
                                const Gap(4),
 | 
			
		||||
                                Text(RelativeTime(context).format(nty.createdAt)).fontSize(12),
 | 
			
		||||
                                Text(RelativeTime(context)
 | 
			
		||||
                                        .format(nty.createdAt))
 | 
			
		||||
                                    .fontSize(12),
 | 
			
		||||
                              ],
 | 
			
		||||
                            ).opacity(0.75),
 | 
			
		||||
                          ],
 | 
			
		||||
@@ -235,8 +268,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        icon: const Icon(Symbols.check),
 | 
			
		||||
                        padding: EdgeInsets.all(0),
 | 
			
		||||
                        visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
 | 
			
		||||
                        onPressed: _isSubmitting ? null : () => _markOneAsRead(nty),
 | 
			
		||||
                        visualDensity:
 | 
			
		||||
                            const VisualDensity(horizontal: -4, vertical: -4),
 | 
			
		||||
                        onPressed:
 | 
			
		||||
                            _isSubmitting ? null : () => _markOneAsRead(nty),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ).padding(horizontal: 16);
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,8 @@ class PostDetailScreen extends StatefulWidget {
 | 
			
		||||
  final SnPost? preload;
 | 
			
		||||
  final Function? onBack;
 | 
			
		||||
 | 
			
		||||
  const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack});
 | 
			
		||||
  const PostDetailScreen(
 | 
			
		||||
      {super.key, required this.slug, this.preload, this.onBack});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<PostDetailScreen> createState() => _PostDetailScreenState();
 | 
			
		||||
@@ -88,14 +89,16 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
 | 
			
		||||
                    TextSpan(
 | 
			
		||||
                      text: _data?.body['title'] ?? 'postNoun'.tr(),
 | 
			
		||||
                      style: Theme.of(context).textTheme.titleLarge!.copyWith(
 | 
			
		||||
                            color: Theme.of(context).appBarTheme.foregroundColor!,
 | 
			
		||||
                            color:
 | 
			
		||||
                                Theme.of(context).appBarTheme.foregroundColor!,
 | 
			
		||||
                          ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const TextSpan(text: '\n'),
 | 
			
		||||
                    TextSpan(
 | 
			
		||||
                      text: 'postDetail'.tr(),
 | 
			
		||||
                      style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
			
		||||
                            color: Theme.of(context).appBarTheme.foregroundColor!,
 | 
			
		||||
                            color:
 | 
			
		||||
                                Theme.of(context).appBarTheme.foregroundColor!,
 | 
			
		||||
                          ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ]),
 | 
			
		||||
@@ -124,8 +127,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            if (_data != null && _data!.type != 'video') const SliverToBoxAdapter(child: Divider(height: 1)),
 | 
			
		||||
            if (_data != null && _data!.type != 'video')
 | 
			
		||||
            if (_data != null)
 | 
			
		||||
              SliverToBoxAdapter(
 | 
			
		||||
                child: Divider(height: 1).padding(top: 8),
 | 
			
		||||
              ),
 | 
			
		||||
            if (_data != null)
 | 
			
		||||
              SliverToBoxAdapter(
 | 
			
		||||
                child: Container(
 | 
			
		||||
                  constraints: BoxConstraints(maxWidth: maxWidth),
 | 
			
		||||
@@ -141,7 +147,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
 | 
			
		||||
                  ).padding(horizontal: 20, vertical: 12).center(),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            if (_data != null && ua.isAuthorized && _data!.type != 'video')
 | 
			
		||||
            if (_data != null && ua.isAuthorized)
 | 
			
		||||
              SliverToBoxAdapter(
 | 
			
		||||
                child: PostCommentQuickAction(
 | 
			
		||||
                  parentPost: _data!,
 | 
			
		||||
@@ -158,13 +164,15 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            if (_data != null && _data!.type != 'video')
 | 
			
		||||
            if (_data != null) SliverGap(8),
 | 
			
		||||
            if (_data != null)
 | 
			
		||||
              PostCommentSliverList(
 | 
			
		||||
                key: _childListKey,
 | 
			
		||||
                parentPost: _data!,
 | 
			
		||||
                maxWidth: maxWidth,
 | 
			
		||||
              ),
 | 
			
		||||
            if (_data != null && _data!.type == 'video') SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
 | 
			
		||||
            if (_data != null)
 | 
			
		||||
              SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/post.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
@@ -77,7 +77,8 @@ class _PostDraftBoxState extends State<PostDraftBox> {
 | 
			
		||||
                    },
 | 
			
		||||
                  );
 | 
			
		||||
                },
 | 
			
		||||
                separatorBuilder: (_, __) => const Gap(8),
 | 
			
		||||
                separatorBuilder: (_, __) =>
 | 
			
		||||
                    const Divider().padding(vertical: 2),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
 
 | 
			
		||||
@@ -45,12 +45,14 @@ class PostEditorExtra {
 | 
			
		||||
  final String? title;
 | 
			
		||||
  final String? description;
 | 
			
		||||
  final List<PostWriteMedia>? attachments;
 | 
			
		||||
  final SnRealm? realm;
 | 
			
		||||
 | 
			
		||||
  const PostEditorExtra({
 | 
			
		||||
    this.text,
 | 
			
		||||
    this.title,
 | 
			
		||||
    this.description,
 | 
			
		||||
    this.attachments,
 | 
			
		||||
    this.realm,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -263,6 +265,7 @@ class _PostEditorScreenState extends State<PostEditorScreen>
 | 
			
		||||
      _writeController.descriptionController.text =
 | 
			
		||||
          widget.extraProps!.description ?? '';
 | 
			
		||||
      _writeController.addAttachments(widget.extraProps!.attachments ?? []);
 | 
			
		||||
      _writeController.setRealm(widget.extraProps!.realm);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -45,7 +45,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchPosts() async {
 | 
			
		||||
    if (_searchTerm.isEmpty && _searchCategories.isEmpty && _searchTags.isEmpty) return;
 | 
			
		||||
    if (_searchTerm.isEmpty && _searchCategories.isEmpty && _searchTags.isEmpty)
 | 
			
		||||
      return;
 | 
			
		||||
    if (_postCount != null && _posts.length >= _postCount!) return;
 | 
			
		||||
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
@@ -152,7 +153,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
 | 
			
		||||
                },
 | 
			
		||||
              );
 | 
			
		||||
            },
 | 
			
		||||
            separatorBuilder: (_, __) => const Gap(8),
 | 
			
		||||
            separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
 | 
			
		||||
          ),
 | 
			
		||||
          Positioned(
 | 
			
		||||
            top: 16,
 | 
			
		||||
@@ -166,7 +167,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
 | 
			
		||||
                  padding: const WidgetStatePropertyAll(
 | 
			
		||||
                    EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  onTapOutside: (_) =>
 | 
			
		||||
                      FocusManager.instance.primaryFocus?.unfocus(),
 | 
			
		||||
                  onChanged: (value) {
 | 
			
		||||
                    _searchTerm = value;
 | 
			
		||||
                  },
 | 
			
		||||
 
 | 
			
		||||
@@ -34,9 +34,11 @@ class PostPublisherScreen extends StatefulWidget {
 | 
			
		||||
  State<PostPublisherScreen> createState() => _PostPublisherScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTickerProviderStateMixin {
 | 
			
		||||
class _PostPublisherScreenState extends State<PostPublisherScreen>
 | 
			
		||||
    with SingleTickerProviderStateMixin {
 | 
			
		||||
  late final ScrollController _scrollController = ScrollController();
 | 
			
		||||
  late final TabController _tabController = TabController(length: 3, vsync: this);
 | 
			
		||||
  late final TabController _tabController =
 | 
			
		||||
      TabController(length: 3, vsync: this);
 | 
			
		||||
 | 
			
		||||
  SnPublisher? _publisher;
 | 
			
		||||
  SnAccount? _account;
 | 
			
		||||
@@ -66,7 +68,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
      _account = await ud.getAccount(_publisher?.accountId);
 | 
			
		||||
      _accountRelationship = await rel.getRelationship(_account!.id);
 | 
			
		||||
      if (_publisher?.realmId != null && _publisher!.realmId != 0) {
 | 
			
		||||
        final resp = await sn.client.get('/cgi/id/realms/${_publisher!.realmId}');
 | 
			
		||||
        final resp =
 | 
			
		||||
            await sn.client.get('/cgi/id/realms/${_publisher!.realmId}');
 | 
			
		||||
        _realm = SnRealm.fromJson(resp.data);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (_) {
 | 
			
		||||
@@ -133,12 +136,14 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
  double _appBarBlur = 0.0;
 | 
			
		||||
 | 
			
		||||
  late final _appBarWidth = MediaQuery.of(context).size.width;
 | 
			
		||||
  late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
 | 
			
		||||
  late final _appBarHeight =
 | 
			
		||||
      (_appBarWidth * kBannerAspectRatio).roundToDouble();
 | 
			
		||||
 | 
			
		||||
  void _updateAppBarBlur() {
 | 
			
		||||
    if (_scrollController.offset > _appBarHeight) return;
 | 
			
		||||
    setState(() {
 | 
			
		||||
      _appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
 | 
			
		||||
      _appBarBlur =
 | 
			
		||||
          (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -193,7 +198,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
        'related': _account!.name,
 | 
			
		||||
      });
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
 | 
			
		||||
      context.showSnackbar(
 | 
			
		||||
          'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
@@ -209,9 +215,11 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final rel = context.read<SnRelationshipProvider>();
 | 
			
		||||
      await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
 | 
			
		||||
      await rel.updateRelationship(
 | 
			
		||||
          _account!.id, 1, _accountRelationship?.permNodes ?? {});
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
 | 
			
		||||
      context.showSnackbar(
 | 
			
		||||
          'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
@@ -299,7 +307,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
                              text: TextSpan(children: [
 | 
			
		||||
                                TextSpan(
 | 
			
		||||
                                  text: _publisher!.nick,
 | 
			
		||||
                                  style: Theme.of(context).textTheme.titleLarge!.copyWith(
 | 
			
		||||
                                  style: Theme.of(context)
 | 
			
		||||
                                      .textTheme
 | 
			
		||||
                                      .titleLarge!
 | 
			
		||||
                                      .copyWith(
 | 
			
		||||
                                        color: Colors.white,
 | 
			
		||||
                                        shadows: labelShadows,
 | 
			
		||||
                                      ),
 | 
			
		||||
@@ -307,7 +318,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
                                const TextSpan(text: '\n'),
 | 
			
		||||
                                TextSpan(
 | 
			
		||||
                                  text: '@${_publisher!.name}',
 | 
			
		||||
                                  style: Theme.of(context).textTheme.bodySmall!.copyWith(
 | 
			
		||||
                                  style: Theme.of(context)
 | 
			
		||||
                                      .textTheme
 | 
			
		||||
                                      .bodySmall!
 | 
			
		||||
                                      .copyWith(
 | 
			
		||||
                                        color: Colors.white,
 | 
			
		||||
                                        shadows: labelShadows,
 | 
			
		||||
                                      ),
 | 
			
		||||
@@ -330,13 +344,16 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
                                  )
 | 
			
		||||
                                else
 | 
			
		||||
                                  Container(
 | 
			
		||||
                                    color: Theme.of(context).colorScheme.surfaceContainer,
 | 
			
		||||
                                    color: Theme.of(context)
 | 
			
		||||
                                        .colorScheme
 | 
			
		||||
                                        .surfaceContainer,
 | 
			
		||||
                                  ),
 | 
			
		||||
                                Positioned(
 | 
			
		||||
                                  top: 0,
 | 
			
		||||
                                  left: 0,
 | 
			
		||||
                                  right: 0,
 | 
			
		||||
                                  height: 56 + MediaQuery.of(context).padding.top,
 | 
			
		||||
                                  height:
 | 
			
		||||
                                      56 + MediaQuery.of(context).padding.top,
 | 
			
		||||
                                  child: ClipRect(
 | 
			
		||||
                                    child: BackdropFilter(
 | 
			
		||||
                                      filter: ImageFilter.blur(
 | 
			
		||||
@@ -345,7 +362,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
                                      ),
 | 
			
		||||
                                      child: Container(
 | 
			
		||||
                                        color: Colors.black.withOpacity(
 | 
			
		||||
                                          clampDouble(_appBarBlur * 0.1, 0, 0.5),
 | 
			
		||||
                                          clampDouble(
 | 
			
		||||
                                              _appBarBlur * 0.1, 0, 0.5),
 | 
			
		||||
                                        ),
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ),
 | 
			
		||||
@@ -372,11 +390,14 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
                                const Gap(16),
 | 
			
		||||
                                Expanded(
 | 
			
		||||
                                  child: Column(
 | 
			
		||||
                                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                                    crossAxisAlignment:
 | 
			
		||||
                                        CrossAxisAlignment.start,
 | 
			
		||||
                                    children: [
 | 
			
		||||
                                      Text(
 | 
			
		||||
                                        _publisher!.nick,
 | 
			
		||||
                                        style: Theme.of(context).textTheme.titleMedium,
 | 
			
		||||
                                        style: Theme.of(context)
 | 
			
		||||
                                            .textTheme
 | 
			
		||||
                                            .titleMedium,
 | 
			
		||||
                                      ).bold(),
 | 
			
		||||
                                      Text('@${_publisher!.name}').fontSize(13),
 | 
			
		||||
                                    ],
 | 
			
		||||
@@ -387,7 +408,9 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
                                    style: ButtonStyle(
 | 
			
		||||
                                      elevation: WidgetStatePropertyAll(0),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    onPressed: _isSubscribing ? null : _toggleSubscription,
 | 
			
		||||
                                    onPressed: _isSubscribing
 | 
			
		||||
                                        ? null
 | 
			
		||||
                                        : _toggleSubscription,
 | 
			
		||||
                                    label: Text('subscribe').tr(),
 | 
			
		||||
                                    icon: const Icon(Symbols.add),
 | 
			
		||||
                                  )
 | 
			
		||||
@@ -396,14 +419,17 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
                                    style: ButtonStyle(
 | 
			
		||||
                                      elevation: WidgetStatePropertyAll(0),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    onPressed: _isSubscribing ? null : _toggleSubscription,
 | 
			
		||||
                                    onPressed: _isSubscribing
 | 
			
		||||
                                        ? null
 | 
			
		||||
                                        : _toggleSubscription,
 | 
			
		||||
                                    label: Text('unsubscribe').tr(),
 | 
			
		||||
                                    icon: const Icon(Symbols.remove),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                PopupMenuButton(
 | 
			
		||||
                                  padding: EdgeInsets.zero,
 | 
			
		||||
                                  style: ButtonStyle(
 | 
			
		||||
                                    visualDensity: VisualDensity(horizontal: -4, vertical: -4),
 | 
			
		||||
                                    visualDensity: VisualDensity(
 | 
			
		||||
                                        horizontal: -4, vertical: -4),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  itemBuilder: (BuildContext context) => [
 | 
			
		||||
                                    PopupMenuItem(
 | 
			
		||||
@@ -443,7 +469,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
                              ],
 | 
			
		||||
                            ),
 | 
			
		||||
                            const Gap(12),
 | 
			
		||||
                            Text(_publisher!.description).padding(horizontal: 8),
 | 
			
		||||
                            Text(_publisher!.description)
 | 
			
		||||
                                .padding(horizontal: 8),
 | 
			
		||||
                            const Gap(12),
 | 
			
		||||
                            Column(
 | 
			
		||||
                              children: [
 | 
			
		||||
@@ -451,8 +478,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
                                  children: [
 | 
			
		||||
                                    const Icon(Symbols.calendar_add_on),
 | 
			
		||||
                                    const Gap(8),
 | 
			
		||||
                                    Text('publisherJoinedAt')
 | 
			
		||||
                                        .tr(args: [DateFormat('y/M/d').format(_publisher!.createdAt)]),
 | 
			
		||||
                                    Text('publisherJoinedAt').tr(args: [
 | 
			
		||||
                                      DateFormat('y/M/d')
 | 
			
		||||
                                          .format(_publisher!.createdAt)
 | 
			
		||||
                                    ]),
 | 
			
		||||
                                  ],
 | 
			
		||||
                                ),
 | 
			
		||||
                                Row(
 | 
			
		||||
@@ -460,7 +489,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
                                    const Icon(Symbols.trending_up),
 | 
			
		||||
                                    const Gap(8),
 | 
			
		||||
                                    Text('publisherSocialPointTotal').plural(
 | 
			
		||||
                                      _publisher!.totalUpvote - _publisher!.totalDownvote,
 | 
			
		||||
                                      _publisher!.totalUpvote -
 | 
			
		||||
                                          _publisher!.totalDownvote,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ],
 | 
			
		||||
                                ),
 | 
			
		||||
@@ -470,18 +500,22 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
                                      const Icon(Symbols.group_work),
 | 
			
		||||
                                      const Gap(8),
 | 
			
		||||
                                      InkWell(
 | 
			
		||||
                                        child: Text('publisherAffiliatedBy').tr(args: [
 | 
			
		||||
                                        child: Text('publisherAffiliatedBy')
 | 
			
		||||
                                            .tr(args: [
 | 
			
		||||
                                          '@${_realm?.alias ?? 'unknown'}',
 | 
			
		||||
                                        ]),
 | 
			
		||||
                                        onTap: () {
 | 
			
		||||
                                          GoRouter.of(context).pushNamed(
 | 
			
		||||
                                            'realmDetail',
 | 
			
		||||
                                            pathParameters: {'alias': _realm!.alias},
 | 
			
		||||
                                            pathParameters: {
 | 
			
		||||
                                              'alias': _realm!.alias
 | 
			
		||||
                                            },
 | 
			
		||||
                                          );
 | 
			
		||||
                                        },
 | 
			
		||||
                                      ),
 | 
			
		||||
                                      const Gap(8),
 | 
			
		||||
                                      AccountImage(content: _realm?.avatar, radius: 8),
 | 
			
		||||
                                      AccountImage(
 | 
			
		||||
                                          content: _realm?.avatar, radius: 8),
 | 
			
		||||
                                    ],
 | 
			
		||||
                                  ),
 | 
			
		||||
                                Row(
 | 
			
		||||
@@ -502,7 +536,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
 | 
			
		||||
                                      },
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    const Gap(8),
 | 
			
		||||
                                    AccountImage(content: _account?.avatar, radius: 8),
 | 
			
		||||
                                    AccountImage(
 | 
			
		||||
                                        content: _account?.avatar, radius: 8),
 | 
			
		||||
                                  ],
 | 
			
		||||
                                ),
 | 
			
		||||
                              ],
 | 
			
		||||
@@ -606,7 +641,7 @@ class _PublisherPostList extends StatelessWidget {
 | 
			
		||||
          onDeleted: onDeleted,
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
      separatorBuilder: (_, __) => const Gap(8),
 | 
			
		||||
      separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										149
									
								
								lib/screens/realm/community.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								lib/screens/realm/community.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,149 @@
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/post.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/screens/post/post_editor.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
 | 
			
		||||
import 'package:surface/widgets/post/post_item.dart';
 | 
			
		||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
 | 
			
		||||
 | 
			
		||||
class RealmCommunityScreen extends StatefulWidget {
 | 
			
		||||
  final String alias;
 | 
			
		||||
  const RealmCommunityScreen({super.key, required this.alias});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<RealmCommunityScreen> createState() => _RealmCommunityScreenState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _RealmCommunityScreenState extends State<RealmCommunityScreen> {
 | 
			
		||||
  SnRealm? _realm;
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchRealm() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final resp = await sn.client.get('/cgi/id/realms/${widget.alias}');
 | 
			
		||||
      _realm = SnRealm.fromJson(resp.data);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
      rethrow;
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() {});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool _isBusy = false;
 | 
			
		||||
  int? _totalCount;
 | 
			
		||||
  final List<SnPost> _posts = List.empty(growable: true);
 | 
			
		||||
 | 
			
		||||
  Future<void> _fetchPosts() async {
 | 
			
		||||
    setState(() => _isBusy = true);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      final pt = context.read<SnPostContentProvider>();
 | 
			
		||||
      final out = await pt.listPosts(
 | 
			
		||||
        take: 10,
 | 
			
		||||
        offset: _posts.length,
 | 
			
		||||
        realm: _realm?.id.toString(),
 | 
			
		||||
      );
 | 
			
		||||
      _totalCount = out.$2;
 | 
			
		||||
      _posts.addAll(out.$1);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showErrorDialog(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
      setState(() => _isBusy = false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _fetchRealm();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        title: Text(_realm?.name ?? 'loading'.tr()),
 | 
			
		||||
      ),
 | 
			
		||||
      floatingActionButton: _realm != null
 | 
			
		||||
          ? FloatingActionButton(
 | 
			
		||||
              child: const Icon(Symbols.edit),
 | 
			
		||||
              onPressed: () {
 | 
			
		||||
                GoRouter.of(context).pushNamed(
 | 
			
		||||
                  'postEditor',
 | 
			
		||||
                  extra: PostEditorExtra(realm: _realm!),
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
            )
 | 
			
		||||
          : null,
 | 
			
		||||
      body: Column(
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
        children: [
 | 
			
		||||
          if (_realm == null)
 | 
			
		||||
            Expanded(
 | 
			
		||||
              child: Center(
 | 
			
		||||
                child: CircularProgressIndicator().center(),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          if (_realm != null)
 | 
			
		||||
            Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
                Text('realmCommunity'.tr(args: [_realm!.name]))
 | 
			
		||||
                    .fontSize(17)
 | 
			
		||||
                    .padding(horizontal: 20, bottom: 4),
 | 
			
		||||
                Text('postTotalCount'.plural(_totalCount ?? 0))
 | 
			
		||||
                    .fontSize(13)
 | 
			
		||||
                    .opacity(0.8)
 | 
			
		||||
                    .padding(horizontal: 20, bottom: 4),
 | 
			
		||||
              ],
 | 
			
		||||
            ).padding(horizontal: 20, vertical: 16),
 | 
			
		||||
          const Divider(height: 1),
 | 
			
		||||
          if (_realm != null)
 | 
			
		||||
            Expanded(
 | 
			
		||||
              child: MediaQuery.removePadding(
 | 
			
		||||
                context: context,
 | 
			
		||||
                removeTop: true,
 | 
			
		||||
                child: RefreshIndicator(
 | 
			
		||||
                  onRefresh: _fetchPosts,
 | 
			
		||||
                  child: InfiniteList(
 | 
			
		||||
                    padding: const EdgeInsets.only(top: 8),
 | 
			
		||||
                    itemCount: _posts.length,
 | 
			
		||||
                    isLoading: _isBusy,
 | 
			
		||||
                    hasReachedMax:
 | 
			
		||||
                        _totalCount != null && _posts.length >= _totalCount!,
 | 
			
		||||
                    onFetchData: _fetchPosts,
 | 
			
		||||
                    itemBuilder: (context, idx) {
 | 
			
		||||
                      final post = _posts[idx];
 | 
			
		||||
                      return OpenablePostItem(
 | 
			
		||||
                        data: post,
 | 
			
		||||
                        maxWidth: 640,
 | 
			
		||||
                        onChanged: (data) {
 | 
			
		||||
                          setState(() => _posts[idx] = data);
 | 
			
		||||
                        },
 | 
			
		||||
                        onDeleted: () {
 | 
			
		||||
                          setState(() => _posts.removeAt(idx));
 | 
			
		||||
                        },
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                    separatorBuilder: (_, __) =>
 | 
			
		||||
                        const Divider().padding(vertical: 2),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -318,7 +318,7 @@ class _RealmPostListWidgetState extends State<_RealmPostListWidget> {
 | 
			
		||||
              },
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
          separatorBuilder: (_, __) => const Gap(8),
 | 
			
		||||
          separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    ).padding(top: 8);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,10 @@ import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/channel.dart';
 | 
			
		||||
import 'package:surface/providers/config.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/sn_realm.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/types/chat.dart';
 | 
			
		||||
import 'package:surface/types/realm.dart';
 | 
			
		||||
@@ -57,7 +59,9 @@ class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
 | 
			
		||||
        title: Text('screenRealmDiscovery').tr(),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
            icon: _isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
 | 
			
		||||
            icon: _isCompactView
 | 
			
		||||
                ? const Icon(Symbols.view_list)
 | 
			
		||||
                : const Icon(Symbols.view_module),
 | 
			
		||||
            onPressed: () {
 | 
			
		||||
              setState(() => _isCompactView = !_isCompactView);
 | 
			
		||||
              context.read<ConfigProvider>().realmCompactView = _isCompactView;
 | 
			
		||||
@@ -117,7 +121,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
 | 
			
		||||
    try {
 | 
			
		||||
      setState(() => _isBusy = true);
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
 | 
			
		||||
      final resp =
 | 
			
		||||
          await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
 | 
			
		||||
      final out = List<SnChannel>.from(
 | 
			
		||||
        resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
 | 
			
		||||
      );
 | 
			
		||||
@@ -135,10 +140,13 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
 | 
			
		||||
      setState(() => _isJoining = true);
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      final ua = context.read<UserProvider>();
 | 
			
		||||
      await sn.client.post('/cgi/id/realms/${widget.realm.alias}/members', data: {
 | 
			
		||||
      final rel = context.read<SnRealmProvider>();
 | 
			
		||||
      await sn.client
 | 
			
		||||
          .post('/cgi/id/realms/${widget.realm.alias}/members', data: {
 | 
			
		||||
        'related': ua.user?.name,
 | 
			
		||||
      });
 | 
			
		||||
      await _joinSelectedChannels();
 | 
			
		||||
      rel.addAvailableRealm(widget.realm);
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
      context.showSnackbar('realmJoined'.tr(args: [widget.realm.name]));
 | 
			
		||||
      Navigator.pop(context);
 | 
			
		||||
@@ -156,13 +164,20 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
 | 
			
		||||
      try {
 | 
			
		||||
        final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
        final ua = context.read<UserProvider>();
 | 
			
		||||
        await sn.client.post('/cgi/im/channels/${widget.realm.alias}/$channel/members', data: {
 | 
			
		||||
          'related': ua.user?.name,
 | 
			
		||||
        });
 | 
			
		||||
        await sn.client.post(
 | 
			
		||||
            '/cgi/im/channels/${widget.realm.alias}/$channel/members',
 | 
			
		||||
            data: {
 | 
			
		||||
              'related': ua.user?.name,
 | 
			
		||||
            });
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        if (!mounted) return;
 | 
			
		||||
        context.showErrorDialog(err);
 | 
			
		||||
      }
 | 
			
		||||
      final ct = context.read<ChatChannelProvider>();
 | 
			
		||||
      for (final channel
 | 
			
		||||
          in _channels!.where((ele) => _planJoinChannels.contains(ele.alias))) {
 | 
			
		||||
        ct.addAvailableChannel(channel);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -182,7 +197,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
 | 
			
		||||
          children: [
 | 
			
		||||
            const Icon(Symbols.group_add, size: 24),
 | 
			
		||||
            const Gap(16),
 | 
			
		||||
            Text('realmJoin', style: Theme.of(context).textTheme.titleLarge).tr(),
 | 
			
		||||
            Text('realmJoin', style: Theme.of(context).textTheme.titleLarge)
 | 
			
		||||
                .tr(),
 | 
			
		||||
          ],
 | 
			
		||||
        ).padding(horizontal: 20, top: 16, bottom: 12),
 | 
			
		||||
        Row(
 | 
			
		||||
@@ -216,7 +232,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
 | 
			
		||||
        Container(
 | 
			
		||||
          width: double.infinity,
 | 
			
		||||
          color: Theme.of(context).colorScheme.surfaceContainerHigh,
 | 
			
		||||
          child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
 | 
			
		||||
          child: Text('realmCommunityPublicChannelsHint'.tr(),
 | 
			
		||||
                  style: Theme.of(context).textTheme.bodyMedium)
 | 
			
		||||
              .padding(horizontal: 24, vertical: 8),
 | 
			
		||||
        ),
 | 
			
		||||
        Expanded(
 | 
			
		||||
 
 | 
			
		||||
@@ -336,6 +336,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
 | 
			
		||||
                    setState(() {});
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                CheckboxListTile(
 | 
			
		||||
                  secondary: const Icon(Symbols.hide),
 | 
			
		||||
                  title: Text('settingsHideBottomNav').tr(),
 | 
			
		||||
                  subtitle: Text('settingsHideBottomNavDescription').tr(),
 | 
			
		||||
                  contentPadding: const EdgeInsets.only(left: 24, right: 17),
 | 
			
		||||
                  value: _prefs.getBool(kAppHideBottomNav) ?? false,
 | 
			
		||||
                  onChanged: (value) {
 | 
			
		||||
                    _prefs.setBool(kAppHideBottomNav, value ?? false);
 | 
			
		||||
                    final cfg = context.read<ConfigProvider>();
 | 
			
		||||
                    cfg.calcDrawerSize(context);
 | 
			
		||||
                    setState(() {});
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  leading: const Icon(Symbols.font_download),
 | 
			
		||||
                  title: Text('settingsCustomFonts').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/sn_sticker.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/types/attachment.dart';
 | 
			
		||||
import 'package:surface/widgets/app_bar_leading.dart';
 | 
			
		||||
import 'package:surface/widgets/attachment/attachment_item.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
@@ -134,7 +133,7 @@ class _StickerScreenState extends State<StickerScreen>
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AppScaffold(
 | 
			
		||||
      appBar: AppBar(
 | 
			
		||||
        leading: AutoAppBarLeading(),
 | 
			
		||||
        leading: PageBackButton(),
 | 
			
		||||
        title: Text('screenStickers').tr(),
 | 
			
		||||
        actions: [
 | 
			
		||||
          IconButton(
 | 
			
		||||
 
 | 
			
		||||
@@ -88,6 +88,8 @@ Future<ThemeData> createAppTheme(
 | 
			
		||||
        TargetPlatform.windows: ZoomPageTransitionsBuilder(),
 | 
			
		||||
      },
 | 
			
		||||
    ),
 | 
			
		||||
    progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false),
 | 
			
		||||
    sliderTheme: SliderThemeData(year2023: false),
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ class ChatMessage extends StatelessWidget {
 | 
			
		||||
      key: Key('chat-message-${data.id}'),
 | 
			
		||||
      iconOnLeftSwipe: Symbols.reply,
 | 
			
		||||
      iconOnRightSwipe: Symbols.edit,
 | 
			
		||||
      swipeSensitivity: 20,
 | 
			
		||||
      swipeSensitivity: 10,
 | 
			
		||||
      onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
 | 
			
		||||
      onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
 | 
			
		||||
      child: ContextMenuArea(
 | 
			
		||||
 
 | 
			
		||||
@@ -36,10 +36,12 @@ class ChatTypingIndicator extends StatelessWidget {
 | 
			
		||||
                      'messageTyping'
 | 
			
		||||
                          .plural(controller.typingMembers.length, args: [
 | 
			
		||||
                        controller.typingMembers
 | 
			
		||||
                            .map((ele) => (ele.nick?.isNotEmpty ?? false)
 | 
			
		||||
                                ? ele.nick!
 | 
			
		||||
                                : ud.getFromCache(ele.accountId)?.name ??
 | 
			
		||||
                                    'unknown')
 | 
			
		||||
                            .map(
 | 
			
		||||
                              (ele) => (ele.nick?.isNotEmpty ?? false)
 | 
			
		||||
                                  ? ele.nick!
 | 
			
		||||
                                  : ud.getFromCache(ele.accountId)?.nick ??
 | 
			
		||||
                                      'unknown',
 | 
			
		||||
                            )
 | 
			
		||||
                            .join(', '),
 | 
			
		||||
                      ]),
 | 
			
		||||
                    ),
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,12 @@ class ConnectionIndicator extends StatelessWidget {
 | 
			
		||||
    final ws = context.watch<WebSocketProvider>();
 | 
			
		||||
    final cfg = context.watch<ConfigProvider>();
 | 
			
		||||
 | 
			
		||||
    final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0;
 | 
			
		||||
    final marginLeft =
 | 
			
		||||
        cfg.drawerIsCollapsed
 | 
			
		||||
            ? 0.0
 | 
			
		||||
            : cfg.drawerIsExpanded
 | 
			
		||||
            ? 304.0
 | 
			
		||||
            : 80.0;
 | 
			
		||||
 | 
			
		||||
    return ListenableBuilder(
 | 
			
		||||
      listenable: ws,
 | 
			
		||||
@@ -32,37 +37,39 @@ class ConnectionIndicator extends StatelessWidget {
 | 
			
		||||
                elevation: 2,
 | 
			
		||||
                shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
 | 
			
		||||
                color: Theme.of(context).colorScheme.secondaryContainer,
 | 
			
		||||
                child: ua.isAuthorized
 | 
			
		||||
                    ? Row(
 | 
			
		||||
                        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                        mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          if (ws.isBusy)
 | 
			
		||||
                            Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
 | 
			
		||||
                          else if (!ws.isConnected)
 | 
			
		||||
                            Text('serverDisconnected')
 | 
			
		||||
                                .tr()
 | 
			
		||||
                                .textColor(Theme.of(context).colorScheme.onSecondaryContainer)
 | 
			
		||||
                          else
 | 
			
		||||
                            Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
 | 
			
		||||
                          const Gap(8),
 | 
			
		||||
                          if (ws.isBusy)
 | 
			
		||||
                            const CircularProgressIndicator(strokeWidth: 2.5)
 | 
			
		||||
                                .width(12)
 | 
			
		||||
                                .height(12)
 | 
			
		||||
                                .padding(horizontal: 4, right: 4)
 | 
			
		||||
                          else if (!ws.isConnected)
 | 
			
		||||
                            const Icon(Symbols.power_off, size: 18)
 | 
			
		||||
                          else
 | 
			
		||||
                            const Icon(Symbols.power, size: 18),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ).padding(horizontal: 8, vertical: 4)
 | 
			
		||||
                    : const SizedBox.shrink(),
 | 
			
		||||
              ).opacity(show ? 1 : 0, animate: true).animate(
 | 
			
		||||
                    const Duration(milliseconds: 300),
 | 
			
		||||
                    Curves.easeInOut,
 | 
			
		||||
                  ),
 | 
			
		||||
                child:
 | 
			
		||||
                    ua.isAuthorized
 | 
			
		||||
                        ? Row(
 | 
			
		||||
                          mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                          mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                          crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                          children: [
 | 
			
		||||
                            if (ws.isBusy)
 | 
			
		||||
                              Text(
 | 
			
		||||
                                'serverConnecting',
 | 
			
		||||
                              ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
 | 
			
		||||
                            else if (!ws.isConnected)
 | 
			
		||||
                              Text(
 | 
			
		||||
                                'serverDisconnected',
 | 
			
		||||
                              ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
 | 
			
		||||
                            else
 | 
			
		||||
                              Text(
 | 
			
		||||
                                'serverConnected',
 | 
			
		||||
                              ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
 | 
			
		||||
                            const Gap(8),
 | 
			
		||||
                            if (ws.isBusy)
 | 
			
		||||
                              const CircularProgressIndicator(
 | 
			
		||||
                                strokeWidth: 2.5,
 | 
			
		||||
                                padding: EdgeInsets.zero,
 | 
			
		||||
                              ).width(12).height(12).padding(horizontal: 4, right: 4)
 | 
			
		||||
                            else if (!ws.isConnected)
 | 
			
		||||
                              const Icon(Symbols.power_off, size: 18)
 | 
			
		||||
                            else
 | 
			
		||||
                              const Icon(Symbols.power, size: 18),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ).padding(horizontal: 8, vertical: 4)
 | 
			
		||||
                        : const SizedBox.shrink(),
 | 
			
		||||
              ).opacity(show ? 1 : 0, animate: true).animate(const Duration(milliseconds: 300), Curves.easeInOut),
 | 
			
		||||
              onTap: () {
 | 
			
		||||
                if (!ws.isConnected && !ws.isBusy) {
 | 
			
		||||
                  ws.connect();
 | 
			
		||||
 
 | 
			
		||||
@@ -19,89 +19,87 @@ class NewsFeedEntry extends StatelessWidget {
 | 
			
		||||
        .cast<SnNewsArticle>()
 | 
			
		||||
        .toList();
 | 
			
		||||
 | 
			
		||||
    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),
 | 
			
		||||
          Container(
 | 
			
		||||
            margin: const EdgeInsets.only(bottom: 12),
 | 
			
		||||
            height: 150,
 | 
			
		||||
            child: ListView.separated(
 | 
			
		||||
              scrollDirection: Axis.horizontal,
 | 
			
		||||
              itemCount: news.length,
 | 
			
		||||
              padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
              itemBuilder: (context, idx) {
 | 
			
		||||
                return Container(
 | 
			
		||||
                  width: 360,
 | 
			
		||||
                  decoration: BoxDecoration(
 | 
			
		||||
                    border: Border.all(
 | 
			
		||||
                      color: Theme.of(context).dividerColor,
 | 
			
		||||
                      width: 1,
 | 
			
		||||
                    ),
 | 
			
		||||
                    borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
    return 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),
 | 
			
		||||
        Container(
 | 
			
		||||
          margin: const EdgeInsets.only(bottom: 12),
 | 
			
		||||
          height: 150,
 | 
			
		||||
          child: ListView.separated(
 | 
			
		||||
            scrollDirection: Axis.horizontal,
 | 
			
		||||
            itemCount: news.length,
 | 
			
		||||
            padding: const EdgeInsets.symmetric(horizontal: 12),
 | 
			
		||||
            itemBuilder: (context, idx) {
 | 
			
		||||
              return Container(
 | 
			
		||||
                width: 360,
 | 
			
		||||
                decoration: BoxDecoration(
 | 
			
		||||
                  border: Border.all(
 | 
			
		||||
                    color: Theme.of(context).dividerColor,
 | 
			
		||||
                    width: 1,
 | 
			
		||||
                  ),
 | 
			
		||||
                  child: Material(
 | 
			
		||||
                    elevation: 0,
 | 
			
		||||
                    color: Theme.of(context).colorScheme.surface,
 | 
			
		||||
                  borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                ),
 | 
			
		||||
                child: Material(
 | 
			
		||||
                  elevation: 0,
 | 
			
		||||
                  color: Theme.of(context).colorScheme.surface,
 | 
			
		||||
                  borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                  child: InkWell(
 | 
			
		||||
                    borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                    child: InkWell(
 | 
			
		||||
                      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                      child: Column(
 | 
			
		||||
                        crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Text(
 | 
			
		||||
                            news[idx].title,
 | 
			
		||||
                            maxLines: 2,
 | 
			
		||||
                            style: Theme.of(context).textTheme.titleMedium,
 | 
			
		||||
                          ).padding(horizontal: 16, top: 12, bottom: 4),
 | 
			
		||||
                          Text(
 | 
			
		||||
                            news[idx].description,
 | 
			
		||||
                            maxLines: 2,
 | 
			
		||||
                            style: Theme.of(context).textTheme.bodyMedium,
 | 
			
		||||
                          ).padding(horizontal: 16, vertical: 4),
 | 
			
		||||
                          const Gap(4),
 | 
			
		||||
                          Row(
 | 
			
		||||
                            children: [
 | 
			
		||||
                              Text(
 | 
			
		||||
                                DateFormat('y/M/d HH:mm')
 | 
			
		||||
                                    .format(news[idx].createdAt.toLocal()),
 | 
			
		||||
                                style: Theme.of(context).textTheme.bodySmall,
 | 
			
		||||
                              ),
 | 
			
		||||
                              const Gap(4),
 | 
			
		||||
                              Text(
 | 
			
		||||
                                RelativeTime(context)
 | 
			
		||||
                                    .format(news[idx].createdAt.toLocal()),
 | 
			
		||||
                                style: Theme.of(context).textTheme.bodySmall,
 | 
			
		||||
                              ),
 | 
			
		||||
                            ],
 | 
			
		||||
                          ).opacity(0.8).padding(horizontal: 16),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        GoRouter.of(context).pushNamed(
 | 
			
		||||
                          'newsDetail',
 | 
			
		||||
                          pathParameters: {'hash': news[idx].hash},
 | 
			
		||||
                        );
 | 
			
		||||
                      },
 | 
			
		||||
                    child: Column(
 | 
			
		||||
                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                      children: [
 | 
			
		||||
                        Text(
 | 
			
		||||
                          news[idx].title,
 | 
			
		||||
                          maxLines: 2,
 | 
			
		||||
                          style: Theme.of(context).textTheme.titleMedium,
 | 
			
		||||
                        ).padding(horizontal: 16, top: 12, bottom: 4),
 | 
			
		||||
                        Text(
 | 
			
		||||
                          news[idx].description,
 | 
			
		||||
                          maxLines: 2,
 | 
			
		||||
                          style: Theme.of(context).textTheme.bodyMedium,
 | 
			
		||||
                        ).padding(horizontal: 16, vertical: 4),
 | 
			
		||||
                        const Gap(4),
 | 
			
		||||
                        Row(
 | 
			
		||||
                          children: [
 | 
			
		||||
                            Text(
 | 
			
		||||
                              DateFormat('y/M/d HH:mm')
 | 
			
		||||
                                  .format(news[idx].createdAt.toLocal()),
 | 
			
		||||
                              style: Theme.of(context).textTheme.bodySmall,
 | 
			
		||||
                            ),
 | 
			
		||||
                            const Gap(4),
 | 
			
		||||
                            Text(
 | 
			
		||||
                              RelativeTime(context)
 | 
			
		||||
                                  .format(news[idx].createdAt.toLocal()),
 | 
			
		||||
                              style: Theme.of(context).textTheme.bodySmall,
 | 
			
		||||
                            ),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ).opacity(0.8).padding(horizontal: 16),
 | 
			
		||||
                      ],
 | 
			
		||||
                    ),
 | 
			
		||||
                    onTap: () {
 | 
			
		||||
                      GoRouter.of(context).pushNamed(
 | 
			
		||||
                        'newsDetail',
 | 
			
		||||
                        pathParameters: {'hash': news[idx].hash},
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
              separatorBuilder: (_, __) => const Gap(12),
 | 
			
		||||
            ),
 | 
			
		||||
                ),
 | 
			
		||||
              );
 | 
			
		||||
            },
 | 
			
		||||
            separatorBuilder: (_, __) => const Gap(12),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
        ),
 | 
			
		||||
      ],
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,16 +12,14 @@ class FeedUnknownEntry extends StatelessWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Card(
 | 
			
		||||
      child: Column(
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
        children: [
 | 
			
		||||
          const Icon(Symbols.help, size: 36),
 | 
			
		||||
          const Gap(4),
 | 
			
		||||
          Text('feedUnknownItem').tr(),
 | 
			
		||||
          Text(data.type, style: GoogleFonts.robotoMono()),
 | 
			
		||||
        ],
 | 
			
		||||
      ).padding(horizontal: 12, vertical: 8),
 | 
			
		||||
    );
 | 
			
		||||
    return Column(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
      children: [
 | 
			
		||||
        const Icon(Symbols.help, size: 36),
 | 
			
		||||
        const Gap(4),
 | 
			
		||||
        Text('feedUnknownItem').tr(),
 | 
			
		||||
        Text(data.type, style: GoogleFonts.robotoMono()),
 | 
			
		||||
      ],
 | 
			
		||||
    ).padding(horizontal: 12, vertical: 8);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -76,7 +76,10 @@ class _LoadingIndicatorState extends State<LoadingIndicator>
 | 
			
		||||
                  const SizedBox(
 | 
			
		||||
                    height: 16,
 | 
			
		||||
                    width: 16,
 | 
			
		||||
                    child: CircularProgressIndicator(strokeWidth: 2.5),
 | 
			
		||||
                    child: CircularProgressIndicator(
 | 
			
		||||
                      strokeWidth: 2.5,
 | 
			
		||||
                      padding: EdgeInsets.zero,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  const Gap(16),
 | 
			
		||||
                  Text('loading').tr(),
 | 
			
		||||
 
 | 
			
		||||
@@ -176,7 +176,7 @@ class MarkdownTextContent extends StatelessWidget {
 | 
			
		||||
                    child: ClipRRect(
 | 
			
		||||
                      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                      child: AspectRatio(
 | 
			
		||||
                        aspectRatio: attachment.metadata['ratio'] ??
 | 
			
		||||
                        aspectRatio: attachment.metadata['ratio']?.toDouble() ??
 | 
			
		||||
                            switch (attachment.mimetype
 | 
			
		||||
                                    .split('/')
 | 
			
		||||
                                    .firstOrNull) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
@@ -83,6 +84,16 @@ class AppSystemMenuBar extends StatelessWidget {
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
            PlatformMenuItem(
 | 
			
		||||
              shortcut: const SingleActivator(
 | 
			
		||||
                LogicalKeyboardKey.keyH,
 | 
			
		||||
                meta: true,
 | 
			
		||||
              ),
 | 
			
		||||
              label: 'trayMenuHide'.tr(),
 | 
			
		||||
              onSelected: () {
 | 
			
		||||
                appWindow.hide();
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            if (onQuit != null)
 | 
			
		||||
              PlatformMenuItem(
 | 
			
		||||
                shortcut: const SingleActivator(
 | 
			
		||||
 
 | 
			
		||||
@@ -37,17 +37,15 @@ class _AppBottomNavigationBarState extends State<AppBottomNavigationBar> {
 | 
			
		||||
          ...nav.destinations.where((ele) => ele.isPinned),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        return BottomNavigationBar(
 | 
			
		||||
          currentIndex: nav.getIndexInRange(0, nav.pinnedDestinationCount),
 | 
			
		||||
          type: BottomNavigationBarType.fixed,
 | 
			
		||||
          showUnselectedLabels: false,
 | 
			
		||||
          items: destinations.map((ele) {
 | 
			
		||||
            return BottomNavigationBarItem(
 | 
			
		||||
        return NavigationBar(
 | 
			
		||||
          selectedIndex: nav.getIndexInRange(0, nav.pinnedDestinationCount),
 | 
			
		||||
          destinations: destinations.map((ele) {
 | 
			
		||||
            return NavigationDestination(
 | 
			
		||||
              icon: ele.icon,
 | 
			
		||||
              label: ele.label.tr(),
 | 
			
		||||
            );
 | 
			
		||||
          }).toList(),
 | 
			
		||||
          onTap: (idx) {
 | 
			
		||||
          onDestinationSelected: (idx) {
 | 
			
		||||
            nav.setIndex(idx);
 | 
			
		||||
            GoRouter.of(context).goNamed(destinations[idx].screen);
 | 
			
		||||
          },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,25 @@
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:animations/animations.dart';
 | 
			
		||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:gap/gap.dart';
 | 
			
		||||
import 'package:go_router/go_router.dart';
 | 
			
		||||
import 'package:material_symbols_icons/symbols.dart';
 | 
			
		||||
import 'package:provider/provider.dart';
 | 
			
		||||
import 'package:responsive_framework/responsive_framework.dart';
 | 
			
		||||
import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/providers/channel.dart';
 | 
			
		||||
import 'package:surface/providers/config.dart';
 | 
			
		||||
import 'package:surface/providers/navigation.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/providers/sn_realm.dart';
 | 
			
		||||
import 'package:surface/providers/userinfo.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/universal_image.dart';
 | 
			
		||||
import 'package:surface/widgets/version_label.dart';
 | 
			
		||||
 | 
			
		||||
class AppNavigationDrawer extends StatefulWidget {
 | 
			
		||||
@@ -25,74 +36,308 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
			
		||||
      context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context));
 | 
			
		||||
      context
 | 
			
		||||
          .read<NavigationProvider>()
 | 
			
		||||
          .autoDetectIndex(GoRouter.maybeOf(context));
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final ua = context.read<UserProvider>();
 | 
			
		||||
    final nav = context.watch<NavigationProvider>();
 | 
			
		||||
    final cfg = context.watch<ConfigProvider>();
 | 
			
		||||
 | 
			
		||||
    final routeName = GoRouter.of(context)
 | 
			
		||||
        .routerDelegate
 | 
			
		||||
        .currentConfiguration
 | 
			
		||||
        .last
 | 
			
		||||
        .route
 | 
			
		||||
        .name;
 | 
			
		||||
    final showNavButtons = cfg.hideBottomNav ||
 | 
			
		||||
        !(nav.showBottomNavScreen.contains(routeName)
 | 
			
		||||
            ? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
 | 
			
		||||
            : false);
 | 
			
		||||
 | 
			
		||||
    final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
 | 
			
		||||
 | 
			
		||||
    return ListenableBuilder(
 | 
			
		||||
      listenable: nav,
 | 
			
		||||
      builder: (context, _) {
 | 
			
		||||
        final destinations = [
 | 
			
		||||
          ...nav.destinations.where((ele) => ele.isPinned),
 | 
			
		||||
          ...nav.destinations.where((ele) => !ele.isPinned),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        return NavigationDrawer(
 | 
			
		||||
        return Drawer(
 | 
			
		||||
          elevation: widget.elevation,
 | 
			
		||||
          backgroundColor: backgroundColor,
 | 
			
		||||
          selectedIndex: nav.currentIndex,
 | 
			
		||||
          children: [
 | 
			
		||||
            if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS) && !cfg.drawerIsExpanded)
 | 
			
		||||
              Container(
 | 
			
		||||
                decoration: BoxDecoration(
 | 
			
		||||
                  border: Border(
 | 
			
		||||
                    bottom: BorderSide(
 | 
			
		||||
                      color: Theme.of(context).dividerColor,
 | 
			
		||||
                      width: 1 / MediaQuery.of(context).devicePixelRatio,
 | 
			
		||||
          shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(0))),
 | 
			
		||||
          child: Column(
 | 
			
		||||
            mainAxisSize: MainAxisSize.max,
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
            children: [
 | 
			
		||||
              if (!kIsWeb &&
 | 
			
		||||
                  (Platform.isWindows ||
 | 
			
		||||
                      Platform.isLinux ||
 | 
			
		||||
                      Platform.isMacOS) &&
 | 
			
		||||
                  !cfg.drawerIsExpanded)
 | 
			
		||||
                Container(
 | 
			
		||||
                  decoration: BoxDecoration(
 | 
			
		||||
                    border: Border(
 | 
			
		||||
                      bottom: BorderSide(
 | 
			
		||||
                        color: Theme.of(context).dividerColor,
 | 
			
		||||
                        width: 1 / MediaQuery.of(context).devicePixelRatio,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                  child: WindowTitleBarBox(),
 | 
			
		||||
                ),
 | 
			
		||||
                child: WindowTitleBarBox(),
 | 
			
		||||
              Gap(MediaQuery.of(context).padding.top),
 | 
			
		||||
              Expanded(
 | 
			
		||||
                child: _DrawerContentList(),
 | 
			
		||||
              ),
 | 
			
		||||
            Column(
 | 
			
		||||
              mainAxisSize: MainAxisSize.min,
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
                Text('Solar Network').bold(),
 | 
			
		||||
                AppVersionLabel(),
 | 
			
		||||
              ],
 | 
			
		||||
            ).padding(
 | 
			
		||||
              horizontal: 32,
 | 
			
		||||
              vertical: 12,
 | 
			
		||||
            ),
 | 
			
		||||
            ...destinations.where((ele) => ele.isPinned).map((ele) {
 | 
			
		||||
              return NavigationDrawerDestination(
 | 
			
		||||
                icon: ele.icon,
 | 
			
		||||
                label: Text(ele.label).tr(),
 | 
			
		||||
              );
 | 
			
		||||
            }),
 | 
			
		||||
            const Divider(),
 | 
			
		||||
            ...destinations.where((ele) => !ele.isPinned).map((ele) {
 | 
			
		||||
              return NavigationDrawerDestination(
 | 
			
		||||
                icon: ele.icon,
 | 
			
		||||
                label: Text(ele.label).tr(),
 | 
			
		||||
              );
 | 
			
		||||
            }),
 | 
			
		||||
          ],
 | 
			
		||||
          onDestinationSelected: (idx) {
 | 
			
		||||
            nav.setIndex(idx);
 | 
			
		||||
            GoRouter.of(context).goNamed(destinations[idx].screen);
 | 
			
		||||
            Scaffold.of(context).closeDrawer();
 | 
			
		||||
          },
 | 
			
		||||
              if (showNavButtons)
 | 
			
		||||
                Row(
 | 
			
		||||
                  spacing: 8,
 | 
			
		||||
                  children:
 | 
			
		||||
                      nav.destinations.where((ele) => ele.isPinned).mapIndexed(
 | 
			
		||||
                    (idx, ele) {
 | 
			
		||||
                      return Expanded(
 | 
			
		||||
                        child: Tooltip(
 | 
			
		||||
                          message: ele.label.tr(),
 | 
			
		||||
                          child: IconButton(
 | 
			
		||||
                            icon: ele.icon,
 | 
			
		||||
                            color: nav.currentIndex == idx
 | 
			
		||||
                                ? Theme.of(context)
 | 
			
		||||
                                    .colorScheme
 | 
			
		||||
                                    .onPrimaryContainer
 | 
			
		||||
                                : Theme.of(context).colorScheme.onSurface,
 | 
			
		||||
                            style: ButtonStyle(
 | 
			
		||||
                              backgroundColor: WidgetStatePropertyAll(
 | 
			
		||||
                                nav.currentIndex == idx
 | 
			
		||||
                                    ? Theme.of(context)
 | 
			
		||||
                                        .colorScheme
 | 
			
		||||
                                        .primaryContainer
 | 
			
		||||
                                    : Colors.transparent,
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                            onPressed: () {
 | 
			
		||||
                              GoRouter.of(context).goNamed(ele.screen);
 | 
			
		||||
                              Scaffold.of(context).closeDrawer();
 | 
			
		||||
                              nav.setIndex(idx);
 | 
			
		||||
                            },
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                  ).toList(),
 | 
			
		||||
                ).padding(horizontal: 16, bottom: 8),
 | 
			
		||||
              Align(
 | 
			
		||||
                alignment: Alignment.bottomCenter,
 | 
			
		||||
                child: ListTile(
 | 
			
		||||
                  contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
                  leading: AccountImage(
 | 
			
		||||
                    content: ua.user?.avatar,
 | 
			
		||||
                    fallbackWidget:
 | 
			
		||||
                        ua.isAuthorized ? null : const Icon(Symbols.login),
 | 
			
		||||
                  ),
 | 
			
		||||
                  title: ua.isAuthorized
 | 
			
		||||
                      ? Text(ua.user?.nick ?? 'unknown'.tr()).fontSize(15)
 | 
			
		||||
                      : Text('screenAuthLogin').tr(),
 | 
			
		||||
                  subtitle: ua.isAuthorized
 | 
			
		||||
                      ? Text('@${ua.user?.name ?? 'unknown'.tr()}').fontSize(13)
 | 
			
		||||
                      : Text('navBottomUnauthorizedCaption').fontSize(13).tr(),
 | 
			
		||||
                  trailing: Row(
 | 
			
		||||
                    mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      if (ua.isAuthorized)
 | 
			
		||||
                        IconButton(
 | 
			
		||||
                          icon: const Icon(Symbols.notifications, fill: 1),
 | 
			
		||||
                          padding: EdgeInsets.zero,
 | 
			
		||||
                          visualDensity: VisualDensity.compact,
 | 
			
		||||
                          onPressed: () {
 | 
			
		||||
                            GoRouter.of(context).pushNamed('notification');
 | 
			
		||||
                            Scaffold.of(context).closeDrawer();
 | 
			
		||||
                          },
 | 
			
		||||
                        ),
 | 
			
		||||
                      IconButton(
 | 
			
		||||
                        icon: const Icon(Symbols.settings, fill: 1),
 | 
			
		||||
                        padding: EdgeInsets.zero,
 | 
			
		||||
                        visualDensity: VisualDensity.compact,
 | 
			
		||||
                        onPressed: () {
 | 
			
		||||
                          GoRouter.of(context).pushNamed('settings');
 | 
			
		||||
                          Scaffold.of(context).closeDrawer();
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).pushNamed('account');
 | 
			
		||||
                    Scaffold.of(context).closeDrawer();
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              Gap(MediaQuery.of(context).padding.bottom + 8),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _DrawerContentList extends StatelessWidget {
 | 
			
		||||
  const _DrawerContentList();
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final ct = context.read<ChatChannelProvider>();
 | 
			
		||||
    final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
    final nav = context.watch<NavigationProvider>();
 | 
			
		||||
    final rel = context.watch<SnRealmProvider>();
 | 
			
		||||
 | 
			
		||||
    return PageTransitionSwitcher(
 | 
			
		||||
      duration: const Duration(milliseconds: 300),
 | 
			
		||||
      transitionBuilder: (Widget child, Animation<double> primaryAnimation,
 | 
			
		||||
          Animation<double> secondaryAnimation) {
 | 
			
		||||
        return SharedAxisTransition(
 | 
			
		||||
          animation: primaryAnimation,
 | 
			
		||||
          secondaryAnimation: secondaryAnimation,
 | 
			
		||||
          fillColor: Colors.transparent,
 | 
			
		||||
          transitionType: SharedAxisTransitionType.horizontal,
 | 
			
		||||
          child: child,
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
      child: nav.focusedRealm == null
 | 
			
		||||
          ? ListView(
 | 
			
		||||
              key: const Key('realm-list-view'),
 | 
			
		||||
              padding: EdgeInsets.zero,
 | 
			
		||||
              children: [
 | 
			
		||||
                Column(
 | 
			
		||||
                  mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Text('Solar Network').bold(),
 | 
			
		||||
                    AppVersionLabel(),
 | 
			
		||||
                  ],
 | 
			
		||||
                ).padding(
 | 
			
		||||
                  horizontal: 32,
 | 
			
		||||
                  vertical: 12,
 | 
			
		||||
                ),
 | 
			
		||||
                ...rel.availableRealms.map((ele) {
 | 
			
		||||
                  return ListTile(
 | 
			
		||||
                    minTileHeight: 48,
 | 
			
		||||
                    contentPadding: EdgeInsets.symmetric(horizontal: 24),
 | 
			
		||||
                    leading: AccountImage(
 | 
			
		||||
                      content: ele.avatar,
 | 
			
		||||
                      radius: 16,
 | 
			
		||||
                    ),
 | 
			
		||||
                    title: Text(ele.name),
 | 
			
		||||
                    onTap: () {
 | 
			
		||||
                      nav.setFocusedRealm(ele);
 | 
			
		||||
                    },
 | 
			
		||||
                  );
 | 
			
		||||
                }),
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  minTileHeight: 48,
 | 
			
		||||
                  contentPadding: EdgeInsets.only(left: 28, right: 16),
 | 
			
		||||
                  leading: const Icon(Symbols.globe).padding(right: 4),
 | 
			
		||||
                  title: Text('screenRealmDiscovery').tr(),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).pushNamed('realmDiscovery');
 | 
			
		||||
                    Scaffold.of(context).closeDrawer();
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            )
 | 
			
		||||
          : ListView(
 | 
			
		||||
              key: ValueKey(nav.focusedRealm),
 | 
			
		||||
              padding: EdgeInsets.zero,
 | 
			
		||||
              children: [
 | 
			
		||||
                if (nav.focusedRealm!.banner != null)
 | 
			
		||||
                  AspectRatio(
 | 
			
		||||
                    aspectRatio: 16 / 9,
 | 
			
		||||
                    child: AutoResizeUniversalImage(
 | 
			
		||||
                      sn.getAttachmentUrl(
 | 
			
		||||
                        nav.focusedRealm!.banner!,
 | 
			
		||||
                      ),
 | 
			
		||||
                      fit: BoxFit.cover,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  minTileHeight: 48,
 | 
			
		||||
                  tileColor: Theme.of(context).colorScheme.surfaceContainer,
 | 
			
		||||
                  contentPadding: EdgeInsets.only(
 | 
			
		||||
                    left: 24,
 | 
			
		||||
                    right: 16,
 | 
			
		||||
                  ),
 | 
			
		||||
                  leading: AccountImage(
 | 
			
		||||
                    content: nav.focusedRealm!.avatar,
 | 
			
		||||
                    radius: 16,
 | 
			
		||||
                  ),
 | 
			
		||||
                  trailing: IconButton(
 | 
			
		||||
                    icon: const Icon(Symbols.close),
 | 
			
		||||
                    padding: EdgeInsets.zero,
 | 
			
		||||
                    constraints: const BoxConstraints(),
 | 
			
		||||
                    visualDensity: VisualDensity.compact,
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      nav.setFocusedRealm(null);
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                  title: Text(nav.focusedRealm!.name),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).goNamed(
 | 
			
		||||
                      'realmDetail',
 | 
			
		||||
                      pathParameters: {
 | 
			
		||||
                        'alias': nav.focusedRealm!.alias,
 | 
			
		||||
                      },
 | 
			
		||||
                    );
 | 
			
		||||
                    Scaffold.of(context).closeDrawer();
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                ListTile(
 | 
			
		||||
                  minTileHeight: 48,
 | 
			
		||||
                  contentPadding: EdgeInsets.only(
 | 
			
		||||
                    left: 28,
 | 
			
		||||
                    right: 8,
 | 
			
		||||
                  ),
 | 
			
		||||
                  leading: const Icon(Symbols.globe),
 | 
			
		||||
                  title: Text('community').tr(),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
                    GoRouter.of(context).goNamed(
 | 
			
		||||
                      'realmCommunity',
 | 
			
		||||
                      pathParameters: {
 | 
			
		||||
                        'alias': nav.focusedRealm!.alias,
 | 
			
		||||
                      },
 | 
			
		||||
                    );
 | 
			
		||||
                    Scaffold.of(context).closeDrawer();
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
                if (ct.availableChannels
 | 
			
		||||
                    .where((ele) => ele.realmId == nav.focusedRealm?.id)
 | 
			
		||||
                    .isNotEmpty)
 | 
			
		||||
                  const Divider(height: 1),
 | 
			
		||||
                ...(ct.availableChannels
 | 
			
		||||
                    .where((ele) => ele.realmId == nav.focusedRealm?.id)
 | 
			
		||||
                    .map((ele) {
 | 
			
		||||
                  return ListTile(
 | 
			
		||||
                    minTileHeight: 48,
 | 
			
		||||
                    contentPadding: EdgeInsets.only(
 | 
			
		||||
                      left: 28,
 | 
			
		||||
                      right: 8,
 | 
			
		||||
                    ),
 | 
			
		||||
                    leading: const Icon(Symbols.tag),
 | 
			
		||||
                    title: Text(ele.name),
 | 
			
		||||
                    onTap: () {
 | 
			
		||||
                      GoRouter.of(context).goNamed(
 | 
			
		||||
                        'chatRoom',
 | 
			
		||||
                        pathParameters: {
 | 
			
		||||
                          'scope': ele.realm?.alias ?? 'global',
 | 
			
		||||
                          'alias': ele.alias,
 | 
			
		||||
                        },
 | 
			
		||||
                      );
 | 
			
		||||
                      Scaffold.of(context).closeDrawer();
 | 
			
		||||
                    },
 | 
			
		||||
                  );
 | 
			
		||||
                }))
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -107,6 +107,7 @@ class AppRootScaffold extends StatelessWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    final cfg = context.watch<ConfigProvider>();
 | 
			
		||||
    final nav = context.watch<NavigationProvider>();
 | 
			
		||||
    final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
 | 
			
		||||
 | 
			
		||||
    final isCollapseDrawer = cfg.drawerIsCollapsed;
 | 
			
		||||
@@ -118,8 +119,9 @@ class AppRootScaffold extends StatelessWidget {
 | 
			
		||||
        .last
 | 
			
		||||
        .route
 | 
			
		||||
        .name;
 | 
			
		||||
    final isShowBottomNavigation =
 | 
			
		||||
        NavigationProvider.kShowBottomNavScreen.contains(routeName)
 | 
			
		||||
    final isShowBottomNavigation = cfg.hideBottomNav
 | 
			
		||||
        ? false
 | 
			
		||||
        : nav.showBottomNavScreen.contains(routeName)
 | 
			
		||||
            ? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
 | 
			
		||||
            : false;
 | 
			
		||||
    final isPopable = !NavigationProvider.kAllDestination
 | 
			
		||||
 
 | 
			
		||||
@@ -23,57 +23,54 @@ class FediversePostWidget extends StatelessWidget {
 | 
			
		||||
    return Center(
 | 
			
		||||
      child: Container(
 | 
			
		||||
        constraints: BoxConstraints(maxWidth: maxWidth),
 | 
			
		||||
        child: Card(
 | 
			
		||||
          margin: EdgeInsets.zero,
 | 
			
		||||
          child: Column(
 | 
			
		||||
            crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
            children: [
 | 
			
		||||
              Row(
 | 
			
		||||
                children: [
 | 
			
		||||
                  AccountImage(
 | 
			
		||||
                    content: data.user.avatar,
 | 
			
		||||
                    radius: 20,
 | 
			
		||||
                  ),
 | 
			
		||||
                  const Gap(12),
 | 
			
		||||
                  Column(
 | 
			
		||||
                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                    children: [
 | 
			
		||||
                      Text(
 | 
			
		||||
                        data.user.nick.isNotEmpty
 | 
			
		||||
                            ? data.user.nick
 | 
			
		||||
                            : '@${data.user.name}',
 | 
			
		||||
                        maxLines: 1,
 | 
			
		||||
                      ).bold(),
 | 
			
		||||
                      Row(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Text(
 | 
			
		||||
                            data.user.identifier.contains('@')
 | 
			
		||||
                                ? data.user.identifier
 | 
			
		||||
                                : '${data.user.identifier}@${data.user.origin}',
 | 
			
		||||
                            maxLines: 1,
 | 
			
		||||
                          ).fontSize(13),
 | 
			
		||||
                          const Gap(4),
 | 
			
		||||
                          Text(
 | 
			
		||||
                            RelativeTime(context)
 | 
			
		||||
                                .format(data.createdAt.toLocal()),
 | 
			
		||||
                          ).fontSize(13),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ).padding(horizontal: 12, vertical: 8),
 | 
			
		||||
              MarkdownTextContent(
 | 
			
		||||
                isAutoWarp: true,
 | 
			
		||||
                content: html2md.convert(data.content),
 | 
			
		||||
              ).padding(horizontal: 16, bottom: 6),
 | 
			
		||||
              if (data.images.isNotEmpty)
 | 
			
		||||
                _FediversePostImageList(
 | 
			
		||||
                  data: data,
 | 
			
		||||
                  maxWidth: maxWidth,
 | 
			
		||||
        child: Column(
 | 
			
		||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
          children: [
 | 
			
		||||
            Row(
 | 
			
		||||
              children: [
 | 
			
		||||
                AccountImage(
 | 
			
		||||
                  content: data.user.avatar,
 | 
			
		||||
                  radius: 20,
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
                const Gap(12),
 | 
			
		||||
                Column(
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Text(
 | 
			
		||||
                      data.user.nick.isNotEmpty
 | 
			
		||||
                          ? data.user.nick
 | 
			
		||||
                          : '@${data.user.name}',
 | 
			
		||||
                      maxLines: 1,
 | 
			
		||||
                    ).bold(),
 | 
			
		||||
                    Row(
 | 
			
		||||
                      children: [
 | 
			
		||||
                        Text(
 | 
			
		||||
                          data.user.identifier.contains('@')
 | 
			
		||||
                              ? data.user.identifier
 | 
			
		||||
                              : '${data.user.identifier}@${data.user.origin}',
 | 
			
		||||
                          maxLines: 1,
 | 
			
		||||
                        ).fontSize(13),
 | 
			
		||||
                        const Gap(4),
 | 
			
		||||
                        Text(
 | 
			
		||||
                          RelativeTime(context)
 | 
			
		||||
                              .format(data.createdAt.toLocal()),
 | 
			
		||||
                        ).fontSize(13),
 | 
			
		||||
                      ],
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ).padding(horizontal: 12, vertical: 8),
 | 
			
		||||
            MarkdownTextContent(
 | 
			
		||||
              isAutoWarp: true,
 | 
			
		||||
              content: html2md.convert(data.content),
 | 
			
		||||
            ).padding(horizontal: 16, bottom: 6),
 | 
			
		||||
            if (data.images.isNotEmpty)
 | 
			
		||||
              _FediversePostImageList(
 | 
			
		||||
                data: data,
 | 
			
		||||
                maxWidth: maxWidth,
 | 
			
		||||
              ),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
 | 
			
		||||
      final sn = context.read<SnNetworkProvider>();
 | 
			
		||||
      await sn.client
 | 
			
		||||
          .put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
 | 
			
		||||
        'publisher': answer.publisherId,
 | 
			
		||||
        'publisher': widget.parentPost.publisherId,
 | 
			
		||||
        'answer_id': answer.id,
 | 
			
		||||
      });
 | 
			
		||||
      if (!mounted) return;
 | 
			
		||||
@@ -151,6 +151,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            Navigator.pop(context);
 | 
			
		||||
            GoRouter.of(context).pushNamed(
 | 
			
		||||
              'postDetail',
 | 
			
		||||
              pathParameters: {'slug': _posts[idx].id.toString()},
 | 
			
		||||
@@ -225,6 +226,9 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
 | 
			
		||||
                        onPost: () {
 | 
			
		||||
                          _childListKey.currentState!.refresh();
 | 
			
		||||
                        },
 | 
			
		||||
                        onExpand: () {
 | 
			
		||||
                          Navigator.pop(context);
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,7 @@ class OpenablePostItem extends StatelessWidget {
 | 
			
		||||
          transitionType: ContainerTransitionType.fade,
 | 
			
		||||
          closedElevation: 0,
 | 
			
		||||
          closedColor: Theme.of(context).colorScheme.surface.withOpacity(
 | 
			
		||||
                cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
 | 
			
		||||
                cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0 : 1,
 | 
			
		||||
              ),
 | 
			
		||||
          closedShape: const RoundedRectangleBorder(
 | 
			
		||||
            borderRadius: BorderRadius.all(Radius.circular(16)),
 | 
			
		||||
@@ -122,6 +122,7 @@ class PostItem extends StatefulWidget {
 | 
			
		||||
  final bool showMenu;
 | 
			
		||||
  final bool showFullPost;
 | 
			
		||||
  final bool showAvatar;
 | 
			
		||||
  final bool showCompactAvatar;
 | 
			
		||||
  final bool showExpandableComments;
 | 
			
		||||
  final double? maxWidth;
 | 
			
		||||
  final Function(SnPost data)? onChanged;
 | 
			
		||||
@@ -137,6 +138,7 @@ class PostItem extends StatefulWidget {
 | 
			
		||||
    this.showMenu = true,
 | 
			
		||||
    this.showFullPost = false,
 | 
			
		||||
    this.showAvatar = true,
 | 
			
		||||
    this.showCompactAvatar = false,
 | 
			
		||||
    this.showExpandableComments = false,
 | 
			
		||||
    this.maxWidth,
 | 
			
		||||
    this.onChanged,
 | 
			
		||||
@@ -166,6 +168,14 @@ class _PostItemState extends State<PostItem> {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void didUpdateWidget(covariant PostItem oldWidget) {
 | 
			
		||||
    _displayText = widget.data.body['content'] ?? '';
 | 
			
		||||
    _displayTitle = widget.data.body['title'] ?? '';
 | 
			
		||||
    _displayDescription = widget.data.body['description'] ?? '';
 | 
			
		||||
    super.didUpdateWidget(oldWidget);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> _translateText() async {
 | 
			
		||||
    final ta = context.read<SnTranslator>();
 | 
			
		||||
    setState(() => _isTranslating = true);
 | 
			
		||||
@@ -269,6 +279,8 @@ class _PostItemState extends State<PostItem> {
 | 
			
		||||
    final ua = context.read<UserProvider>();
 | 
			
		||||
    final isAuthor =
 | 
			
		||||
        ua.isAuthorized && widget.data.publisher.accountId == ua.user?.id;
 | 
			
		||||
    final isParentAuthor = ua.isAuthorized &&
 | 
			
		||||
        widget.data.replyTo?.publisher.accountId == ua.user?.id;
 | 
			
		||||
 | 
			
		||||
    final displayableAttachments = widget.data.preload?.attachments
 | 
			
		||||
        ?.where((ele) =>
 | 
			
		||||
@@ -284,6 +296,244 @@ class _PostItemState extends State<PostItem> {
 | 
			
		||||
      attachmentSize -= 80;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (widget.showFullPost) {
 | 
			
		||||
      return Column(
 | 
			
		||||
        crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
        children: [
 | 
			
		||||
          Container(
 | 
			
		||||
            constraints: BoxConstraints(
 | 
			
		||||
              maxWidth: widget.maxWidth ?? double.infinity,
 | 
			
		||||
            ),
 | 
			
		||||
            child: Column(
 | 
			
		||||
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
              children: [
 | 
			
		||||
                Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    if (widget.showAvatar)
 | 
			
		||||
                      _PostAvatar(
 | 
			
		||||
                        data: widget.data,
 | 
			
		||||
                        isCompact: false,
 | 
			
		||||
                      ),
 | 
			
		||||
                    if (widget.showAvatar) const Gap(12),
 | 
			
		||||
                    Expanded(
 | 
			
		||||
                      child: Row(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          if (widget.showCompactAvatar)
 | 
			
		||||
                            _PostAvatar(
 | 
			
		||||
                              data: widget.data,
 | 
			
		||||
                              isCompact: true,
 | 
			
		||||
                            ),
 | 
			
		||||
                          if (widget.showAvatar) const Gap(8),
 | 
			
		||||
                          _PostContentHeader(
 | 
			
		||||
                            isRelativeDate: !widget.showFullPost,
 | 
			
		||||
                            isCompact: false,
 | 
			
		||||
                            data: widget.data,
 | 
			
		||||
                          ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    _PostActionPopup(
 | 
			
		||||
                      data: widget.data,
 | 
			
		||||
                      isAuthor: isAuthor,
 | 
			
		||||
                      isParentAuthor: isParentAuthor,
 | 
			
		||||
                      onShare: () => _doShare(context),
 | 
			
		||||
                      onShareImage: () => _doShareViaPicture(context),
 | 
			
		||||
                      onSelectAnswer: widget.onSelectAnswer,
 | 
			
		||||
                      onDeleted: () {
 | 
			
		||||
                        widget.onDeleted?.call();
 | 
			
		||||
                      },
 | 
			
		||||
                      onTranslate: () {
 | 
			
		||||
                        _translateText();
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
                const Gap(8),
 | 
			
		||||
                if (widget.data.preload?.thumbnail != null)
 | 
			
		||||
                  Container(
 | 
			
		||||
                    margin: const EdgeInsets.only(bottom: 8),
 | 
			
		||||
                    decoration: BoxDecoration(
 | 
			
		||||
                      borderRadius: const BorderRadius.all(
 | 
			
		||||
                        Radius.circular(8),
 | 
			
		||||
                      ),
 | 
			
		||||
                      border: Border.all(
 | 
			
		||||
                        color: Theme.of(context).dividerColor,
 | 
			
		||||
                        width: 1,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    child: AspectRatio(
 | 
			
		||||
                      aspectRatio: 16 / 9,
 | 
			
		||||
                      child: ClipRRect(
 | 
			
		||||
                        borderRadius: const BorderRadius.all(
 | 
			
		||||
                          Radius.circular(8),
 | 
			
		||||
                        ),
 | 
			
		||||
                        child: AutoResizeUniversalImage(
 | 
			
		||||
                          sn.getAttachmentUrl(
 | 
			
		||||
                            widget.data.preload!.thumbnail!.rid,
 | 
			
		||||
                          ),
 | 
			
		||||
                          fit: BoxFit.cover,
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                if (widget.data.preload?.video != null)
 | 
			
		||||
                  _PostVideoPlayer(data: widget.data).padding(bottom: 8),
 | 
			
		||||
                if (widget.data.type == 'question')
 | 
			
		||||
                  _PostQuestionHint(data: widget.data).padding(bottom: 8),
 | 
			
		||||
                if (_displayDescription.isNotEmpty || _displayTitle.isNotEmpty)
 | 
			
		||||
                  _PostHeadline(
 | 
			
		||||
                    title: _displayTitle,
 | 
			
		||||
                    description: _displayDescription,
 | 
			
		||||
                    data: widget.data,
 | 
			
		||||
                    isEnlarge:
 | 
			
		||||
                        widget.data.type == 'article' && widget.showFullPost,
 | 
			
		||||
                  ).padding(bottom: 8),
 | 
			
		||||
                if (widget.data.type == 'article' && !widget.showFullPost)
 | 
			
		||||
                  Text('postArticle')
 | 
			
		||||
                      .tr()
 | 
			
		||||
                      .fontSize(13)
 | 
			
		||||
                      .opacity(0.75)
 | 
			
		||||
                      .padding(bottom: 8),
 | 
			
		||||
                if ((_displayText.isNotEmpty) &&
 | 
			
		||||
                    (widget.showFullPost || widget.data.type != 'article'))
 | 
			
		||||
                  _PostContentBody(
 | 
			
		||||
                    text: _displayText,
 | 
			
		||||
                    data: widget.data,
 | 
			
		||||
                    isSelectable: widget.showFullPost,
 | 
			
		||||
                    isEnlarge:
 | 
			
		||||
                        widget.data.type == 'article' && widget.showFullPost,
 | 
			
		||||
                  ).padding(bottom: 6),
 | 
			
		||||
                if (widget.data.visibility > 0)
 | 
			
		||||
                  _PostVisibilityHint(data: widget.data).padding(
 | 
			
		||||
                    vertical: 4,
 | 
			
		||||
                  ),
 | 
			
		||||
                if (widget.data.body['content_truncated'] == true)
 | 
			
		||||
                  _PostTruncatedHint(data: widget.data).padding(
 | 
			
		||||
                    vertical: 4,
 | 
			
		||||
                  ),
 | 
			
		||||
                if (widget.data.tags.isNotEmpty)
 | 
			
		||||
                  _PostTagsList(data: widget.data).padding(top: 4, bottom: 6),
 | 
			
		||||
                Column(
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
			
		||||
                  spacing: 4,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    if (widget.showViews)
 | 
			
		||||
                      Row(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Icon(Symbols.play_circle, size: 20),
 | 
			
		||||
                          const Gap(4),
 | 
			
		||||
                          Text('postViews').plural(widget.data.totalViews),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ).opacity(0.75),
 | 
			
		||||
                    if (_isTranslating)
 | 
			
		||||
                      AnimateWidgetExtensions(Row(
 | 
			
		||||
                        children: [
 | 
			
		||||
                          Icon(Symbols.translate, size: 20),
 | 
			
		||||
                          const Gap(4),
 | 
			
		||||
                          Text('translating').tr(),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ))
 | 
			
		||||
                          .animate(onPlay: (e) => e.repeat())
 | 
			
		||||
                          .fadeIn(duration: 500.ms, curve: Curves.easeOut)
 | 
			
		||||
                          .then()
 | 
			
		||||
                          .fadeOut(
 | 
			
		||||
                            duration: 500.ms,
 | 
			
		||||
                            delay: 1000.ms,
 | 
			
		||||
                            curve: Curves.easeIn,
 | 
			
		||||
                          ),
 | 
			
		||||
                    if (_isTranslated)
 | 
			
		||||
                      InkWell(
 | 
			
		||||
                        child: Row(
 | 
			
		||||
                          children: [
 | 
			
		||||
                            Icon(Symbols.translate, size: 20),
 | 
			
		||||
                            const Gap(4),
 | 
			
		||||
                            Text('translated').tr(),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ).opacity(0.75),
 | 
			
		||||
                        onTap: () {
 | 
			
		||||
                          setState(() {
 | 
			
		||||
                            _displayText = widget.data.body['content'] ?? '';
 | 
			
		||||
                            _displayTitle = widget.data.body['title'] ?? '';
 | 
			
		||||
                            _displayDescription =
 | 
			
		||||
                                widget.data.body['description'] ?? '';
 | 
			
		||||
                            _isTranslated = false;
 | 
			
		||||
                          });
 | 
			
		||||
                        },
 | 
			
		||||
                      ),
 | 
			
		||||
                    if (widget.data.repostTo != null)
 | 
			
		||||
                      _PostQuoteContent(child: widget.data.repostTo!).padding(
 | 
			
		||||
                        top: 4,
 | 
			
		||||
                        bottom: widget.data.preload?.attachments?.isNotEmpty ??
 | 
			
		||||
                                false
 | 
			
		||||
                            ? 12
 | 
			
		||||
                            : 0,
 | 
			
		||||
                      ),
 | 
			
		||||
                  ],
 | 
			
		||||
                ).padding(
 | 
			
		||||
                  bottom: widget.showViews || _isTranslated || _isTranslating
 | 
			
		||||
                      ? 8
 | 
			
		||||
                      : 0,
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ).padding(horizontal: 12, top: 8),
 | 
			
		||||
          ),
 | 
			
		||||
          if (displayableAttachments?.isNotEmpty ?? false)
 | 
			
		||||
            AttachmentList(
 | 
			
		||||
              data: displayableAttachments!,
 | 
			
		||||
              bordered: true,
 | 
			
		||||
              maxHeight: widget.showFullPost ? null : 480,
 | 
			
		||||
              minWidth: attachmentSize,
 | 
			
		||||
              maxWidth: attachmentSize,
 | 
			
		||||
              fit: widget.showFullPost ? BoxFit.cover : BoxFit.contain,
 | 
			
		||||
              padding: EdgeInsets.only(left: 12, right: 12),
 | 
			
		||||
            ),
 | 
			
		||||
          if (widget.data.preload?.poll != null)
 | 
			
		||||
            StyledWidget(Container(
 | 
			
		||||
              constraints:
 | 
			
		||||
                  BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
 | 
			
		||||
              child: PostPoll(poll: widget.data.preload!.poll!),
 | 
			
		||||
            ))
 | 
			
		||||
                .padding(
 | 
			
		||||
                  left: 12,
 | 
			
		||||
                  right: 12,
 | 
			
		||||
                  top: 12,
 | 
			
		||||
                  bottom: 4,
 | 
			
		||||
                )
 | 
			
		||||
                .center(),
 | 
			
		||||
          if (widget.data.body['content'] != null &&
 | 
			
		||||
              (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
 | 
			
		||||
            LinkPreviewWidget(
 | 
			
		||||
              text: widget.data.body['content'],
 | 
			
		||||
            ).padding(left: 12, right: 4),
 | 
			
		||||
          if (widget.showExpandableComments)
 | 
			
		||||
            _PostCommentIntent(
 | 
			
		||||
              data: widget.data,
 | 
			
		||||
              showAvatar: widget.showAvatar,
 | 
			
		||||
              maxWidth: widget.maxWidth ?? double.infinity,
 | 
			
		||||
            ).padding(left: 12, right: 12)
 | 
			
		||||
          else
 | 
			
		||||
            _PostFeaturedComment(data: widget.data, maxWidth: widget.maxWidth)
 | 
			
		||||
                .padding(left: 12, right: 12),
 | 
			
		||||
          if (widget.showReactions)
 | 
			
		||||
            Center(
 | 
			
		||||
              child: Container(
 | 
			
		||||
                constraints: BoxConstraints(
 | 
			
		||||
                  maxWidth: (widget.maxWidth ?? double.infinity) + 24,
 | 
			
		||||
                ),
 | 
			
		||||
                child: Padding(
 | 
			
		||||
                  padding: const EdgeInsets.only(top: 4),
 | 
			
		||||
                  child: _PostReactionList(
 | 
			
		||||
                    data: widget.data,
 | 
			
		||||
                    padding: EdgeInsets.only(left: 12, right: 12),
 | 
			
		||||
                    onChanged: _onChanged,
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
        ],
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Column(
 | 
			
		||||
      crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
      children: [
 | 
			
		||||
@@ -309,15 +559,28 @@ class _PostItemState extends State<PostItem> {
 | 
			
		||||
                        Row(
 | 
			
		||||
                          children: [
 | 
			
		||||
                            Expanded(
 | 
			
		||||
                              child: _PostContentHeader(
 | 
			
		||||
                                isRelativeDate: !widget.showFullPost,
 | 
			
		||||
                                isCompact: true,
 | 
			
		||||
                                data: widget.data,
 | 
			
		||||
                              child: Row(
 | 
			
		||||
                                children: [
 | 
			
		||||
                                  if (widget.showCompactAvatar)
 | 
			
		||||
                                    _PostAvatar(
 | 
			
		||||
                                      data: widget.data,
 | 
			
		||||
                                      isCompact: true,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  if (widget.showCompactAvatar) const Gap(8),
 | 
			
		||||
                                  Expanded(
 | 
			
		||||
                                    child: _PostContentHeader(
 | 
			
		||||
                                      isRelativeDate: !widget.showFullPost,
 | 
			
		||||
                                      isCompact: true,
 | 
			
		||||
                                      data: widget.data,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                ],
 | 
			
		||||
                              ),
 | 
			
		||||
                            ),
 | 
			
		||||
                            _PostActionPopup(
 | 
			
		||||
                              data: widget.data,
 | 
			
		||||
                              isAuthor: isAuthor,
 | 
			
		||||
                              isParentAuthor: isParentAuthor,
 | 
			
		||||
                              onShare: () => _doShare(context),
 | 
			
		||||
                              onShareImage: () => _doShareViaPicture(context),
 | 
			
		||||
                              onSelectAnswer: widget.onSelectAnswer,
 | 
			
		||||
@@ -329,7 +592,7 @@ class _PostItemState extends State<PostItem> {
 | 
			
		||||
                              },
 | 
			
		||||
                            ),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ),
 | 
			
		||||
                        ).padding(bottom: widget.showCompactAvatar ? 4 : 0),
 | 
			
		||||
                        if (widget.data.preload?.thumbnail != null)
 | 
			
		||||
                          Container(
 | 
			
		||||
                            margin: const EdgeInsets.only(bottom: 8),
 | 
			
		||||
@@ -389,15 +652,6 @@ class _PostItemState extends State<PostItem> {
 | 
			
		||||
                            isEnlarge: widget.data.type == 'article' &&
 | 
			
		||||
                                widget.showFullPost,
 | 
			
		||||
                          ).padding(bottom: 6),
 | 
			
		||||
                        if (widget.data.repostTo != null)
 | 
			
		||||
                          _PostQuoteContent(child: widget.data.repostTo!)
 | 
			
		||||
                              .padding(
 | 
			
		||||
                            bottom:
 | 
			
		||||
                                widget.data.preload?.attachments?.isNotEmpty ??
 | 
			
		||||
                                        false
 | 
			
		||||
                                    ? 12
 | 
			
		||||
                                    : 0,
 | 
			
		||||
                          ),
 | 
			
		||||
                        if (widget.data.visibility > 0)
 | 
			
		||||
                          _PostVisibilityHint(data: widget.data).padding(
 | 
			
		||||
                            vertical: 4,
 | 
			
		||||
@@ -462,12 +716,25 @@ class _PostItemState extends State<PostItem> {
 | 
			
		||||
                              ),
 | 
			
		||||
                          ],
 | 
			
		||||
                        ).padding(
 | 
			
		||||
                          bottom: widget.showViews ||
 | 
			
		||||
                                  _isTranslated ||
 | 
			
		||||
                                  _isTranslating
 | 
			
		||||
                          bottom: (widget.showViews ||
 | 
			
		||||
                                      _isTranslated ||
 | 
			
		||||
                                      _isTranslating) &&
 | 
			
		||||
                                  (widget.data.repostTo != null ||
 | 
			
		||||
                                      (widget.data.preload?.attachments
 | 
			
		||||
                                              ?.isNotEmpty ??
 | 
			
		||||
                                          false))
 | 
			
		||||
                              ? 8
 | 
			
		||||
                              : 0,
 | 
			
		||||
                        ),
 | 
			
		||||
                        if (widget.data.repostTo != null)
 | 
			
		||||
                          _PostQuoteContent(child: widget.data.repostTo!)
 | 
			
		||||
                              .padding(
 | 
			
		||||
                            bottom:
 | 
			
		||||
                                (widget.data.preload?.attachments?.isNotEmpty ??
 | 
			
		||||
                                        false)
 | 
			
		||||
                                    ? 8
 | 
			
		||||
                                    : 0,
 | 
			
		||||
                          ),
 | 
			
		||||
                      ],
 | 
			
		||||
                    ),
 | 
			
		||||
                  )
 | 
			
		||||
@@ -502,19 +769,28 @@ class _PostItemState extends State<PostItem> {
 | 
			
		||||
        if (widget.showExpandableComments)
 | 
			
		||||
          _PostCommentIntent(
 | 
			
		||||
            data: widget.data,
 | 
			
		||||
            maxWidth: (widget.maxWidth ?? double.infinity) -
 | 
			
		||||
                (widget.showAvatar ? 72 : 24),
 | 
			
		||||
            showAvatar: widget.showAvatar,
 | 
			
		||||
          ).padding(left: widget.showAvatar ? 60 : 12, right: 12)
 | 
			
		||||
        else
 | 
			
		||||
        else if (widget.showComments)
 | 
			
		||||
          _PostFeaturedComment(data: widget.data, maxWidth: widget.maxWidth)
 | 
			
		||||
              .padding(left: widget.showAvatar ? 60 : 12, right: 12),
 | 
			
		||||
        if (widget.showReactions)
 | 
			
		||||
          Padding(
 | 
			
		||||
            padding: const EdgeInsets.only(top: 4),
 | 
			
		||||
            child: _PostReactionList(
 | 
			
		||||
              data: widget.data,
 | 
			
		||||
              padding:
 | 
			
		||||
                  EdgeInsets.only(left: widget.showAvatar ? 60 : 12, right: 12),
 | 
			
		||||
              onChanged: _onChanged,
 | 
			
		||||
          Container(
 | 
			
		||||
            constraints: BoxConstraints(
 | 
			
		||||
              maxWidth: widget.maxWidth ?? double.infinity,
 | 
			
		||||
            ),
 | 
			
		||||
            child: Padding(
 | 
			
		||||
              padding: const EdgeInsets.only(top: 4),
 | 
			
		||||
              child: _PostReactionList(
 | 
			
		||||
                data: widget.data,
 | 
			
		||||
                padding: EdgeInsets.only(
 | 
			
		||||
                  left: widget.showAvatar ? 60 : 12,
 | 
			
		||||
                  right: 12,
 | 
			
		||||
                ),
 | 
			
		||||
                onChanged: _onChanged,
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
      ],
 | 
			
		||||
@@ -1045,6 +1321,7 @@ class _PostAvatar extends StatelessWidget {
 | 
			
		||||
class _PostActionPopup extends StatelessWidget {
 | 
			
		||||
  final SnPost data;
 | 
			
		||||
  final bool isAuthor;
 | 
			
		||||
  final bool isParentAuthor;
 | 
			
		||||
  final Function onDeleted;
 | 
			
		||||
  final Function() onShare, onShareImage;
 | 
			
		||||
  final Function()? onSelectAnswer;
 | 
			
		||||
@@ -1052,6 +1329,7 @@ class _PostActionPopup extends StatelessWidget {
 | 
			
		||||
  const _PostActionPopup({
 | 
			
		||||
    required this.data,
 | 
			
		||||
    required this.isAuthor,
 | 
			
		||||
    required this.isParentAuthor,
 | 
			
		||||
    required this.onDeleted,
 | 
			
		||||
    required this.onShare,
 | 
			
		||||
    required this.onShareImage,
 | 
			
		||||
@@ -1125,7 +1403,7 @@ class _PostActionPopup extends StatelessWidget {
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          if (onTranslate != null) PopupMenuDivider(),
 | 
			
		||||
          if (isAuthor && onSelectAnswer != null)
 | 
			
		||||
          if (isParentAuthor && onSelectAnswer != null)
 | 
			
		||||
            PopupMenuItem(
 | 
			
		||||
              child: Row(
 | 
			
		||||
                children: [
 | 
			
		||||
@@ -1138,7 +1416,7 @@ class _PostActionPopup extends StatelessWidget {
 | 
			
		||||
                onSelectAnswer?.call();
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
          if (isAuthor && onSelectAnswer != null) PopupMenuDivider(),
 | 
			
		||||
          if (isParentAuthor && onSelectAnswer != null) PopupMenuDivider(),
 | 
			
		||||
          if (isAuthor)
 | 
			
		||||
            PopupMenuItem(
 | 
			
		||||
              child: Row(
 | 
			
		||||
@@ -1299,19 +1577,24 @@ class _PostContentHeader extends StatelessWidget {
 | 
			
		||||
    if (isCompact) {
 | 
			
		||||
      return Row(
 | 
			
		||||
        children: [
 | 
			
		||||
          Text(data.publisher.nick).bold(),
 | 
			
		||||
          Flexible(
 | 
			
		||||
            child: Text(
 | 
			
		||||
              data.publisher.nick,
 | 
			
		||||
              maxLines: 1,
 | 
			
		||||
            ).bold(),
 | 
			
		||||
          ),
 | 
			
		||||
          const Gap(4),
 | 
			
		||||
          Row(
 | 
			
		||||
            children: [
 | 
			
		||||
              Text(
 | 
			
		||||
                isRelativeDate
 | 
			
		||||
                    ? RelativeTime(context)
 | 
			
		||||
                        .format((data.publishedAt ?? data.createdAt).toLocal())
 | 
			
		||||
                    : DateFormat('y/M/d HH:mm')
 | 
			
		||||
                        .format((data.publishedAt ?? data.createdAt).toLocal()),
 | 
			
		||||
              ).fontSize(13),
 | 
			
		||||
            ],
 | 
			
		||||
          ).opacity(0.8),
 | 
			
		||||
          Flexible(
 | 
			
		||||
            child: Text(
 | 
			
		||||
              isRelativeDate
 | 
			
		||||
                  ? RelativeTime(context)
 | 
			
		||||
                      .format((data.publishedAt ?? data.createdAt).toLocal())
 | 
			
		||||
                  : DateFormat('y/M/d HH:mm')
 | 
			
		||||
                      .format((data.publishedAt ?? data.createdAt).toLocal()),
 | 
			
		||||
              maxLines: 1,
 | 
			
		||||
              overflow: TextOverflow.fade,
 | 
			
		||||
            ).fontSize(13).opacity(0.8),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -1330,7 +1613,10 @@ class _PostContentHeader extends StatelessWidget {
 | 
			
		||||
          ),
 | 
			
		||||
          Row(
 | 
			
		||||
            children: [
 | 
			
		||||
              Text('@${data.publisher.name}').fontSize(13),
 | 
			
		||||
              Text(
 | 
			
		||||
                '@${data.publisher.name}',
 | 
			
		||||
                maxLines: 1,
 | 
			
		||||
              ).fontSize(13),
 | 
			
		||||
              const Gap(4),
 | 
			
		||||
              Text(
 | 
			
		||||
                isRelativeDate
 | 
			
		||||
@@ -1338,6 +1624,8 @@ class _PostContentHeader extends StatelessWidget {
 | 
			
		||||
                        .format((data.publishedAt ?? data.createdAt).toLocal())
 | 
			
		||||
                    : DateFormat('y/M/d HH:mm')
 | 
			
		||||
                        .format((data.publishedAt ?? data.createdAt).toLocal()),
 | 
			
		||||
                maxLines: 1,
 | 
			
		||||
                overflow: TextOverflow.fade,
 | 
			
		||||
              ).fontSize(13),
 | 
			
		||||
            ],
 | 
			
		||||
          ).opacity(0.8),
 | 
			
		||||
@@ -1403,10 +1691,19 @@ class _PostQuoteContent extends StatelessWidget {
 | 
			
		||||
          children: [
 | 
			
		||||
            Column(
 | 
			
		||||
              children: [
 | 
			
		||||
                _PostContentHeader(
 | 
			
		||||
                  data: child,
 | 
			
		||||
                  isCompact: true,
 | 
			
		||||
                  isRelativeDate: isRelativeDate,
 | 
			
		||||
                Row(
 | 
			
		||||
                  children: [
 | 
			
		||||
                    _PostAvatar(
 | 
			
		||||
                      data: child,
 | 
			
		||||
                      isCompact: true,
 | 
			
		||||
                    ),
 | 
			
		||||
                    const Gap(8),
 | 
			
		||||
                    _PostContentHeader(
 | 
			
		||||
                      data: child,
 | 
			
		||||
                      isCompact: true,
 | 
			
		||||
                      isRelativeDate: isRelativeDate,
 | 
			
		||||
                    ),
 | 
			
		||||
                  ],
 | 
			
		||||
                ).padding(bottom: 4),
 | 
			
		||||
                _PostContentBody(
 | 
			
		||||
                  data: child,
 | 
			
		||||
@@ -1594,7 +1891,12 @@ class _PostTruncatedHint extends StatelessWidget {
 | 
			
		||||
class _PostCommentIntent extends StatefulWidget {
 | 
			
		||||
  final SnPost data;
 | 
			
		||||
  final bool showAvatar;
 | 
			
		||||
  const _PostCommentIntent({required this.data, this.showAvatar = false});
 | 
			
		||||
  final double maxWidth;
 | 
			
		||||
  const _PostCommentIntent({
 | 
			
		||||
    required this.data,
 | 
			
		||||
    this.showAvatar = false,
 | 
			
		||||
    required this.maxWidth,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<_PostCommentIntent> createState() => _PostCommentIntentState();
 | 
			
		||||
@@ -1633,53 +1935,69 @@ class _PostCommentIntentState extends State<_PostCommentIntent> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return Column(
 | 
			
		||||
      children: [
 | 
			
		||||
        if (_comments.isNotEmpty)
 | 
			
		||||
          Card(
 | 
			
		||||
            margin: EdgeInsets.zero,
 | 
			
		||||
            child: Column(
 | 
			
		||||
              spacing: 8,
 | 
			
		||||
              children: [
 | 
			
		||||
                for (final ele in _comments)
 | 
			
		||||
                  PostItem(
 | 
			
		||||
                    data: ele,
 | 
			
		||||
                    showAvatar: false,
 | 
			
		||||
                    showExpandableComments: true,
 | 
			
		||||
                    showReactions: false,
 | 
			
		||||
                    showViews: false,
 | 
			
		||||
                    maxWidth: double.infinity,
 | 
			
		||||
                  ).padding(vertical: 8, left: 6),
 | 
			
		||||
              ],
 | 
			
		||||
            ),
 | 
			
		||||
          ).padding(bottom: 8),
 | 
			
		||||
        Row(
 | 
			
		||||
          children: [
 | 
			
		||||
            Transform.flip(
 | 
			
		||||
              flipX: true,
 | 
			
		||||
              child: const Icon(Symbols.comment, size: 20),
 | 
			
		||||
            ),
 | 
			
		||||
            const Gap(4),
 | 
			
		||||
            Text('postCommentsDetailed'.plural(widget.data.metric.replyCount)),
 | 
			
		||||
            if (widget.data.metric.replyCount > 0 && !_isAllLoaded)
 | 
			
		||||
              SizedBox(
 | 
			
		||||
                width: 20,
 | 
			
		||||
                height: 20,
 | 
			
		||||
                child: IconButton(
 | 
			
		||||
                  visualDensity: VisualDensity(horizontal: -4, vertical: -4),
 | 
			
		||||
                  constraints: const BoxConstraints(),
 | 
			
		||||
                  padding: EdgeInsets.zero,
 | 
			
		||||
                  icon: const Icon(Symbols.expand_more, size: 18),
 | 
			
		||||
                  onPressed: _isBusy
 | 
			
		||||
                      ? null
 | 
			
		||||
                      : () {
 | 
			
		||||
                          _fetchComments();
 | 
			
		||||
                        },
 | 
			
		||||
                ),
 | 
			
		||||
              ).padding(left: 8),
 | 
			
		||||
          ],
 | 
			
		||||
        ).opacity(0.75).padding(horizontal: widget.showAvatar ? 4 : 0),
 | 
			
		||||
      ],
 | 
			
		||||
    return Container(
 | 
			
		||||
      constraints: BoxConstraints(maxWidth: widget.maxWidth),
 | 
			
		||||
      child: Column(
 | 
			
		||||
        children: [
 | 
			
		||||
          if (_comments.isNotEmpty)
 | 
			
		||||
            Card(
 | 
			
		||||
              elevation: 4,
 | 
			
		||||
              margin: EdgeInsets.zero,
 | 
			
		||||
              child: Column(
 | 
			
		||||
                spacing: 8,
 | 
			
		||||
                children: [
 | 
			
		||||
                  for (final ele in _comments)
 | 
			
		||||
                    InkWell(
 | 
			
		||||
                      borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
			
		||||
                      child: PostItem(
 | 
			
		||||
                        data: ele,
 | 
			
		||||
                        showAvatar: false,
 | 
			
		||||
                        showCompactAvatar: true,
 | 
			
		||||
                        showExpandableComments: true,
 | 
			
		||||
                        showReactions: false,
 | 
			
		||||
                        showViews: false,
 | 
			
		||||
                        maxWidth: double.infinity,
 | 
			
		||||
                      ).padding(vertical: 8, left: 6),
 | 
			
		||||
                      onTap: () {
 | 
			
		||||
                        GoRouter.of(context).pushNamed(
 | 
			
		||||
                          'postDetail',
 | 
			
		||||
                          pathParameters: {'slug': ele.id.toString()},
 | 
			
		||||
                          extra: ele,
 | 
			
		||||
                        );
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
            ).padding(vertical: 8),
 | 
			
		||||
          Row(
 | 
			
		||||
            children: [
 | 
			
		||||
              Transform.flip(
 | 
			
		||||
                flipX: true,
 | 
			
		||||
                child: const Icon(Symbols.comment, size: 20),
 | 
			
		||||
              ),
 | 
			
		||||
              const Gap(4),
 | 
			
		||||
              Text(
 | 
			
		||||
                  'postCommentsDetailed'.plural(widget.data.metric.replyCount)),
 | 
			
		||||
              if (widget.data.metric.replyCount > 0 && !_isAllLoaded)
 | 
			
		||||
                SizedBox(
 | 
			
		||||
                  width: 20,
 | 
			
		||||
                  height: 20,
 | 
			
		||||
                  child: IconButton(
 | 
			
		||||
                    visualDensity: VisualDensity(horizontal: -4, vertical: -4),
 | 
			
		||||
                    constraints: const BoxConstraints(),
 | 
			
		||||
                    padding: EdgeInsets.zero,
 | 
			
		||||
                    icon: const Icon(Symbols.expand_more, size: 18),
 | 
			
		||||
                    onPressed: _isBusy
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : () {
 | 
			
		||||
                            _fetchComments();
 | 
			
		||||
                          },
 | 
			
		||||
                  ),
 | 
			
		||||
                ).padding(left: 8),
 | 
			
		||||
            ],
 | 
			
		||||
          ).opacity(0.75).padding(horizontal: widget.showAvatar ? 4 : 0),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import 'dart:ui';
 | 
			
		||||
import 'package:croppy/croppy.dart';
 | 
			
		||||
import 'package:dismissible_page/dismissible_page.dart';
 | 
			
		||||
import 'package:easy_localization/easy_localization.dart';
 | 
			
		||||
import 'package:file_picker/file_picker.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
@@ -491,6 +492,14 @@ class AddPostMediaButton extends StatelessWidget {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _selectFile() async {
 | 
			
		||||
    final result = await FilePicker.platform.pickFiles(type: FileType.any);
 | 
			
		||||
    if (result == null) return;
 | 
			
		||||
    onAdd(
 | 
			
		||||
      result.files.map((e) => PostWriteMedia.fromFile(e.xFile)),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _pasteMedia() async {
 | 
			
		||||
    final imageBytes = await Pasteboard.image;
 | 
			
		||||
    if (imageBytes == null) return;
 | 
			
		||||
@@ -605,6 +614,18 @@ class AddPostMediaButton extends StatelessWidget {
 | 
			
		||||
            _selectMedia();
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        PopupMenuItem(
 | 
			
		||||
          child: Row(
 | 
			
		||||
            children: [
 | 
			
		||||
              const Icon(Symbols.file_upload),
 | 
			
		||||
              const Gap(16),
 | 
			
		||||
              Text('addAttachmentFromFiles').tr(),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
          onTap: () {
 | 
			
		||||
            _selectFile();
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
        PopupMenuItem(
 | 
			
		||||
          child: Row(
 | 
			
		||||
            children: [
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import 'package:styled_widget/styled_widget.dart';
 | 
			
		||||
import 'package:surface/controllers/post_write_controller.dart';
 | 
			
		||||
import 'package:surface/providers/config.dart';
 | 
			
		||||
import 'package:surface/providers/sn_network.dart';
 | 
			
		||||
import 'package:surface/screens/post/post_editor.dart';
 | 
			
		||||
import 'package:surface/types/post.dart';
 | 
			
		||||
import 'package:surface/widgets/account/account_image.dart';
 | 
			
		||||
import 'package:surface/widgets/dialog.dart';
 | 
			
		||||
@@ -17,8 +18,10 @@ import 'package:surface/widgets/loading_indicator.dart';
 | 
			
		||||
class PostMiniEditor extends StatefulWidget {
 | 
			
		||||
  final int? postReplyId;
 | 
			
		||||
  final Function? onPost;
 | 
			
		||||
  final Function? onExpand;
 | 
			
		||||
 | 
			
		||||
  const PostMiniEditor({super.key, this.postReplyId, this.onPost});
 | 
			
		||||
  const PostMiniEditor(
 | 
			
		||||
      {super.key, this.postReplyId, this.onPost, this.onExpand});
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  State<PostMiniEditor> createState() => _PostMiniEditorState();
 | 
			
		||||
@@ -214,12 +217,16 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
 | 
			
		||||
                    onPressed: () {
 | 
			
		||||
                      GoRouter.of(context).pushNamed(
 | 
			
		||||
                        'postEditor',
 | 
			
		||||
                        extra: PostEditorExtra(
 | 
			
		||||
                          text: _writeController.contentController.text,
 | 
			
		||||
                        ),
 | 
			
		||||
                        queryParameters: {
 | 
			
		||||
                          if (widget.postReplyId != null)
 | 
			
		||||
                            'replying': widget.postReplyId.toString(),
 | 
			
		||||
                          'mode': 'stories',
 | 
			
		||||
                        },
 | 
			
		||||
                      );
 | 
			
		||||
                      widget.onExpand?.call();
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                  TextButton.icon(
 | 
			
		||||
 
 | 
			
		||||
@@ -102,6 +102,6 @@ static void my_application_init(MyApplication* self) {}
 | 
			
		||||
MyApplication* my_application_new() {
 | 
			
		||||
  return MY_APPLICATION(g_object_new(my_application_get_type(),
 | 
			
		||||
                                     "application-id", APPLICATION_ID,
 | 
			
		||||
                                     "flags", G_APPLICATION_NON_UNIQUE,
 | 
			
		||||
                                     "flags", G_APPLICATION_DEFAULT_FLAGS,
 | 
			
		||||
                                     nullptr));
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -190,6 +190,8 @@ PODS:
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/fts5 (3.49.1):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/math (3.49.1):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/perf-threadsafe (3.49.1):
 | 
			
		||||
    - sqlite3/common
 | 
			
		||||
  - sqlite3/rtree (3.49.1):
 | 
			
		||||
@@ -200,6 +202,7 @@ PODS:
 | 
			
		||||
    - sqlite3 (~> 3.49.1)
 | 
			
		||||
    - sqlite3/dbstatvtab
 | 
			
		||||
    - sqlite3/fts5
 | 
			
		||||
    - sqlite3/math
 | 
			
		||||
    - sqlite3/perf-threadsafe
 | 
			
		||||
    - sqlite3/rtree
 | 
			
		||||
  - tray_manager (0.0.1):
 | 
			
		||||
@@ -390,7 +393,7 @@ SPEC CHECKSUMS:
 | 
			
		||||
  shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
 | 
			
		||||
  sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
 | 
			
		||||
  sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
 | 
			
		||||
  sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
 | 
			
		||||
  sqlite3_flutter_libs: 487032b9008b28de37c72a3aa66849ef3745f3e6
 | 
			
		||||
  tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
 | 
			
		||||
  url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
 | 
			
		||||
  video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,8 @@
 | 
			
		||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 | 
			
		||||
<plist version="1.0">
 | 
			
		||||
<dict>
 | 
			
		||||
	<key>LSSupportsOpeningDocumentsInPlace</key>
 | 
			
		||||
	<true/>
 | 
			
		||||
	<key>CFBundleDevelopmentRegion</key>
 | 
			
		||||
	<string>$(DEVELOPMENT_LANGUAGE)</string>
 | 
			
		||||
	<key>CFBundleExecutable</key>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										61
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -213,10 +213,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: chalkdart
 | 
			
		||||
      sha256: e7cfcc9a9d9546843304c1ff87fe0696c7eb82ee70e6df63f555f321b15a40d8
 | 
			
		||||
      sha256: "82dfa884e3cf97641eb0742a3b9ffd41490666b9ece548b2e32cbfefe540bf86"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.3"
 | 
			
		||||
    version: "2.4.0"
 | 
			
		||||
  characters:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -517,10 +517,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: fast_rsa
 | 
			
		||||
      sha256: "205a36c0412b9fabebf3e18ccb5221d819cc28cfb3da988c0bf7b646368d0270"
 | 
			
		||||
      sha256: a26ad752734dc52fd51abd55248df868d7480e68d8cc8dd12413b0124bba0a7e
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.8.0"
 | 
			
		||||
    version: "3.8.1"
 | 
			
		||||
  ffi:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -541,10 +541,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: file_picker
 | 
			
		||||
      sha256: ee11ce89f8937c39181bc88d57a455972f7545b86150d8f287d0d9cf95bcdf0a
 | 
			
		||||
      sha256: "8d938fd5c11dc81bf1acd4f7f0486c683fe9e79a0b13419e27730f9ce4d8a25b"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "9.1.0"
 | 
			
		||||
    version: "9.2.1"
 | 
			
		||||
  file_saver:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -746,10 +746,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: flutter_expandable_fab
 | 
			
		||||
      sha256: "85275279d19faf4fbe5639dc1f139b4555b150e079d056f085601a45688af12c"
 | 
			
		||||
      sha256: b14caf78720a48f650e6e1a38d724e33b1f5348d646fa1c266570c31a7f87ef3
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.3.0"
 | 
			
		||||
    version: "2.4.0"
 | 
			
		||||
  flutter_highlight:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -953,10 +953,10 @@ packages:
 | 
			
		||||
    dependency: "direct dev"
 | 
			
		||||
    description:
 | 
			
		||||
      name: freezed
 | 
			
		||||
      sha256: a6274c34d61b3d68082f2b0e9a641a3ec197e525d269f35b82f62d5b2c6d9f75
 | 
			
		||||
      sha256: "7ed2ddaa47524976d5f2aa91432a79da36a76969edd84170777ac5bea82d797c"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.0.3"
 | 
			
		||||
    version: "3.0.4"
 | 
			
		||||
  freezed_annotation:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1177,10 +1177,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: image_picker_linux
 | 
			
		||||
      sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
 | 
			
		||||
      sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.2.1+1"
 | 
			
		||||
    version: "0.2.1+2"
 | 
			
		||||
  image_picker_macos:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1385,10 +1385,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: material_symbols_icons
 | 
			
		||||
      sha256: db745002d0323c32097f5bc23711c02b0fb36ef616011a38c8c1798d5684d368
 | 
			
		||||
      sha256: "99d5b0e7c65232dfe1247e0ac67eeeee2cab9da2d860748fc495d34f5e9e6397"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.2810.0"
 | 
			
		||||
    version: "4.2811.0"
 | 
			
		||||
  media_kit:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -2102,10 +2102,10 @@ packages:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: sqlite3_flutter_libs
 | 
			
		||||
      sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
 | 
			
		||||
      sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.5.31"
 | 
			
		||||
    version: "0.5.32"
 | 
			
		||||
  sqlparser:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -2174,34 +2174,34 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: talker
 | 
			
		||||
      sha256: a074e0a2c1db1b09c1856050c5284a14ff64faea003403409871ab8335738d08
 | 
			
		||||
      sha256: "45abef5b92f9b9bd42c3f20133ad4b20ab12e1da2aa206fc0a40ea874bed7c5d"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.7.0"
 | 
			
		||||
    version: "4.7.1"
 | 
			
		||||
  talker_dio_logger:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: talker_dio_logger
 | 
			
		||||
      sha256: "6d713d26bdf90c7440222758a4e02ebfbdb3b323cdbdb50b78082db720f69427"
 | 
			
		||||
      sha256: "52c1b554cccedec6073637a6d4f6a3e267dd4451c1545fe57e1b26897a560ccb"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.7.0"
 | 
			
		||||
    version: "4.7.1"
 | 
			
		||||
  talker_flutter:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: talker_flutter
 | 
			
		||||
      sha256: "25a9faa98a0dcbab8d1ec366f19b3e91d422ede523461ec6e26e198b280cb98d"
 | 
			
		||||
      sha256: "77458ca11638dfefb651e898a26101ee54e60dc0b168ad7481a05b1c97ce2680"
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.7.0"
 | 
			
		||||
    version: "4.7.1"
 | 
			
		||||
  talker_logger:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: talker_logger
 | 
			
		||||
      sha256: "0a1a295a9d036a3416e89c7155eaaca9ca6e3e0abb5401a40597f98b9cfc8fe6"
 | 
			
		||||
      sha256: ed9b20b8c09efff9f6b7c63fc6630ee2f84aa92661ae09e5ba04e77272bf2ad2
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.7.0"
 | 
			
		||||
    version: "4.7.1"
 | 
			
		||||
  term_glyph:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -2238,10 +2238,10 @@ packages:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: tray_manager
 | 
			
		||||
      sha256: "80be6c508159a6f3c57983de795209ac13453e9832fd574143b06dceee188ed2"
 | 
			
		||||
      sha256: c2da0f0f1ddb455e721cf68d05d1281fec75cf5df0a1d3cb67b6ca0bdfd5709d
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.3.2"
 | 
			
		||||
    version: "0.4.0"
 | 
			
		||||
  tuple:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -2525,11 +2525,10 @@ packages:
 | 
			
		||||
  workmanager:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      path: workmanager
 | 
			
		||||
      ref: main
 | 
			
		||||
      resolved-ref: "4ce065135dc1b91fee918f81596b42a56850391d"
 | 
			
		||||
      url: "https://github.com/fluttercommunity/flutter_workmanager.git"
 | 
			
		||||
    source: git
 | 
			
		||||
      name: workmanager
 | 
			
		||||
      sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00
 | 
			
		||||
      url: "https://pub.dev"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.5.2"
 | 
			
		||||
  xdg_directories:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								pubspec.yaml
									
									
									
									
									
								
							@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
 | 
			
		||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
			
		||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
			
		||||
# of the product and file versions while build-number is used as the build suffix.
 | 
			
		||||
version: 2.4.2+80
 | 
			
		||||
version: 2.4.2+84
 | 
			
		||||
 | 
			
		||||
environment:
 | 
			
		||||
  sdk: ^3.5.4
 | 
			
		||||
@@ -103,11 +103,7 @@ dependencies:
 | 
			
		||||
  flutter_svg: ^2.0.16
 | 
			
		||||
  home_widget: ^0.7.0
 | 
			
		||||
  receive_sharing_intent: ^1.8.1
 | 
			
		||||
  workmanager:
 | 
			
		||||
    git:
 | 
			
		||||
      url: https://github.com/fluttercommunity/flutter_workmanager.git
 | 
			
		||||
      path: workmanager
 | 
			
		||||
      ref: main
 | 
			
		||||
  workmanager: ^0.5.2
 | 
			
		||||
  flutter_app_update: ^3.2.2
 | 
			
		||||
  in_app_review: ^2.0.10
 | 
			
		||||
  version: ^3.0.2
 | 
			
		||||
@@ -120,7 +116,7 @@ dependencies:
 | 
			
		||||
  flutter_inappwebview: ^6.1.5
 | 
			
		||||
  html: ^0.15.5
 | 
			
		||||
  xml: ^6.5.0
 | 
			
		||||
  tray_manager: ^0.3.2
 | 
			
		||||
  tray_manager: ^0.4.0
 | 
			
		||||
  hotkey_manager: ^0.2.3
 | 
			
		||||
  image_picker_android: ^0.8.12+20
 | 
			
		||||
  cached_network_image_platform_interface: ^4.1.1
 | 
			
		||||
@@ -179,6 +175,7 @@ flutter:
 | 
			
		||||
    - assets/icon/icon-light-radius.png
 | 
			
		||||
    - assets/icon/tray-icon.ico
 | 
			
		||||
    - assets/icon/tray-icon.png
 | 
			
		||||
    - assets/icon/kanban-1st.jpg
 | 
			
		||||
    - assets/translations/
 | 
			
		||||
 | 
			
		||||
  # An image asset can refer to one or more resolution-specific "variants", see
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import 'package:drift/internal/migrations.dart';
 | 
			
		||||
import 'schema_v1.dart' as v1;
 | 
			
		||||
import 'schema_v2.dart' as v2;
 | 
			
		||||
import 'schema_v3.dart' as v3;
 | 
			
		||||
import 'schema_v4.dart' as v4;
 | 
			
		||||
 | 
			
		||||
class GeneratedHelper implements SchemaInstantiationHelper {
 | 
			
		||||
  @override
 | 
			
		||||
@@ -17,10 +18,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
 | 
			
		||||
        return v2.DatabaseAtV2(db);
 | 
			
		||||
      case 3:
 | 
			
		||||
        return v3.DatabaseAtV3(db);
 | 
			
		||||
      case 4:
 | 
			
		||||
        return v4.DatabaseAtV4(db);
 | 
			
		||||
      default:
 | 
			
		||||
        throw MissingSchemaException(version, versions);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static const versions = const [1, 2, 3];
 | 
			
		||||
  static const versions = const [1, 2, 3, 4];
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2391
									
								
								test/drift/my_database/generated/schema_v4.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2391
									
								
								test/drift/my_database/generated/schema_v4.dart
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -8,8 +8,29 @@ auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
 | 
			
		||||
#include "flutter_window.h"
 | 
			
		||||
#include "utils.h"
 | 
			
		||||
 | 
			
		||||
HANDLE g_hMutex = NULL;
 | 
			
		||||
 | 
			
		||||
bool CheckIfAlreadyRunning() {
 | 
			
		||||
    g_hMutex = CreateMutex(NULL, FALSE, L"Global\\SolianDesktop");
 | 
			
		||||
 | 
			
		||||
    if (g_hMutex == NULL) {
 | 
			
		||||
        return true; // Mutex creation failed
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (GetLastError() == ERROR_ALREADY_EXISTS) {
 | 
			
		||||
        CloseHandle(g_hMutex);
 | 
			
		||||
        return true; // Another instance is running
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
 | 
			
		||||
                      _In_ wchar_t *command_line, _In_ int show_command) {
 | 
			
		||||
  if (CheckIfAlreadyRunning()) {
 | 
			
		||||
    return EXIT_SUCCESS;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Attach to console when present (e.g., 'flutter run') or create a
 | 
			
		||||
  // new console when running with a debugger.
 | 
			
		||||
  if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user