Compare commits
30 Commits
ac2aec48aa
...
2.4.2+84
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e6c3f42f6 | |||
| dc38b46b2c | |||
| b4990308e9 | |||
| 237abe564d | |||
| 71b41d470a | |||
| 7052b5b635 | |||
| f356e08f79 | |||
| 152872db65 | |||
| dfe117d04f | |||
| caf63f0cbe | |||
| b8f5cc82f9 | |||
| 360bc50f21 | |||
| 2de93a0486 | |||
| 02227852f8 | |||
| ad16de595b | |||
| 9f8c8923d9 | |||
| 060bfa4887 | |||
| e68ada2d04 | |||
| d6013078bd | |||
| 5976d61997 | |||
| b492db90ca | |||
| c9f69fed2c | |||
| d2f4e7a969 | |||
| aecd04e0b9 | |||
| e5212419ae | |||
| ec7650a920 | |||
| 7b96013406 | |||
| fc5a79b29b | |||
| 4146820be5 | |||
| 9ec0f1ff19 |
@@ -130,7 +130,7 @@
|
|||||||
"accountPublishersSubtitle": "Manage your publish identities.",
|
"accountPublishersSubtitle": "Manage your publish identities.",
|
||||||
"accountSettings": "Account Settings",
|
"accountSettings": "Account Settings",
|
||||||
"accountSettingsSubtitle": "Manage your account and make it yours.",
|
"accountSettingsSubtitle": "Manage your account and make it yours.",
|
||||||
"accountProfileEdit": "Edit your profile",
|
"accountProfileEdit": "Edit Profile",
|
||||||
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
|
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
|
||||||
"accountWallet": "Wallet",
|
"accountWallet": "Wallet",
|
||||||
"accountWalletSubtitle": "View your balance and transactions.",
|
"accountWalletSubtitle": "View your balance and transactions.",
|
||||||
@@ -207,6 +207,7 @@
|
|||||||
"one": "{} comment",
|
"one": "{} comment",
|
||||||
"other": "{} comments"
|
"other": "{} comments"
|
||||||
},
|
},
|
||||||
|
"postCommentExpand": "Show comments",
|
||||||
"settingsAppearance": "Appearance",
|
"settingsAppearance": "Appearance",
|
||||||
"settingsCustomFonts": "Custom Fonts",
|
"settingsCustomFonts": "Custom Fonts",
|
||||||
"settingsCustomFontsDescription": "Set custom fonts for the application.",
|
"settingsCustomFontsDescription": "Set custom fonts for the application.",
|
||||||
@@ -337,6 +338,7 @@
|
|||||||
"fieldAttachmentRandomId": "Random ID",
|
"fieldAttachmentRandomId": "Random ID",
|
||||||
"fieldAttachmentAlt": "Alternative text",
|
"fieldAttachmentAlt": "Alternative text",
|
||||||
"addAttachmentFromAlbum": "Add from album",
|
"addAttachmentFromAlbum": "Add from album",
|
||||||
|
"addAttachmentFromFiles": "Add from files",
|
||||||
"addAttachmentFromClipboard": "Paste file",
|
"addAttachmentFromClipboard": "Paste file",
|
||||||
"addAttachmentFromCameraPhoto": "Take photo",
|
"addAttachmentFromCameraPhoto": "Take photo",
|
||||||
"addAttachmentFromCameraVideo": "Take video",
|
"addAttachmentFromCameraVideo": "Take video",
|
||||||
@@ -811,6 +813,8 @@
|
|||||||
"accountActionEvent": "Action Events",
|
"accountActionEvent": "Action Events",
|
||||||
"accountActionEventDescription": "View your action event logs.",
|
"accountActionEventDescription": "View your action event logs.",
|
||||||
"eventMetadata": "Metadata",
|
"eventMetadata": "Metadata",
|
||||||
|
"accountAuthTickets": "Auth Sessions",
|
||||||
|
"accountAuthTicketsDescription": "View and manage your auth sessions.",
|
||||||
"authTicketCreatedAt": "Issued at {}",
|
"authTicketCreatedAt": "Issued at {}",
|
||||||
"authTicketExpiredAt": "Expired at {}",
|
"authTicketExpiredAt": "Expired at {}",
|
||||||
"authTicketLastGrantAt": "Last granted at {}",
|
"authTicketLastGrantAt": "Last granted at {}",
|
||||||
@@ -837,5 +841,54 @@
|
|||||||
"fieldContactContent": "Contact method",
|
"fieldContactContent": "Contact method",
|
||||||
"accountContactMethodsPublicHint": "This contact method will be displayed publicly on your profile.",
|
"accountContactMethodsPublicHint": "This contact method will be displayed publicly on your profile.",
|
||||||
"accountContactMethodsDelete": "Delete Contact Method",
|
"accountContactMethodsDelete": "Delete Contact Method",
|
||||||
"accountContactMethodsDeleteDescription": "Are you sure you want to delete contact method {}? This operation is irreversible."
|
"accountContactMethodsDeleteDescription": "Are you sure you want to delete contact method {}? This operation is irreversible.",
|
||||||
|
"postCommentAdd": "Write a comment",
|
||||||
|
"translate": "Translate",
|
||||||
|
"translating": "Translating…",
|
||||||
|
"translated": "Translated",
|
||||||
|
"settingsAutoTranslate": "Auto Translate",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,6 +205,7 @@
|
|||||||
"one": "{} 条评论",
|
"one": "{} 条评论",
|
||||||
"other": "{} 条评论"
|
"other": "{} 条评论"
|
||||||
},
|
},
|
||||||
|
"postCommentExpand": "展开评论",
|
||||||
"settingsAppearance": "外观",
|
"settingsAppearance": "外观",
|
||||||
"settingsCustomFonts": "自定义字体",
|
"settingsCustomFonts": "自定义字体",
|
||||||
"settingsCustomFontsDescription": "设置应用程序使用的字体。",
|
"settingsCustomFontsDescription": "设置应用程序使用的字体。",
|
||||||
@@ -335,6 +336,7 @@
|
|||||||
"fieldAttachmentRandomId": "访问 ID",
|
"fieldAttachmentRandomId": "访问 ID",
|
||||||
"fieldAttachmentAlt": "概述文字",
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "从相册中添加附件",
|
"addAttachmentFromAlbum": "从相册中添加附件",
|
||||||
|
"addAttachmentFromFiles": "从文件中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘贴附件",
|
"addAttachmentFromClipboard": "粘贴附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍摄照片",
|
"addAttachmentFromCameraPhoto": "拍摄照片",
|
||||||
"addAttachmentFromCameraVideo": "拍摄视频",
|
"addAttachmentFromCameraVideo": "拍摄视频",
|
||||||
@@ -837,5 +839,54 @@
|
|||||||
"fieldContactContent": "联系方式",
|
"fieldContactContent": "联系方式",
|
||||||
"accountContactMethodsPublicHint": "这个联系方式公开地显示在个人资料中。",
|
"accountContactMethodsPublicHint": "这个联系方式公开地显示在个人资料中。",
|
||||||
"accountContactMethodsDelete": "删除联系方式",
|
"accountContactMethodsDelete": "删除联系方式",
|
||||||
"accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。"
|
"accountContactMethodsDeleteDescription": "你确定要删除联系方式 {} 吗?这个操作不可撤销。",
|
||||||
|
"postCommentAdd": "撰写一条评论",
|
||||||
|
"translate": "翻译",
|
||||||
|
"translating": "正在翻译……",
|
||||||
|
"translated": "已翻译",
|
||||||
|
"settingsAutoTranslate": "自动翻译",
|
||||||
|
"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": "人机验证"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,6 +205,7 @@
|
|||||||
"one": "{} 條評論",
|
"one": "{} 條評論",
|
||||||
"other": "{} 條評論"
|
"other": "{} 條評論"
|
||||||
},
|
},
|
||||||
|
"postCommentExpand": "展開評論",
|
||||||
"settingsAppearance": "外觀",
|
"settingsAppearance": "外觀",
|
||||||
"settingsCustomFonts": "自定義字體",
|
"settingsCustomFonts": "自定義字體",
|
||||||
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
|
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
|
||||||
@@ -335,6 +336,7 @@
|
|||||||
"fieldAttachmentRandomId": "訪問 ID",
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
"fieldAttachmentAlt": "概述文字",
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
|
"addAttachmentFromFiles": "從文件中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘貼附件",
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||||
@@ -837,5 +839,54 @@
|
|||||||
"fieldContactContent": "聯繫方式",
|
"fieldContactContent": "聯繫方式",
|
||||||
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
|
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
|
||||||
"accountContactMethodsDelete": "刪除聯繫方式",
|
"accountContactMethodsDelete": "刪除聯繫方式",
|
||||||
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。"
|
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
|
||||||
|
"postCommentAdd": "撰寫一條評論",
|
||||||
|
"translate": "翻譯",
|
||||||
|
"translating": "正在翻譯……",
|
||||||
|
"translated": "已翻譯",
|
||||||
|
"settingsAutoTranslate": "自動翻譯",
|
||||||
|
"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": "人機驗證"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,6 +205,7 @@
|
|||||||
"one": "{} 條評論",
|
"one": "{} 條評論",
|
||||||
"other": "{} 條評論"
|
"other": "{} 條評論"
|
||||||
},
|
},
|
||||||
|
"postCommentExpand": "展開評論",
|
||||||
"settingsAppearance": "外觀",
|
"settingsAppearance": "外觀",
|
||||||
"settingsCustomFonts": "自定義字體",
|
"settingsCustomFonts": "自定義字體",
|
||||||
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
|
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
|
||||||
@@ -335,6 +336,7 @@
|
|||||||
"fieldAttachmentRandomId": "訪問 ID",
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
"fieldAttachmentAlt": "概述文字",
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
|
"addAttachmentFromFiles": "從文件中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘貼附件",
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||||
@@ -837,5 +839,54 @@
|
|||||||
"fieldContactContent": "聯繫方式",
|
"fieldContactContent": "聯繫方式",
|
||||||
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
|
"accountContactMethodsPublicHint": "這個聯繫方式公開地顯示在個人資料中。",
|
||||||
"accountContactMethodsDelete": "刪除聯繫方式",
|
"accountContactMethodsDelete": "刪除聯繫方式",
|
||||||
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。"
|
"accountContactMethodsDeleteDescription": "你確定要刪除聯繫方式 {} 嗎?這個操作不可撤銷。",
|
||||||
|
"postCommentAdd": "撰寫一條評論",
|
||||||
|
"translate": "翻譯",
|
||||||
|
"translating": "正在翻譯……",
|
||||||
|
"translated": "已翻譯",
|
||||||
|
"settingsAutoTranslate": "自動翻譯",
|
||||||
|
"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": "人機驗證"
|
||||||
}
|
}
|
||||||
|
|||||||
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/common
|
||||||
- sqlite3/fts5 (3.49.1):
|
- sqlite3/fts5 (3.49.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
|
- sqlite3/math (3.49.1):
|
||||||
|
- sqlite3/common
|
||||||
- sqlite3/perf-threadsafe (3.49.1):
|
- sqlite3/perf-threadsafe (3.49.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/rtree (3.49.1):
|
- sqlite3/rtree (3.49.1):
|
||||||
@@ -242,6 +244,7 @@ PODS:
|
|||||||
- sqlite3 (~> 3.49.1)
|
- sqlite3 (~> 3.49.1)
|
||||||
- sqlite3/dbstatvtab
|
- sqlite3/dbstatvtab
|
||||||
- sqlite3/fts5
|
- sqlite3/fts5
|
||||||
|
- sqlite3/math
|
||||||
- sqlite3/perf-threadsafe
|
- sqlite3/perf-threadsafe
|
||||||
- sqlite3/rtree
|
- sqlite3/rtree
|
||||||
- SwiftyGif (5.4.5)
|
- SwiftyGif (5.4.5)
|
||||||
@@ -457,7 +460,7 @@ SPEC CHECKSUMS:
|
|||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||||
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
sqlite3_flutter_libs: 487032b9008b28de37c72a3aa66849ef3745f3e6
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||||
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
||||||
|
|||||||
@@ -79,6 +79,8 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
<true/>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import 'package:surface/database/attachment.dart';
|
|||||||
import 'package:surface/database/chat.dart';
|
import 'package:surface/database/chat.dart';
|
||||||
import 'package:surface/database/database.steps.dart';
|
import 'package:surface/database/database.steps.dart';
|
||||||
import 'package:surface/database/keypair.dart';
|
import 'package:surface/database/keypair.dart';
|
||||||
|
import 'package:surface/database/realm.dart';
|
||||||
import 'package:surface/database/sticker.dart';
|
import 'package:surface/database/sticker.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
|
import 'package:surface/types/realm.dart';
|
||||||
|
|
||||||
part 'database.g.dart';
|
part 'database.g.dart';
|
||||||
|
|
||||||
@@ -22,12 +24,13 @@ part 'database.g.dart';
|
|||||||
SnLocalAttachment,
|
SnLocalAttachment,
|
||||||
SnLocalSticker,
|
SnLocalSticker,
|
||||||
SnLocalStickerPack,
|
SnLocalStickerPack,
|
||||||
|
SnLocalRealm,
|
||||||
])
|
])
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
|
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 3;
|
int get schemaVersion => 4;
|
||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
static QueryExecutor _openConnection() {
|
||||||
return driftDatabase(
|
return driftDatabase(
|
||||||
@@ -49,6 +52,10 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
// Nothing else to do here
|
// Nothing else to do here
|
||||||
}, from2To3: (m, schema) async {
|
}, from2To3: (m, schema) async {
|
||||||
// Nothing else to do here, too
|
// 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 {
|
abstract class _$AppDatabase extends GeneratedDatabase {
|
||||||
_$AppDatabase(QueryExecutor e) : super(e);
|
_$AppDatabase(QueryExecutor e) : super(e);
|
||||||
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
||||||
@@ -2470,6 +2815,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
late final $SnLocalStickerTable snLocalSticker = $SnLocalStickerTable(this);
|
late final $SnLocalStickerTable snLocalSticker = $SnLocalStickerTable(this);
|
||||||
late final $SnLocalStickerPackTable snLocalStickerPack =
|
late final $SnLocalStickerPackTable snLocalStickerPack =
|
||||||
$SnLocalStickerPackTable(this);
|
$SnLocalStickerPackTable(this);
|
||||||
|
late final $SnLocalRealmTable snLocalRealm = $SnLocalRealmTable(this);
|
||||||
late final Index idxChannelAlias = Index('idx_channel_alias',
|
late final Index idxChannelAlias = Index('idx_channel_alias',
|
||||||
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
|
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
|
||||||
late final Index idxChatChannel = Index('idx_chat_channel',
|
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)');
|
'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
|
||||||
late final Index idxAttachmentAccount = Index('idx_attachment_account',
|
late final Index idxAttachmentAccount = Index('idx_attachment_account',
|
||||||
'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
|
'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
|
@override
|
||||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||||
@@ -2493,11 +2843,14 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
snLocalAttachment,
|
snLocalAttachment,
|
||||||
snLocalSticker,
|
snLocalSticker,
|
||||||
snLocalStickerPack,
|
snLocalStickerPack,
|
||||||
|
snLocalRealm,
|
||||||
idxChannelAlias,
|
idxChannelAlias,
|
||||||
idxChatChannel,
|
idxChatChannel,
|
||||||
idxAccountName,
|
idxAccountName,
|
||||||
idxAttachmentRid,
|
idxAttachmentRid,
|
||||||
idxAttachmentAccount
|
idxAttachmentAccount,
|
||||||
|
idxRealmAlias,
|
||||||
|
idxRealmAccount
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3888,6 +4241,192 @@ typedef $$SnLocalStickerPackTableProcessedTableManager = ProcessedTableManager<
|
|||||||
),
|
),
|
||||||
SnLocalStickerPackData,
|
SnLocalStickerPackData,
|
||||||
PrefetchHooks Function()>;
|
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 {
|
class $AppDatabaseManager {
|
||||||
final _$AppDatabase _db;
|
final _$AppDatabase _db;
|
||||||
@@ -3908,4 +4447,6 @@ class $AppDatabaseManager {
|
|||||||
$$SnLocalStickerTableTableManager(_db, _db.snLocalSticker);
|
$$SnLocalStickerTableTableManager(_db, _db.snLocalSticker);
|
||||||
$$SnLocalStickerPackTableTableManager get snLocalStickerPack =>
|
$$SnLocalStickerPackTableTableManager get snLocalStickerPack =>
|
||||||
$$SnLocalStickerPackTableTableManager(_db, _db.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>;
|
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({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
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, Schema3 schema) from2To3,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -428,6 +633,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from2To3(migrator, schema);
|
await from2To3(migrator, schema);
|
||||||
return 3;
|
return 3;
|
||||||
|
case 3:
|
||||||
|
final schema = Schema4(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from3To4(migrator, schema);
|
||||||
|
return 4;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -437,9 +647,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
i1.OnUpgrade stepByStep({
|
i1.OnUpgrade stepByStep({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
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, Schema3 schema) from2To3,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||||
}) =>
|
}) =>
|
||||||
i0.VersionedSchema.stepByStepHelper(
|
i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
from2To3: from2To3,
|
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()();
|
||||||
|
}
|
||||||
120
lib/main.dart
120
lib/main.dart
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math' hide log;
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
@@ -12,6 +13,7 @@ import 'package:firebase_core/firebase_core.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
@@ -19,6 +21,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
import 'package:responsive_framework/responsive_framework.dart';
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/firebase_options.dart';
|
import 'package:surface/firebase_options.dart';
|
||||||
import 'package:surface/logger.dart';
|
import 'package:surface/logger.dart';
|
||||||
import 'package:surface/providers/channel.dart';
|
import 'package:surface/providers/channel.dart';
|
||||||
@@ -37,6 +40,7 @@ import 'package:surface/providers/sn_realm.dart';
|
|||||||
import 'package:surface/providers/sn_sticker.dart';
|
import 'package:surface/providers/sn_sticker.dart';
|
||||||
import 'package:surface/providers/special_day.dart';
|
import 'package:surface/providers/special_day.dart';
|
||||||
import 'package:surface/providers/theme.dart';
|
import 'package:surface/providers/theme.dart';
|
||||||
|
import 'package:surface/providers/translation.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
@@ -45,6 +49,7 @@ import 'package:surface/router.dart';
|
|||||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/menu_bar.dart';
|
import 'package:surface/widgets/menu_bar.dart';
|
||||||
|
import 'package:surface/widgets/version_label.dart';
|
||||||
import 'package:tray_manager/tray_manager.dart';
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
import 'package:version/version.dart';
|
import 'package:version/version.dart';
|
||||||
import 'package:workmanager/workmanager.dart';
|
import 'package:workmanager/workmanager.dart';
|
||||||
@@ -167,6 +172,7 @@ class SolianApp extends StatelessWidget {
|
|||||||
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
|
||||||
|
Provider(create: (ctx) => SnTranslator()),
|
||||||
|
|
||||||
// Additional helper layer
|
// Additional helper layer
|
||||||
Provider(create: (ctx) => SpecialDayProvider(ctx)),
|
Provider(create: (ctx) => SpecialDayProvider(ctx)),
|
||||||
@@ -226,6 +232,9 @@ class _AppSplashScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||||
|
bool _isBusy = false;
|
||||||
|
String _phaseText = 'appInitStarting';
|
||||||
|
|
||||||
void _tryRequestRating() async {
|
void _tryRequestRating() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
if (prefs.containsKey('first_boot_time')) {
|
if (prefs.containsKey('first_boot_time')) {
|
||||||
@@ -274,7 +283,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
mounted) {
|
mounted) {
|
||||||
final config = context.read<ConfigProvider>();
|
final config = context.read<ConfigProvider>();
|
||||||
config.setUpdate(
|
config.setUpdate(
|
||||||
remoteVersionString, resp.data?['body'] ?? 'No changelog');
|
remoteVersionString,
|
||||||
|
resp.data?['body'] ?? 'No changelog',
|
||||||
|
);
|
||||||
logging.info("[Update] Update available: $remoteVersionString");
|
logging.info("[Update] Update available: $remoteVersionString");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -283,6 +294,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setPhaseText(String text) {
|
||||||
|
_phaseText = 'appInit${text.capitalize()}'.tr();
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _initialize() async {
|
Future<void> _initialize() async {
|
||||||
try {
|
try {
|
||||||
final cfg = context.read<ConfigProvider>();
|
final cfg = context.read<ConfigProvider>();
|
||||||
@@ -295,31 +311,45 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
// The Network initialization must be done after the HomeWidget initialization
|
// 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 save the server url to the HomeWidget
|
||||||
// The Network initialization will also save initialize the Config, so it not need to be initialized again
|
// The Network initialization will also save initialize the Config, so it not need to be initialized again
|
||||||
|
_setPhaseText('network');
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.initializeUserAgent();
|
await sn.initializeUserAgent();
|
||||||
await sn.setConfigWithNative();
|
await sn.setConfigWithNative();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_setPhaseText('userdata');
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
await ua.initialize();
|
await ua.initialize();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_setPhaseText('websocket');
|
||||||
final ws = context.read<WebSocketProvider>();
|
final ws = context.read<WebSocketProvider>();
|
||||||
await ws.tryConnect();
|
await ws.tryConnect();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_setPhaseText('notification');
|
||||||
final notify = context.read<NotificationProvider>();
|
final notify = context.read<NotificationProvider>();
|
||||||
notify.listen();
|
notify.listen();
|
||||||
await notify.registerPushNotifications();
|
await notify.registerPushNotifications();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_setPhaseText('keyPair');
|
||||||
final kp = context.read<KeyPairProvider>();
|
final kp = context.read<KeyPairProvider>();
|
||||||
await kp.reloadActive();
|
await kp.reloadActive();
|
||||||
kp.listen();
|
kp.listen();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_setPhaseText('stickers');
|
||||||
final sticker = context.read<SnStickerProvider>();
|
final sticker = context.read<SnStickerProvider>();
|
||||||
await sticker.listSticker();
|
await sticker.listSticker();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_setPhaseText('userDirectory');
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
final userCacheSize = await ud.loadAccountCache();
|
await ud.loadAccountCache();
|
||||||
logging.info('[Users] Loaded local user cache, size: $userCacheSize');
|
if (!mounted) return;
|
||||||
logging.info('[Bootstrap] Everything initialized!');
|
_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) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
await context.showErrorDialog(err);
|
await context.showErrorDialog(err);
|
||||||
@@ -395,6 +425,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_isBusy = true;
|
||||||
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
|
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
|
||||||
_appLifecycleListener = AppLifecycleListener(
|
_appLifecycleListener = AppLifecycleListener(
|
||||||
onExitRequested: _onExitRequested,
|
onExitRequested: _onExitRequested,
|
||||||
@@ -408,6 +439,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
_postInitialization();
|
_postInitialization();
|
||||||
_tryRequestRating();
|
_tryRequestRating();
|
||||||
_checkForUpdate();
|
_checkForUpdate();
|
||||||
|
setState(() => _isBusy = false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +529,44 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return SizeChangedLayoutNotifier(
|
return SizeChangedLayoutNotifier(
|
||||||
child: widget.child,
|
child: _isBusy
|
||||||
|
? Material(
|
||||||
|
key: Key('app-splash-screen-$_isBusy'),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
CustomPaint(painter: GraphPainter()),
|
||||||
|
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,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -505,3 +574,44 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GraphPainter extends CustomPainter {
|
||||||
|
final Random random = Random();
|
||||||
|
final int numNodes = 20;
|
||||||
|
final double maxDistance = 100; // Max distance to draw a line
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paintNode = Paint()..color = Colors.white;
|
||||||
|
final paintEdge = Paint()
|
||||||
|
..color = Colors.white.withOpacity(0.3)
|
||||||
|
..strokeWidth = 1;
|
||||||
|
|
||||||
|
// Generate random points
|
||||||
|
List<Offset> nodes = List.generate(
|
||||||
|
numNodes,
|
||||||
|
(_) => Offset(
|
||||||
|
random.nextDouble() * size.width,
|
||||||
|
random.nextDouble() * size.height,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw edges between close nodes
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
for (var j = i + 1; j < nodes.length; j++) {
|
||||||
|
double distance = (nodes[i] - nodes[j]).distance;
|
||||||
|
if (distance < maxDistance) {
|
||||||
|
canvas.drawLine(nodes[i], nodes[j], paintEdge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw nodes
|
||||||
|
for (var node in nodes) {
|
||||||
|
canvas.drawCircle(node, 4, paintNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,19 @@ class ChatChannelProvider extends ChangeNotifier {
|
|||||||
_rels = context.read<SnRealmProvider>();
|
_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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
|
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
channels.map(
|
channels.map(
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ const kAppExpandChatLink = 'app_expand_chat_link';
|
|||||||
const kAppRealmCompactView = 'app_realm_compact_view';
|
const kAppRealmCompactView = 'app_realm_compact_view';
|
||||||
const kAppCustomFonts = 'app_custom_fonts';
|
const kAppCustomFonts = 'app_custom_fonts';
|
||||||
const kAppMixedFeed = 'app_mixed_feed';
|
const kAppMixedFeed = 'app_mixed_feed';
|
||||||
|
const kAppAutoTranslate = 'app_auto_translate';
|
||||||
|
const kAppHideBottomNav = 'app_hide_bottom_nav';
|
||||||
|
|
||||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||||
'settingsImageQualityLowest': FilterQuality.none,
|
'settingsImageQualityLowest': FilterQuality.none,
|
||||||
@@ -86,6 +88,24 @@ class ConfigProvider extends ChangeNotifier {
|
|||||||
return prefs.getBool(kAppMixedFeed) ?? true;
|
return prefs.getBool(kAppMixedFeed) ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get autoTranslate {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
set mixedFeed(bool value) {
|
set mixedFeed(bool value) {
|
||||||
prefs.setBool(kAppMixedFeed, value);
|
prefs.setBool(kAppMixedFeed, value);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:surface/types/realm.dart';
|
||||||
|
|
||||||
class AppNavDestination {
|
class AppNavDestination {
|
||||||
final String label;
|
final String label;
|
||||||
@@ -24,13 +25,10 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
int? get currentIndex => _currentIndex;
|
int? get currentIndex => _currentIndex;
|
||||||
|
|
||||||
static const List<String> kShowBottomNavScreen = [
|
List<String> get showBottomNavScreen => destinations
|
||||||
'home',
|
.where((ele) => ele.isPinned)
|
||||||
'explore',
|
.map((ele) => ele.screen)
|
||||||
'account',
|
.toList();
|
||||||
'album',
|
|
||||||
'chat',
|
|
||||||
];
|
|
||||||
|
|
||||||
static const List<AppNavDestination> kAllDestination = [
|
static const List<AppNavDestination> kAllDestination = [
|
||||||
AppNavDestination(
|
AppNavDestination(
|
||||||
@@ -88,7 +86,7 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
'home',
|
'home',
|
||||||
'explore',
|
'explore',
|
||||||
'chat',
|
'chat',
|
||||||
'account',
|
'realm',
|
||||||
];
|
];
|
||||||
|
|
||||||
List<AppNavDestination> destinations = [];
|
List<AppNavDestination> destinations = [];
|
||||||
@@ -143,4 +141,11 @@ class NavigationProvider extends ChangeNotifier {
|
|||||||
_currentIndex = idx;
|
_currentIndex = idx;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SnRealm? focusedRealm;
|
||||||
|
|
||||||
|
void setFocusedRealm(SnRealm? realm) {
|
||||||
|
focusedRealm = realm;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,13 +321,13 @@ class SnAttachmentProvider {
|
|||||||
uuid: ele.uuid,
|
uuid: ele.uuid,
|
||||||
content: ele,
|
content: ele,
|
||||||
accountId: ele.accountId,
|
accountId: ele.accountId,
|
||||||
cacheExpiredAt: DateTime.now().add(const Duration(days: 7)),
|
cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
|
||||||
),
|
),
|
||||||
onConflict: DoUpdate(
|
onConflict: DoUpdate(
|
||||||
(_) => SnLocalAttachmentCompanion.custom(
|
(_) => SnLocalAttachmentCompanion.custom(
|
||||||
content: Constant(jsonEncode(ele.toJson())),
|
content: Constant(jsonEncode(ele.toJson())),
|
||||||
cacheExpiredAt:
|
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:flutter/material.dart';
|
||||||
import 'package:provider/provider.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/providers/sn_network.dart';
|
||||||
import 'package:surface/types/realm.dart';
|
import 'package:surface/types/realm.dart';
|
||||||
|
|
||||||
class SnRealmProvider {
|
class SnRealmProvider {
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
|
late final DatabaseProvider _dt;
|
||||||
|
|
||||||
SnRealmProvider(BuildContext context) {
|
SnRealmProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
|
_dt = context.read<DatabaseProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, SnRealm> _cache = {};
|
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 {
|
Future<List<SnRealm>> listAvailableRealms() async {
|
||||||
final resp = await _sn.client.get('/cgi/id/realms/me/available');
|
final resp = await _sn.client.get('/cgi/id/realms/me/available');
|
||||||
@@ -21,6 +35,7 @@ class SnRealmProvider {
|
|||||||
_cache[realm.alias] = realm;
|
_cache[realm.alias] = realm;
|
||||||
_cache[realm.id.toString()] = realm;
|
_cache[realm.id.toString()] = realm;
|
||||||
}
|
}
|
||||||
|
_saveToLocal(out);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,10 +43,43 @@ class SnRealmProvider {
|
|||||||
if (_cache.containsKey(aliasOrId.toString())) {
|
if (_cache.containsKey(aliasOrId.toString())) {
|
||||||
return _cache[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 resp = await _sn.client.get('/cgi/id/realms/$aliasOrId');
|
||||||
final out = SnRealm.fromJson(resp.data);
|
final out = SnRealm.fromJson(resp.data);
|
||||||
_cache[out.alias] = out;
|
_cache[out.alias] = out;
|
||||||
_cache[out.id.toString()] = out;
|
_cache[out.id.toString()] = out;
|
||||||
|
_saveToLocal([out]);
|
||||||
return 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))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
lib/providers/translation.dart
Normal file
56
lib/providers/translation.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
class SnTranslator {
|
||||||
|
final Dio client = Dio(
|
||||||
|
BaseOptions(
|
||||||
|
baseUrl: kTranslateApiBaseUrl,
|
||||||
|
connectTimeout: Duration(seconds: 3),
|
||||||
|
sendTimeout: Duration(seconds: 3),
|
||||||
|
receiveTimeout: Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Map<String, String> _cache = {};
|
||||||
|
|
||||||
|
Future<String> translate(
|
||||||
|
String text, {
|
||||||
|
required String to,
|
||||||
|
String from = 'auto',
|
||||||
|
bool skipCache = false,
|
||||||
|
}) async {
|
||||||
|
if (text.isEmpty) return text;
|
||||||
|
|
||||||
|
final cacheKey = md5.convert(utf8.encode('$text$from$to')).toString();
|
||||||
|
if (!skipCache && _cache.containsKey(cacheKey)) {
|
||||||
|
return _cache[cacheKey]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
logging.info('[Translator] Translate $text from $from to $to');
|
||||||
|
|
||||||
|
final resp = await client.post(
|
||||||
|
'/translate',
|
||||||
|
data: {
|
||||||
|
'q': text,
|
||||||
|
'source': from,
|
||||||
|
'target': to,
|
||||||
|
'format': 'text',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (resp.statusCode == 200) {
|
||||||
|
final out = resp.data['translatedText'];
|
||||||
|
if (out.isNotEmpty) {
|
||||||
|
logging.info('[Translator] Translated $text from $from to $to');
|
||||||
|
_cache[cacheKey] = out;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw Exception('translate failed: $resp');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import 'package:surface/screens/account/badges.dart';
|
|||||||
import 'package:surface/screens/account/contact_methods.dart';
|
import 'package:surface/screens/account/contact_methods.dart';
|
||||||
import 'package:surface/screens/account/factor_settings.dart';
|
import 'package:surface/screens/account/factor_settings.dart';
|
||||||
import 'package:surface/screens/account/keypairs.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_page.dart';
|
||||||
import 'package:surface/screens/account/profile_edit.dart';
|
import 'package:surface/screens/account/profile_edit.dart';
|
||||||
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
import 'package:surface/screens/account/publishers/publisher_edit.dart';
|
||||||
@@ -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/publisher_page.dart';
|
||||||
import 'package:surface/screens/post/post_search.dart';
|
import 'package:surface/screens/post/post_search.dart';
|
||||||
import 'package:surface/screens/realm.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/manage.dart';
|
||||||
import 'package:surface/screens/realm/realm_detail.dart';
|
import 'package:surface/screens/realm/realm_detail.dart';
|
||||||
import 'package:surface/screens/realm/realm_discovery.dart';
|
import 'package:surface/screens/realm/realm_discovery.dart';
|
||||||
@@ -161,6 +164,18 @@ final _appRoutes = [
|
|||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'accountSettings',
|
name: 'accountSettings',
|
||||||
builder: (context, state) => AccountSettingsScreen(),
|
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(
|
GoRoute(
|
||||||
path: '/settings/factors',
|
path: '/settings/factors',
|
||||||
@@ -245,6 +260,13 @@ final _appRoutes = [
|
|||||||
child: const RealmScreen(),
|
child: const RealmScreen(),
|
||||||
),
|
),
|
||||||
routes: [
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: '/:alias/community',
|
||||||
|
name: 'realmCommunity',
|
||||||
|
builder: (context, state) => RealmCommunityScreen(
|
||||||
|
alias: state.pathParameters['alias']!,
|
||||||
|
),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/manage',
|
path: '/manage',
|
||||||
name: 'realmManage',
|
name: 'realmManage',
|
||||||
|
|||||||
@@ -97,6 +97,26 @@ class AccountSettingsScreen extends StatelessWidget {
|
|||||||
GoRouter.of(context).pushNamed('accountContactMethods');
|
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(
|
ListTile(
|
||||||
title: Text('accountProfileEdit').tr(),
|
title: Text('accountProfileEdit').tr(),
|
||||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
subtitle: Text('accountProfileEditSubtitle').tr(),
|
||||||
|
|||||||
122
lib/screens/account/prefs/notify.dart
Normal file
122
lib/screens/account/prefs/notify.dart
Normal file
@@ -0,0 +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';
|
||||||
|
|
||||||
|
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(
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/screens/captcha.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@@ -33,10 +34,20 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
final username = _usernameController.value.text;
|
final username = _usernameController.value.text;
|
||||||
final nickname = _nicknameController.value.text;
|
final nickname = _nicknameController.value.text;
|
||||||
final password = _passwordController.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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => TurnstileScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (captchaTk == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.post('/cgi/id/users', data: {
|
await sn.client.post('/cgi/id/users', data: {
|
||||||
@@ -45,6 +56,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
'email': email,
|
'email': email,
|
||||||
'password': password,
|
'password': password,
|
||||||
'language': EasyLocalization.of(context)!.currentLocale.toString(),
|
'language': EasyLocalization.of(context)!.currentLocale.toString(),
|
||||||
|
'captcha_token': captchaTk,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
@@ -91,8 +103,11 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.length < 4 || value.length > 32) {
|
if (value == null ||
|
||||||
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
value.length < 4 ||
|
||||||
|
value.length > 32) {
|
||||||
|
return 'fieldUsernameLengthLimit'
|
||||||
|
.tr(args: [4.toString(), 32.toString()]);
|
||||||
}
|
}
|
||||||
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
||||||
return 'fieldUsernameAlphanumOnly'.tr();
|
return 'fieldUsernameAlphanumOnly'.tr();
|
||||||
@@ -108,13 +123,17 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldUsername'.tr(),
|
labelText: 'fieldUsername'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.length < 4 || value.length > 32) {
|
if (value == null ||
|
||||||
return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
value.length < 4 ||
|
||||||
|
value.length > 32) {
|
||||||
|
return 'fieldNicknameLengthLimit'
|
||||||
|
.tr(args: [4.toString(), 32.toString()]);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -127,7 +146,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldNickname'.tr(),
|
labelText: 'fieldNickname'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -149,7 +169,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldEmail'.tr(),
|
labelText: 'fieldEmail'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -169,7 +190,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
border: const UnderlineInputBorder(),
|
border: const UnderlineInputBorder(),
|
||||||
labelText: 'fieldPassword'.tr(),
|
labelText: 'fieldPassword'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 7),
|
).padding(horizontal: 7),
|
||||||
@@ -186,9 +208,13 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
|||||||
Text(
|
Text(
|
||||||
'termAcceptNextWithAgree'.tr(),
|
'termAcceptNextWithAgree'.tr(),
|
||||||
textAlign: TextAlign.end,
|
textAlign: TextAlign.end,
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
style:
|
||||||
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
),
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha((255 * 0.75).round()),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Material(
|
Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
|
|||||||
38
lib/screens/captcha.dart
Normal file
38
lib/screens/captcha.dart
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
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 TurnstileScreen extends StatefulWidget {
|
||||||
|
const TurnstileScreen({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TurnstileScreen> createState() => _TurnstileScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TurnstileScreenState extends State<TurnstileScreen> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
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> {
|
class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
bool _isCalling = false;
|
bool _isCalling = false;
|
||||||
|
bool _isJoining = false;
|
||||||
|
|
||||||
SnChannel? _channel;
|
SnChannel? _channel;
|
||||||
|
SnChannelMember? _currentMember;
|
||||||
SnChannelMember? _otherMember;
|
SnChannelMember? _otherMember;
|
||||||
SnChatCall? _ongoingCall;
|
SnChatCall? _ongoingCall;
|
||||||
|
|
||||||
@@ -67,7 +69,24 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
|
|
||||||
StreamSubscription? _wsSubscription;
|
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 {
|
Future<void> _fetchChannel() async {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
@@ -76,6 +95,12 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
_channel = await chan.getChannel('${widget.scope}:${widget.alias}');
|
_channel = await chan.getChannel('${widget.scope}:${widget.alias}');
|
||||||
|
|
||||||
if (!mounted || _channel == null) return;
|
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 ud = context.read<UserDirectoryProvider>();
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
if (_channel!.type == 1) {
|
if (_channel!.type == 1) {
|
||||||
@@ -204,11 +229,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> _initializeChat() async {
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_messageController = ChatMessageController(context);
|
|
||||||
_fetchChannel().then((_) async {
|
_fetchChannel().then((_) async {
|
||||||
|
if (_currentMember == null) return;
|
||||||
await _messageController.initialize(_channel!);
|
await _messageController.initialize(_channel!);
|
||||||
|
|
||||||
if (widget.extra != null) {
|
if (widget.extra != null) {
|
||||||
@@ -230,6 +253,13 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
_fetchOngoingCall(),
|
_fetchOngoingCall(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_messageController = ChatMessageController(context);
|
||||||
|
_initializeChat();
|
||||||
|
|
||||||
_wsSubscription = _ws.pk.stream.listen((event) {
|
_wsSubscription = _ws.pk.stream.listen((event) {
|
||||||
switch (event.method) {
|
switch (event.method) {
|
||||||
@@ -281,25 +311,27 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
: _channel?.name ?? 'loading'.tr(),
|
: _channel?.name ?? 'loading'.tr(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
if (_currentMember != null)
|
||||||
onPressed: () {
|
IconButton(
|
||||||
setState(() => _isEncrypted = !_isEncrypted);
|
onPressed: () {
|
||||||
_inputGlobalKey.currentState?.setEncrypted(_isEncrypted);
|
setState(() => _isEncrypted = !_isEncrypted);
|
||||||
},
|
_inputGlobalKey.currentState?.setEncrypted(_isEncrypted);
|
||||||
icon: _isEncrypted
|
},
|
||||||
? const Icon(Symbols.lock)
|
icon: _isEncrypted
|
||||||
: const Icon(Symbols.no_encryption),
|
? const Icon(Symbols.lock)
|
||||||
),
|
: const Icon(Symbols.no_encryption),
|
||||||
IconButton(
|
),
|
||||||
icon: _ongoingCall == null
|
if (_currentMember != null)
|
||||||
? const Icon(Symbols.call)
|
IconButton(
|
||||||
: const Icon(Symbols.call_end),
|
icon: _ongoingCall == null
|
||||||
onPressed: _isCalling
|
? const Icon(Symbols.call)
|
||||||
? null
|
: const Icon(Symbols.call_end),
|
||||||
: _ongoingCall == null
|
onPressed: _isCalling
|
||||||
? _makeCall
|
? null
|
||||||
: _endCall,
|
: _ongoingCall == null
|
||||||
),
|
? _makeCall
|
||||||
|
: _endCall,
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.more_vert),
|
icon: const Icon(Symbols.more_vert),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -348,7 +380,41 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
).height(_ongoingCall != null ? 54 : 0, animate: true).animate(
|
).height(_ongoingCall != null ? 54 : 0, animate: true).animate(
|
||||||
const Duration(milliseconds: 300),
|
const Duration(milliseconds: 300),
|
||||||
Curves.fastLinearToSlowEaseIn),
|
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(
|
Expanded(
|
||||||
child: const CircularProgressIndicator().center(),
|
child: const CircularProgressIndicator().center(),
|
||||||
)
|
)
|
||||||
@@ -403,7 +469,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!_messageController.isPending)
|
if (!_messageController.isPending && _currentMember != null)
|
||||||
Material(
|
Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
data: ele.toJson(),
|
data: ele.toJson(),
|
||||||
createdAt: ele.createdAt)),
|
createdAt: ele.createdAt)),
|
||||||
);
|
);
|
||||||
_hasLoadedAll = postCount >= _feed.length;
|
_hasLoadedAll = _feed.length >= postCount;
|
||||||
|
|
||||||
if (mounted) setState(() => _isBusy = false);
|
if (mounted) setState(() => _isBusy = false);
|
||||||
}
|
}
|
||||||
@@ -551,12 +551,20 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
|||||||
maxWidth: 640,
|
maxWidth: 640,
|
||||||
);
|
);
|
||||||
case 'reader.news':
|
case 'reader.news':
|
||||||
return NewsFeedEntry(data: ele);
|
return Center(
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: NewsFeedEntry(data: ele),
|
||||||
|
),
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return FeedUnknownEntry(data: ele);
|
return Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: FeedUnknownEntry(data: ele),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
separatorBuilder: (_, __) => const Gap(8),
|
separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart';
|
|||||||
import 'package:surface/providers/special_day.dart';
|
import 'package:surface/providers/special_day.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/widget.dart';
|
import 'package:surface/providers/widget.dart';
|
||||||
|
import 'package:surface/screens/captcha.dart';
|
||||||
import 'package:surface/types/check_in.dart';
|
import 'package:surface/types/check_in.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
@@ -389,7 +390,7 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
|
|||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
const Gap(10),
|
const Gap(10),
|
||||||
Text('serviceStatusOperational').tr(),
|
Text('loading').tr(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: switch (_serviceStatus) {
|
: switch (_serviceStatus) {
|
||||||
@@ -434,6 +435,7 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
|
|||||||
padding: EdgeInsets.only(top: 6),
|
padding: EdgeInsets.only(top: 6),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
for (final entry in _statuses!.entries)
|
for (final entry in _statuses!.entries)
|
||||||
Tooltip(
|
Tooltip(
|
||||||
@@ -441,6 +443,8 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
|
|||||||
? 'serviceName${kServicesName[entry.key]}'.tr()
|
? 'serviceName${kServicesName[entry.key]}'.tr()
|
||||||
: 'unknown'.tr(),
|
: 'unknown'.tr(),
|
||||||
child: Chip(
|
child: Chip(
|
||||||
|
visualDensity:
|
||||||
|
VisualDensity(horizontal: -4, vertical: -4),
|
||||||
avatar: entry.value
|
avatar: entry.value
|
||||||
? const Icon(
|
? const Icon(
|
||||||
Symbols.circle,
|
Symbols.circle,
|
||||||
@@ -505,11 +509,20 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _doCheckIn() async {
|
Future<void> _doCheckIn() async {
|
||||||
|
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => TurnstileScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (captchaTk == null) return;
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final home = context.read<HomeWidgetProvider>();
|
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);
|
_todayRecord = SnCheckInRecord.fromJson(resp.data);
|
||||||
await home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
|
await home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -877,8 +890,10 @@ class _HomeDashRecommendationPostWidgetState
|
|||||||
).tr(),
|
).tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text('${_currentPage + 1}/${_posts?.length ?? 0}',
|
Text(
|
||||||
style: GoogleFonts.robotoMono())
|
'${_currentPage + 1}/${_posts?.length ?? 0}',
|
||||||
|
style: GoogleFonts.robotoMono(),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
).padding(horizontal: 18, top: 12, bottom: 8),
|
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -896,6 +911,7 @@ class _HomeDashRecommendationPostWidgetState
|
|||||||
child: PostItem(
|
child: PostItem(
|
||||||
data: _posts![index],
|
data: _posts![index],
|
||||||
showMenu: false,
|
showMenu: false,
|
||||||
|
showFullPost: true,
|
||||||
).padding(bottom: 8),
|
).padding(bottom: 8),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context)
|
GoRouter.of(context)
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
queryParameters: {'take': 10, 'offset': _notifications.length},
|
queryParameters: {'take': 10, 'offset': _notifications.length},
|
||||||
);
|
);
|
||||||
_totalCount = resp.data['count'];
|
_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();
|
nty.updateTray();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -98,7 +101,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
nty.clear();
|
nty.clear();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('notificationMarkAllReadPrompt'.plural(resp.data['count']));
|
context.showSnackbar(
|
||||||
|
'notificationMarkAllReadPrompt'.plural(resp.data['count']));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -122,7 +126,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
_fetchNotifications();
|
_fetchNotifications();
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']));
|
context.showSnackbar(
|
||||||
|
'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -143,7 +148,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
|
|
||||||
if (!ua.isAuthorized) {
|
if (!ua.isAuthorized) {
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(leading: AutoAppBarLeading(), title: Text('screenNotification').tr()),
|
appBar: AppBar(
|
||||||
|
leading: AutoAppBarLeading(),
|
||||||
|
title: Text('screenNotification').tr()),
|
||||||
body: Center(child: UnauthorizedHint()),
|
body: Center(child: UnauthorizedHint()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -153,7 +160,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text('screenNotification').tr(),
|
title: Text('screenNotification').tr(),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(icon: const Icon(Symbols.checklist), onPressed: _isSubmitting ? null : _markAllAsRead),
|
IconButton(
|
||||||
|
icon: const Icon(Symbols.checklist),
|
||||||
|
onPressed: _isSubmitting ? null : _markAllAsRead),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -167,13 +176,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
return _fetchNotifications();
|
return _fetchNotifications();
|
||||||
},
|
},
|
||||||
child: InfiniteList(
|
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,
|
itemCount: _notifications.length,
|
||||||
onFetchData: () {
|
onFetchData: () {
|
||||||
_fetchNotifications();
|
_fetchNotifications();
|
||||||
},
|
},
|
||||||
isLoading: _isBusy,
|
isLoading: _isBusy,
|
||||||
hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!,
|
hasReachedMax: _totalCount != null &&
|
||||||
|
_notifications.length >= _totalCount!,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
final nty = _notifications[idx];
|
final nty = _notifications[idx];
|
||||||
return Row(
|
return Row(
|
||||||
@@ -186,12 +199,19 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (nty.readAt == null)
|
if (nty.readAt == null)
|
||||||
StyledWidget(Badge(label: Text('notificationUnread').tr())).padding(bottom: 4),
|
StyledWidget(Badge(
|
||||||
Text(nty.title, style: Theme.of(context).textTheme.titleMedium),
|
label: Text('notificationUnread').tr()))
|
||||||
|
.padding(bottom: 4),
|
||||||
|
Text(nty.title,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium),
|
||||||
if (nty.subtitle != null)
|
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),
|
if (nty.subtitle != null) const Gap(4),
|
||||||
SelectionArea(child: MarkdownTextContent(content: nty.body, isAutoWarp: true)),
|
SelectionArea(
|
||||||
|
child: MarkdownTextContent(
|
||||||
|
content: nty.body, isAutoWarp: true)),
|
||||||
if ([
|
if ([
|
||||||
'interactive.reply',
|
'interactive.reply',
|
||||||
'interactive.feedback',
|
'interactive.feedback',
|
||||||
@@ -201,31 +221,43 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
GestureDetector(
|
GestureDetector(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(
|
||||||
border: Border.all(color: Theme.of(context).dividerColor, width: 1),
|
Radius.circular(8)),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1),
|
||||||
),
|
),
|
||||||
child: PostItem(
|
child: PostItem(
|
||||||
data: SnPost.fromJson(nty.metadata['related_post']!),
|
data: SnPost.fromJson(
|
||||||
|
nty.metadata['related_post']!),
|
||||||
showComments: false,
|
showComments: false,
|
||||||
showReactions: false,
|
showReactions: false,
|
||||||
showMenu: false,
|
showMenu: false,
|
||||||
),
|
).padding(vertical: 4),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postDetail',
|
'postDetail',
|
||||||
pathParameters: {'slug': nty.metadata['related_post']!['id'].toString()},
|
pathParameters: {
|
||||||
|
'slug': nty
|
||||||
|
.metadata['related_post']!['id']
|
||||||
|
.toString()
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
).padding(top: 8),
|
).padding(top: 8),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(DateFormat('yy/MM/dd').format(nty.createdAt)).fontSize(12),
|
Text(DateFormat('yy/MM/dd')
|
||||||
|
.format(nty.createdAt))
|
||||||
|
.fontSize(12),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text('·', style: TextStyle(fontSize: 12)),
|
Text('·', style: TextStyle(fontSize: 12)),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text(RelativeTime(context).format(nty.createdAt)).fontSize(12),
|
Text(RelativeTime(context)
|
||||||
|
.format(nty.createdAt))
|
||||||
|
.fontSize(12),
|
||||||
],
|
],
|
||||||
).opacity(0.75),
|
).opacity(0.75),
|
||||||
],
|
],
|
||||||
@@ -235,8 +267,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Symbols.check),
|
icon: const Icon(Symbols.check),
|
||||||
padding: EdgeInsets.all(0),
|
padding: EdgeInsets.all(0),
|
||||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity:
|
||||||
onPressed: _isSubmitting ? null : () => _markOneAsRead(nty),
|
const VisualDensity(horizontal: -4, vertical: -4),
|
||||||
|
onPressed:
|
||||||
|
_isSubmitting ? null : () => _markOneAsRead(nty),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 16);
|
).padding(horizontal: 16);
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ class PostDetailScreen extends StatefulWidget {
|
|||||||
final SnPost? preload;
|
final SnPost? preload;
|
||||||
final Function? onBack;
|
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
|
@override
|
||||||
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
||||||
@@ -88,14 +89,16 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
TextSpan(
|
TextSpan(
|
||||||
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
color:
|
||||||
|
Theme.of(context).appBarTheme.foregroundColor!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const TextSpan(text: '\n'),
|
const TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'postDetail'.tr(),
|
text: 'postDetail'.tr(),
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
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)
|
||||||
if (_data != null && _data!.type != 'video')
|
SliverToBoxAdapter(
|
||||||
|
child: Divider(height: 1).padding(top: 8),
|
||||||
|
),
|
||||||
|
if (_data != null)
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
@@ -141,7 +147,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
).padding(horizontal: 20, vertical: 12).center(),
|
).padding(horizontal: 20, vertical: 12).center(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_data != null && ua.isAuthorized && _data!.type != 'video')
|
if (_data != null && ua.isAuthorized)
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: PostCommentQuickAction(
|
child: PostCommentQuickAction(
|
||||||
parentPost: _data!,
|
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(
|
PostCommentSliverList(
|
||||||
key: _childListKey,
|
key: _childListKey,
|
||||||
parentPost: _data!,
|
parentPost: _data!,
|
||||||
maxWidth: maxWidth,
|
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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/dialog.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? title;
|
||||||
final String? description;
|
final String? description;
|
||||||
final List<PostWriteMedia>? attachments;
|
final List<PostWriteMedia>? attachments;
|
||||||
|
final SnRealm? realm;
|
||||||
|
|
||||||
const PostEditorExtra({
|
const PostEditorExtra({
|
||||||
this.text,
|
this.text,
|
||||||
this.title,
|
this.title,
|
||||||
this.description,
|
this.description,
|
||||||
this.attachments,
|
this.attachments,
|
||||||
|
this.realm,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +265,7 @@ class _PostEditorScreenState extends State<PostEditorScreen>
|
|||||||
_writeController.descriptionController.text =
|
_writeController.descriptionController.text =
|
||||||
widget.extraProps!.description ?? '';
|
widget.extraProps!.description ?? '';
|
||||||
_writeController.addAttachments(widget.extraProps!.attachments ?? []);
|
_writeController.addAttachments(widget.extraProps!.attachments ?? []);
|
||||||
|
_writeController.setRealm(widget.extraProps!.realm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _fetchPosts() async {
|
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;
|
if (_postCount != null && _posts.length >= _postCount!) return;
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
@@ -152,7 +153,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (_, __) => const Gap(8),
|
separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 16,
|
top: 16,
|
||||||
@@ -166,7 +167,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
padding: const WidgetStatePropertyAll(
|
padding: const WidgetStatePropertyAll(
|
||||||
EdgeInsets.symmetric(horizontal: 24),
|
EdgeInsets.symmetric(horizontal: 24),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
_searchTerm = value;
|
_searchTerm = value;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ class PostPublisherScreen extends StatefulWidget {
|
|||||||
State<PostPublisherScreen> createState() => _PostPublisherScreenState();
|
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 ScrollController _scrollController = ScrollController();
|
||||||
late final TabController _tabController = TabController(length: 3, vsync: this);
|
late final TabController _tabController =
|
||||||
|
TabController(length: 3, vsync: this);
|
||||||
|
|
||||||
SnPublisher? _publisher;
|
SnPublisher? _publisher;
|
||||||
SnAccount? _account;
|
SnAccount? _account;
|
||||||
@@ -66,7 +68,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
_account = await ud.getAccount(_publisher?.accountId);
|
_account = await ud.getAccount(_publisher?.accountId);
|
||||||
_accountRelationship = await rel.getRelationship(_account!.id);
|
_accountRelationship = await rel.getRelationship(_account!.id);
|
||||||
if (_publisher?.realmId != null && _publisher!.realmId != 0) {
|
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);
|
_realm = SnRealm.fromJson(resp.data);
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -133,12 +136,14 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
double _appBarBlur = 0.0;
|
double _appBarBlur = 0.0;
|
||||||
|
|
||||||
late final _appBarWidth = MediaQuery.of(context).size.width;
|
late final _appBarWidth = MediaQuery.of(context).size.width;
|
||||||
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
|
late final _appBarHeight =
|
||||||
|
(_appBarWidth * kBannerAspectRatio).roundToDouble();
|
||||||
|
|
||||||
void _updateAppBarBlur() {
|
void _updateAppBarBlur() {
|
||||||
if (_scrollController.offset > _appBarHeight) return;
|
if (_scrollController.offset > _appBarHeight) return;
|
||||||
setState(() {
|
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,
|
'related': _account!.name,
|
||||||
});
|
});
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
context.showSnackbar(
|
||||||
|
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -209,9 +215,11 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final rel = context.read<SnRelationshipProvider>();
|
final rel = context.read<SnRelationshipProvider>();
|
||||||
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
await rel.updateRelationship(
|
||||||
|
_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
context.showSnackbar(
|
||||||
|
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
@@ -299,7 +307,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
text: TextSpan(children: [
|
text: TextSpan(children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: _publisher!.nick,
|
text: _publisher!.nick,
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleLarge!
|
||||||
|
.copyWith(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shadows: labelShadows,
|
shadows: labelShadows,
|
||||||
),
|
),
|
||||||
@@ -307,7 +318,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
const TextSpan(text: '\n'),
|
const TextSpan(text: '\n'),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: '@${_publisher!.name}',
|
text: '@${_publisher!.name}',
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall!
|
||||||
|
.copyWith(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
shadows: labelShadows,
|
shadows: labelShadows,
|
||||||
),
|
),
|
||||||
@@ -330,13 +344,16 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
Container(
|
Container(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainer,
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: 56 + MediaQuery.of(context).padding.top,
|
height:
|
||||||
|
56 + MediaQuery.of(context).padding.top,
|
||||||
child: ClipRect(
|
child: ClipRect(
|
||||||
child: BackdropFilter(
|
child: BackdropFilter(
|
||||||
filter: ImageFilter.blur(
|
filter: ImageFilter.blur(
|
||||||
@@ -345,7 +362,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Colors.black.withOpacity(
|
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),
|
const Gap(16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_publisher!.nick,
|
_publisher!.nick,
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium,
|
||||||
).bold(),
|
).bold(),
|
||||||
Text('@${_publisher!.name}').fontSize(13),
|
Text('@${_publisher!.name}').fontSize(13),
|
||||||
],
|
],
|
||||||
@@ -387,7 +408,9 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
elevation: WidgetStatePropertyAll(0),
|
elevation: WidgetStatePropertyAll(0),
|
||||||
),
|
),
|
||||||
onPressed: _isSubscribing ? null : _toggleSubscription,
|
onPressed: _isSubscribing
|
||||||
|
? null
|
||||||
|
: _toggleSubscription,
|
||||||
label: Text('subscribe').tr(),
|
label: Text('subscribe').tr(),
|
||||||
icon: const Icon(Symbols.add),
|
icon: const Icon(Symbols.add),
|
||||||
)
|
)
|
||||||
@@ -396,14 +419,17 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
elevation: WidgetStatePropertyAll(0),
|
elevation: WidgetStatePropertyAll(0),
|
||||||
),
|
),
|
||||||
onPressed: _isSubscribing ? null : _toggleSubscription,
|
onPressed: _isSubscribing
|
||||||
|
? null
|
||||||
|
: _toggleSubscription,
|
||||||
label: Text('unsubscribe').tr(),
|
label: Text('unsubscribe').tr(),
|
||||||
icon: const Icon(Symbols.remove),
|
icon: const Icon(Symbols.remove),
|
||||||
),
|
),
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
style: ButtonStyle(
|
style: ButtonStyle(
|
||||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
visualDensity: VisualDensity(
|
||||||
|
horizontal: -4, vertical: -4),
|
||||||
),
|
),
|
||||||
itemBuilder: (BuildContext context) => [
|
itemBuilder: (BuildContext context) => [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
@@ -443,7 +469,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Text(_publisher!.description).padding(horizontal: 8),
|
Text(_publisher!.description)
|
||||||
|
.padding(horizontal: 8),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -451,8 +478,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.calendar_add_on),
|
const Icon(Symbols.calendar_add_on),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('publisherJoinedAt')
|
Text('publisherJoinedAt').tr(args: [
|
||||||
.tr(args: [DateFormat('y/M/d').format(_publisher!.createdAt)]),
|
DateFormat('y/M/d')
|
||||||
|
.format(_publisher!.createdAt)
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@@ -460,7 +489,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
const Icon(Symbols.trending_up),
|
const Icon(Symbols.trending_up),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
Text('publisherSocialPointTotal').plural(
|
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 Icon(Symbols.group_work),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
InkWell(
|
InkWell(
|
||||||
child: Text('publisherAffiliatedBy').tr(args: [
|
child: Text('publisherAffiliatedBy')
|
||||||
|
.tr(args: [
|
||||||
'@${_realm?.alias ?? 'unknown'}',
|
'@${_realm?.alias ?? 'unknown'}',
|
||||||
]),
|
]),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'realmDetail',
|
'realmDetail',
|
||||||
pathParameters: {'alias': _realm!.alias},
|
pathParameters: {
|
||||||
|
'alias': _realm!.alias
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
AccountImage(content: _realm?.avatar, radius: 8),
|
AccountImage(
|
||||||
|
content: _realm?.avatar, radius: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@@ -502,7 +536,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Gap(8),
|
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,
|
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);
|
).padding(top: 8);
|
||||||
|
|||||||
@@ -336,6 +336,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
setState(() {});
|
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(
|
ListTile(
|
||||||
leading: const Icon(Symbols.font_download),
|
leading: const Icon(Symbols.font_download),
|
||||||
title: Text('settingsCustomFonts').tr(),
|
title: Text('settingsCustomFonts').tr(),
|
||||||
@@ -387,6 +400,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
.fontSize(17)
|
.fontSize(17)
|
||||||
.tr()
|
.tr()
|
||||||
.padding(horizontal: 20, bottom: 4),
|
.padding(horizontal: 20, bottom: 4),
|
||||||
|
CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.translate),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
title: Text('settingsAutoTranslate').tr(),
|
||||||
|
subtitle: Text('settingsAutoTranslateDescription').tr(),
|
||||||
|
value: _prefs.getBool(kAppAutoTranslate) ?? false,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_prefs.setBool(kAppAutoTranslate, value ?? false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
CheckboxListTile(
|
CheckboxListTile(
|
||||||
secondary: const Icon(Symbols.vibration),
|
secondary: const Icon(Symbols.vibration),
|
||||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ Future<ThemeData> createAppTheme(
|
|||||||
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false),
|
||||||
|
sliderTheme: SliderThemeData(year2023: false),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class AccountImage extends StatelessWidget {
|
|||||||
final Widget? fallbackWidget;
|
final Widget? fallbackWidget;
|
||||||
final Widget? badge;
|
final Widget? badge;
|
||||||
final Offset? badgeOffset;
|
final Offset? badgeOffset;
|
||||||
|
final FilterQuality? filterQuality;
|
||||||
|
|
||||||
const AccountImage({
|
const AccountImage({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -25,6 +26,7 @@ class AccountImage extends StatelessWidget {
|
|||||||
this.fallbackWidget,
|
this.fallbackWidget,
|
||||||
this.badge,
|
this.badge,
|
||||||
this.badgeOffset,
|
this.badgeOffset,
|
||||||
|
this.filterQuality,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -54,6 +56,7 @@ class AccountImage extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: AutoResizeUniversalImage(
|
: AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(url),
|
sn.getAttachmentUrl(url),
|
||||||
|
filterQuality: filterQuality,
|
||||||
key: Key('attachment-${content.hashCode}'),
|
key: Key('attachment-${content.hashCode}'),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -45,11 +45,25 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
case 'image':
|
case 'image':
|
||||||
return Hero(
|
return Hero(
|
||||||
tag: 'attachment-${data!.rid}-$tag',
|
tag: 'attachment-${data!.rid}-$tag',
|
||||||
child: AutoResizeUniversalImage(
|
child: Stack(
|
||||||
sn.getAttachmentUrl(data!.rid),
|
fit: StackFit.expand,
|
||||||
key: Key('attachment-${data!.rid}-$tag'),
|
children: [
|
||||||
fit: fit,
|
ImageFiltered(
|
||||||
filterQuality: filterQuality,
|
imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||||
|
child: AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(data!.rid),
|
||||||
|
key: Key('attachment-${data!.rid}-$tag-blur-background'),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
filterQuality: filterQuality,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AutoResizeUniversalImage(
|
||||||
|
sn.getAttachmentUrl(data!.rid),
|
||||||
|
key: Key('attachment-${data!.rid}-$tag'),
|
||||||
|
fit: fit,
|
||||||
|
filterQuality: filterQuality,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
case 'video':
|
case 'video':
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/keypair.dart';
|
import 'package:surface/providers/keypair.dart';
|
||||||
|
import 'package:surface/providers/translation.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
@@ -18,6 +19,7 @@ import 'package:surface/widgets/account/account_popover.dart';
|
|||||||
import 'package:surface/widgets/account/badge.dart';
|
import 'package:surface/widgets/account/badge.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||||
import 'package:surface/widgets/context_menu.dart';
|
import 'package:surface/widgets/context_menu.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/link_preview.dart';
|
import 'package:surface/widgets/link_preview.dart';
|
||||||
import 'package:surface/widgets/markdown_content.dart';
|
import 'package:surface/widgets/markdown_content.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
@@ -63,7 +65,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
key: Key('chat-message-${data.id}'),
|
key: Key('chat-message-${data.id}'),
|
||||||
iconOnLeftSwipe: Symbols.reply,
|
iconOnLeftSwipe: Symbols.reply,
|
||||||
iconOnRightSwipe: Symbols.edit,
|
iconOnRightSwipe: Symbols.edit,
|
||||||
swipeSensitivity: 20,
|
swipeSensitivity: 10,
|
||||||
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
||||||
onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
|
onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
|
||||||
child: ContextMenuArea(
|
child: ContextMenuArea(
|
||||||
@@ -228,7 +230,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChatMessageText extends StatelessWidget {
|
class _ChatMessageText extends StatefulWidget {
|
||||||
final SnChatMessage data;
|
final SnChatMessage data;
|
||||||
final Function(SnChatMessage)? onReply;
|
final Function(SnChatMessage)? onReply;
|
||||||
final Function(SnChatMessage)? onEdit;
|
final Function(SnChatMessage)? onEdit;
|
||||||
@@ -237,13 +239,56 @@ class _ChatMessageText extends StatelessWidget {
|
|||||||
const _ChatMessageText(
|
const _ChatMessageText(
|
||||||
{required this.data, this.onReply, this.onEdit, this.onDelete});
|
{required this.data, this.onReply, this.onEdit, this.onDelete});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ChatMessageText> createState() => _ChatMessageTextState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChatMessageTextState extends State<_ChatMessageText> {
|
||||||
|
late String _displayText = widget.data.body['text'] ?? '';
|
||||||
|
bool _isTranslated = false;
|
||||||
|
bool _isTranslating = false;
|
||||||
|
|
||||||
|
Future<void> _translateText() async {
|
||||||
|
if (widget.data.body['text'] == null || widget.data.body['text']!.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final ta = context.read<SnTranslator>();
|
||||||
|
setState(() => _isTranslating = true);
|
||||||
|
try {
|
||||||
|
final to = EasyLocalization.of(context)!.locale.languageCode;
|
||||||
|
_displayText = await ta.translate(
|
||||||
|
widget.data.body['text'],
|
||||||
|
to: to,
|
||||||
|
);
|
||||||
|
_isTranslated = true;
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
} catch (err) {
|
||||||
|
if (mounted) context.showErrorDialog(err);
|
||||||
|
} finally {
|
||||||
|
setState(() => _isTranslating = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
if (cfg.autoTranslate) {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
_translateText();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
|
|
||||||
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id;
|
final isOwner =
|
||||||
|
ua.isAuthorized && widget.data.sender.accountId == ua.user?.id;
|
||||||
|
|
||||||
if (data.body['text'] != null && data.body['text'].isNotEmpty) {
|
if (widget.data.body['text'] != null &&
|
||||||
|
widget.data.body['text'].isNotEmpty) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -252,38 +297,50 @@ class _ChatMessageText extends StatelessWidget {
|
|||||||
final List<ContextMenuButtonItem> items =
|
final List<ContextMenuButtonItem> items =
|
||||||
editableTextState.contextMenuButtonItems;
|
editableTextState.contextMenuButtonItems;
|
||||||
|
|
||||||
if (onReply != null) {
|
if (widget.onReply != null) {
|
||||||
items.insert(
|
items.insert(
|
||||||
0,
|
0,
|
||||||
ContextMenuButtonItem(
|
ContextMenuButtonItem(
|
||||||
label: 'reply'.tr(),
|
label: 'reply'.tr(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ContextMenuController.removeAny();
|
ContextMenuController.removeAny();
|
||||||
onReply?.call(data);
|
widget.onReply?.call(widget.data);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isOwner && onEdit != null) {
|
if (isOwner && widget.onEdit != null) {
|
||||||
items.insert(
|
items.insert(
|
||||||
1,
|
1,
|
||||||
ContextMenuButtonItem(
|
ContextMenuButtonItem(
|
||||||
label: 'edit'.tr(),
|
label: 'edit'.tr(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ContextMenuController.removeAny();
|
ContextMenuController.removeAny();
|
||||||
onEdit?.call(data);
|
widget.onEdit?.call(widget.data);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isOwner && onDelete != null) {
|
if (isOwner && widget.onDelete != null) {
|
||||||
items.insert(
|
items.insert(
|
||||||
2,
|
2,
|
||||||
ContextMenuButtonItem(
|
ContextMenuButtonItem(
|
||||||
label: 'delete'.tr(),
|
label: 'delete'.tr(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ContextMenuController.removeAny();
|
ContextMenuController.removeAny();
|
||||||
onDelete?.call(data);
|
widget.onDelete?.call(widget.data);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (widget.data.body['algorithm'] == 'plain') {
|
||||||
|
items.insert(
|
||||||
|
3,
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'translate'.tr(),
|
||||||
|
onPressed: () {
|
||||||
|
ContextMenuController.removeAny();
|
||||||
|
_translateText();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -294,26 +351,47 @@ class _ChatMessageText extends StatelessWidget {
|
|||||||
buttonItems: items,
|
buttonItems: items,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: switch (data.body['algorithm']) {
|
child: switch (widget.data.body['algorithm']) {
|
||||||
'rsa' => _ChatDecryptMessage(message: data),
|
'rsa' => _ChatDecryptMessage(message: widget.data),
|
||||||
_ => MarkdownTextContent(
|
_ => MarkdownTextContent(
|
||||||
content: data.body['text'],
|
content: _displayText,
|
||||||
isAutoWarp: true,
|
isAutoWarp: true,
|
||||||
isEnlargeSticker:
|
isEnlargeSticker: RegExp(r"^:([-\w]+):$")
|
||||||
RegExp(r"^:([-\w]+):$").hasMatch(data.body['text'] ?? ''),
|
.hasMatch(widget.data.body['text'] ?? ''),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (data.updatedAt != data.createdAt)
|
if (widget.data.updatedAt != widget.data.createdAt)
|
||||||
Text('messageEditedHint'.tr()).fontSize(13).opacity(0.75),
|
Text('messageEditedHint'.tr()).fontSize(13).opacity(0.75),
|
||||||
|
if (_isTranslating)
|
||||||
|
AnimateWidgetExtensions(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: Text('translated').tr().opacity(0.75),
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_displayText = widget.data.body['text'] ?? '';
|
||||||
|
_isTranslated = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else if (data.body['attachments']?.isNotEmpty) {
|
} else if (widget.data.body['attachments']?.isNotEmpty) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.file_present, size: 20),
|
const Icon(Symbols.file_present, size: 20),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
Text('messageFileHint'.plural(data.body['attachments']!.length)),
|
Text('messageFileHint'
|
||||||
|
.plural(widget.data.body['attachments']!.length)),
|
||||||
],
|
],
|
||||||
).opacity(0.8);
|
).opacity(0.8);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,12 @@ class ChatTypingIndicator extends StatelessWidget {
|
|||||||
'messageTyping'
|
'messageTyping'
|
||||||
.plural(controller.typingMembers.length, args: [
|
.plural(controller.typingMembers.length, args: [
|
||||||
controller.typingMembers
|
controller.typingMembers
|
||||||
.map((ele) => (ele.nick?.isNotEmpty ?? false)
|
.map(
|
||||||
? ele.nick!
|
(ele) => (ele.nick?.isNotEmpty ?? false)
|
||||||
: ud.getFromCache(ele.accountId)?.name ??
|
? ele.nick!
|
||||||
'unknown')
|
: ud.getFromCache(ele.accountId)?.nick ??
|
||||||
|
'unknown',
|
||||||
|
)
|
||||||
.join(', '),
|
.join(', '),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -19,89 +19,87 @@ class NewsFeedEntry extends StatelessWidget {
|
|||||||
.cast<SnNewsArticle>()
|
.cast<SnNewsArticle>()
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return Card(
|
return Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Row(
|
||||||
Row(
|
children: [
|
||||||
children: [
|
const Icon(Symbols.newspaper),
|
||||||
const Icon(Symbols.newspaper),
|
const Gap(8),
|
||||||
const Gap(8),
|
Text(
|
||||||
Text(
|
'newsToday',
|
||||||
'newsToday',
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
).tr()
|
||||||
).tr()
|
],
|
||||||
],
|
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||||
).padding(horizontal: 18, top: 12, bottom: 8),
|
Container(
|
||||||
Container(
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
height: 150,
|
||||||
height: 150,
|
child: ListView.separated(
|
||||||
child: ListView.separated(
|
scrollDirection: Axis.horizontal,
|
||||||
scrollDirection: Axis.horizontal,
|
itemCount: news.length,
|
||||||
itemCount: news.length,
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
itemBuilder: (context, idx) {
|
||||||
itemBuilder: (context, idx) {
|
return Container(
|
||||||
return Container(
|
width: 360,
|
||||||
width: 360,
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
border: Border.all(
|
||||||
border: Border.all(
|
color: Theme.of(context).dividerColor,
|
||||||
color: Theme.of(context).dividerColor,
|
width: 1,
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
),
|
),
|
||||||
child: Material(
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
elevation: 0,
|
),
|
||||||
color: Theme.of(context).colorScheme.surface,
|
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)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: InkWell(
|
child: Column(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Text(
|
||||||
children: [
|
news[idx].title,
|
||||||
Text(
|
maxLines: 2,
|
||||||
news[idx].title,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
maxLines: 2,
|
).padding(horizontal: 16, top: 12, bottom: 4),
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
Text(
|
||||||
).padding(horizontal: 16, top: 12, bottom: 4),
|
news[idx].description,
|
||||||
Text(
|
maxLines: 2,
|
||||||
news[idx].description,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
maxLines: 2,
|
).padding(horizontal: 16, vertical: 4),
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
const Gap(4),
|
||||||
).padding(horizontal: 16, vertical: 4),
|
Row(
|
||||||
const Gap(4),
|
children: [
|
||||||
Row(
|
Text(
|
||||||
children: [
|
DateFormat('y/M/d HH:mm')
|
||||||
Text(
|
.format(news[idx].createdAt.toLocal()),
|
||||||
DateFormat('y/M/d HH:mm')
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
.format(news[idx].createdAt.toLocal()),
|
),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
const Gap(4),
|
||||||
),
|
Text(
|
||||||
const Gap(4),
|
RelativeTime(context)
|
||||||
Text(
|
.format(news[idx].createdAt.toLocal()),
|
||||||
RelativeTime(context)
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
.format(news[idx].createdAt.toLocal()),
|
),
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
],
|
||||||
),
|
).opacity(0.8).padding(horizontal: 16),
|
||||||
],
|
],
|
||||||
).opacity(0.8).padding(horizontal: 16),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
GoRouter.of(context).pushNamed(
|
|
||||||
'newsDetail',
|
|
||||||
pathParameters: {'hash': news[idx].hash},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
const Icon(Symbols.help, size: 36),
|
||||||
const Icon(Symbols.help, size: 36),
|
const Gap(4),
|
||||||
const Gap(4),
|
Text('feedUnknownItem').tr(),
|
||||||
Text('feedUnknownItem').tr(),
|
Text(data.type, style: GoogleFonts.robotoMono()),
|
||||||
Text(data.type, style: GoogleFonts.robotoMono()),
|
],
|
||||||
],
|
).padding(horizontal: 12, vertical: 8);
|
||||||
).padding(horizontal: 12, vertical: 8),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ class _LoadingIndicatorState extends State<LoadingIndicator>
|
|||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 16,
|
height: 16,
|
||||||
width: 16,
|
width: 16,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2.5),
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
Text('loading').tr(),
|
Text('loading').tr(),
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: attachment.metadata['ratio'] ??
|
aspectRatio: attachment.metadata['ratio']?.toDouble() ??
|
||||||
switch (attachment.mimetype
|
switch (attachment.mimetype
|
||||||
.split('/')
|
.split('/')
|
||||||
.firstOrNull) {
|
.firstOrNull) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -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)
|
if (onQuit != null)
|
||||||
PlatformMenuItem(
|
PlatformMenuItem(
|
||||||
shortcut: const SingleActivator(
|
shortcut: const SingleActivator(
|
||||||
|
|||||||
@@ -37,17 +37,15 @@ class _AppBottomNavigationBarState extends State<AppBottomNavigationBar> {
|
|||||||
...nav.destinations.where((ele) => ele.isPinned),
|
...nav.destinations.where((ele) => ele.isPinned),
|
||||||
];
|
];
|
||||||
|
|
||||||
return BottomNavigationBar(
|
return NavigationBar(
|
||||||
currentIndex: nav.getIndexInRange(0, nav.pinnedDestinationCount),
|
selectedIndex: nav.getIndexInRange(0, nav.pinnedDestinationCount),
|
||||||
type: BottomNavigationBarType.fixed,
|
destinations: destinations.map((ele) {
|
||||||
showUnselectedLabels: false,
|
return NavigationDestination(
|
||||||
items: destinations.map((ele) {
|
|
||||||
return BottomNavigationBarItem(
|
|
||||||
icon: ele.icon,
|
icon: ele.icon,
|
||||||
label: ele.label.tr(),
|
label: ele.label.tr(),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onTap: (idx) {
|
onDestinationSelected: (idx) {
|
||||||
nav.setIndex(idx);
|
nav.setIndex(idx);
|
||||||
GoRouter.of(context).goNamed(destinations[idx].screen);
|
GoRouter.of(context).goNamed(destinations[idx].screen);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:animations/animations.dart';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/channel.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/navigation.dart';
|
import 'package:surface/providers/navigation.dart';
|
||||||
|
import 'package:surface/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';
|
import 'package:surface/widgets/version_label.dart';
|
||||||
|
|
||||||
class AppNavigationDrawer extends StatefulWidget {
|
class AppNavigationDrawer extends StatefulWidget {
|
||||||
@@ -25,12 +34,15 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context));
|
context
|
||||||
|
.read<NavigationProvider>()
|
||||||
|
.autoDetectIndex(GoRouter.maybeOf(context));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
final nav = context.watch<NavigationProvider>();
|
final nav = context.watch<NavigationProvider>();
|
||||||
final cfg = context.watch<ConfigProvider>();
|
final cfg = context.watch<ConfigProvider>();
|
||||||
|
|
||||||
@@ -39,60 +51,258 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
|||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: nav,
|
listenable: nav,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
final destinations = [
|
return Drawer(
|
||||||
...nav.destinations.where((ele) => ele.isPinned),
|
|
||||||
...nav.destinations.where((ele) => !ele.isPinned),
|
|
||||||
];
|
|
||||||
|
|
||||||
return NavigationDrawer(
|
|
||||||
elevation: widget.elevation,
|
elevation: widget.elevation,
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
selectedIndex: nav.currentIndex,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.max,
|
||||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS) && !cfg.drawerIsExpanded)
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Container(
|
children: [
|
||||||
decoration: BoxDecoration(
|
if (!kIsWeb &&
|
||||||
border: Border(
|
(Platform.isWindows ||
|
||||||
bottom: BorderSide(
|
Platform.isLinux ||
|
||||||
color: Theme.of(context).dividerColor,
|
Platform.isMacOS) &&
|
||||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
!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(
|
if (cfg.hideBottomNav)
|
||||||
mainAxisSize: MainAxisSize.min,
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
spacing: 8,
|
||||||
children: [
|
children: nav.destinations.where((ele) => ele.isPinned).map(
|
||||||
Text('Solar Network').bold(),
|
(ele) {
|
||||||
AppVersionLabel(),
|
return Expanded(
|
||||||
],
|
child: Tooltip(
|
||||||
).padding(
|
message: ele.label.tr(),
|
||||||
horizontal: 32,
|
child: IconButton.filledTonal(
|
||||||
vertical: 12,
|
icon: ele.icon,
|
||||||
),
|
color: Theme.of(context)
|
||||||
...destinations.where((ele) => ele.isPinned).map((ele) {
|
.colorScheme
|
||||||
return NavigationDrawerDestination(
|
.onPrimaryContainer,
|
||||||
icon: ele.icon,
|
onPressed: () {
|
||||||
label: Text(ele.label).tr(),
|
GoRouter.of(context).goNamed(ele.screen);
|
||||||
);
|
Scaffold.of(context).closeDrawer();
|
||||||
}),
|
},
|
||||||
const Divider(),
|
),
|
||||||
...destinations.where((ele) => !ele.isPinned).map((ele) {
|
),
|
||||||
return NavigationDrawerDestination(
|
);
|
||||||
icon: ele.icon,
|
},
|
||||||
label: Text(ele.label).tr(),
|
).toList(),
|
||||||
);
|
).padding(horizontal: 16),
|
||||||
}),
|
Align(
|
||||||
],
|
alignment: Alignment.bottomCenter,
|
||||||
onDestinationSelected: (idx) {
|
child: ListTile(
|
||||||
nav.setIndex(idx);
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
GoRouter.of(context).goNamed(destinations[idx].screen);
|
leading: AccountImage(content: ua.user?.avatar),
|
||||||
Scaffold.of(context).closeDrawer();
|
title: Text(ua.user?.nick ?? 'unknown'.tr()).fontSize(15),
|
||||||
},
|
subtitle:
|
||||||
|
Text('@${ua.user?.name ?? 'unknown'.tr()}').fontSize(13),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.read<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,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 48,
|
||||||
|
contentPadding: EdgeInsets.only(left: 28, right: 16),
|
||||||
|
leading: const Icon(Symbols.home),
|
||||||
|
title: Text('screenHome').tr(),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).goNamed('home');
|
||||||
|
Scaffold.of(context).closeDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
...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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: 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).pushNamed(
|
||||||
|
'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).pushNamed(
|
||||||
|
'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).pushNamed(
|
||||||
|
'chatRoom',
|
||||||
|
pathParameters: {
|
||||||
|
'scope': ele.realm?.alias ?? 'global',
|
||||||
|
'alias': ele.alias,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Scaffold.of(context).closeDrawer();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cfg = context.watch<ConfigProvider>();
|
final cfg = context.watch<ConfigProvider>();
|
||||||
|
final nav = context.watch<NavigationProvider>();
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
final isCollapseDrawer = cfg.drawerIsCollapsed;
|
final isCollapseDrawer = cfg.drawerIsCollapsed;
|
||||||
@@ -118,8 +119,9 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
.last
|
.last
|
||||||
.route
|
.route
|
||||||
.name;
|
.name;
|
||||||
final isShowBottomNavigation =
|
final isShowBottomNavigation = cfg.hideBottomNav
|
||||||
NavigationProvider.kShowBottomNavScreen.contains(routeName)
|
? false
|
||||||
|
: nav.showBottomNavScreen.contains(routeName)
|
||||||
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
|
? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
|
||||||
: false;
|
: false;
|
||||||
final isPopable = !NavigationProvider.kAllDestination
|
final isPopable = !NavigationProvider.kAllDestination
|
||||||
|
|||||||
@@ -23,57 +23,54 @@ class FediversePostWidget extends StatelessWidget {
|
|||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
child: Card(
|
child: Column(
|
||||||
margin: EdgeInsets.zero,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
AccountImage(
|
||||||
children: [
|
content: data.user.avatar,
|
||||||
AccountImage(
|
radius: 20,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
],
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:responsive_framework/responsive_framework.dart';
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
@@ -14,14 +15,13 @@ import 'package:surface/widgets/post/post_item.dart';
|
|||||||
import 'package:surface/widgets/post/post_mini_editor.dart';
|
import 'package:surface/widgets/post/post_mini_editor.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
import '../../providers/sn_network.dart';
|
|
||||||
|
|
||||||
class PostCommentQuickAction extends StatelessWidget {
|
class PostCommentQuickAction extends StatelessWidget {
|
||||||
final double? maxWidth;
|
final double? maxWidth;
|
||||||
final SnPost parentPost;
|
final SnPost parentPost;
|
||||||
final Function? onPosted;
|
final Function? onPosted;
|
||||||
|
|
||||||
const PostCommentQuickAction({super.key, this.maxWidth, required this.parentPost, this.onPosted});
|
const PostCommentQuickAction(
|
||||||
|
{super.key, this.maxWidth, required this.parentPost, this.onPosted});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -30,7 +30,9 @@ class PostCommentQuickAction extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
height: 240,
|
height: 240,
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||||
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.symmetric(vertical: 8) : EdgeInsets.zero,
|
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||||
|
? const EdgeInsets.symmetric(vertical: 8)
|
||||||
|
: EdgeInsets.zero,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
||||||
? const BorderRadius.all(Radius.circular(8))
|
? const BorderRadius.all(Radius.circular(8))
|
||||||
@@ -99,7 +101,8 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
|||||||
Future<void> _selectAnswer(SnPost answer) async {
|
Future<void> _selectAnswer(SnPost answer) async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.client.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
|
await sn.client
|
||||||
|
.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
|
||||||
'publisher': answer.publisherId,
|
'publisher': answer.publisherId,
|
||||||
'answer_id': answer.id,
|
'answer_id': answer.id,
|
||||||
});
|
});
|
||||||
@@ -135,7 +138,10 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
|||||||
child: PostItem(
|
child: PostItem(
|
||||||
data: _posts[idx],
|
data: _posts[idx],
|
||||||
maxWidth: widget.maxWidth,
|
maxWidth: widget.maxWidth,
|
||||||
onSelectAnswer: widget.parentPost.type == 'question' ? () => _selectAnswer(_posts[idx]) : null,
|
showExpandableComments: true,
|
||||||
|
onSelectAnswer: widget.parentPost.type == 'question'
|
||||||
|
? () => _selectAnswer(_posts[idx])
|
||||||
|
: null,
|
||||||
onChanged: (data) {
|
onChanged: (data) {
|
||||||
setState(() => _posts[idx] = data);
|
setState(() => _posts[idx] = data);
|
||||||
},
|
},
|
||||||
@@ -145,6 +151,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postDetail',
|
'postDetail',
|
||||||
pathParameters: {'slug': _posts[idx].id.toString()},
|
pathParameters: {'slug': _posts[idx].id.toString()},
|
||||||
@@ -153,7 +160,8 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
separatorBuilder: (context, index) =>
|
||||||
|
const Divider().padding(vertical: 2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,11 +169,13 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
|
|||||||
class PostCommentListPopup extends StatefulWidget {
|
class PostCommentListPopup extends StatefulWidget {
|
||||||
final SnPost post;
|
final SnPost post;
|
||||||
final int commentCount;
|
final int commentCount;
|
||||||
|
final int depth;
|
||||||
|
|
||||||
const PostCommentListPopup({
|
const PostCommentListPopup({
|
||||||
super.key,
|
super.key,
|
||||||
required this.post,
|
required this.post,
|
||||||
this.commentCount = 0,
|
this.commentCount = 0,
|
||||||
|
this.depth = 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -180,48 +190,57 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
|
|||||||
final ua = context.watch<UserProvider>();
|
final ua = context.watch<UserProvider>();
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
return Column(
|
return SizedBox(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
height: MediaQuery.of(context).size.height * 0.85,
|
||||||
children: [
|
child: Column(
|
||||||
Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
children: [
|
||||||
children: [
|
Row(
|
||||||
const Icon(Symbols.comment, size: 24),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
const Gap(16),
|
children: [
|
||||||
Text('postCommentsDetailed').plural(widget.commentCount).textStyle(Theme.of(context).textTheme.titleLarge!),
|
const Icon(Symbols.comment, size: 24),
|
||||||
],
|
const Gap(16),
|
||||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
Text('postCommentsDetailed')
|
||||||
Expanded(
|
.plural(widget.commentCount)
|
||||||
child: CustomScrollView(
|
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||||
slivers: [
|
],
|
||||||
if (ua.isAuthorized)
|
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||||
SliverToBoxAdapter(
|
Expanded(
|
||||||
child: Container(
|
child: CustomScrollView(
|
||||||
height: 240,
|
slivers: [
|
||||||
decoration: BoxDecoration(
|
if (ua.isAuthorized)
|
||||||
border: Border.symmetric(
|
SliverToBoxAdapter(
|
||||||
horizontal: BorderSide(
|
child: Container(
|
||||||
color: Theme.of(context).dividerColor,
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
width: 1 / devicePixelRatio,
|
height: 240,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.symmetric(
|
||||||
|
horizontal: BorderSide(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1 / devicePixelRatio,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
child: PostMiniEditor(
|
||||||
child: PostMiniEditor(
|
postReplyId: widget.post.id,
|
||||||
postReplyId: widget.post.id,
|
onPost: () {
|
||||||
onPost: () {
|
_childListKey.currentState!.refresh();
|
||||||
_childListKey.currentState!.refresh();
|
},
|
||||||
},
|
onExpand: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
PostCommentSliverList(
|
||||||
|
parentPost: widget.post,
|
||||||
|
key: _childListKey,
|
||||||
),
|
),
|
||||||
PostCommentSliverList(
|
],
|
||||||
parentPost: widget.post,
|
),
|
||||||
key: _childListKey,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import 'dart:ui';
|
|||||||
import 'package:croppy/croppy.dart';
|
import 'package:croppy/croppy.dart';
|
||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package: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 {
|
void _pasteMedia() async {
|
||||||
final imageBytes = await Pasteboard.image;
|
final imageBytes = await Pasteboard.image;
|
||||||
if (imageBytes == null) return;
|
if (imageBytes == null) return;
|
||||||
@@ -605,6 +614,18 @@ class AddPostMediaButton extends StatelessWidget {
|
|||||||
_selectMedia();
|
_selectMedia();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.file_upload),
|
||||||
|
const Gap(16),
|
||||||
|
Text('addAttachmentFromFiles').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_selectFile();
|
||||||
|
},
|
||||||
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:styled_widget/styled_widget.dart';
|
|||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/sn_network.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/post.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
@@ -17,8 +18,10 @@ import 'package:surface/widgets/loading_indicator.dart';
|
|||||||
class PostMiniEditor extends StatefulWidget {
|
class PostMiniEditor extends StatefulWidget {
|
||||||
final int? postReplyId;
|
final int? postReplyId;
|
||||||
final Function? onPost;
|
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
|
@override
|
||||||
State<PostMiniEditor> createState() => _PostMiniEditorState();
|
State<PostMiniEditor> createState() => _PostMiniEditorState();
|
||||||
@@ -214,12 +217,16 @@ class _PostMiniEditorState extends State<PostMiniEditor> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context).pushNamed(
|
GoRouter.of(context).pushNamed(
|
||||||
'postEditor',
|
'postEditor',
|
||||||
|
extra: PostEditorExtra(
|
||||||
|
text: _writeController.contentController.text,
|
||||||
|
),
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
if (widget.postReplyId != null)
|
if (widget.postReplyId != null)
|
||||||
'replying': widget.postReplyId.toString(),
|
'replying': widget.postReplyId.toString(),
|
||||||
'mode': 'stories',
|
'mode': 'stories',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
widget.onExpand?.call();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
|
|||||||
@@ -80,59 +80,64 @@ class _PostPollState extends State<PostPoll> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return LayoutBuilder(
|
||||||
margin: EdgeInsets.zero,
|
builder: (context, constraints) {
|
||||||
child: Column(
|
return Card(
|
||||||
children: [
|
margin: EdgeInsets.zero,
|
||||||
for (final option in _poll.options)
|
child: Column(
|
||||||
Stack(
|
children: [
|
||||||
children: [
|
for (final option in _poll.options)
|
||||||
ClipRRect(
|
Stack(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
children: [
|
||||||
child: Container(
|
ClipRRect(
|
||||||
height: 60,
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
width: MediaQuery.of(context).size.width *
|
child: Container(
|
||||||
(_poll.metric.byOptionsPercentage[option.id] ?? 0)
|
height: 60,
|
||||||
.toDouble(),
|
width: constraints.maxWidth *
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
(_poll.metric.byOptionsPercentage[option.id] ?? 0)
|
||||||
),
|
.toDouble(),
|
||||||
),
|
color:
|
||||||
ListTile(
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
shape: RoundedRectangleBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(8),
|
),
|
||||||
),
|
ListTile(
|
||||||
minTileHeight: 60,
|
shape: RoundedRectangleBorder(
|
||||||
leading: _answeredChoice == option.id
|
borderRadius: BorderRadius.circular(8),
|
||||||
? const Icon(Symbols.circle, fill: 1)
|
),
|
||||||
: const Icon(Symbols.circle),
|
minTileHeight: 60,
|
||||||
title: Text(option.name),
|
leading: _answeredChoice == option.id
|
||||||
subtitle: Column(
|
? const Icon(Symbols.circle, fill: 1)
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
: const Icon(Symbols.circle),
|
||||||
mainAxisSize: MainAxisSize.min,
|
title: Text(option.name),
|
||||||
children: [
|
subtitle: Column(
|
||||||
Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
'pollVotes'
|
mainAxisSize: MainAxisSize.min,
|
||||||
.plural(_poll.metric.byOptions[option.id] ?? 0),
|
children: [
|
||||||
),
|
Text(
|
||||||
Text(' · ').padding(horizontal: 4),
|
'pollVotes'.plural(
|
||||||
Text(
|
_poll.metric.byOptions[option.id] ?? 0),
|
||||||
'${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%',
|
),
|
||||||
|
Text(' · ').padding(horizontal: 4),
|
||||||
|
Text(
|
||||||
|
'${((_poll.metric.byOptionsPercentage[option.id] ?? 0).toDouble() * 100).toStringAsFixed(2)}%',
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
if (option.description.isNotEmpty)
|
||||||
|
Text(option.description),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (option.description.isNotEmpty)
|
onTap: _isBusy ? null : () => _voteForOption(option),
|
||||||
Text(option.description),
|
),
|
||||||
],
|
],
|
||||||
),
|
)
|
||||||
onTap: _isBusy ? null : () => _voteForOption(option),
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
)
|
},
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,6 @@ static void my_application_init(MyApplication* self) {}
|
|||||||
MyApplication* my_application_new() {
|
MyApplication* my_application_new() {
|
||||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||||
"application-id", APPLICATION_ID,
|
"application-id", APPLICATION_ID,
|
||||||
"flags", G_APPLICATION_NON_UNIQUE,
|
"flags", G_APPLICATION_DEFAULT_FLAGS,
|
||||||
nullptr));
|
nullptr));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,8 +90,6 @@ PODS:
|
|||||||
- gal (1.0.0):
|
- gal (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- geolocator_apple (1.2.0):
|
|
||||||
- FlutterMacOS
|
|
||||||
- GoogleAppMeasurement (11.8.0):
|
- GoogleAppMeasurement (11.8.0):
|
||||||
- GoogleAppMeasurement/AdIdSupport (= 11.8.0)
|
- GoogleAppMeasurement/AdIdSupport (= 11.8.0)
|
||||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
|
||||||
@@ -148,7 +146,7 @@ PODS:
|
|||||||
- HotKey
|
- HotKey
|
||||||
- in_app_review (2.0.0):
|
- in_app_review (2.0.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- livekit_client (2.4.0):
|
- livekit_client (2.4.1):
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- WebRTC-SDK (= 125.6422.06)
|
||||||
@@ -192,6 +190,8 @@ PODS:
|
|||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/fts5 (3.49.1):
|
- sqlite3/fts5 (3.49.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
|
- sqlite3/math (3.49.1):
|
||||||
|
- sqlite3/common
|
||||||
- sqlite3/perf-threadsafe (3.49.1):
|
- sqlite3/perf-threadsafe (3.49.1):
|
||||||
- sqlite3/common
|
- sqlite3/common
|
||||||
- sqlite3/rtree (3.49.1):
|
- sqlite3/rtree (3.49.1):
|
||||||
@@ -202,6 +202,7 @@ PODS:
|
|||||||
- sqlite3 (~> 3.49.1)
|
- sqlite3 (~> 3.49.1)
|
||||||
- sqlite3/dbstatvtab
|
- sqlite3/dbstatvtab
|
||||||
- sqlite3/fts5
|
- sqlite3/fts5
|
||||||
|
- sqlite3/math
|
||||||
- sqlite3/perf-threadsafe
|
- sqlite3/perf-threadsafe
|
||||||
- sqlite3/rtree
|
- sqlite3/rtree
|
||||||
- tray_manager (0.0.1):
|
- tray_manager (0.0.1):
|
||||||
@@ -232,7 +233,6 @@ DEPENDENCIES:
|
|||||||
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
|
||||||
- geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos`)
|
|
||||||
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
|
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
|
||||||
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
||||||
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
|
||||||
@@ -307,8 +307,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
gal:
|
gal:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
|
||||||
geolocator_apple:
|
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos
|
|
||||||
hotkey_manager_macos:
|
hotkey_manager_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos
|
||||||
in_app_review:
|
in_app_review:
|
||||||
@@ -372,14 +370,13 @@ SPEC CHECKSUMS:
|
|||||||
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
|
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
|
||||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||||
geolocator_apple: 72a78ae3f3e4ec0db62117bd93e34523f5011d58
|
|
||||||
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
|
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||||
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
|
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
|
||||||
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
|
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
|
||||||
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
||||||
livekit_client: 2e766be2c3ee6274a8e2633b356b98b5eb842987
|
livekit_client: d03409f83df069a1bb00a4c8dc78c54fb2287262
|
||||||
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff
|
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff
|
||||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||||
@@ -396,7 +393,7 @@ SPEC CHECKSUMS:
|
|||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||||
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
sqlite3_flutter_libs: 487032b9008b28de37c72a3aa66849ef3745f3e6
|
||||||
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
||||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||||
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
|
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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
|
|||||||
50
pubspec.lock
50
pubspec.lock
@@ -213,10 +213,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: chalkdart
|
name: chalkdart
|
||||||
sha256: e7cfcc9a9d9546843304c1ff87fe0696c7eb82ee70e6df63f555f321b15a40d8
|
sha256: "82dfa884e3cf97641eb0742a3b9ffd41490666b9ece548b2e32cbfefe540bf86"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.3"
|
version: "2.4.0"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -314,7 +314,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.4+2"
|
version: "0.3.4+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||||
@@ -517,10 +517,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: fast_rsa
|
name: fast_rsa
|
||||||
sha256: "205a36c0412b9fabebf3e18ccb5221d819cc28cfb3da988c0bf7b646368d0270"
|
sha256: a26ad752734dc52fd51abd55248df868d7480e68d8cc8dd12413b0124bba0a7e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.8.0"
|
version: "3.8.1"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -541,10 +541,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: ee11ce89f8937c39181bc88d57a455972f7545b86150d8f287d0d9cf95bcdf0a
|
sha256: "8d938fd5c11dc81bf1acd4f7f0486c683fe9e79a0b13419e27730f9ce4d8a25b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.1.0"
|
version: "9.2.1"
|
||||||
file_saver:
|
file_saver:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -746,10 +746,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_expandable_fab
|
name: flutter_expandable_fab
|
||||||
sha256: "85275279d19faf4fbe5639dc1f139b4555b150e079d056f085601a45688af12c"
|
sha256: b14caf78720a48f650e6e1a38d724e33b1f5348d646fa1c266570c31a7f87ef3
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.4.0"
|
||||||
flutter_highlight:
|
flutter_highlight:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -953,10 +953,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: freezed
|
name: freezed
|
||||||
sha256: a6274c34d61b3d68082f2b0e9a641a3ec197e525d269f35b82f62d5b2c6d9f75
|
sha256: "7ed2ddaa47524976d5f2aa91432a79da36a76969edd84170777ac5bea82d797c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.4"
|
||||||
freezed_annotation:
|
freezed_annotation:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1177,10 +1177,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_linux
|
name: image_picker_linux
|
||||||
sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
|
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.1+1"
|
version: "0.2.1+2"
|
||||||
image_picker_macos:
|
image_picker_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1385,10 +1385,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: material_symbols_icons
|
name: material_symbols_icons
|
||||||
sha256: db745002d0323c32097f5bc23711c02b0fb36ef616011a38c8c1798d5684d368
|
sha256: "99d5b0e7c65232dfe1247e0ac67eeeee2cab9da2d860748fc495d34f5e9e6397"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2810.0"
|
version: "4.2811.0"
|
||||||
media_kit:
|
media_kit:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -2102,10 +2102,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqlite3_flutter_libs
|
name: sqlite3_flutter_libs
|
||||||
sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
|
sha256: "1a96b59227828d9eb1463191d684b37a27d66ee5ed7597fcf42eee6452c88a14"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.31"
|
version: "0.5.32"
|
||||||
sqlparser:
|
sqlparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2174,34 +2174,34 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: talker
|
name: talker
|
||||||
sha256: a074e0a2c1db1b09c1856050c5284a14ff64faea003403409871ab8335738d08
|
sha256: "45abef5b92f9b9bd42c3f20133ad4b20ab12e1da2aa206fc0a40ea874bed7c5d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.7.0"
|
version: "4.7.1"
|
||||||
talker_dio_logger:
|
talker_dio_logger:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: talker_dio_logger
|
name: talker_dio_logger
|
||||||
sha256: "6d713d26bdf90c7440222758a4e02ebfbdb3b323cdbdb50b78082db720f69427"
|
sha256: "52c1b554cccedec6073637a6d4f6a3e267dd4451c1545fe57e1b26897a560ccb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.7.0"
|
version: "4.7.1"
|
||||||
talker_flutter:
|
talker_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: talker_flutter
|
name: talker_flutter
|
||||||
sha256: "25a9faa98a0dcbab8d1ec366f19b3e91d422ede523461ec6e26e198b280cb98d"
|
sha256: "77458ca11638dfefb651e898a26101ee54e60dc0b168ad7481a05b1c97ce2680"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.7.0"
|
version: "4.7.1"
|
||||||
talker_logger:
|
talker_logger:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: talker_logger
|
name: talker_logger
|
||||||
sha256: "0a1a295a9d036a3416e89c7155eaaca9ca6e3e0abb5401a40597f98b9cfc8fe6"
|
sha256: ed9b20b8c09efff9f6b7c63fc6630ee2f84aa92661ae09e5ba04e77272bf2ad2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.7.0"
|
version: "4.7.1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 2.4.2+79
|
version: 2.4.2+83
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@@ -142,6 +142,7 @@ dependencies:
|
|||||||
flutter_blurhash: ^0.8.2
|
flutter_blurhash: ^0.8.2
|
||||||
timelines_plus: ^1.0.6
|
timelines_plus: ^1.0.6
|
||||||
latlong2: ^0.9.1
|
latlong2: ^0.9.1
|
||||||
|
crypto: ^3.0.6
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:drift/internal/migrations.dart';
|
|||||||
import 'schema_v1.dart' as v1;
|
import 'schema_v1.dart' as v1;
|
||||||
import 'schema_v2.dart' as v2;
|
import 'schema_v2.dart' as v2;
|
||||||
import 'schema_v3.dart' as v3;
|
import 'schema_v3.dart' as v3;
|
||||||
|
import 'schema_v4.dart' as v4;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
@@ -17,10 +18,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v2.DatabaseAtV2(db);
|
return v2.DatabaseAtV2(db);
|
||||||
case 3:
|
case 3:
|
||||||
return v3.DatabaseAtV3(db);
|
return v3.DatabaseAtV3(db);
|
||||||
|
case 4:
|
||||||
|
return v4.DatabaseAtV4(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
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 "flutter_window.h"
|
||||||
#include "utils.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,
|
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
_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
|
// Attach to console when present (e.g., 'flutter run') or create a
|
||||||
// new console when running with a debugger.
|
// new console when running with a debugger.
|
||||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user