Compare commits

..

50 Commits

Author SHA1 Message Date
a1c4e5eca0 ♻️ Refactored large screen user experience 2025-03-27 23:18:40 +08:00
595050f89f ♻️ Explore two column 2025-03-27 22:58:06 +08:00
0722c99f21 ♻️ Openable Post Item now push pages 2025-03-27 22:46:36 +08:00
12d03836f9 ♻️ Updated nav & account page two column design 2025-03-27 22:42:44 +08:00
f78d3f4fd5 🔀 Merge pull request #18 from Texas0295/master
Fix workflow
2025-03-27 22:04:02 +08:00
e798a8ba76 fix workflow 2025-03-27 21:24:38 +08:00
c28a664373 Memorable window size 2025-03-27 00:37:45 +08:00
4589722c3b Weird boot sound effects 2025-03-27 00:22:41 +08:00
38e1c51b45 🐛 Fix linux compile issue 2025-03-26 23:32:23 +08:00
610ddec05c Sound effects on notify 2025-03-26 23:16:55 +08:00
d0276f9ac6 🐛 Fix some date issue 2025-03-26 22:51:09 +08:00
c1e89a2ee6 Punishments 2025-03-26 22:43:27 +08:00
ecc79368a1 🐛 Fix attachment border in list 2025-03-26 00:29:00 +08:00
e6d732c86a 💄 Optimize status text 2025-03-26 00:26:37 +08:00
dd055fb077 💄 Optimization and bug fixes 2025-03-26 00:24:07 +08:00
280840c6d8 ⬆️ Upgrade deps 2025-03-25 21:33:58 +08:00
bde62a7b2c Add cache for audio and video (experimental) 2025-03-25 00:33:39 +08:00
5445c570a2 Add deps for google_mobile_ads 2025-03-24 23:12:49 +08:00
b2302f5b3c 🐛 Make initialize for push notification no longer waited 2025-03-24 20:55:55 +08:00
d7359cfd0d 🐛 Fixes and optimization in programs 2025-03-24 00:09:36 +08:00
9cc577adbe Programs, members
🐛 Fix web assets redirecting issue
2025-03-23 22:34:58 +08:00
dd196b7754 Golden points 2025-03-23 18:23:18 +08:00
16c07c2133 🐛 Fix deps 2025-03-23 17:01:21 +08:00
6bcb658d44 🐛 Fix platform specific captcha solution cause build failed. 2025-03-23 16:47:06 +08:00
9311bfc3b5 ⬆️ Upgrade deps & replace to own translation api 2025-03-23 16:26:41 +08:00
8dd6435a30 🐛 Fix some issues on Android and Web 2025-03-23 16:24:53 +08:00
21a1d4a2ad 🐛 Fix unable select answer 2025-03-23 00:01:48 +08:00
603875b1af 🐛 Fix styling issue 2025-03-22 23:07:13 +08:00
4209a13c84 🐛 Fix no nav to use 2025-03-22 22:51:50 +08:00
55b79bfd8f 🐛 Finish bug fixes 2025-03-22 21:50:01 +08:00
6e6c3f42f6 🚀 Launch 2.4.2+84 2025-03-22 20:28:53 +08:00
dc38b46b2c Support captcha 2025-03-22 20:24:05 +08:00
b4990308e9 ♻️ Refactored nav completely 2025-03-22 18:39:01 +08:00
237abe564d ♻️ Refactored drawer nav 2025-03-22 18:14:36 +08:00
71b41d470a Splash screen loading 2025-03-22 16:36:10 +08:00
7052b5b635 Join channel hint
🗃️ Realm local db
2025-03-22 14:12:46 +08:00
f356e08f79 💄 New navigation draft (skip ci) 2025-03-22 12:48:55 +08:00
152872db65 💄 Show nick instead of name in typing indicator 2025-03-22 00:16:59 +08:00
dfe117d04f Auth preference screen 2025-03-22 00:14:55 +08:00
caf63f0cbe Notification preferences 2025-03-21 23:59:42 +08:00
b8f5cc82f9 Add attachments from file 2025-03-20 23:20:24 +08:00
360bc50f21 🐛 Fix linux G_APPLICATION_FLAGS_NONE api deprecated 2025-03-20 22:52:32 +08:00
2de93a0486 🐛 Close #15 with vide coding (not tested) 2025-03-20 22:45:53 +08:00
02227852f8 🚀 Launch 2.4.2+83 for some platforms 2025-03-19 00:48:36 +08:00
ad16de595b 🐛 Fix menubar missing hide 2025-03-19 00:30:58 +08:00
9f8c8923d9 🐛 Bug fixes in posts 2025-03-19 00:29:29 +08:00
060bfa4887 🐛 Fix explore unmixed feed pagination issue 2025-03-19 00:23:57 +08:00
e68ada2d04 💄 Optimize post comments 2025-03-19 00:21:54 +08:00
d6013078bd 🚀 Launch 2.4.2+81 2025-03-17 00:38:15 +08:00
5976d61997 💄 Bunch of optimization 2025-03-17 00:36:20 +08:00
109 changed files with 8894 additions and 1521 deletions

View File

@ -52,10 +52,12 @@ jobs:
- run: | - run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev sudo apt-get install -y ninja-build libgtk-3-dev
sudo apt-get install libmpv-dev mpv sudo apt-get install -y libmpv-dev mpv
sudo apt-get install libayatana-appindicator3-dev sudo apt-get install -y libayatana-appindicator3-dev
sudo apt-get install keybinder-3.0 sudo apt-get install -y keybinder-3.0
sudo apt-get install libnotify-dev sudo apt-get install -y libnotify-dev
sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
sudo apt-get install -y gstreamer-1.0
- run: flutter pub get - run: flutter pub get
- run: flutter build linux - run: flutter build linux
- name: Archive production artifacts - name: Archive production artifacts

Binary file not shown.

Binary file not shown.

BIN
assets/icon/kanban-1st.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

View File

@ -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.",
@ -338,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",
@ -638,6 +639,7 @@
"postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}", "postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
"postQuestionAnswered": "Answered Question", "postQuestionAnswered": "Answered Question",
"postQuestionAnswerSelect": "Select as Answer", "postQuestionAnswerSelect": "Select as Answer",
"postQuestionAnswerTitle": "Selected Question",
"postQuestionAnswerSelected": "Answer has been selected, reward has been applied.", "postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
"postVideoUpload": "Upload Video", "postVideoUpload": "Upload Video",
"realmJoin": "Join Realm", "realmJoin": "Join Realm",
@ -846,5 +848,96 @@
"translating": "Translating…", "translating": "Translating…",
"translated": "Translated", "translated": "Translated",
"settingsAutoTranslate": "Auto Translate", "settingsAutoTranslate": "Auto Translate",
"settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages." "settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages.",
"trayMenuHide": "Hide",
"accountSettingsNotify": "Notify Settings",
"accountSettingsNotifyDescription": "Adjust the types of notifications you receive.",
"accountSettingsSecurity": "Security Settings",
"accountSettingsSecurityDescription": "Adjust your account security settings.",
"save": "Save",
"notificationTopicPostFeedback": "Post Feedback",
"notificationTopicPostReply": "Post Replies",
"notificationTopicPostSubscription": "Post Subscriptions",
"notificationTopicMessaging": "New Messages",
"notificationTopicMessagingCall": "Incoming Calls",
"notificationTopicGeneral": "General",
"authMaximumAuthSteps": "Maximum Authenticate Steps",
"authMaximumAuthStepsDescription": {
"one": "Maximum ask for {} step authenticate",
"other": "Maximum ask for {} steps authenticate"
},
"authAlwaysRisky": "Always Risky",
"authAlwaysRiskyDescription": "Always ask for the highest steps count of authentication when logging in.",
"chatUnjoined": "Unjoined Channel",
"chatUnjoinedDescription": "You haven't joined this channel, so you can't send messages either view messages in it.",
"chatUnjoinedPublicDescription": "Fortunately, this is a public channel, so you can join it as you want.",
"chatJoin": "Join the Channel",
"appInitStarting": "Starting",
"appInitNetwork": "Initializing Network",
"appInitUserdata": "Initializing User Data",
"appInitWebsocket": "Establishing Solar Link",
"appInitNotification": "Initializing Push Notifications",
"appInitKeyPair": "Initializing Key Pairs",
"appInitStickers": "Initializing Stickers",
"appInitUserDirectory": "Initializing User Directory",
"appInitRealm": "Initializing Realms",
"appInitChat": "Initializing Chat",
"appInitDone": "Completed",
"community": "Community",
"realmCommunity": "{}'s Community",
"postTotalCount": {
"one": "Total {} post",
"other": "Total {} posts"
},
"settingsHideBottomNav": "Hide Bottom Navigation",
"settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.",
"reCaptcha": "reCaptcha",
"friends": "Friends",
"friendsDescription": "Manage your friendships.",
"album": "Album",
"albumDescription": "View albums and manage attachments.",
"stickers": "Stickers",
"stickersDescription": "View sticker packs and manage stickers.",
"navBottomUnauthorizedCaption": "Or create an account",
"walletCurrencyGoldenShort": "GDP",
"walletCurrencyGolden": {
"one": "{} Golden Point",
"other": "{} Golden Points"
},
"walletTransactionTypeNormal": "Source Point",
"walletTransactionTypeGolden": "Golden Point",
"accountProgram": "Programs",
"accountProgramDescription": "Explore the available member programs.",
"accountProgramJoin": "Join Program",
"accountProgramJoinRequirements": "Requirements",
"accountProgramJoinPricing": "Pricing",
"accountProgramJoinPricingHint": "Billed every (30 days) month.",
"accountProgramLeaveHint": "After leaving the program, the source points will not be refunded.",
"accountProgramJoined": "Joined Program.",
"accountProgramAlreadyJoined": "Joined",
"accountProgramLeft": "Left Program.",
"leave": "Leave",
"attachmentFailedToLoadMedia": "Unable to load media file, please try again later. If this error occurs repeatedly, the source file may not exist or the network connection may be abnormal.",
"accountPunishments": "Punishments",
"accountPunishmentsDescription": "View your account's reputation status.",
"punishmentType0": "Strike",
"punishmentType1": "Limited",
"punishmentType2": "Banned",
"punishmentOverall": "Overall Status",
"punishmentStatusNormal": "All abilities normal",
"punishmentStatusWarned": "All abilities normal, but at least one strike is in effect",
"punishmentStatusLimited": "Some abilities limited, at least one limited punishment is in effect",
"punishmentStatusLimitedFully": "All abilities limited, at least one completely limited punishment is in effect",
"punishmentStatusBanned": "All services are terminated, banned",
"punishmentCreatedAt": "Applied since {}",
"punishmentExpiredAt": "Expired at {}",
"punishmentExpiredNever": "Never expired",
"punishmentModerator": "Moderator who made this punishment",
"punishmentMadeBySystem": "Made by auto-mod system",
"settingsAprilFoolFeatures": "April Fool Features",
"settingsAprilFoolFeaturesDescription": "Enable April Fool features during April Fool, this option will only be visible during April Fool.",
"settingsSoundEffects": "Sound Effects",
"settingsSoundEffectsDescription": "Enable the sound effects around the app.",
"settingsResetMemorizedWindowSize": "Reset Window Size",
"settingsResetMemorizedWindowSizeDescription": "Reset the memorized window size, and set it to the default size."
} }

View File

@ -336,6 +336,7 @@
"fieldAttachmentRandomId": "访问 ID", "fieldAttachmentRandomId": "访问 ID",
"fieldAttachmentAlt": "概述文字", "fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "从相册中添加附件", "addAttachmentFromAlbum": "从相册中添加附件",
"addAttachmentFromFiles": "从文件中添加附件",
"addAttachmentFromClipboard": "粘贴附件", "addAttachmentFromClipboard": "粘贴附件",
"addAttachmentFromCameraPhoto": "拍摄照片", "addAttachmentFromCameraPhoto": "拍摄照片",
"addAttachmentFromCameraVideo": "拍摄视频", "addAttachmentFromCameraVideo": "拍摄视频",
@ -844,5 +845,96 @@
"translating": "正在翻译……", "translating": "正在翻译……",
"translated": "已翻译", "translated": "已翻译",
"settingsAutoTranslate": "自动翻译", "settingsAutoTranslate": "自动翻译",
"settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。" "settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。",
"trayMenuHide": "隐藏",
"accountSettingsNotify": "通知设置",
"accountSettingsNotifyDescription": "调整你所收到的通知种类。",
"accountSettingsSecurity": "安全设置",
"accountSettingsSecurityDescription": "调整你的帐户安全设置。",
"save": "保存",
"notificationTopicPostFeedback": "帖子数据反馈",
"notificationTopicPostReply": "帖子回复",
"notificationTopicPostSubscription": "帖子订阅",
"notificationTopicMessaging": "消息",
"notificationTopicMessagingCall": "通话",
"notificationTopicGeneral": "杂项",
"authMaximumAuthSteps": "最大验证步骤",
"authMaximumAuthStepsDescription": {
"one": "登入时最多要求 {} 步验证",
"other": "登入时最多要求 {} 步验证"
},
"authAlwaysRisky": "总是风险",
"authAlwaysRiskyDescription": "在登入时始终按最高标准要求验证。",
"chatUnjoined": "未加入频道",
"chatUnjoinedDescription": "你没有加入这个频道,所以你也无法发送消息或者查看这个频道中的消息。",
"chatUnjoinedPublicDescription": "但幸运的是,这是一个公开频道,所以你可以主动加入。",
"chatJoin": "加入频道",
"appInitStarting": "启动中",
"appInitNetwork": "正在初始化网络",
"appInitUserdata": "正在初始化用户数据",
"appInitWebsocket": "正在建立 Solar Link",
"appInitNotification": "正在初始化推送通知",
"appInitKeyPair": "正在初始化密钥对",
"appInitStickers": "正在初始化贴图包",
"appInitUserDirectory": "正在初始化用户目录",
"appInitRealm": "正在初始化领域信息",
"appInitChat": "正在初始化聊天",
"appInitDone": "完成",
"community": "社区",
"realmCommunity": "{}的社区",
"postTotalCount": {
"zero": "没有帖子",
"one": "共 {} 条帖子"
},
"settingsHideBottomNav": "隐藏底部导航栏",
"settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。",
"reCaptcha": "人机验证",
"friends": "好友",
"friendsDescription": "管理好友关系。",
"album": "相册",
"albumDescription": "查看相册与管理上传附件。",
"stickers": "贴图",
"stickersDescription": "查看贴图包与管理贴图。",
"navBottomUnauthorizedCaption": "或者注册一个账号",
"walletCurrencyGoldenShort": "金点",
"walletCurrencyGolden": {
"one": "{} 金点",
"other": "{} 金点"
},
"walletTransactionTypeNormal": "源点",
"walletTransactionTypeGolden": "金点",
"accountProgram": "计划",
"accountProgramDescription": "了解可用的成员计划。",
"accountProgramJoin": "加入计划",
"accountProgramJoinRequirements": "要求",
"accountProgramJoinPricing": "价格",
"accountProgramJoinPricingHint": "按月30 天)收费",
"accountProgramLeaveHint": "离开计划后,之前花费的源点不会退款。",
"accountProgramJoined": "已加入计划。",
"accountProgramLeft": "已离开计划。",
"accountProgramAlreadyJoined": "已加入",
"leave": "离开",
"attachmentFailedToLoadMedia": "无法加载媒体文件,请稍后重试。若此错误重复出现,可能源文件不存在或者网络连接异常。",
"accountPunishments": "处分",
"accountPunishmentsDescription": "查看你帐号的信誉状态。",
"punishmentType0": "警告",
"punishmentType1": "停权",
"punishmentType2": "封禁",
"punishmentOverall": "总体状态",
"punishmentStatusNormal": "所有功能正常",
"punishmentStatusWarned": "所有功能正常,但有警告生效",
"punishmentStatusLimited": "部份功能暂时受限,有至少一个停权生效",
"punishmentStatusLimitedFully": "所有功能暂时受限,有至少一个完全停权生效",
"punishmentStatusBanned": "所有服务终止,已被封禁",
"punishmentCreatedAt": "宣布于 {}",
"punishmentExpiredAt": "到期于 {}",
"punishmentExpiredNever": "永久生效",
"punishmentModerator": "责任管理员",
"punishmentMadeBySystem": "由系统自动裁决",
"settingsAprilFoolFeatures": "愚人节特性",
"settingsAprilFoolFeaturesDescription": "在愚人节期间启用愚人节特性,该选项只会在愚人节期间显示。",
"settingsSoundEffects": "声音效果",
"settingsSoundEffectsDescription": "在一些场合下启用声音特效。",
"settingsResetMemorizedWindowSize": "重置窗口大小",
"settingsResetMemorizedWindowSizeDescription": "重置记忆的窗口大小,以重新设置为默认大小。"
} }

View File

@ -336,6 +336,7 @@
"fieldAttachmentRandomId": "訪問 ID", "fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字", "fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件", "addAttachmentFromAlbum": "從相冊中添加附件",
"addAttachmentFromFiles": "從文件中添加附件",
"addAttachmentFromClipboard": "粘貼附件", "addAttachmentFromClipboard": "粘貼附件",
"addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraPhoto": "拍攝照片",
"addAttachmentFromCameraVideo": "拍攝視頻", "addAttachmentFromCameraVideo": "拍攝視頻",
@ -844,5 +845,55 @@
"translating": "正在翻譯……", "translating": "正在翻譯……",
"translated": "已翻譯", "translated": "已翻譯",
"settingsAutoTranslate": "自動翻譯", "settingsAutoTranslate": "自動翻譯",
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。" "settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。",
"trayMenuHide": "隱藏",
"accountSettingsNotify": "通知設置",
"accountSettingsNotifyDescription": "調整你所收到的通知種類。",
"accountSettingsSecurity": "安全設置",
"accountSettingsSecurityDescription": "調整你的帳户安全設置。",
"save": "保存",
"notificationTopicPostFeedback": "帖子數據反饋",
"notificationTopicPostReply": "帖子回覆",
"notificationTopicPostSubscription": "帖子訂閲",
"notificationTopicMessaging": "消息",
"notificationTopicMessagingCall": "通話",
"notificationTopicGeneral": "雜項",
"authMaximumAuthSteps": "最大驗證步驟",
"authMaximumAuthStepsDescription": {
"one": "登入時最多要求 {} 步驗證",
"other": "登入時最多要求 {} 步驗證"
},
"authAlwaysRisky": "總是風險",
"authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。",
"chatUnjoined": "未加入頻道",
"chatUnjoinedDescription": "你沒有加入這個頻道,所以你也無法發送消息或者查看這個頻道中的消息。",
"chatUnjoinedPublicDescription": "但幸運的是,這是一個公開頻道,所以你可以主動加入。",
"chatJoin": "加入頻道",
"appInitStarting": "啓動中",
"appInitNetwork": "正在初始化網絡",
"appInitUserdata": "正在初始化用户數據",
"appInitWebsocket": "正在建立 Solar Link",
"appInitNotification": "正在初始化推送通知",
"appInitKeyPair": "正在初始化密鑰對",
"appInitStickers": "正在初始化貼圖包",
"appInitUserDirectory": "正在初始化用户目錄",
"appInitRealm": "正在初始化領域信息",
"appInitChat": "正在初始化聊天",
"appInitDone": "完成",
"community": "社區",
"realmCommunity": "{}的社區",
"postTotalCount": {
"zero": "沒有帖子",
"one": "共 {} 條帖子"
},
"settingsHideBottomNav": "隱藏底部導航欄",
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
"reCaptcha": "人機驗證",
"friends": "好友",
"friendsDescription": "管理好友關係。",
"album": "相冊",
"albumDescription": "查看相冊與管理上傳附件。",
"stickers": "貼圖",
"stickersDescription": "查看貼圖包與管理貼圖。",
"navBottomUnauthorizedCaption": "或者註冊一個賬號"
} }

View File

@ -336,6 +336,7 @@
"fieldAttachmentRandomId": "訪問 ID", "fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字", "fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件", "addAttachmentFromAlbum": "從相冊中添加附件",
"addAttachmentFromFiles": "從文件中添加附件",
"addAttachmentFromClipboard": "粘貼附件", "addAttachmentFromClipboard": "粘貼附件",
"addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraPhoto": "拍攝照片",
"addAttachmentFromCameraVideo": "拍攝視頻", "addAttachmentFromCameraVideo": "拍攝視頻",
@ -844,5 +845,55 @@
"translating": "正在翻譯……", "translating": "正在翻譯……",
"translated": "已翻譯", "translated": "已翻譯",
"settingsAutoTranslate": "自動翻譯", "settingsAutoTranslate": "自動翻譯",
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。" "settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。",
"trayMenuHide": "隱藏",
"accountSettingsNotify": "通知設置",
"accountSettingsNotifyDescription": "調整你所收到的通知種類。",
"accountSettingsSecurity": "安全設置",
"accountSettingsSecurityDescription": "調整你的帳戶安全設置。",
"save": "保存",
"notificationTopicPostFeedback": "帖子數據反饋",
"notificationTopicPostReply": "帖子回覆",
"notificationTopicPostSubscription": "帖子訂閱",
"notificationTopicMessaging": "消息",
"notificationTopicMessagingCall": "通話",
"notificationTopicGeneral": "雜項",
"authMaximumAuthSteps": "最大驗證步驟",
"authMaximumAuthStepsDescription": {
"one": "登入時最多要求 {} 步驗證",
"other": "登入時最多要求 {} 步驗證"
},
"authAlwaysRisky": "總是風險",
"authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。",
"chatUnjoined": "未加入頻道",
"chatUnjoinedDescription": "你沒有加入這個頻道,所以你也無法發送消息或者查看這個頻道中的消息。",
"chatUnjoinedPublicDescription": "但幸運的是,這是一個公開頻道,所以你可以主動加入。",
"chatJoin": "加入頻道",
"appInitStarting": "啟動中",
"appInitNetwork": "正在初始化網絡",
"appInitUserdata": "正在初始化用戶數據",
"appInitWebsocket": "正在建立 Solar Link",
"appInitNotification": "正在初始化推送通知",
"appInitKeyPair": "正在初始化密鑰對",
"appInitStickers": "正在初始化貼圖包",
"appInitUserDirectory": "正在初始化用戶目錄",
"appInitRealm": "正在初始化領域信息",
"appInitChat": "正在初始化聊天",
"appInitDone": "完成",
"community": "社區",
"realmCommunity": "{}的社區",
"postTotalCount": {
"zero": "沒有帖子",
"one": "共 {} 條帖子"
},
"settingsHideBottomNav": "隱藏底部導航欄",
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
"reCaptcha": "人機驗證",
"friends": "好友",
"friendsDescription": "管理好友關係。",
"album": "相冊",
"albumDescription": "查看相冊與管理上傳附件。",
"stickers": "貼圖",
"stickersDescription": "查看貼圖包與管理貼圖。",
"navBottomUnauthorizedCaption": "或者註冊一個賬號"
} }

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,7 @@
PODS: PODS:
- Alamofire (5.10.2) - Alamofire (5.10.2)
- audioplayers_darwin (0.0.1):
- Flutter
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- croppy (0.0.1): - croppy (0.0.1):
@ -189,8 +191,6 @@ PODS:
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
- media_kit_libs_ios_video (1.0.4): - media_kit_libs_ios_video (1.0.4):
- Flutter - Flutter
- media_kit_native_event_loop (1.0.0):
- Flutter
- media_kit_video (0.0.1): - media_kit_video (0.0.1):
- Flutter - Flutter
- nanopb (3.30910.0): - nanopb (3.30910.0):
@ -212,8 +212,6 @@ PODS:
- receive_sharing_intent (1.8.1): - receive_sharing_intent (1.8.1):
- Flutter - Flutter
- SAMKeychain (1.5.3) - SAMKeychain (1.5.3)
- screen_brightness_ios (0.1.0):
- Flutter
- SDWebImage (5.20.1): - SDWebImage (5.20.1):
- SDWebImage/Core (= 5.20.1) - SDWebImage/Core (= 5.20.1)
- SDWebImage/Core (5.20.1) - SDWebImage/Core (5.20.1)
@ -232,6 +230,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 +242,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)
@ -259,6 +260,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- Alamofire - Alamofire
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- croppy (from `.symlinks/plugins/croppy/ios`) - croppy (from `.symlinks/plugins/croppy/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
@ -282,14 +284,12 @@ DEPENDENCIES:
- Kingfisher (~> 8.0) - Kingfisher (~> 8.0)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`) - livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
@ -325,6 +325,8 @@ SPEC REPOS:
- WebRTC-SDK - WebRTC-SDK
EXTERNAL SOURCES: EXTERNAL SOURCES:
audioplayers_darwin:
:path: ".symlinks/plugins/audioplayers_darwin/ios"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
croppy: croppy:
@ -369,8 +371,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/livekit_client/ios" :path: ".symlinks/plugins/livekit_client/ios"
media_kit_libs_ios_video: media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios" :path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop:
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
media_kit_video: media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios" :path: ".symlinks/plugins/media_kit_video/ios"
package_info_plus: package_info_plus:
@ -383,8 +383,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
receive_sharing_intent: receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios" :path: ".symlinks/plugins/receive_sharing_intent/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
share_plus: share_plus:
:path: ".symlinks/plugins/share_plus/ios" :path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation: shared_preferences_foundation:
@ -406,65 +404,64 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496 Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d audioplayers_darwin: ccf9c770ee768abb07e26d90af093f7bab1c12ab
croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321 connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
fast_rsa: dc48fb05f26bb108863de122b2a9f5554e8e2591 fast_rsa: d99f8e1809a4a312fa9216d830186869b2e9eb65
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49 file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
firebase_analytics: e3b6782e70e32b7fa18f7cd233e3201975dd86aa firebase_analytics: 4e93dbe66872104d28ae9750fec1800e1fd66858
firebase_core: ac395f994af4e28f6a38b59e05a88ca57abeb874 firebase_core: 8d552814f6c01ccde5d88939fced4ec26f2f5510
firebase_messaging: 7e223f4ee7ca053bf4ce43748e84a6d774ec9728 firebase_messaging: 8b96a4f09841c15a16b96973ef5c3dcfc1a064e4
FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917 FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8 FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282 flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1 flutter_webrtc: 57f32415b8744e806f9c2a96ccdb60c6a627ba33
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896 GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
livekit_client: 170022ce5f7c8c70d7f862ac9c17e11508ad5fbc livekit_client: 08755cabfa4da4ed455642f460cfbb39bc518070
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1 receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: 33d0f23bddeb5d209ae959153883247be6703713 SDWebImage: 33d0f23bddeb5d209ae959153883247be6703713
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe video_compress: f2133a07762889d67f0711ac831faa26f956980e
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56 wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc

View File

@ -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>

View File

@ -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);
}), }),
); );
} }

View File

@ -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);
} }

View File

@ -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
View 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()();
}

View File

@ -3,6 +3,7 @@ import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:audioplayers/audioplayers.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:croppy/croppy.dart'; import 'package:croppy/croppy.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.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';
@ -46,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';
@ -71,13 +75,40 @@ void appBackgroundDispatcher() {
}); });
} }
// Desktop size tools
Future<Size> _getSavedWindowSize() async {
final prefs = await SharedPreferences.getInstance();
String? sizeString = prefs.getString(kAppWindowSize);
if (sizeString != null) {
List<String> parts = sizeString.split('x');
if (parts.length == 2) {
double? width = double.tryParse(parts[0]);
double? height = double.tryParse(parts[1]);
if (width != null && height != null) {
return Size(width, height);
}
}
}
return const Size(1280, 720); // Default size
}
Future<void> _saveWindowSize() async {
final prefs = await SharedPreferences.getInstance();
final size = appWindow.size;
await prefs.setString(kAppWindowSize, '${size.width}x${size.height}');
}
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
final Size savedSize = await _getSavedWindowSize();
doWhenWindowReady(() { doWhenWindowReady(() {
appWindow.minSize = Size(480, 640); appWindow.minSize = Size(480, 640);
appWindow.size = Size(1280, 720); appWindow.size = savedSize;
appWindow.alignment = Alignment.center; appWindow.alignment = Alignment.center;
appWindow.show(); appWindow.show();
}); });
@ -87,18 +118,15 @@ void main() async {
if (!kIsWeb && !Platform.isLinux) { if (!kIsWeb && !Platform.isLinux) {
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform);
);
} }
GoRouter.optionURLReflectsImperativeAPIs = true; GoRouter.optionURLReflectsImperativeAPIs = true;
usePathUrlStrategy(); usePathUrlStrategy();
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Workmanager().initialize( Workmanager()
appBackgroundDispatcher, .initialize(appBackgroundDispatcher, isInDebugMode: kDebugMode);
isInDebugMode: kDebugMode,
);
if (Platform.isAndroid) { if (Platform.isAndroid) {
Workmanager().registerPeriodicTask( Workmanager().registerPeriodicTask(
"widget-update-random-post", "widget-update-random-post",
@ -133,7 +161,7 @@ class SolianApp extends StatelessWidget {
Locale('en', 'US'), Locale('en', 'US'),
Locale('zh', 'CN'), Locale('zh', 'CN'),
Locale('zh', 'TW'), Locale('zh', 'TW'),
Locale('zh', 'HK'), Locale('zh', 'HK')
], ],
fallbackLocale: Locale('en', 'US'), fallbackLocale: Locale('en', 'US'),
useFallbackTranslations: true, useFallbackTranslations: true,
@ -157,7 +185,7 @@ class SolianApp extends StatelessWidget {
Provider(create: (ctx) => SnNetworkProvider(ctx)), Provider(create: (ctx) => SnNetworkProvider(ctx)),
Provider(create: (ctx) => UserDirectoryProvider(ctx)), Provider(create: (ctx) => UserDirectoryProvider(ctx)),
Provider(create: (ctx) => SnAttachmentProvider(ctx)), Provider(create: (ctx) => SnAttachmentProvider(ctx)),
Provider(create: (ctx) => SnRealmProvider(ctx)), ChangeNotifierProvider(create: (ctx) => SnRealmProvider(ctx)),
Provider(create: (ctx) => SnPostContentProvider(ctx)), Provider(create: (ctx) => SnPostContentProvider(ctx)),
Provider(create: (ctx) => SnRelationshipProvider(ctx)), Provider(create: (ctx) => SnRelationshipProvider(ctx)),
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)), Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
@ -228,6 +256,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')) {
@ -257,11 +288,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
final resp = await Dio( final resp = await Dio(
BaseOptions( BaseOptions(
sendTimeout: const Duration(seconds: 60), sendTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60), receiveTimeout: const Duration(seconds: 60)),
),
).get( ).get(
'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest', 'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest');
);
final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0'; final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0';
final remoteVersion = Version.parse(remoteVersionString.split('+').first); final remoteVersion = Version.parse(remoteVersionString.split('+').first);
final localVersion = Version.parse(localVersionString.split('+').first); final localVersion = Version.parse(localVersionString.split('+').first);
@ -276,9 +305,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
mounted) { mounted) {
final config = context.read<ConfigProvider>(); final config = context.read<ConfigProvider>();
config.setUpdate( config.setUpdate(
remoteVersionString, remoteVersionString, resp.data?['body'] ?? 'No changelog');
resp.data?['body'] ?? 'No changelog',
);
logging.info("[Update] Update available: $remoteVersionString"); logging.info("[Update] Update available: $remoteVersionString");
} }
} catch (e) { } catch (e) {
@ -287,6 +314,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>();
@ -299,31 +331,52 @@ 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();
try {
if (!mounted) return; if (!mounted) return;
final notify = context.read<NotificationProvider>(); _setPhaseText('keyPair');
notify.listen();
await notify.registerPushNotifications();
if (!mounted) return;
final kp = context.read<KeyPairProvider>(); final kp = context.read<KeyPairProvider>();
await kp.reloadActive(); await kp.reloadActive();
kp.listen(); kp.listen();
} catch (_) {}
if (ua.isAuthorized) {
if (!mounted) return; if (!mounted) return;
_setPhaseText('notification');
final notify = context.read<NotificationProvider>();
notify.listen();
try {
notify.registerPushNotifications();
} catch (_) {}
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');
_playIntro();
}
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
await context.showErrorDialog(err); await context.showErrorDialog(err);
@ -339,28 +392,28 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
// The quit key has been removed, and the logic of the quit key is moved to system menu bar activator. // The quit key has been removed, and the logic of the quit key is moved to system menu bar activator.
} }
void _playIntro() async {
final cfg = context.read<ConfigProvider>();
if (!cfg.soundEffects) return;
final player = AudioPlayer(playerId: 'launch-intro-player');
await player.play(AssetSource('audio/sfx/launch-intro.mp3'), volume: 0.5);
player.onPlayerComplete.listen((_) {
player.dispose();
});
}
final Menu _appTrayMenu = Menu( final Menu _appTrayMenu = Menu(
items: [ items: [
MenuItem( MenuItem(key: 'version_label', label: 'Solian', disabled: true),
key: 'version_label',
label: 'Solian',
disabled: true,
),
MenuItem.separator(), MenuItem.separator(),
MenuItem.checkbox( MenuItem.checkbox(
checked: false, checked: false,
key: 'mute_notification', key: 'mute_notification',
label: 'trayMenuMuteNotification'.tr(), label: 'trayMenuMuteNotification'.tr()),
),
MenuItem.separator(), MenuItem.separator(),
MenuItem( MenuItem(key: 'window_show', label: 'trayMenuShow'.tr()),
key: 'window_show', MenuItem(key: 'exit', label: 'trayMenuExit'.tr()),
label: 'trayMenuShow'.tr(),
),
MenuItem(
key: 'exit',
label: 'trayMenuExit'.tr(),
),
], ],
); );
@ -388,9 +441,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
await localNotifier.setup( await localNotifier.setup(
appName: 'Solian', appName: 'Solian', shortcutPolicy: ShortcutPolicy.requireCreate);
shortcutPolicy: ShortcutPolicy.requireCreate,
);
} }
AppLifecycleListener? _appLifecycleListener; AppLifecycleListener? _appLifecycleListener;
@ -399,10 +450,10 @@ 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 =
onExitRequested: _onExitRequested, AppLifecycleListener(onExitRequested: _onExitRequested);
);
} }
_trayInitialization(); _trayInitialization();
@ -412,6 +463,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
_postInitialization(); _postInitialization();
_tryRequestRating(); _tryRequestRating();
_checkForUpdate(); _checkForUpdate();
setState(() => _isBusy = false);
}); });
} }
@ -421,6 +473,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
} }
void _quitApp() { void _quitApp() {
_saveWindowSize();
_appLifecycleListener?.dispose(); _appLifecycleListener?.dispose();
if (Platform.isWindows) { if (Platform.isWindows) {
appWindow.close(); appWindow.close();
@ -501,7 +554,49 @@ 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: [
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/icon/kanban-1st.jpg'),
fit: BoxFit.cover,
opacity: 0.1,
),
color: Theme.of(context).colorScheme.surface,
backgroundBlendMode: BlendMode.darken,
),
),
Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 240),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/icon/icon.png',
width: 64,
height: 64,
color:
Theme.of(context).colorScheme.onSurface,
),
Text('Solar Network').bold(),
AppVersionLabel(),
Gap(8),
Text(_phaseText, textAlign: TextAlign.center),
Gap(16),
const LinearProgressIndicator(),
],
),
),
),
],
),
)
: widget.child,
); );
}, },
), ),

View File

@ -28,6 +28,24 @@ 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();
});
}
void addAvailableChannel(SnChannel channel) {
_availableChannels.add(channel);
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(

View File

@ -21,6 +21,10 @@ 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 kAppAutoTranslate = 'app_auto_translate';
const kAppHideBottomNav = 'app_hide_bottom_nav';
const kAppSoundEffects = 'app_sound_effects';
const kAppAprilFoolFeatures = 'app_april_fool_features';
const kAppWindowSize = 'app_window_size';
const Map<String, FilterQuality> kImageQualityLevel = { const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none, 'settingsImageQualityLowest': FilterQuality.none,
@ -91,6 +95,33 @@ class ConfigProvider extends ChangeNotifier {
return prefs.getBool(kAppAutoTranslate) ?? false; return prefs.getBool(kAppAutoTranslate) ?? false;
} }
bool get hideBottomNav {
return prefs.getBool(kAppHideBottomNav) ?? false;
}
bool get aprilFoolFeatures {
return prefs.getBool(kAppAprilFoolFeatures) ?? true;
}
bool get soundEffects {
return prefs.getBool(kAppSoundEffects) ?? true;
}
set soundEffects(bool value) {
prefs.setBool(kAppSoundEffects, value);
notifyListeners();
}
set aprilFoolFeatures(bool value) {
prefs.setBool(kAppAprilFoolFeatures, value);
notifyListeners();
}
set hideBottomNav(bool value) {
prefs.setBool(kAppHideBottomNav, value);
notifyListeners();
}
set autoTranslate(bool value) { set autoTranslate(bool value) {
prefs.setBool(kAppAutoTranslate, value); prefs.setBool(kAppAutoTranslate, value);
notifyListeners(); notifyListeners();

View File

@ -4,6 +4,21 @@ 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 AppNavListItem {
final String title;
final String subtitle;
final String screen;
final IconData icon;
const AppNavListItem({
required this.title,
required this.subtitle,
required this.screen,
required this.icon,
});
}
class AppNavDestination { class AppNavDestination {
final String label; final String label;
@ -24,13 +39,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(
@ -48,11 +60,6 @@ class NavigationProvider extends ChangeNotifier {
screen: 'chat', screen: 'chat',
label: 'screenChat', label: 'screenChat',
), ),
AppNavDestination(
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
screen: 'account',
label: 'screenAccount',
),
AppNavDestination( AppNavDestination(
icon: Icon(Symbols.group, weight: 400, opticalSize: 20), icon: Icon(Symbols.group, weight: 400, opticalSize: 20),
screen: 'realm', screen: 'realm',
@ -64,31 +71,16 @@ class NavigationProvider extends ChangeNotifier {
label: 'screenNews', label: 'screenNews',
), ),
AppNavDestination( AppNavDestination(
icon: Icon(Symbols.emoji_emotions, weight: 400, opticalSize: 20), icon: Icon(Symbols.settings, weight: 400, opticalSize: 20),
screen: 'stickers', screen: 'settings',
label: 'screenStickers', label: 'screenSettings',
),
AppNavDestination(
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
screen: 'album',
label: 'screenAlbum',
),
AppNavDestination(
icon: Icon(Symbols.diversity_4, weight: 400, opticalSize: 20),
screen: 'friend',
label: 'screenFriend',
),
AppNavDestination(
icon: Icon(Symbols.notifications, weight: 400, opticalSize: 20),
screen: 'notification',
label: 'screenNotification',
), ),
]; ];
static const List<String> kDefaultPinnedDestination = [ static const List<String> kDefaultPinnedDestination = [
'home', 'home',
'explore', 'explore',
'chat', 'chat',
'account', 'realm',
]; ];
List<AppNavDestination> destinations = []; List<AppNavDestination> destinations = [];
@ -143,4 +135,11 @@ class NavigationProvider extends ChangeNotifier {
_currentIndex = idx; _currentIndex = idx;
notifyListeners(); notifyListeners();
} }
SnRealm? focusedRealm;
void setFocusedRealm(SnRealm? realm) {
focusedRealm = realm;
notifyListeners();
}
} }

View File

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:audioplayers/audioplayers.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -22,6 +23,8 @@ class NotificationProvider extends ChangeNotifier {
late final WebSocketProvider _ws; late final WebSocketProvider _ws;
late final ConfigProvider _cfg; late final ConfigProvider _cfg;
final AudioPlayer _notifySoundPlayer = AudioPlayer(playerId: 'notify-sound');
NotificationProvider(BuildContext context) { NotificationProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>(); _sn = context.read<SnNetworkProvider>();
_ua = context.read<UserProvider>(); _ua = context.read<UserProvider>();
@ -66,14 +69,19 @@ class NotificationProvider extends ChangeNotifier {
} }
logging.info('[Push Notification] Device Push Token is $token'); logging.info('[Push Notification] Device Push Token is $token');
try {
await _sn.client.post( await _sn.client.post(
'/cgi/id/notifications/subscription', '/cgi/id/notifications/subscription',
data: { data: {
'provider': provider, 'provider': provider,
'device_token': token, 'device_token': token,
'device_id': deviceUuid, 'device_id': deviceUuid
}, },
); );
} catch (err) {
logging.error(
'[Push Notification] Unable to register push notifications: $err');
}
} }
int showingCount = 0; int showingCount = 0;
@ -91,6 +99,16 @@ class NotificationProvider extends ChangeNotifier {
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true; final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
if (doHaptic) HapticFeedback.mediumImpact(); if (doHaptic) HapticFeedback.mediumImpact();
// April fool notification sfx
if (_cfg.prefs.getBool(kAppAprilFoolFeatures) ?? true) {
final now = DateTime.now();
if (now.day == 1 && now.month == 4) {
_notifySoundPlayer.play(
AssetSource('audio/notify/metal-pipe.mp3'),
);
}
}
if (notification.topic == 'messaging.message' && if (notification.topic == 'messaging.message' &&
skippableNotifyChannel != null) { skippableNotifyChannel != null) {
if (notification.metadata['channel_id'] != null && if (notification.metadata['channel_id'] != null &&

View File

@ -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))),
), ),
), ),
); );

View File

@ -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 extends ChangeNotifier {
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,17 +35,56 @@ 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;
} }
void addAvailableRealm(SnRealm realm) {
_availableRealms.add(realm);
notifyListeners();
}
Future<SnRealm> getRealm(dynamic aliasOrId) async { Future<SnRealm> getRealm(dynamic aliasOrId) async {
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))),
),
),
);
}
}
} }

View File

@ -4,8 +4,7 @@ import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:surface/logger.dart'; import 'package:surface/logger.dart';
// TODO self host translate api const kTranslateApiBaseUrl = 'https://translate.solsynth.dev';
const kTranslateApiBaseUrl = 'https://translate.disroot.org';
class SnTranslator { class SnTranslator {
final Dio client = Dio( final Dio client = Dio(

View File

@ -64,6 +64,7 @@ class UserProvider extends ChangeNotifier {
} }
Future<SnAccount?> refreshUser() async { Future<SnAccount?> refreshUser() async {
if (!isAuthorized) return null;
final resp = await _sn.client.get('/cgi/id/users/me'); final resp = await _sn.client.get('/cgi/id/users/me');
final out = SnAccount.fromJson(resp.data); final out = SnAccount.fromJson(resp.data);

View File

@ -3,14 +3,18 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:surface/screens/abuse_report.dart'; import 'package:surface/screens/abuse_report.dart';
import 'package:surface/screens/account.dart'; import 'package:surface/screens/account.dart';
import 'package:surface/screens/account/account_settings.dart'; import 'package:surface/screens/account/punishments.dart';
import 'package:surface/screens/account/settings.dart';
import 'package:surface/screens/account/action_events.dart'; import 'package:surface/screens/account/action_events.dart';
import 'package:surface/screens/account/badges.dart'; 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/programs.dart';
import 'package:surface/screens/account/publishers/publisher_edit.dart'; import 'package:surface/screens/account/publishers/publisher_edit.dart';
import 'package:surface/screens/account/publishers/publisher_new.dart'; import 'package:surface/screens/account/publishers/publisher_new.dart';
import 'package:surface/screens/account/publishers/publishers.dart'; import 'package:surface/screens/account/publishers/publishers.dart';
@ -37,6 +41,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';
@ -67,8 +72,8 @@ final _appRoutes = [
), ),
GoRoute( GoRoute(
path: '/posts', path: '/posts',
name: 'explore', name: 'posts',
builder: (context, state) => const ExploreScreen(), builder: (_, __) => const SizedBox.shrink(),
routes: [ routes: [
GoRoute( GoRoute(
path: '/draft', path: '/draft',
@ -106,27 +111,62 @@ final _appRoutes = [
state.uri.queryParameters['categories']?.split(','), state.uri.queryParameters['categories']?.split(','),
), ),
), ),
],
),
ShellRoute(
builder: (context, state, child) => ResponsiveScaffold(
asideFlex: 2,
contentFlex: 3,
aside: const ExploreScreen(),
child: child,
),
routes: [
GoRoute(
path: '/explore',
name: 'explore',
builder: (context, state) => const ResponsiveScaffoldLanding(
child: ExploreScreen(),
),
),
GoRoute(
path: '/posts/:slug',
name: 'postDetail',
builder: (context, state) => PostDetailScreen(
key: ValueKey(state.pathParameters['slug']!),
slug: state.pathParameters['slug']!,
preload: state.extra as SnPost?,
),
),
GoRoute( GoRoute(
path: '/publishers/:name', path: '/publishers/:name',
name: 'postPublisher', name: 'postPublisher',
builder: (context, state) => builder: (context, state) =>
PostPublisherScreen(name: state.pathParameters['name']!), PostPublisherScreen(name: state.pathParameters['name']!),
), ),
GoRoute(
path: '/:slug',
name: 'postDetail',
builder: (context, state) => PostDetailScreen(
slug: state.pathParameters['slug']!,
preload: state.extra as SnPost?,
),
),
], ],
), ),
ShellRoute(
builder: (context, state, child) => ResponsiveScaffold(
aside: const AccountScreen(),
child: child,
),
routes: [
GoRoute( GoRoute(
path: '/account', path: '/account',
name: 'account', name: 'account',
builder: (context, state) => const AccountScreen(), builder: (context, state) =>
const ResponsiveScaffoldLanding(child: AccountScreen()),
routes: [ routes: [
GoRoute(
path: '/punishments',
name: 'accountPunishments',
builder: (context, state) => const PunishmentsScreen(),
),
GoRoute(
path: '/programs',
name: 'accountProgram',
builder: (context, state) => const AccountProgramScreen(),
),
GoRoute( GoRoute(
path: '/contacts', path: '/contacts',
name: 'accountContactMethods', name: 'accountContactMethods',
@ -161,6 +201,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',
@ -189,24 +241,35 @@ final _appRoutes = [
name: state.pathParameters['name']!, name: state.pathParameters['name']!,
), ),
), ),
],
),
],
),
GoRoute( GoRoute(
path: '/profile/:name', path: '/accounts/:name',
name: 'accountProfilePage', name: 'accountProfilePage',
pageBuilder: (context, state) => NoTransitionPage( pageBuilder: (context, state) => NoTransitionPage(
child: UserScreen(name: state.pathParameters['name']!), child: UserScreen(name: state.pathParameters['name']!),
), ),
), ),
], ShellRoute(
), builder: (context, state, child) =>
ResponsiveScaffold(aside: const ChatScreen(), child: child),
routes: [
GoRoute( GoRoute(
path: '/chat', path: '/chat',
name: 'chat', name: 'chat',
builder: (context, state) => const ChatScreen(), builder: (context, state) => const ResponsiveScaffoldLanding(
child: ChatScreen(),
),
routes: [ routes: [
GoRoute( GoRoute(
path: '/:scope/:alias', path: '/:scope/:alias',
name: 'chatRoom', name: 'chatRoom',
builder: (context, state) => ChatRoomScreen( builder: (context, state) => ChatRoomScreen(
key: ValueKey(
'${state.pathParameters['scope']!}:${state.pathParameters['alias']!}',
),
scope: state.pathParameters['scope']!, scope: state.pathParameters['scope']!,
alias: state.pathParameters['alias']!, alias: state.pathParameters['alias']!,
extra: state.extra as ChatRoomScreenExtra?, extra: state.extra as ChatRoomScreenExtra?,
@ -237,6 +300,8 @@ final _appRoutes = [
), ),
], ],
), ),
],
),
GoRoute( GoRoute(
path: '/realm', path: '/realm',
name: 'realm', name: 'realm',
@ -245,6 +310,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',

View File

@ -8,6 +8,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/database.dart'; import 'package:surface/providers/database.dart';
import 'package:surface/providers/navigation.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart'; import 'package:surface/providers/websocket.dart';
@ -22,27 +23,97 @@ import 'package:surface/widgets/universal_image.dart';
class AccountScreen extends StatelessWidget { class AccountScreen extends StatelessWidget {
const AccountScreen({super.key}); const AccountScreen({super.key});
static const List<AppNavListItem> kNavList = [
AppNavListItem(
title: "accountPublishers",
subtitle: "accountPublishersSubtitle",
screen: "accountPublishers",
icon: Symbols.face,
),
AppNavListItem(
title: "accountProgram",
subtitle: "accountProgramDescription",
screen: "accountProgram",
icon: Symbols.communities,
),
AppNavListItem(
title: "friends",
subtitle: "friendsDescription",
screen: "friend",
icon: Symbols.person,
),
AppNavListItem(
title: "album",
subtitle: "albumDescription",
screen: "album",
icon: Symbols.photo_library,
),
AppNavListItem(
title: "stickers",
subtitle: "stickersDescription",
screen: "stickers",
icon: Symbols.emoji_emotions,
),
AppNavListItem(
title: "accountWallet",
subtitle: "accountWalletSubtitle",
screen: "accountWallet",
icon: Symbols.wallet,
),
AppNavListItem(
title: "accountBadges",
subtitle: "accountBadgesDescription",
screen: "accountBadges",
icon: Symbols.award_star,
),
AppNavListItem(
title: "accountKeyPairs",
subtitle: "accountKeyPairsDescription",
screen: "accountKeyPairs",
icon: Symbols.key,
),
AppNavListItem(
title: "accountPunishments",
subtitle: "accountPunishmentsDescription",
screen: "accountPunishments",
icon: Symbols.credit_score,
),
AppNavListItem(
title: "accountActionEvent",
subtitle: "accountActionEventDescription",
screen: "accountActionEvents",
icon: Symbols.history,
),
AppNavListItem(
title: "accountAuthTickets",
subtitle: "accountAuthTicketsDescription",
screen: "accountAuthTickets",
icon: Symbols.confirmation_number,
),
AppNavListItem(
title: "accountSettings",
subtitle: "accountSettingsSubtitle",
screen: "accountSettings",
icon: Symbols.manage_accounts,
),
AppNavListItem(
title: "abuseReport",
subtitle: "abuseReportActionDescription",
screen: "abuseReport",
icon: Symbols.flag,
),
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text( title: Text("screenAccount").tr(),
"screenAccount",
style: TextStyle(
color: Colors.white,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 5.0,
color: Color.fromARGB(255, 0, 0, 0),
),
],
),
).tr(),
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
? Stack( ? Stack(
fit: StackFit.expand, fit: StackFit.expand,
@ -71,15 +142,6 @@ class AccountScreen extends StatelessWidget {
], ],
) )
: null, : null,
actions: [
IconButton(
icon: const Icon(Symbols.settings, fill: 1),
onPressed: () {
GoRouter.of(context).pushNamed('settings');
},
),
const Gap(8),
],
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: ua.isAuthorized child: ua.isAuthorized
@ -118,7 +180,18 @@ class _AuthorizedAccountScreen extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AccountImage(content: ua.user!.avatar, radius: 28), GestureDetector(
child: AccountImage(
content: ua.user!.avatar,
radius: 28,
),
onTap: () {
GoRouter.of(context)
.pushNamed('accountProfilePage', pathParameters: {
'name': ua.user!.name,
});
},
),
_AccountStatusWidget(account: ua.user!), _AccountStatusWidget(account: ua.user!),
], ],
), ),
@ -147,99 +220,25 @@ class _AuthorizedAccountScreen extends StatelessWidget {
); );
}).padding(all: 20), }).padding(all: 20),
).padding(horizontal: 8, top: 16, bottom: 4), ).padding(horizontal: 8, top: 16, bottom: 4),
ListTile( for (final item in AccountScreen.kNavList)
title: Text('accountPublishers').tr(), Tooltip(
subtitle: Text('accountPublishersSubtitle').tr(), message: item.subtitle.tr(),
child: ListTile(
minTileHeight: 48,
title: Text(item.title).tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.face), leading: Icon(item.icon),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('accountPublishers'); GoRouter.of(context).pushNamed(item.screen);
}, },
), ),
ListTile(
title: Text('abuseReport').tr(),
subtitle: Text('abuseReportActionDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.flag),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('abuseReport');
},
), ),
ListTile( Tooltip(
title: Text('factorSettings').tr(), message: 'accountLogoutSubtitle'.tr(),
subtitle: Text('factorSettingsSubtitle').tr(), child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.lock),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('factorSettings');
},
),
ListTile(
title: Text('accountWallet').tr(),
subtitle: Text('accountWalletSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.wallet),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountWallet');
},
),
ListTile(
title: Text('accountBadges').tr(),
subtitle: Text('accountBadgesDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.award_star),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountBadges');
},
),
ListTile(
title: Text('accountKeyPairs').tr(),
subtitle: Text('accountKeyPairsDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.key),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountKeyPairs');
},
),
ListTile(
title: Text('accountActionEvent').tr(),
subtitle: Text('accountActionEventDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.history),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountActionEvents');
},
),
ListTile(
title: Text('accountAuthTickets').tr(),
subtitle: Text('accountAuthTicketsDescription').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.confirmation_number),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountAuthTickets');
},
),
ListTile(
title: Text('accountSettings').tr(),
subtitle: Text('accountSettingsSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.manage_accounts),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('accountSettings');
},
),
ListTile(
title: Text('accountLogout').tr(), title: Text('accountLogout').tr(),
subtitle: Text('accountLogoutSubtitle').tr(), minTileHeight: 48,
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.logout), leading: const Icon(Symbols.logout),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
@ -257,6 +256,7 @@ class _AuthorizedAccountScreen extends StatelessWidget {
context.read<DatabaseProvider>().removeDatabase(); context.read<DatabaseProvider>().removeDatabase();
}, },
), ),
),
], ],
); );
} }
@ -298,9 +298,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
GoRouter.of(context).pushNamed('authLogin').then((value) { GoRouter.of(context).pushNamed('authLogin').then((value) {
if (value == true && context.mounted) { if (value == true && context.mounted) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
context.showSnackbar('loginSuccess'.tr(args: [ ua.refreshUser();
'@${ua.user?.name} (${ua.user?.nick})',
]));
} }
}); });
}, },

View File

@ -59,6 +59,7 @@ class _ActionEventScreenState extends State<ActionEventScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountActionEvent').tr(), title: Text('accountActionEvent').tr(),

View File

@ -91,6 +91,7 @@ class _AccountAuthTicketState extends State<AccountAuthTicket> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountAuthTickets').tr(), title: Text('accountAuthTickets').tr(),

View File

@ -70,6 +70,7 @@ class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: Text('screenAccountBadges').tr(), title: Text('screenAccountBadges').tr(),
), ),

View File

@ -69,6 +69,7 @@ class _AccountContactMethodState extends State<AccountContactMethod> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountContactMethods').tr(), title: Text('accountContactMethods').tr(),

View File

@ -16,7 +16,11 @@ final Map<int, (String, String, IconData)> kFactorTypes = {
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password), 0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email), 1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer), 2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active), 3: (
'authFactorInAppNotify',
'authFactorInAppNotifyDescription',
Symbols.notifications_active
),
}; };
class FactorSettingsScreen extends StatefulWidget { class FactorSettingsScreen extends StatefulWidget {
@ -36,7 +40,10 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/me/factors'); final resp = await sn.client.get('/cgi/id/users/me/factors');
_factors = List<SnAuthFactor>.from( _factors = List<SnAuthFactor>.from(
resp.data?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>)).toList() ?? [], resp.data
?.map((e) => SnAuthFactor.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
); );
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
@ -55,6 +62,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: PageBackButton(), leading: PageBackButton(),
title: Text('screenFactorSettings').tr(), title: Text('screenFactorSettings').tr(),
@ -96,7 +104,8 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
return ListTile( return ListTile(
title: Text(kFactorTypes[ele.type]!.$1).tr(), title: Text(kFactorTypes[ele.type]!.$1).tr(),
subtitle: Text(kFactorTypes[ele.type]!.$2).tr(), subtitle: Text(kFactorTypes[ele.type]!.$2).tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 12), contentPadding:
const EdgeInsets.only(left: 24, right: 12),
leading: Icon(kFactorTypes[ele.type]!.$3), leading: Icon(kFactorTypes[ele.type]!.$3),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Symbols.close), icon: const Icon(Symbols.close),
@ -105,14 +114,17 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
context context
.showConfirmDialog( .showConfirmDialog(
'authFactorDelete'.tr(), 'authFactorDelete'.tr(),
'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]), 'authFactorDeleteDescription'.tr(
args: [kFactorTypes[ele.type]!.$1.tr()]),
) )
.then((val) async { .then((val) async {
if (!val) return; if (!val) return;
try { try {
if (!context.mounted) return; if (!context.mounted) return;
final sn = context.read<SnNetworkProvider>(); final sn =
await sn.client.delete('/cgi/id/users/me/factors/${ele.id}'); context.read<SnNetworkProvider>();
await sn.client.delete(
'/cgi/id/users/me/factors/${ele.id}');
_fetchFactors(); _fetchFactors();
} catch (err) { } catch (err) {
if (!context.mounted) return; if (!context.mounted) return;
@ -191,7 +203,9 @@ class _FactorNewDialogState extends State<_FactorNewDialog> {
value: _factorType, value: _factorType,
items: kFactorTypes.entries.map( items: kFactorTypes.entries.map(
(ele) { (ele) {
final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key); final contains = widget.currentlyHave
.map((ele) => ele.type)
.contains(ele.key);
return DropdownMenuItem<int>( return DropdownMenuItem<int>(
enabled: !contains, enabled: !contains,
value: ele.key, value: ele.key,

View File

@ -37,6 +37,7 @@ class _KeyPairScreenState extends State<KeyPairScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: Text('screenKeyPairs').tr(), title: Text('screenKeyPairs').tr(),
), ),

View File

@ -1,11 +1,123 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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'; import 'package:surface/widgets/navigation/app_scaffold.dart';
class AccountNotifyPrefsScreen extends StatelessWidget { final Map<String, String> kNotifyTopicMap = {
'interactive.reply': 'notificationTopicPostReply'.tr(),
'interactive.feedback': 'notificationTopicPostFeedback'.tr(),
'interactive.subscription': 'notificationTopicPostSubscription'.tr(),
'messaging.message': 'notificationTopicMessaging'.tr(),
'messaging.call': 'notificationTopicMessagingCall'.tr(),
'general': 'notificationTopicGeneral'.tr(),
};
class AccountNotifyPrefsScreen extends StatefulWidget {
const AccountNotifyPrefsScreen({super.key}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold(); return AppScaffold(
noBackground: true,
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;
});
},
);
},
),
),
],
),
);
} }
} }

View File

@ -0,0 +1,148 @@
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(
noBackground: true,
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);
},
),
],
),
),
],
),
);
}
}

View File

@ -66,21 +66,23 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
_locationController.text = prof.profile!.location; _locationController.text = prof.profile!.location;
_avatar = prof.avatar; _avatar = prof.avatar;
_banner = prof.banner; _banner = prof.banner;
_links = prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList(); _links =
prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList();
_birthday = prof.profile!.birthday?.toLocal(); _birthday = prof.profile!.birthday?.toLocal();
if (_birthday != null) { if (_birthday != null) {
_birthdayController.text = DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal()); _birthdayController.text =
DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal());
} }
} }
void _selectBirthday() async { void _selectBirthday() async {
await showCupertinoModalPopup<DateTime?>( await showCupertinoModalPopup<DateTime?>(
context: context, context: context,
builder: builder: (BuildContext context) => Container(
(BuildContext context) => Container(
height: 216, height: 216,
padding: const EdgeInsets.only(top: 6.0), padding: const EdgeInsets.only(top: 6.0),
margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), margin:
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: SafeArea( child: SafeArea(
top: false, top: false,
@ -91,7 +93,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
onDateTimeChanged: (DateTime newDate) { onDateTimeChanged: (DateTime newDate) {
setState(() { setState(() {
_birthday = newDate; _birthday = newDate;
_birthdayController.text = DateFormat(_kDateFormat).format(_birthday!); _birthdayController.text =
DateFormat(_kDateFormat).format(_birthday!);
}); });
}, },
), ),
@ -109,11 +112,12 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
Uint8List? rawBytes; Uint8List? rawBytes;
if (!skipCrop) { if (!skipCrop) {
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); final ImageProvider imageProvider =
final aspectRatios = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; final aspectRatios = place == 'banner'
final result = ? [CropAspectRatio(width: 16, height: 7)]
(!kIsWeb && (Platform.isIOS || Platform.isMacOS)) : [CropAspectRatio(width: 1, height: 1)];
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
? await showCupertinoImageCropper( ? await showCupertinoImageCropper(
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
context, context,
@ -131,7 +135,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!
.buffer
.asUint8List();
} else { } else {
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
@ -152,7 +158,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
if (!mounted) return; if (!mounted) return;
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.client.put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid}); await sn.client
.put('/cgi/id/users/me/$place', data: {'attachment': attachment.rid});
if (!mounted) return; if (!mounted) return;
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
@ -188,7 +195,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
'location': _locationController.value.text, 'location': _locationController.value.text,
'birthday': _birthday?.toUtc().toIso8601String(), 'birthday': _birthday?.toUtc().toIso8601String(),
'links': { 'links': {
for (final link in _links!.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty)) link.$1: link.$2, for (final link in _links!
.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty))
link.$1: link.$2,
}, },
}, },
); );
@ -235,7 +244,10 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
appBar: AppBar(leading: const PageBackButton(), title: Text('screenAccountProfileEdit').tr()), noBackground: true,
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAccountProfileEdit').tr()),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -253,10 +265,13 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: Container( child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context)
child: .colorScheme
_banner != null .surfaceContainerHigh,
? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover) child: _banner != null
? AutoResizeUniversalImage(
sn.getAttachmentUrl(_banner!),
fit: BoxFit.cover)
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
), ),
@ -294,12 +309,16 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
labelText: 'fieldUsername'.tr(), labelText: 'fieldUsername'.tr(),
helperText: 'fieldUsernameCannotEditHint'.tr(), helperText: 'fieldUsernameCannotEditHint'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextField( TextField(
controller: _nicknameController, controller: _nicknameController,
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldNickname'.tr()), decoration: InputDecoration(
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr()),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
Row( Row(
children: [ children: [
@ -311,7 +330,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldFirstName'.tr(), labelText: 'fieldFirstName'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
const Gap(8), const Gap(8),
@ -323,7 +343,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldLastName'.tr(), labelText: 'fieldLastName'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
], ],
@ -338,7 +359,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldGender'.tr(), labelText: 'fieldGender'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
const Gap(4), const Gap(4),
@ -350,7 +372,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldPronouns'.tr(), labelText: 'fieldPronouns'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
], ],
@ -360,8 +383,11 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
maxLines: null, maxLines: null,
minLines: 3, minLines: 3,
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldDescription'.tr()), decoration: InputDecoration(
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), border: const UnderlineInputBorder(),
labelText: 'fieldDescription'.tr()),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -373,18 +399,21 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldTimeZone'.tr(), labelText: 'fieldTimeZone'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
const Gap(4), const Gap(4),
StyledWidget( StyledWidget(
IconButton( IconButton(
icon: const Icon(Symbols.calendar_month), icon: const Icon(Symbols.calendar_month),
visualDensity: VisualDensity(horizontal: -4, vertical: -4), visualDensity:
VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
onPressed: () async { onPressed: () async {
_timezoneController.text = await FlutterTimezone.getLocalTimezone(); _timezoneController.text =
await FlutterTimezone.getLocalTimezone();
}, },
), ),
).padding(top: 6), ).padding(top: 6),
@ -392,7 +421,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
StyledWidget( StyledWidget(
IconButton( IconButton(
icon: const Icon(Symbols.clear), icon: const Icon(Symbols.clear),
visualDensity: VisualDensity(horizontal: -4, vertical: -4), visualDensity:
VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
onPressed: () { onPressed: () {
@ -404,13 +434,18 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
), ),
TextField( TextField(
controller: _locationController, controller: _locationController,
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldLocation'.tr()), decoration: InputDecoration(
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), border: const UnderlineInputBorder(),
labelText: 'fieldLocation'.tr()),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
TextField( TextField(
controller: _birthdayController, controller: _birthdayController,
readOnly: true, readOnly: true,
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldBirthday'.tr()), decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldBirthday'.tr()),
onTap: () => _selectBirthday(), onTap: () => _selectBirthday(),
), ),
if (_links != null) if (_links != null)
@ -418,7 +453,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
margin: const EdgeInsets.only(top: 16, bottom: 4), margin: const EdgeInsets.only(top: 16, bottom: 4),
child: Container( child: Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -427,13 +463,17 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
Expanded( Expanded(
child: Text( child: Text(
'fieldLinks'.tr(), 'fieldLinks'.tr(),
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 17), style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 17),
), ),
), ),
IconButton( IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
visualDensity: VisualDensity(horizontal: -4, vertical: -4), visualDensity:
VisualDensity(horizontal: -4, vertical: -4),
icon: const Icon(Symbols.add), icon: const Icon(Symbols.add),
onPressed: () { onPressed: () {
setState(() => _links!.add(('', ''))); setState(() => _links!.add(('', '')));
@ -457,7 +497,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
onChanged: (value) { onChanged: (value) {
_links![idx] = (value, _links![idx].$2); _links![idx] = (value, _links![idx].$2);
}, },
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager
.instance.primaryFocus
?.unfocus(),
), ),
), ),
const Gap(8), const Gap(8),
@ -473,7 +515,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
onChanged: (value) { onChanged: (value) {
_links![idx] = (_links![idx].$1, value); _links![idx] = (_links![idx].$1, value);
}, },
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager
.instance.primaryFocus
?.unfocus(),
), ),
), ),
], ],

View File

@ -1,3 +1,4 @@
import 'dart:math' as math;
import 'dart:ui'; import 'dart:ui';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
@ -227,7 +228,7 @@ class _UserScreenState extends State<UserScreen>
late final _appBarWidth = MediaQuery.of(context).size.width; late final _appBarWidth = MediaQuery.of(context).size.width;
late final _appBarHeight = late final _appBarHeight =
(_appBarWidth * kBannerAspectRatio).roundToDouble(); math.min((_appBarWidth * kBannerAspectRatio), 360).roundToDouble();
void _updateAppBarBlur() { void _updateAppBarBlur() {
if (_scrollController.offset > _appBarHeight) return; if (_scrollController.offset > _appBarHeight) return;
@ -489,10 +490,10 @@ class _UserScreenState extends State<UserScreen>
), ),
const Gap(8), const Gap(8),
Wrap( Wrap(
spacing: 4,
runSpacing: 4,
children: _account!.badges children: _account!.badges
.map( .map((ele) => AccountBadge(badge: ele))
(ele) => AccountBadge(badge: ele),
)
.toList(), .toList(),
).padding(horizontal: 8), ).padding(horizontal: 8),
const Gap(8), const Gap(8),

View File

@ -0,0 +1,291 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/experience.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/account.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/markdown_content.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class AccountProgramScreen extends StatefulWidget {
const AccountProgramScreen({super.key});
@override
State<AccountProgramScreen> createState() => _AccountProgramScreenState();
}
class _AccountProgramScreenState extends State<AccountProgramScreen> {
bool _isBusy = false;
final List<SnProgram> _programs = List.empty(growable: true);
final List<SnProgramMember> _programMembers = List.empty(growable: true);
Future<void> _fetchPrograms() async {
_programs.clear();
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/programs');
_programs.addAll(
resp.data.map((ele) => SnProgram.fromJson(ele)).cast<SnProgram>(),
);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _fetchProgramMembers() async {
_programMembers.clear();
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/programs/members');
_programMembers.addAll(
resp.data
.map((ele) => SnProgramMember.fromJson(ele))
.cast<SnProgramMember>(),
);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchPrograms();
_fetchProgramMembers();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
noBackground: true,
appBar: AppBar(
title: Text('accountProgram').tr(),
),
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: _programs.length,
itemBuilder: (context, idx) {
final ele = _programs[idx];
return Card(
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(8)),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => _ProgramJoinPopup(
program: ele,
isJoined:
_programMembers.any((e) => e.programId == ele.id),
),
).then((value) {
if (value == true) {
_fetchProgramMembers();
}
});
},
child: Column(
children: [
if (ele.appearance['banner'] != null)
AspectRatio(
aspectRatio: 16 / 5,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context)
.colorScheme
.surfaceVariant,
child: Image.network(
ele.appearance['banner'],
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ele.name,
style: Theme.of(context)
.textTheme
.titleMedium,
).bold(),
Text(
ele.description,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
if (_programMembers
.any((e) => e.programId == ele.id))
Text('accountProgramAlreadyJoined'.tr())
.opacity(0.75),
],
),
),
],
),
),
],
),
),
).padding(horizontal: 8);
},
),
),
],
),
);
}
}
class _ProgramJoinPopup extends StatefulWidget {
final SnProgram program;
final bool isJoined;
const _ProgramJoinPopup({required this.program, required this.isJoined});
@override
State<_ProgramJoinPopup> createState() => _ProgramJoinPopupState();
}
class _ProgramJoinPopupState extends State<_ProgramJoinPopup> {
bool _isBusy = false;
Future<void> _joinProgram() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/id/programs/${widget.program.id}');
if (!mounted) return;
Navigator.pop(context, true);
context.showSnackbar('accountProgramJoined'.tr());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _leaveProgram() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.delete('/cgi/id/programs/${widget.program.id}');
if (!mounted) return;
Navigator.pop(context, true);
context.showSnackbar('accountProgramLeft'.tr());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.75,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.add, size: 24),
const Gap(16),
Text(
'accountProgramJoin',
style: Theme.of(context).textTheme.titleLarge,
).tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.program.appearance['banner'] != null)
AspectRatio(
aspectRatio: 16 / 5,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Image.network(
widget.program.appearance['banner'],
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
).padding(bottom: 12),
Text(
widget.program.name,
style: Theme.of(context).textTheme.titleMedium,
).bold(),
MarkdownTextContent(content: widget.program.description),
const Gap(8),
Text(
'accountProgramJoinRequirements',
style: Theme.of(context).textTheme.titleMedium,
).tr().bold(),
Text('≥EXP ${widget.program.expRequirement}'),
Text('≥Lv${getLevelFromExp(widget.program.expRequirement)}'),
const Gap(8),
Text(
'accountProgramJoinPricing',
style: Theme.of(context).textTheme.titleMedium,
).tr().bold(),
Text('walletCurrency${widget.program.price['currency'].toString().capitalize().replaceFirst('Normal', '')}')
.plural(widget.program.price['amount'].toDouble()),
Text('accountProgramJoinPricingHint').tr().opacity(0.75),
const Gap(8),
if (widget.isJoined)
Text('accountProgramLeaveHint')
.tr()
.opacity(0.75)
.padding(bottom: 8),
if (!widget.isJoined)
ElevatedButton(
onPressed: _isBusy ? null : _joinProgram,
child: Text('join').tr(),
)
else
ElevatedButton(
onPressed: _isBusy ? null : _leaveProgram,
child: Text('leave').tr(),
),
],
).padding(horizontal: 24),
Gap(MediaQuery.of(context).padding.bottom),
],
),
),
);
}
}

View File

@ -27,10 +27,12 @@ class AccountPublisherEditScreen extends StatefulWidget {
const AccountPublisherEditScreen({super.key, required this.name}); const AccountPublisherEditScreen({super.key, required this.name});
@override @override
State<AccountPublisherEditScreen> createState() => _AccountPublisherEditScreenState(); State<AccountPublisherEditScreen> createState() =>
_AccountPublisherEditScreenState();
} }
class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> { class _AccountPublisherEditScreenState
extends State<AccountPublisherEditScreen> {
bool _isBusy = false; bool _isBusy = false;
SnPublisher? _publisher; SnPublisher? _publisher;
@ -115,11 +117,12 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
Uint8List? rawBytes; Uint8List? rawBytes;
if (!skipCrop) { if (!skipCrop) {
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); final ImageProvider imageProvider =
final aspectRatios = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)]; final aspectRatios = place == 'banner'
final result = ? [CropAspectRatio(width: 16, height: 7)]
(!kIsWeb && (Platform.isIOS || Platform.isMacOS)) : [CropAspectRatio(width: 1, height: 1)];
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
? await showCupertinoImageCropper( ? await showCupertinoImageCropper(
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
context, context,
@ -137,7 +140,9 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List(); rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!
.buffer
.asUint8List();
} else { } else {
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = true); setState(() => _isBusy = true);
@ -191,7 +196,10 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
appBar: AppBar(leading: PageBackButton(), title: Text('screenAccountPublisherEdit').tr()), noBackground: true,
appBar: AppBar(
leading: PageBackButton(),
title: Text('screenAccountPublisherEdit').tr()),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
@ -208,10 +216,13 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: Container( child: Container(
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context)
child: .colorScheme
_banner != null .surfaceContainerHigh,
? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover) child: _banner != null
? AutoResizeUniversalImage(
sn.getAttachmentUrl(_banner!),
fit: BoxFit.cover)
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
), ),
@ -245,13 +256,15 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
labelText: 'fieldUsername'.tr(), labelText: 'fieldUsername'.tr(),
helperText: 'fieldUsernameCannotEditHint'.tr(), helperText: 'fieldUsernameCannotEditHint'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(4), const Gap(4),
TextField( TextField(
controller: _nickController, controller: _nickController,
decoration: InputDecoration(labelText: 'fieldNickname'.tr()), decoration: InputDecoration(labelText: 'fieldNickname'.tr()),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(4), const Gap(4),
TextField( TextField(
@ -259,7 +272,8 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
maxLines: null, maxLines: null,
minLines: 3, minLines: 3,
decoration: InputDecoration(labelText: 'fieldDescription'.tr()), decoration: InputDecoration(labelText: 'fieldDescription'.tr()),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), const Gap(12),
Row( Row(

View File

@ -26,6 +26,7 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('screenAccountPublisherNew').tr(), title: Text('screenAccountPublisherNew').tr(),

View File

@ -33,7 +33,8 @@ class _PublisherScreenState extends State<PublisherScreen> {
try { try {
final resp = await sn.client.get('/cgi/co/publishers/me'); final resp = await sn.client.get('/cgi/co/publishers/me');
final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []); final List<SnPublisher> out = List<SnPublisher>.from(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
if (!mounted) return; if (!mounted) return;
@ -81,6 +82,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('screenAccountPublishers').tr(), title: Text('screenAccountPublishers').tr(),
@ -93,7 +95,9 @@ class _PublisherScreenState extends State<PublisherScreen> {
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.add_circle), leading: const Icon(Symbols.add_circle),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) { GoRouter.of(context)
.pushNamed('accountPublisherNew')
.then((value) {
if (value == true) { if (value == true) {
_publishers.clear(); _publishers.clear();
_fetchPublishers(); _fetchPublishers();
@ -119,7 +123,8 @@ class _PublisherScreenState extends State<PublisherScreen> {
return ListTile( return ListTile(
title: Text(publisher.nick), title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'), subtitle: Text('@${publisher.name}'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16), contentPadding:
const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(content: publisher.avatar), leading: AccountImage(content: publisher.avatar),
trailing: PopupMenuButton( trailing: PopupMenuButton(
itemBuilder: (BuildContext context) => [ itemBuilder: (BuildContext context) => [

View File

@ -0,0 +1,187 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/account.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
const kPunishmentIcons = [
Symbols.warning,
Symbols.emergency_home,
Symbols.dangerous,
];
class PunishmentsScreen extends StatefulWidget {
const PunishmentsScreen({super.key});
@override
State<PunishmentsScreen> createState() => _PunishmentsScreenState();
}
class _PunishmentsScreenState extends State<PunishmentsScreen> {
bool _isBusy = false;
List<SnPunishment>? _punishments;
Future<void> _fetchPunishments() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/punishments');
if (!mounted) return;
_punishments = List.from(
resp.data.map((ele) => SnPunishment.fromJson(ele)),
);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_fetchPunishments();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
noBackground: true,
appBar: AppBar(
title: Text('accountPunishments').tr(),
leading: PageBackButton(),
),
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
Card(
margin: EdgeInsets.only(bottom: 8, left: 8, right: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Symbols.visibility, size: 20),
const Gap(6),
Expanded(
child: Text('punishmentOverall').tr().fontSize(16).bold(),
),
],
),
Builder(
builder: (context) {
if (_punishments == null) return Text('loading').tr();
if (_punishments!.any((ele) => ele.type == 2)) {
return Text('punishmentStatusBanned').tr();
}
if (_punishments!.any(
(ele) => ele.type == 1 && ele.permNodes.isEmpty,
)) {
return Text('punishmentStatusLimitedFully').tr();
} else if (_punishments!.any((ele) => ele.type == 1)) {
return Text('punishmentStatusLimited').tr();
}
if (_punishments!.any((ele) => ele.type == 0)) {
return Text('punishmentStatusWarned').tr();
}
return Text('punishmentStatusNormal').tr();
},
),
],
).padding(horizontal: 24, vertical: 16),
),
Expanded(
child: RefreshIndicator(
onRefresh: _fetchPunishments,
child: ListView.separated(
padding: EdgeInsets.zero,
itemCount: _punishments?.length ?? 0,
itemBuilder: (context, index) {
final ele = _punishments![index];
return Card(
margin: EdgeInsets.symmetric(horizontal: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(kPunishmentIcons[ele.type], size: 20),
const Gap(6),
Expanded(
child: Text('punishmentType${ele.type}')
.tr()
.fontSize(16)
.bold(),
),
],
),
Text(ele.reason),
const Gap(4),
Text(
'punishmentCreatedAt'.tr(args: [
DateFormat().format(
ele.createdAt.toLocal(),
)
]),
).opacity(0.8),
Text(
ele.expiredAt == null
? 'punishmentExpiredNever'.tr()
: 'punishmentExpiredAt'.tr(args: [
DateFormat().format(
ele.expiredAt!.toLocal(),
)
]),
).opacity(0.8),
const Gap(8),
if (ele.moderator != null)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('punishmentModerator').tr().opacity(0.75),
InkWell(
child: Row(
children: [
AccountImage(
content: ele.moderator!.avatar,
radius: 8,
),
const Gap(4),
Text(ele.moderator?.nick ?? 'unknown'),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'accountProfilePage',
pathParameters: {
'name': ele.moderator!.name,
},
);
},
),
],
)
else
Text('punishmentMadeBySystem').tr().opacity(0.75),
],
).padding(horizontal: 24, vertical: 16),
);
},
separatorBuilder: (_, __) => const Gap(8),
),
),
),
],
),
);
}
}

View File

@ -37,6 +37,7 @@ class AccountSettingsScreen extends StatelessWidget {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: PageBackButton(), leading: PageBackButton(),
title: Text('screenAccountSettings').tr(), title: Text('screenAccountSettings').tr(),
@ -97,6 +98,36 @@ 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(
title: Text('factorSettings').tr(),
subtitle: Text('factorSettingsSubtitle').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.lock),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
GoRouter.of(context).pushNamed('factorSettings');
},
),
ListTile( ListTile(
title: Text('accountProfileEdit').tr(), title: Text('accountProfileEdit').tr(),
subtitle: Text('accountProfileEditSubtitle').tr(), subtitle: Text('accountProfileEditSubtitle').tr(),

View File

@ -10,7 +10,6 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/attachment/attachment_zoom.dart'; import 'package:surface/widgets/attachment/attachment_zoom.dart';
import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
@ -106,7 +105,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
controller: _scrollController, controller: _scrollController,
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
leading: AutoAppBarLeading(), leading: PageBackButton(),
title: Text('screenAlbum').tr(), title: Text('screenAlbum').tr(),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
@ -119,7 +118,8 @@ class _AlbumScreenState extends State<AlbumScreen> {
child: CircularProgressIndicator( child: CircularProgressIndicator(
value: _billing?.includedRatio ?? 0, value: _billing?.includedRatio ?? 0,
strokeWidth: 8, strokeWidth: 8,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh, backgroundColor:
Theme.of(context).colorScheme.surfaceContainerHigh,
), ),
).padding(all: 12), ).padding(all: 12),
const Gap(24), const Gap(24),
@ -129,7 +129,8 @@ class _AlbumScreenState extends State<AlbumScreen> {
children: [ children: [
Text('attachmentBillingUploaded').tr().bold(), Text('attachmentBillingUploaded').tr().bold(),
Text( Text(
(_billing?.currentBytes ?? 0).formatBytes(decimals: 4), (_billing?.currentBytes ?? 0)
.formatBytes(decimals: 4),
style: GoogleFonts.robotoMono(), style: GoogleFonts.robotoMono(),
), ),
Text('attachmentBillingDiscount').tr().bold(), Text('attachmentBillingDiscount').tr().bold(),

View File

@ -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/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) => CaptchaScreen(),
),
);
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,8 +208,12 @@ 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(

View File

@ -0,0 +1,3 @@
import 'package:flutter/foundation.dart' show kIsWeb;
export 'captcha_native.dart' if (kIsWeb) 'captcha_web.dart';

View File

@ -0,0 +1,37 @@
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 CaptchaScreen extends StatefulWidget {
const CaptchaScreen({super.key});
@override
State<CaptchaScreen> createState() => _CaptchaScreenState();
}
class _CaptchaScreenState extends State<CaptchaScreen> {
@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;
},
),
);
}
}

View File

@ -0,0 +1,54 @@
import 'dart:html' as html;
import 'dart:ui_web' as ui;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class CaptchaScreen extends StatefulWidget {
const CaptchaScreen({super.key});
@override
State<CaptchaScreen> createState() => _CaptchaScreenState();
}
class _CaptchaScreenState extends State<CaptchaScreen> {
@override
void initState() {
super.initState();
_setupWebListener();
}
void _setupWebListener() {
html.window.onMessage.listen((event) {
if (event.data != null && event.data is String) {
final message = event.data as String;
if (message.startsWith("captcha_tk=")) {
String token = message.replaceFirst("captcha_tk=", "");
Navigator.pop(context, token);
}
}
});
final iframe = html.IFrameElement()
..src = '${context.read<ConfigProvider>().serverUrl}/captcha?redirect_uri=web'
..style.border = 'none'
..width = '100%'
..height = '100%';
html.document.body!.append(iframe);
ui.platformViewRegistry.registerViewFactory(
'captcha-iframe',
(int viewId) => iframe,
);
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: Text("reCaptcha").tr()),
body: HtmlElementView(viewType: 'captcha-iframe'),
);
}
}

View File

@ -6,19 +6,16 @@ import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/chat/room.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_select.dart'; import 'package:surface/widgets/account/account_select.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -130,8 +127,6 @@ class _ChatScreenState extends State<ChatScreen> {
} }
} }
SnChannel? _focusChannel;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -140,13 +135,8 @@ class _ChatScreenState extends State<ChatScreen> {
} }
void _onTapChannel(SnChannel channel) { void _onTapChannel(SnChannel channel) {
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP); setState(() => _unreadCounts?[channel.id] = 0);
GoRouter.of(context).pushReplacementNamed(
if (doExpand) {
setState(() => _focusChannel = channel);
return;
}
GoRouter.of(context).pushNamed(
'chatRoom', 'chatRoom',
pathParameters: { pathParameters: {
'scope': channel.realm?.alias ?? 'global', 'scope': channel.realm?.alias ?? 'global',
@ -154,7 +144,6 @@ class _ChatScreenState extends State<ChatScreen> {
}, },
).then((value) { ).then((value) {
if (mounted) { if (mounted) {
_unreadCounts?[channel.id] = 0;
setState(() => _unreadCounts?[channel.id] = 0); setState(() => _unreadCounts?[channel.id] = 0);
_refreshChannels(noRemote: true); _refreshChannels(noRemote: true);
} }
@ -177,10 +166,8 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP); return AppScaffold(
noBackground: true,
final chatList = AppScaffold(
noBackground: doExpand,
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenChat').tr(), title: Text('screenChat').tr(),
@ -268,11 +255,6 @@ class _ChatScreenState extends State<ChatScreen> {
lastMessage: lastMessage, lastMessage: lastMessage,
unreadCount: _unreadCounts?[channel.id], unreadCount: _unreadCounts?[channel.id],
onTap: () { onTap: () {
if (doExpand) {
_unreadCounts?[channel.id] = 0;
setState(() => _focusChannel = channel);
return;
}
_onTapChannel(channel); _onTapChannel(channel);
}, },
); );
@ -284,28 +266,6 @@ class _ChatScreenState extends State<ChatScreen> {
], ],
), ),
); );
if (doExpand) {
return AppBackground(
isRoot: true,
child: Row(
children: [
SizedBox(width: 340, child: chatList),
const VerticalDivider(width: 1),
if (_focusChannel != null)
Expanded(
child: ChatRoomScreen(
key: ValueKey(_focusChannel!.id),
scope: _focusChannel!.realm?.alias ?? 'global',
alias: _focusChannel!.alias,
),
),
],
),
);
}
return chatList;
} }
} }

View File

@ -37,7 +37,8 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
return Stack( return Stack(
children: [ children: [
Container( Container(
color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75), color:
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
child: call.focusTrack != null child: call.focusTrack != null
? InteractiveParticipantWidget( ? InteractiveParticipantWidget(
isFixedAvatar: false, isFixedAvatar: false,
@ -72,7 +73,8 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
color: Theme.of(context).cardColor, color: Theme.of(context).cardColor,
participant: track, participant: track,
onTap: () { onTap: () {
if (track.participant.sid != call.focusTrack?.participant.sid) { if (track.participant.sid !=
call.focusTrack?.participant.sid) {
call.setFocusTrack(track); call.setFocusTrack(track);
} }
}, },
@ -114,10 +116,14 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget( child: InteractiveParticipantWidget(
color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75), color: Theme.of(context)
.colorScheme
.surfaceContainerHigh
.withOpacity(0.75),
participant: track, participant: track,
onTap: () { onTap: () {
if (track.participant.sid != call.focusTrack?.participant.sid) { if (track.participant.sid !=
call.focusTrack?.participant.sid) {
call.setFocusTrack(track); call.setFocusTrack(track);
} }
}, },
@ -149,6 +155,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
listenable: call, listenable: call,
builder: (context, _) { builder: (context, _) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: RichText( title: RichText(
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -183,7 +190,8 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
Builder(builder: (context) { Builder(builder: (context) {
final call = context.read<ChatCallProvider>(); final call = context.read<ChatCallProvider>();
final connectionQuality = final connectionQuality =
call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown; call.room.localParticipant?.connectionQuality ??
livekit.ConnectionQuality.unknown;
return Expanded( return Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -205,24 +213,35 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
children: [ children: [
Text( Text(
{ {
livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(), livekit.ConnectionState.disconnected:
livekit.ConnectionState.connected: 'callStatusConnected'.tr(), 'callStatusDisconnected'.tr(),
livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(), livekit.ConnectionState.connected:
livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(), 'callStatusConnected'.tr(),
livekit.ConnectionState.connecting:
'callStatusConnecting'.tr(),
livekit.ConnectionState.reconnecting:
'callStatusReconnecting'.tr(),
}[call.room.connectionState]!, }[call.room.connectionState]!,
), ),
const Gap(6), const Gap(6),
if (connectionQuality != livekit.ConnectionQuality.unknown) if (connectionQuality !=
livekit.ConnectionQuality.unknown)
Icon( Icon(
{ {
livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt, livekit.ConnectionQuality.excellent:
livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar, Icons.signal_cellular_alt,
livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar, livekit.ConnectionQuality.good:
Icons.signal_cellular_alt_2_bar,
livekit.ConnectionQuality.poor:
Icons.signal_cellular_alt_1_bar,
}[connectionQuality], }[connectionQuality],
color: { color: {
livekit.ConnectionQuality.excellent: Colors.green, livekit.ConnectionQuality.excellent:
livekit.ConnectionQuality.good: Colors.orange, Colors.green,
livekit.ConnectionQuality.poor: Colors.red, livekit.ConnectionQuality.good:
Colors.orange,
livekit.ConnectionQuality.poor:
Colors.red,
}[connectionQuality], }[connectionQuality],
size: 16, size: 16,
) )
@ -244,7 +263,9 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
Row( Row(
children: [ children: [
IconButton( IconButton(
icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view), icon: _layoutMode == 0
? const Icon(Icons.view_list)
: const Icon(Icons.grid_view),
onPressed: () { onPressed: () {
_switchLayout(); _switchLayout();
}, },

View File

@ -220,6 +220,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id; final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(), title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
), ),

View File

@ -49,7 +49,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [], resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
); );
if (_editingChannel != null) { if (_editingChannel != null) {
_belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId); _belongToRealm =
_realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
} }
} catch (err) { } catch (err) {
if (mounted) context.showErrorDialog(err); if (mounted) context.showErrorDialog(err);
@ -97,7 +98,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
'is_community': _isCommunity, 'is_community': _isCommunity,
if (_editingChannel != null && _belongToRealm == null) if (_editingChannel != null && _belongToRealm == null)
'new_belongs_realm': 'global' 'new_belongs_realm': 'global'
else if (_editingChannel != null && _belongToRealm?.id != _editingChannel?.realm?.id) else if (_editingChannel != null &&
_belongToRealm?.id != _editingChannel?.realm?.id)
'new_belongs_realm': _belongToRealm!.alias, 'new_belongs_realm': _belongToRealm!.alias,
}; };
@ -139,8 +141,11 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(), title: widget.editingChannelAlias != null
? Text('screenChatManage').tr()
: Text('screenChatNew').tr(),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
@ -152,7 +157,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
leadingPadding: const EdgeInsets.only(left: 10, right: 20), leadingPadding: const EdgeInsets.only(left: 10, right: 20),
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
content: Text( content: Text(
'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']), 'channelEditingNotice'
.tr(args: ['#${_editingChannel!.alias}']),
), ),
actions: [ actions: [
TextButton( TextButton(
@ -192,12 +198,15 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!), Text(item.name).textStyle(Theme.of(context)
.textTheme
.bodyMedium!),
Text( Text(
item.description, item.description,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
).textStyle(Theme.of(context).textTheme.bodySmall!), ).textStyle(
Theme.of(context).textTheme.bodySmall!),
], ],
), ),
), ),
@ -213,7 +222,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
CircleAvatar( CircleAvatar(
radius: 16, radius: 16,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
foregroundColor: Theme.of(context).colorScheme.onSurface, foregroundColor:
Theme.of(context).colorScheme.onSurface,
child: const Icon(Symbols.clear), child: const Icon(Symbols.clear),
), ),
const Gap(12), const Gap(12),
@ -222,7 +232,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('fieldChatBelongToRealmUnset').tr().textStyle( Text('fieldChatBelongToRealmUnset')
.tr()
.textStyle(
Theme.of(context).textTheme.bodyMedium!, Theme.of(context).textTheme.bodyMedium!,
), ),
], ],
@ -257,7 +269,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
helperText: 'fieldChatAliasHint'.tr(), helperText: 'fieldChatAliasHint'.tr(),
helperMaxLines: 2, helperMaxLines: 2,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(4), const Gap(4),
TextField( TextField(
@ -266,7 +279,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldChatName'.tr(), labelText: 'fieldChatName'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(4), const Gap(4),
TextField( TextField(
@ -277,7 +291,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
border: const UnderlineInputBorder(), border: const UnderlineInputBorder(),
labelText: 'fieldChatDescription'.tr(), labelText: 'fieldChatDescription'.tr(),
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
), ),
const Gap(12), const Gap(12),
CheckboxListTile( CheckboxListTile(

View File

@ -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) {
@ -274,6 +304,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
final ud = context.read<UserDirectoryProvider>(); final ud = context.read<UserDirectoryProvider>();
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
_channel?.type == 1 _channel?.type == 1
@ -281,6 +312,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
: _channel?.name ?? 'loading'.tr(), : _channel?.name ?? 'loading'.tr(),
), ),
actions: [ actions: [
if (_currentMember != null)
IconButton( IconButton(
onPressed: () { onPressed: () {
setState(() => _isEncrypted = !_isEncrypted); setState(() => _isEncrypted = !_isEncrypted);
@ -290,6 +322,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
? const Icon(Symbols.lock) ? const Icon(Symbols.lock)
: const Icon(Symbols.no_encryption), : const Icon(Symbols.no_encryption),
), ),
if (_currentMember != null)
IconButton( IconButton(
icon: _ongoingCall == null icon: _ongoingCall == null
? const Icon(Symbols.call) ? const Icon(Symbols.call)
@ -348,7 +381,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 +470,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
}, },
), ),
), ),
if (!_messageController.isPending) if (!_messageController.isPending && _currentMember != null)
Material( Material(
elevation: 2, elevation: 2,
child: Column( child: Column(

View File

@ -157,6 +157,7 @@ class _ExploreScreenState extends State<ExploreScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.watch<ConfigProvider>(); final cfg = context.watch<ConfigProvider>();
return AppScaffold( return AppScaffold(
noBackground: true,
floatingActionButtonLocation: ExpandableFab.location, floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab( floatingActionButton: ExpandableFab(
key: _fabKey, key: _fabKey,
@ -243,6 +244,7 @@ class _ExploreScreenState extends State<ExploreScreen>
GoRouter.of(context).pushNamed('postShuffle'); GoRouter.of(context).pushNamed('postShuffle');
}, },
), ),
const Gap(48),
Expanded( Expanded(
child: Center( child: Center(
child: IconButton( child: IconButton(
@ -449,7 +451,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);
} }
@ -534,6 +536,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
switch (ele.type) { switch (ele.type) {
case 'interactive.post': case 'interactive.post':
return OpenablePostItem( return OpenablePostItem(
useReplace: true,
data: SnPost.fromJson(ele.data), data: SnPost.fromJson(ele.data),
maxWidth: 640, maxWidth: 640,
onChanged: (data) { onChanged: (data) {
@ -551,9 +554,11 @@ class _PostListWidgetState extends State<_PostListWidget> {
maxWidth: 640, maxWidth: 640,
); );
case 'reader.news': case 'reader.news':
return Container( return Center(
child: Container(
constraints: BoxConstraints(maxWidth: 640), constraints: BoxConstraints(maxWidth: 640),
child: NewsFeedEntry(data: ele), child: NewsFeedEntry(data: ele),
),
); );
default: default:
return Container( return Container(

View File

@ -10,7 +10,6 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/account.dart'; import 'package:surface/types/account.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_select.dart'; import 'package:surface/widgets/account/account_select.dart';
import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
@ -47,8 +46,7 @@ class _FriendScreenState extends State<FriendScreen> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/me/relations?status=1'); final resp = await sn.client.get('/cgi/id/users/me/relations?status=1');
_relations = List<SnRelationship>.from( _relations = List<SnRelationship>.from(
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [], resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []);
);
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -67,8 +65,7 @@ class _FriendScreenState extends State<FriendScreen> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/me/relations?status=0,3'); final resp = await sn.client.get('/cgi/id/users/me/relations?status=0,3');
_requests = List<SnRelationship>.from( _requests = List<SnRelationship>.from(
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [], resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []);
);
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -87,8 +84,7 @@ class _FriendScreenState extends State<FriendScreen> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/me/relations?status=2'); final resp = await sn.client.get('/cgi/id/users/me/relations?status=2');
_blocks = List<SnRelationship>.from( _blocks = List<SnRelationship>.from(
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [], resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []);
);
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -105,10 +101,7 @@ class _FriendScreenState extends State<FriendScreen> {
try { try {
final rel = context.read<SnRelationshipProvider>(); final rel = context.read<SnRelationshipProvider>();
await rel.updateRelationship( await rel.updateRelationship(
relation.relatedId, relation.relatedId, dstStatus, relation.permNodes);
dstStatus,
relation.permNodes,
);
if (!mounted) return; if (!mounted) return;
_fetchRelations(); _fetchRelations();
} catch (err) { } catch (err) {
@ -122,9 +115,8 @@ class _FriendScreenState extends State<FriendScreen> {
Future<void> _deleteRelation(SnRelationship relation) async { Future<void> _deleteRelation(SnRelationship relation) async {
final confirm = await context.showConfirmDialog( final confirm = await context.showConfirmDialog(
'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]), 'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
'friendDeleteDescription'.tr(args: [ 'friendDeleteDescription'
relation.related?.nick ?? 'unknown'.tr(), .tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
]),
); );
if (!confirm) return; if (!confirm) return;
if (!mounted) return; if (!mounted) return;
@ -147,8 +139,10 @@ class _FriendScreenState extends State<FriendScreen> {
void _showRequests() { void _showRequests() {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => _FriendshipListWidget(relations: _requests), builder: (context) => _FriendshipListWidget(relations: _requests))
).then((value) { .then((
value,
) {
if (value != null) { if (value != null) {
_fetchRequests(); _fetchRequests();
_fetchRelations(); _fetchRelations();
@ -159,8 +153,9 @@ class _FriendScreenState extends State<FriendScreen> {
void _showBlocks() { void _showBlocks() {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => _FriendshipListWidget(relations: _blocks), builder: (context) => _FriendshipListWidget(relations: _blocks)).then((
).then((value) { value,
) {
if (value != null) { if (value != null) {
_fetchBlocks(); _fetchBlocks();
_fetchRelations(); _fetchRelations();
@ -173,9 +168,8 @@ class _FriendScreenState extends State<FriendScreen> {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/id/users/me/relations', data: { await sn.client
'related': user.name, .post('/cgi/id/users/me/relations', data: {'related': user.name});
});
if (!mounted) return; if (!mounted) return;
context.showSnackbar('friendRequestSent'.tr()); context.showSnackbar('friendRequestSent'.tr());
} catch (err) { } catch (err) {
@ -201,18 +195,16 @@ class _FriendScreenState extends State<FriendScreen> {
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: PageBackButton(),
title: Text('screenFriend').tr(), title: Text('screenFriend').tr(),
), ),
body: Center( body: Center(child: UnauthorizedHint()),
child: UnauthorizedHint(),
),
); );
} }
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: PageBackButton(),
title: Text('screenFriend').tr(), title: Text('screenFriend').tr(),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
@ -220,9 +212,7 @@ class _FriendScreenState extends State<FriendScreen> {
onPressed: () async { onPressed: () async {
final user = await showModalBottomSheet<SnAccount?>( final user = await showModalBottomSheet<SnAccount?>(
context: context, context: context,
builder: (context) => AccountSelect( builder: (context) => AccountSelect(title: 'friendNew'.tr()),
title: 'friendNew'.tr(),
),
); );
if (!mounted) return; if (!mounted) return;
if (user == null) return; if (user == null) return;
@ -235,9 +225,8 @@ class _FriendScreenState extends State<FriendScreen> {
if (_requests.isNotEmpty) if (_requests.isNotEmpty)
ListTile( ListTile(
title: Text('friendRequests').tr(), title: Text('friendRequests').tr(),
subtitle: Text( subtitle:
'friendRequestsDescription', Text('friendRequestsDescription').plural(_requests.length),
).plural(_requests.length),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.group_add), leading: const Icon(Symbols.group_add),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
@ -246,31 +235,30 @@ class _FriendScreenState extends State<FriendScreen> {
if (_blocks.isNotEmpty) if (_blocks.isNotEmpty)
ListTile( ListTile(
title: Text('friendBlocklist').tr(), title: Text('friendBlocklist').tr(),
subtitle: Text( subtitle:
'friendBlocklistDescription', Text('friendBlocklistDescription').plural(_blocks.length),
).plural(_blocks.length),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.block), leading: const Icon(Symbols.block),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
onTap: _showBlocks, onTap: _showBlocks,
), ),
if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1), if (_requests.isNotEmpty || _blocks.isNotEmpty)
const Divider(height: 1),
Expanded( Expanded(
child: MediaQuery.removePadding( child: MediaQuery.removePadding(
context: context, context: context,
removeTop: true, removeTop: true,
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () => Future.wait([ onRefresh: () =>
_fetchRelations(), Future.wait([_fetchRelations(), _fetchRequests()]),
_fetchRequests(),
]),
child: ListView.builder( child: ListView.builder(
itemCount: _relations.length, itemCount: _relations.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final relation = _relations[index]; final relation = _relations[index];
final other = relation.related; final other = relation.related;
return ListTile( return ListTile(
contentPadding: const EdgeInsets.only(right: 24, left: 16), contentPadding:
const EdgeInsets.only(right: 24, left: 16),
leading: AccountImage(content: other?.avatar), leading: AccountImage(content: other?.avatar),
title: Text(other?.nick ?? 'unknown'), title: Text(other?.nick ?? 'unknown'),
subtitle: Text(other?.nick ?? 'unknown'), subtitle: Text(other?.nick ?? 'unknown'),
@ -286,12 +274,16 @@ class _FriendScreenState extends State<FriendScreen> {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
InkWell( InkWell(
onTap: _isUpdating ? null : () => _changeRelation(relation, 2), onTap: _isUpdating
? null
: () => _changeRelation(relation, 2),
child: Text('friendBlock').tr(), child: Text('friendBlock').tr(),
), ),
const Gap(8), const Gap(8),
InkWell( InkWell(
onTap: _isUpdating ? null : () => _deleteRelation(relation), onTap: _isUpdating
? null
: () => _deleteRelation(relation),
child: Text('friendDeleteAction').tr(), child: Text('friendDeleteAction').tr(),
), ),
], ],
@ -361,10 +353,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
try { try {
final rel = context.read<SnRelationshipProvider>(); final rel = context.read<SnRelationshipProvider>();
await rel.updateRelationship( await rel.updateRelationship(
relation.relatedId, relation.relatedId, dstStatus, relation.permNodes);
dstStatus,
relation.permNodes,
);
if (!mounted) return; if (!mounted) return;
Navigator.pop(context, true); Navigator.pop(context, true);
} catch (err) { } catch (err) {
@ -378,9 +367,8 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
Future<void> _deleteRelation(SnRelationship relation) async { Future<void> _deleteRelation(SnRelationship relation) async {
final confirm = await context.showConfirmDialog( final confirm = await context.showConfirmDialog(
'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]), 'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
'friendDeleteDescription'.tr(args: [ 'friendDeleteDescription'
relation.related?.nick ?? 'unknown'.tr(), .tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
]),
); );
if (!confirm) return; if (!confirm) return;
if (!mounted) return; if (!mounted) return;
@ -420,7 +408,9 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75), Text(kFriendStatus[relation.status] ?? 'unknown')
.tr()
.opacity(0.75),
if (relation.status == 0) if (relation.status == 0)
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
@ -441,7 +431,8 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
InkWell( InkWell(
onTap: _isBusy ? null : () => _changeRelation(relation, 1), onTap:
_isBusy ? null : () => _changeRelation(relation, 1),
child: Text('friendUnblock').tr(), child: Text('friendUnblock').tr(),
), ),
const Gap(8), const Gap(8),

View File

@ -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/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,41 +390,50 @@ 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) {
ServiceStatus.operational => Row( ServiceStatus.operational => Row(
children: [ children: [
const Icon( Icon(
Symbols.check, Symbols.check,
size: 20, size: 20,
color: Colors.green[900],
), ),
const Gap(10), const Gap(10),
Text('serviceStatusOperational').tr(), Text('serviceStatusOperational')
.tr()
.textColor(Colors.green[900]),
], ],
), ),
ServiceStatus.failed => Tooltip( ServiceStatus.failed => Tooltip(
message: 'serviceStatusFailedDescription'.tr(), message: 'serviceStatusFailedDescription'.tr(),
child: Row( child: Row(
children: [ children: [
const Icon( Icon(
Symbols.dangerous, Symbols.dangerous,
size: 20, size: 20,
color: Colors.red[900],
), ),
const Gap(10), const Gap(10),
Text('serviceStatusFailed').tr(), Text('serviceStatusFailed')
.tr()
.textColor(Colors.red[900]),
], ],
), ),
), ),
_ => Row( _ => Row(
children: [ children: [
const Icon( Icon(
Symbols.error, Symbols.error,
size: 20, size: 20,
color: Colors.orange[900],
), ),
const Gap(10), const Gap(10),
Text('serviceStatusDowngraded').tr(), Text('serviceStatusDowngraded')
.tr()
.textColor(Colors.orange[900]),
], ],
), ),
}, },
@ -434,6 +444,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 +452,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 +518,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) => CaptchaScreen(),
),
);
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) {
@ -793,7 +815,7 @@ class _HomeDashNotificationWidgetState
child: IconButton( child: IconButton(
icon: const Icon(Symbols.arrow_right_alt), icon: const Icon(Symbols.arrow_right_alt),
onPressed: () { onPressed: () {
GoRouter.of(context).goNamed('notification'); GoRouter.of(context).pushNamed('notification');
}, },
), ),
), ),
@ -877,8 +899,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 +920,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)

View File

@ -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,10 @@ 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: PageBackButton(),
title: Text('screenNotification').tr(),
),
body: Center(child: UnauthorizedHint()), body: Center(child: UnauthorizedHint()),
); );
} }
@ -153,7 +161,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 +177,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 +200,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 +222,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 +268,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);

View File

@ -12,7 +12,6 @@ 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';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_comment_list.dart'; import 'package:surface/widgets/post/post_comment_list.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
@ -22,7 +21,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();
@ -65,9 +65,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
final double maxWidth = _data?.type == 'video' ? double.infinity : 640; final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
return AppBackground( return AppScaffold(
isRoot: widget.onBack != null, noBackground: true,
child: AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: BackButton( leading: BackButton(
onPressed: () { onPressed: () {
@ -124,8 +123,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 +143,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,16 +160,17 @@ 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)),
], ],
), ),
),
); );
} }
} }

View File

@ -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),
), ),
), ),
), ),

View File

@ -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);
} }
} }

View File

@ -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;
}, },

View File

@ -28,11 +28,8 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
try { try {
final pt = context.read<SnPostContentProvider>(); final pt = context.read<SnPostContentProvider>();
final result = await pt.listPosts( final result =
take: 10, await pt.listPosts(take: 10, offset: _posts.length, isShuffle: true);
offset: _posts.length,
isShuffle: true,
);
_posts.addAll(result.$1); _posts.addAll(result.$1);
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
@ -57,19 +54,14 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(title: Text('postShuffle').tr()),
title: Text('postShuffle').tr(),
),
body: Stack( body: Stack(
children: [ children: [
Column( Column(
children: [ children: [
if (_isBusy || _posts.isEmpty) if (_isBusy || _posts.isEmpty)
const Expanded( const Expanded(
child: Center( child: Center(child: CircularProgressIndicator()))
child: CircularProgressIndicator(),
),
)
else else
Expanded( Expanded(
child: CardSwiper( child: CardSwiper(
@ -81,6 +73,8 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
final ele = _posts[idx]; final ele = _posts[idx];
return SingleChildScrollView( return SingleChildScrollView(
child: Center( child: Center(
child: Card(
color: Theme.of(context).colorScheme.surface,
child: OpenablePostItem( child: OpenablePostItem(
key: ValueKey(ele), key: ValueKey(ele),
data: ele, data: ele,
@ -92,6 +86,7 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
onDeleted: () { onDeleted: () {
_fetchPosts(); _fetchPosts();
}, },
).padding(all: 8),
).padding( ).padding(
all: 24, all: 24,
bottom: bottom:

View File

@ -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: 5, 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 =
math.min((_appBarWidth * kBannerAspectRatio), 360).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);
}); });
} }
@ -160,6 +165,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
type: switch (_tabController.index) { type: switch (_tabController.index) {
1 => 'story', 1 => 'story',
2 => 'article', 2 => 'article',
3 => 'question',
4 => 'video',
_ => null, _ => null,
}, },
); );
@ -193,7 +200,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 +217,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);
@ -276,6 +286,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
noBackground: true,
body: NestedScrollView( body: NestedScrollView(
controller: _scrollController, controller: _scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
@ -299,7 +310,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 +321,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 +347,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 +365,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 +393,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 +411,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 +422,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 +472,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 +481,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 +492,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 +503,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 +539,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),
], ],
), ),
], ],
@ -533,6 +571,18 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), ),
), ),
Tab(
icon: Icon(
Symbols.help,
color: Theme.of(context).colorScheme.onSurface,
),
),
Tab(
icon: Icon(
Symbols.video_call,
color: Theme.of(context).colorScheme.onSurface,
),
),
], ],
), ),
SliverToBoxAdapter(child: const Divider(height: 1)), SliverToBoxAdapter(child: const Divider(height: 1)),
@ -606,7 +656,7 @@ class _PublisherPostList extends StatelessWidget {
onDeleted: onDeleted, onDeleted: onDeleted,
); );
}, },
separatorBuilder: (_, __) => const Gap(8), separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
); );
} }
} }

View 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),
),
),
),
),
],
),
);
}
}

View File

@ -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);

View File

@ -4,8 +4,10 @@ import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/channel.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.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';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
@ -57,7 +59,9 @@ class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
title: Text('screenRealmDiscovery').tr(), title: Text('screenRealmDiscovery').tr(),
actions: [ actions: [
IconButton( IconButton(
icon: _isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module), icon: _isCompactView
? const Icon(Symbols.view_list)
: const Icon(Symbols.view_module),
onPressed: () { onPressed: () {
setState(() => _isCompactView = !_isCompactView); setState(() => _isCompactView = !_isCompactView);
context.read<ConfigProvider>().realmCompactView = _isCompactView; context.read<ConfigProvider>().realmCompactView = _isCompactView;
@ -117,7 +121,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
try { try {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public'); final resp =
await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
final out = List<SnChannel>.from( final out = List<SnChannel>.from(
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(), resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
); );
@ -135,10 +140,13 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
setState(() => _isJoining = true); setState(() => _isJoining = true);
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
await sn.client.post('/cgi/id/realms/${widget.realm.alias}/members', data: { final rel = context.read<SnRealmProvider>();
await sn.client
.post('/cgi/id/realms/${widget.realm.alias}/members', data: {
'related': ua.user?.name, 'related': ua.user?.name,
}); });
await _joinSelectedChannels(); await _joinSelectedChannels();
rel.addAvailableRealm(widget.realm);
if (!mounted) return; if (!mounted) return;
context.showSnackbar('realmJoined'.tr(args: [widget.realm.name])); context.showSnackbar('realmJoined'.tr(args: [widget.realm.name]));
Navigator.pop(context); Navigator.pop(context);
@ -156,13 +164,20 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
await sn.client.post('/cgi/im/channels/${widget.realm.alias}/$channel/members', data: { await sn.client.post(
'/cgi/im/channels/${widget.realm.alias}/$channel/members',
data: {
'related': ua.user?.name, 'related': ua.user?.name,
}); });
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
} }
final ct = context.read<ChatChannelProvider>();
for (final channel
in _channels!.where((ele) => _planJoinChannels.contains(ele.alias))) {
ct.addAvailableChannel(channel);
}
} }
} }
@ -182,7 +197,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
children: [ children: [
const Icon(Symbols.group_add, size: 24), const Icon(Symbols.group_add, size: 24),
const Gap(16), const Gap(16),
Text('realmJoin', style: Theme.of(context).textTheme.titleLarge).tr(), Text('realmJoin', style: Theme.of(context).textTheme.titleLarge)
.tr(),
], ],
).padding(horizontal: 20, top: 16, bottom: 12), ).padding(horizontal: 20, top: 16, bottom: 12),
Row( Row(
@ -216,7 +232,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
Container( Container(
width: double.infinity, width: double.infinity,
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium) child: Text('realmCommunityPublicChannelsHint'.tr(),
style: Theme.of(context).textTheme.bodyMedium)
.padding(horizontal: 24, vertical: 8), .padding(horizontal: 24, vertical: 8),
), ),
Expanded( Expanded(

View File

@ -80,6 +80,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final dt = context.read<DatabaseProvider>(); final dt = context.read<DatabaseProvider>();
final cfg = context.watch<ConfigProvider>();
final now = DateTime.now();
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
@ -336,6 +339,44 @@ 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(() {});
},
),
CheckboxListTile(
value: cfg.soundEffects,
onChanged: (value) {
cfg.soundEffects = value ?? false;
setState(() {});
},
contentPadding: const EdgeInsets.only(left: 24, right: 17),
title: Text('settingsSoundEffects').tr(),
subtitle: Text('settingsSoundEffectsDescription').tr(),
secondary: const Icon(Symbols.sound_sampler),
),
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS))
ListTile(
leading: const Icon(Symbols.window),
title: Text('settingsResetMemorizedWindowSize').tr(),
subtitle:
Text('settingsResetMemorizedWindowSizeDescription')
.tr(),
trailing: const Icon(Symbols.chevron_right),
contentPadding: const EdgeInsets.only(left: 24, right: 24),
onTap: () {
final prefs = context.read<ConfigProvider>().prefs;
prefs.remove(kAppWindowSize);
},
),
ListTile( ListTile(
leading: const Icon(Symbols.font_download), leading: const Icon(Symbols.font_download),
title: Text('settingsCustomFonts').tr(), title: Text('settingsCustomFonts').tr(),
@ -728,6 +769,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
GoRouter.of(context).pushNamed('about'); GoRouter.of(context).pushNamed('about');
}, },
), ),
if (now.day == 1 && now.month == 4)
CheckboxListTile(
title: Text('settingsAprilFoolFeatures').tr(),
subtitle: Text('settingsAprilFoolFeaturesDescription').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
secondary: const Icon(Symbols.new_releases),
value: cfg.aprilFoolFeatures,
onChanged: (value) {
cfg.aprilFoolFeatures = value ?? false;
setState(() {});
},
)
], ],
), ),
], ],

View File

@ -50,6 +50,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
Card( Card(
child: Column( child: Column(
children: [ children: [
const SizedBox(width: double.infinity),
ListTile( ListTile(
contentPadding: contentPadding:
const EdgeInsets.symmetric(horizontal: 24), const EdgeInsets.symmetric(horizontal: 24),

View File

@ -9,7 +9,6 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_sticker.dart'; import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
@ -134,7 +133,7 @@ class _StickerScreenState extends State<StickerScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: PageBackButton(),
title: Text('screenStickers').tr(), title: Text('screenStickers').tr(),
actions: [ actions: [
IconButton( IconButton(

View File

@ -45,10 +45,9 @@ class _WalletScreenState extends State<WalletScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: PageBackButton(), leading: PageBackButton(), title: Text('screenAccountWallet').tr()),
title: Text('screenAccountWallet').tr(),
),
body: Column( body: Column(
children: [ children: [
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
@ -66,25 +65,36 @@ class _WalletScreenState extends State<WalletScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CircleAvatar(
radius: 28,
child: Icon(Symbols.wallet, size: 28),
),
const Gap(12),
SizedBox(width: double.infinity), SizedBox(width: double.infinity),
Text( Text(
NumberFormat.compactCurrency( NumberFormat.compactCurrency(
locale: EasyLocalization.of(context)!.currentLocale.toString(), locale: EasyLocalization.of(context)!
.currentLocale
.toString(),
symbol: '${'walletCurrencyShort'.tr()} ', symbol: '${'walletCurrencyShort'.tr()} ',
decimalDigits: 2, decimalDigits: 2,
).format(double.parse(_wallet!.balance)), ).format(double.parse(_wallet!.balance)),
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
), ),
Text('walletCurrency'.plural(double.parse(_wallet!.balance))), Text('walletCurrency'.plural(double.parse(_wallet!.balance))),
const Gap(16),
Text(
NumberFormat.compactCurrency(
locale: EasyLocalization.of(context)!
.currentLocale
.toString(),
symbol: '${'walletCurrencyGoldenShort'.tr()} ',
decimalDigits: 2,
).format(double.parse(_wallet!.goldenBalance)),
style: Theme.of(context).textTheme.titleLarge,
),
Text('walletCurrencyGolden'
.plural(double.parse(_wallet!.goldenBalance))),
], ],
).padding(horizontal: 20, vertical: 24), ).padding(horizontal: 20, vertical: 24),
).padding(horizontal: 8, top: 16, bottom: 4), ).padding(horizontal: 8, top: 16, bottom: 4),
if (_wallet != null) Expanded(child: _WalletTransactionList(myself: _wallet!)), if (_wallet != null)
Expanded(child: _WalletTransactionList(myself: _wallet!)),
], ],
), ),
); );
@ -109,14 +119,15 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
try { try {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/wa/transactions/me', queryParameters: { final resp = await sn.client.get(
'take': 10, '/cgi/wa/transactions/me',
'offset': _transactions.length, queryParameters: {'take': 10, 'offset': _transactions.length},
});
_totalCount = resp.data['count'];
_transactions.addAll(
resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? [],
); );
_totalCount = resp.data['count'];
_transactions.addAll(resp.data['data']
?.map((e) => SnTransaction.fromJson(e))
.cast<SnTransaction>() ??
[]);
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -141,7 +152,8 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
child: InfiniteList( child: InfiniteList(
itemCount: _transactions.length, itemCount: _transactions.length,
isLoading: _isBusy, isLoading: _isBusy,
hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!, hasReachedMax:
_totalCount != null && _transactions.length >= _totalCount!,
onFetchData: () { onFetchData: () {
_fetchTransactions(); _fetchTransactions();
}, },
@ -149,7 +161,9 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
final ele = _transactions[idx]; final ele = _transactions[idx];
final isIncoming = ele.payeeId == widget.myself.id; final isIncoming = ele.payeeId == widget.myself.id;
return ListTile( return ListTile(
leading: isIncoming ? const Icon(Symbols.call_received) : const Icon(Symbols.call_made), leading: isIncoming
? const Icon(Symbols.call_received)
: const Icon(Symbols.call_made),
title: Text( title: Text(
'${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}', '${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}',
style: TextStyle(color: isIncoming ? Colors.green : Colors.red), style: TextStyle(color: isIncoming ? Colors.green : Colors.red),
@ -159,15 +173,29 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
children: [ children: [
Text(ele.remark), Text(ele.remark),
const Gap(2), const Gap(2),
Row(
children: [
Text(
'walletTransactionType${ele.currency.capitalize()}'
.tr(),
style: Theme.of(context).textTheme.labelSmall,
),
Text(' · ')
.textStyle(Theme.of(context).textTheme.labelSmall!)
.padding(right: 4),
Text( Text(
DateFormat( DateFormat(
null, null,
EasyLocalization.of(context)!.currentLocale.toString(), EasyLocalization.of(context)!
).format(ele.createdAt), .currentLocale
.toString())
.format(ele.createdAt),
style: Theme.of(context).textTheme.labelSmall, style: Theme.of(context).textTheme.labelSmall,
), ),
], ],
), ),
],
),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
); );
}, },
@ -205,17 +233,14 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
autofocus: true, autofocus: true,
obscureText: true, obscureText: true,
controller: passwordController, controller: passwordController,
decoration: InputDecoration( decoration: InputDecoration(labelText: 'fieldPassword'.tr()),
labelText: 'fieldPassword'.tr(),
),
), ),
], ],
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(ctx).pop(), onPressed: () => Navigator.of(ctx).pop(),
child: Text('cancel').tr(), child: Text('cancel').tr()),
),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(ctx).pop(passwordController.text); Navigator.of(ctx).pop(passwordController.text);
@ -234,9 +259,7 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
try { try {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/wa/wallets/me', data: { await sn.client.post('/cgi/wa/wallets/me', data: {'password': password});
'password': password,
});
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -255,20 +278,20 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CircleAvatar( CircleAvatar(radius: 28, child: Icon(Symbols.add, size: 28)),
radius: 28,
child: Icon(Symbols.add, size: 28),
),
const Gap(12), const Gap(12),
Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(), Text('walletCreate',
Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(), style: Theme.of(context).textTheme.titleLarge)
.tr(),
Text('walletCreateSubtitle',
style: Theme.of(context).textTheme.bodyMedium)
.tr(),
const Gap(8), const Gap(8),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: TextButton( child: TextButton(
onPressed: _isBusy ? null : () => _createWallet(), onPressed: _isBusy ? null : () => _createWallet(),
child: Text('next').tr(), child: Text('next').tr()),
),
), ),
], ],
).padding(horizontal: 20, vertical: 24), ).padding(horizontal: 20, vertical: 24),

View File

@ -88,6 +88,8 @@ Future<ThemeData> createAppTheme(
TargetPlatform.windows: ZoomPageTransitionsBuilder(), TargetPlatform.windows: ZoomPageTransitionsBuilder(),
}, },
), ),
progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false),
sliderTheme: SliderThemeData(year2023: false),
); );
} }

View File

@ -184,3 +184,63 @@ abstract class SnActionEvent with _$SnActionEvent {
factory SnActionEvent.fromJson(Map<String, Object?> json) => factory SnActionEvent.fromJson(Map<String, Object?> json) =>
_$SnActionEventFromJson(json); _$SnActionEventFromJson(json);
} }
@freezed
abstract class SnProgram with _$SnProgram {
const factory SnProgram({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String name,
required String description,
required String alias,
required int expRequirement,
required Map<String, dynamic> price,
required Map<String, dynamic> badge,
required Map<String, dynamic> group,
required Map<String, dynamic> appearance,
}) = _SnProgram;
factory SnProgram.fromJson(Map<String, Object?> json) =>
_$SnProgramFromJson(json);
}
@freezed
abstract class SnProgramMember with _$SnProgramMember {
const factory SnProgramMember({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required DateTime lastPaid,
required SnAccount account,
required int accountId,
required SnProgram program,
required int programId,
}) = _SnProgramMember;
factory SnProgramMember.fromJson(Map<String, Object?> json) =>
_$SnProgramMemberFromJson(json);
}
@freezed
abstract class SnPunishment with _$SnPunishment {
const factory SnPunishment({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String reason,
required int type,
@Default({}) Map<String, dynamic> permNodes,
required DateTime? expiredAt,
required SnAccount? account,
required int? accountId,
required SnAccount? moderator,
required int? moderatorId,
}) = _SnPunishment;
factory SnPunishment.fromJson(Map<String, Object?> json) =>
_$SnPunishmentFromJson(json);
}

File diff suppressed because it is too large Load Diff

View File

@ -319,3 +319,104 @@ Map<String, dynamic> _$SnActionEventToJson(_SnActionEvent instance) =>
'account': instance.account.toJson(), 'account': instance.account.toJson(),
'account_id': instance.accountId, 'account_id': instance.accountId,
}; };
_SnProgram _$SnProgramFromJson(Map<String, dynamic> json) => _SnProgram(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
name: json['name'] as String,
description: json['description'] as String,
alias: json['alias'] as String,
expRequirement: (json['exp_requirement'] as num).toInt(),
price: json['price'] as Map<String, dynamic>,
badge: json['badge'] as Map<String, dynamic>,
group: json['group'] as Map<String, dynamic>,
appearance: json['appearance'] as Map<String, dynamic>,
);
Map<String, dynamic> _$SnProgramToJson(_SnProgram instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'name': instance.name,
'description': instance.description,
'alias': instance.alias,
'exp_requirement': instance.expRequirement,
'price': instance.price,
'badge': instance.badge,
'group': instance.group,
'appearance': instance.appearance,
};
_SnProgramMember _$SnProgramMemberFromJson(Map<String, dynamic> json) =>
_SnProgramMember(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
lastPaid: DateTime.parse(json['last_paid'] as String),
account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
accountId: (json['account_id'] as num).toInt(),
program: SnProgram.fromJson(json['program'] as Map<String, dynamic>),
programId: (json['program_id'] as num).toInt(),
);
Map<String, dynamic> _$SnProgramMemberToJson(_SnProgramMember instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'last_paid': instance.lastPaid.toIso8601String(),
'account': instance.account.toJson(),
'account_id': instance.accountId,
'program': instance.program.toJson(),
'program_id': instance.programId,
};
_SnPunishment _$SnPunishmentFromJson(Map<String, dynamic> json) =>
_SnPunishment(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
reason: json['reason'] as String,
type: (json['type'] as num).toInt(),
permNodes: json['perm_nodes'] as Map<String, dynamic>? ?? const {},
expiredAt: json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
account: json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
accountId: (json['account_id'] as num?)?.toInt(),
moderator: json['moderator'] == null
? null
: SnAccount.fromJson(json['moderator'] as Map<String, dynamic>),
moderatorId: (json['moderator_id'] as num?)?.toInt(),
);
Map<String, dynamic> _$SnPunishmentToJson(_SnPunishment instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'reason': instance.reason,
'type': instance.type,
'perm_nodes': instance.permNodes,
'expired_at': instance.expiredAt?.toIso8601String(),
'account': instance.account?.toJson(),
'account_id': instance.accountId,
'moderator': instance.moderator?.toJson(),
'moderator_id': instance.moderatorId,
};

View File

@ -11,6 +11,7 @@ abstract class SnWallet with _$SnWallet {
required DateTime updatedAt, required DateTime updatedAt,
required DateTime? deletedAt, required DateTime? deletedAt,
required String balance, required String balance,
required String goldenBalance,
required String password, required String password,
required int accountId, required int accountId,
}) = _SnWallet; }) = _SnWallet;
@ -27,6 +28,7 @@ abstract class SnTransaction with _$SnTransaction {
required DateTime? deletedAt, required DateTime? deletedAt,
required String remark, required String remark,
required String amount, required String amount,
required String currency,
required SnWallet? payer, required SnWallet? payer,
required SnWallet? payee, required SnWallet? payee,
required int? payerId, required int? payerId,

View File

@ -20,6 +20,7 @@ mixin _$SnWallet {
DateTime get updatedAt; DateTime get updatedAt;
DateTime? get deletedAt; DateTime? get deletedAt;
String get balance; String get balance;
String get goldenBalance;
String get password; String get password;
int get accountId; int get accountId;
@ -46,6 +47,8 @@ mixin _$SnWallet {
(identical(other.deletedAt, deletedAt) || (identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) && other.deletedAt == deletedAt) &&
(identical(other.balance, balance) || other.balance == balance) && (identical(other.balance, balance) || other.balance == balance) &&
(identical(other.goldenBalance, goldenBalance) ||
other.goldenBalance == goldenBalance) &&
(identical(other.password, password) || (identical(other.password, password) ||
other.password == password) && other.password == password) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
@ -55,11 +58,11 @@ mixin _$SnWallet {
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt, int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
deletedAt, balance, password, accountId); deletedAt, balance, goldenBalance, password, accountId);
@override @override
String toString() { String toString() {
return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, password: $password, accountId: $accountId)'; return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, goldenBalance: $goldenBalance, password: $password, accountId: $accountId)';
} }
} }
@ -74,6 +77,7 @@ abstract mixin class $SnWalletCopyWith<$Res> {
DateTime updatedAt, DateTime updatedAt,
DateTime? deletedAt, DateTime? deletedAt,
String balance, String balance,
String goldenBalance,
String password, String password,
int accountId}); int accountId});
} }
@ -95,6 +99,7 @@ class _$SnWalletCopyWithImpl<$Res> implements $SnWalletCopyWith<$Res> {
Object? updatedAt = null, Object? updatedAt = null,
Object? deletedAt = freezed, Object? deletedAt = freezed,
Object? balance = null, Object? balance = null,
Object? goldenBalance = null,
Object? password = null, Object? password = null,
Object? accountId = null, Object? accountId = null,
}) { }) {
@ -119,6 +124,10 @@ class _$SnWalletCopyWithImpl<$Res> implements $SnWalletCopyWith<$Res> {
? _self.balance ? _self.balance
: balance // ignore: cast_nullable_to_non_nullable : balance // ignore: cast_nullable_to_non_nullable
as String, as String,
goldenBalance: null == goldenBalance
? _self.goldenBalance
: goldenBalance // ignore: cast_nullable_to_non_nullable
as String,
password: null == password password: null == password
? _self.password ? _self.password
: password // ignore: cast_nullable_to_non_nullable : password // ignore: cast_nullable_to_non_nullable
@ -140,6 +149,7 @@ class _SnWallet implements SnWallet {
required this.updatedAt, required this.updatedAt,
required this.deletedAt, required this.deletedAt,
required this.balance, required this.balance,
required this.goldenBalance,
required this.password, required this.password,
required this.accountId}); required this.accountId});
factory _SnWallet.fromJson(Map<String, dynamic> json) => factory _SnWallet.fromJson(Map<String, dynamic> json) =>
@ -156,6 +166,8 @@ class _SnWallet implements SnWallet {
@override @override
final String balance; final String balance;
@override @override
final String goldenBalance;
@override
final String password; final String password;
@override @override
final int accountId; final int accountId;
@ -188,6 +200,8 @@ class _SnWallet implements SnWallet {
(identical(other.deletedAt, deletedAt) || (identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) && other.deletedAt == deletedAt) &&
(identical(other.balance, balance) || other.balance == balance) && (identical(other.balance, balance) || other.balance == balance) &&
(identical(other.goldenBalance, goldenBalance) ||
other.goldenBalance == goldenBalance) &&
(identical(other.password, password) || (identical(other.password, password) ||
other.password == password) && other.password == password) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
@ -197,11 +211,11 @@ class _SnWallet implements SnWallet {
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt, int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
deletedAt, balance, password, accountId); deletedAt, balance, goldenBalance, password, accountId);
@override @override
String toString() { String toString() {
return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, password: $password, accountId: $accountId)'; return 'SnWallet(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, balance: $balance, goldenBalance: $goldenBalance, password: $password, accountId: $accountId)';
} }
} }
@ -218,6 +232,7 @@ abstract mixin class _$SnWalletCopyWith<$Res>
DateTime updatedAt, DateTime updatedAt,
DateTime? deletedAt, DateTime? deletedAt,
String balance, String balance,
String goldenBalance,
String password, String password,
int accountId}); int accountId});
} }
@ -239,6 +254,7 @@ class __$SnWalletCopyWithImpl<$Res> implements _$SnWalletCopyWith<$Res> {
Object? updatedAt = null, Object? updatedAt = null,
Object? deletedAt = freezed, Object? deletedAt = freezed,
Object? balance = null, Object? balance = null,
Object? goldenBalance = null,
Object? password = null, Object? password = null,
Object? accountId = null, Object? accountId = null,
}) { }) {
@ -263,6 +279,10 @@ class __$SnWalletCopyWithImpl<$Res> implements _$SnWalletCopyWith<$Res> {
? _self.balance ? _self.balance
: balance // ignore: cast_nullable_to_non_nullable : balance // ignore: cast_nullable_to_non_nullable
as String, as String,
goldenBalance: null == goldenBalance
? _self.goldenBalance
: goldenBalance // ignore: cast_nullable_to_non_nullable
as String,
password: null == password password: null == password
? _self.password ? _self.password
: password // ignore: cast_nullable_to_non_nullable : password // ignore: cast_nullable_to_non_nullable
@ -283,6 +303,7 @@ mixin _$SnTransaction {
DateTime? get deletedAt; DateTime? get deletedAt;
String get remark; String get remark;
String get amount; String get amount;
String get currency;
SnWallet? get payer; SnWallet? get payer;
SnWallet? get payee; SnWallet? get payee;
int? get payerId; int? get payerId;
@ -313,6 +334,8 @@ mixin _$SnTransaction {
other.deletedAt == deletedAt) && other.deletedAt == deletedAt) &&
(identical(other.remark, remark) || other.remark == remark) && (identical(other.remark, remark) || other.remark == remark) &&
(identical(other.amount, amount) || other.amount == amount) && (identical(other.amount, amount) || other.amount == amount) &&
(identical(other.currency, currency) ||
other.currency == currency) &&
(identical(other.payer, payer) || other.payer == payer) && (identical(other.payer, payer) || other.payer == payer) &&
(identical(other.payee, payee) || other.payee == payee) && (identical(other.payee, payee) || other.payee == payee) &&
(identical(other.payerId, payerId) || other.payerId == payerId) && (identical(other.payerId, payerId) || other.payerId == payerId) &&
@ -322,11 +345,11 @@ mixin _$SnTransaction {
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt, int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
deletedAt, remark, amount, payer, payee, payerId, payeeId); deletedAt, remark, amount, currency, payer, payee, payerId, payeeId);
@override @override
String toString() { String toString() {
return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)'; return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, currency: $currency, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)';
} }
} }
@ -343,6 +366,7 @@ abstract mixin class $SnTransactionCopyWith<$Res> {
DateTime? deletedAt, DateTime? deletedAt,
String remark, String remark,
String amount, String amount,
String currency,
SnWallet? payer, SnWallet? payer,
SnWallet? payee, SnWallet? payee,
int? payerId, int? payerId,
@ -371,6 +395,7 @@ class _$SnTransactionCopyWithImpl<$Res>
Object? deletedAt = freezed, Object? deletedAt = freezed,
Object? remark = null, Object? remark = null,
Object? amount = null, Object? amount = null,
Object? currency = null,
Object? payer = freezed, Object? payer = freezed,
Object? payee = freezed, Object? payee = freezed,
Object? payerId = freezed, Object? payerId = freezed,
@ -401,6 +426,10 @@ class _$SnTransactionCopyWithImpl<$Res>
? _self.amount ? _self.amount
: amount // ignore: cast_nullable_to_non_nullable : amount // ignore: cast_nullable_to_non_nullable
as String, as String,
currency: null == currency
? _self.currency
: currency // ignore: cast_nullable_to_non_nullable
as String,
payer: freezed == payer payer: freezed == payer
? _self.payer ? _self.payer
: payer // ignore: cast_nullable_to_non_nullable : payer // ignore: cast_nullable_to_non_nullable
@ -459,6 +488,7 @@ class _SnTransaction implements SnTransaction {
required this.deletedAt, required this.deletedAt,
required this.remark, required this.remark,
required this.amount, required this.amount,
required this.currency,
required this.payer, required this.payer,
required this.payee, required this.payee,
required this.payerId, required this.payerId,
@ -479,6 +509,8 @@ class _SnTransaction implements SnTransaction {
@override @override
final String amount; final String amount;
@override @override
final String currency;
@override
final SnWallet? payer; final SnWallet? payer;
@override @override
final SnWallet? payee; final SnWallet? payee;
@ -516,6 +548,8 @@ class _SnTransaction implements SnTransaction {
other.deletedAt == deletedAt) && other.deletedAt == deletedAt) &&
(identical(other.remark, remark) || other.remark == remark) && (identical(other.remark, remark) || other.remark == remark) &&
(identical(other.amount, amount) || other.amount == amount) && (identical(other.amount, amount) || other.amount == amount) &&
(identical(other.currency, currency) ||
other.currency == currency) &&
(identical(other.payer, payer) || other.payer == payer) && (identical(other.payer, payer) || other.payer == payer) &&
(identical(other.payee, payee) || other.payee == payee) && (identical(other.payee, payee) || other.payee == payee) &&
(identical(other.payerId, payerId) || other.payerId == payerId) && (identical(other.payerId, payerId) || other.payerId == payerId) &&
@ -525,11 +559,11 @@ class _SnTransaction implements SnTransaction {
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt, int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
deletedAt, remark, amount, payer, payee, payerId, payeeId); deletedAt, remark, amount, currency, payer, payee, payerId, payeeId);
@override @override
String toString() { String toString() {
return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)'; return 'SnTransaction(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, remark: $remark, amount: $amount, currency: $currency, payer: $payer, payee: $payee, payerId: $payerId, payeeId: $payeeId)';
} }
} }
@ -548,6 +582,7 @@ abstract mixin class _$SnTransactionCopyWith<$Res>
DateTime? deletedAt, DateTime? deletedAt,
String remark, String remark,
String amount, String amount,
String currency,
SnWallet? payer, SnWallet? payer,
SnWallet? payee, SnWallet? payee,
int? payerId, int? payerId,
@ -578,6 +613,7 @@ class __$SnTransactionCopyWithImpl<$Res>
Object? deletedAt = freezed, Object? deletedAt = freezed,
Object? remark = null, Object? remark = null,
Object? amount = null, Object? amount = null,
Object? currency = null,
Object? payer = freezed, Object? payer = freezed,
Object? payee = freezed, Object? payee = freezed,
Object? payerId = freezed, Object? payerId = freezed,
@ -608,6 +644,10 @@ class __$SnTransactionCopyWithImpl<$Res>
? _self.amount ? _self.amount
: amount // ignore: cast_nullable_to_non_nullable : amount // ignore: cast_nullable_to_non_nullable
as String, as String,
currency: null == currency
? _self.currency
: currency // ignore: cast_nullable_to_non_nullable
as String,
payer: freezed == payer payer: freezed == payer
? _self.payer ? _self.payer
: payer // ignore: cast_nullable_to_non_nullable : payer // ignore: cast_nullable_to_non_nullable

View File

@ -14,6 +14,7 @@ _SnWallet _$SnWalletFromJson(Map<String, dynamic> json) => _SnWallet(
? null ? null
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
balance: json['balance'] as String, balance: json['balance'] as String,
goldenBalance: json['golden_balance'] as String,
password: json['password'] as String, password: json['password'] as String,
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
); );
@ -24,6 +25,7 @@ Map<String, dynamic> _$SnWalletToJson(_SnWallet instance) => <String, dynamic>{
'updated_at': instance.updatedAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
'balance': instance.balance, 'balance': instance.balance,
'golden_balance': instance.goldenBalance,
'password': instance.password, 'password': instance.password,
'account_id': instance.accountId, 'account_id': instance.accountId,
}; };
@ -38,6 +40,7 @@ _SnTransaction _$SnTransactionFromJson(Map<String, dynamic> json) =>
: DateTime.parse(json['deleted_at'] as String), : DateTime.parse(json['deleted_at'] as String),
remark: json['remark'] as String, remark: json['remark'] as String,
amount: json['amount'] as String, amount: json['amount'] as String,
currency: json['currency'] as String,
payer: json['payer'] == null payer: json['payer'] == null
? null ? null
: SnWallet.fromJson(json['payer'] as Map<String, dynamic>), : SnWallet.fromJson(json['payer'] as Map<String, dynamic>),
@ -56,6 +59,7 @@ Map<String, dynamic> _$SnTransactionToJson(_SnTransaction instance) =>
'deleted_at': instance.deletedAt?.toIso8601String(), 'deleted_at': instance.deletedAt?.toIso8601String(),
'remark': instance.remark, 'remark': instance.remark,
'amount': instance.amount, 'amount': instance.amount,
'currency': instance.currency,
'payer': instance.payer?.toJson(), 'payer': instance.payer?.toJson(),
'payee': instance.payee?.toJson(), 'payee': instance.payee?.toJson(),
'payer_id': instance.payerId, 'payer_id': instance.payerId,

View File

@ -5,6 +5,7 @@ import 'dart:math' as math;
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:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@ -12,6 +13,7 @@ import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart'; import 'package:media_kit_video/media_kit_video.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/logger.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
@ -222,20 +224,71 @@ class _AttachmentItemContentVideoState
: sn.getAttachmentUrl(widget.data.compressed!.rid); : sn.getAttachmentUrl(widget.data.compressed!.rid);
_videoPlayer = Player(); _videoPlayer = Player();
_videoController = VideoController(_videoPlayer!); _videoController = VideoController(_videoPlayer!);
_videoPlayer!.open(Media(url), play: !widget.isAutoload);
String? uri;
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
if (inCacheInfo == null) {
logging.info('[MediaPlayer] Miss cache: $url');
final fileStream = DefaultCacheManager().getFileStream(
url,
withProgress: true,
);
await for (var fileInfo in fileStream) {
if (fileInfo is FileInfo) {
uri = fileInfo.file.path;
break;
}
}
} else {
uri = inCacheInfo.file.path;
logging.info('[MediaPlayer] Hit cache: $url');
}
if (uri == null) {
if (mounted) {
context.showErrorDialog('attachmentFailedToLoadMedia'.tr());
}
return;
} }
void _toggleOriginal() { _videoPlayer!.open(Media(uri), play: !widget.isAutoload);
}
void _toggleOriginal() async {
if (!mounted) return; if (!mounted) return;
if (widget.data.compressedId == null) return; if (widget.data.compressedId == null) return;
setState(() => _showOriginal = !_showOriginal); setState(() => _showOriginal = !_showOriginal);
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
_videoPlayer?.open( final url = _showOriginal
Media(
_showOriginal
? sn.getAttachmentUrl(widget.data.rid) ? sn.getAttachmentUrl(widget.data.rid)
: sn.getAttachmentUrl(widget.data.compressed!.rid), : sn.getAttachmentUrl(widget.data.compressed!.rid);
),
String? uri;
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
if (inCacheInfo == null) {
logging.info('[MediaPlayer] Miss cache: $url');
final fileStream = DefaultCacheManager().getFileStream(
url,
withProgress: true,
);
await for (var fileInfo in fileStream) {
if (fileInfo is FileInfo) {
uri = fileInfo.file.path;
break;
}
}
} else {
uri = inCacheInfo.file.path;
logging.info('[MediaPlayer] Hit cache: $url');
}
if (uri == null) {
if (mounted) {
context.showErrorDialog('attachmentFailedToLoadMedia'.tr());
}
return;
}
_videoPlayer?.open(
Media(uri),
play: true, play: true,
); );
} }
@ -439,7 +492,33 @@ class _AttachmentItemContentAudioState
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final url = sn.getAttachmentUrl(widget.data.rid); final url = sn.getAttachmentUrl(widget.data.rid);
_audioPlayer = Player(); _audioPlayer = Player();
await _audioPlayer!.open(Media(url), play: !widget.isAutoload);
String? uri;
final inCacheInfo = await DefaultCacheManager().getFileFromCache(url);
if (inCacheInfo == null) {
logging.info('[MediaPlayer] Miss cache: $url');
final fileStream = DefaultCacheManager().getFileStream(
url,
withProgress: true,
);
await for (var fileInfo in fileStream) {
if (fileInfo is FileInfo) {
uri = fileInfo.file.path;
break;
}
}
} else {
uri = inCacheInfo.file.path;
logging.info('[MediaPlayer] Hit cache: $url');
}
if (uri == null) {
if (mounted) {
context.showErrorDialog('attachmentFailedToLoadMedia'.tr());
}
return;
}
await _audioPlayer!.open(Media(uri), play: !widget.isAutoload);
_audioPlayer!.stream.playing.listen((v) => setState(() => _isPlaying = v)); _audioPlayer!.stream.playing.listen((v) => setState(() => _isPlaying = v));
_audioPlayer!.stream.position.listen((v) => setState(() => _position = v)); _audioPlayer!.stream.position.listen((v) => setState(() => _position = v));
_audioPlayer!.stream.duration.listen((v) => setState(() => _duration = v)); _audioPlayer!.stream.duration.listen((v) => setState(() => _duration = v));
@ -567,6 +646,7 @@ class _AttachmentItemContentAudioState
), ),
), ),
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 16),
constraints: const BoxConstraints(maxWidth: 320), constraints: const BoxConstraints(maxWidth: 320),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@ -224,8 +224,10 @@ class _AttachmentListState extends State<AttachmentList> {
(widget.data[idx]?.data['ratio'] ?? 1).toDouble(), (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
if (widget.data[idx]?.mediaType != SnMediaType.image) if (widget.data[idx]?.mediaType !=
SnMediaType.image) {
return; return;
}
context.pushTransparentRoute( context.pushTransparentRoute(
AttachmentZoomView( AttachmentZoomView(
data: widget.data data: widget.data
@ -246,8 +248,10 @@ class _AttachmentListState extends State<AttachmentList> {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: backgroundColor, color: backgroundColor,
border: border: Border.all(
Border(top: borderSide, bottom: borderSide), width: 1,
color: Theme.of(context).dividerColor,
),
borderRadius: AttachmentList.kDefaultRadius, borderRadius: AttachmentList.kDefaultRadius,
), ),
child: ClipRRect( child: ClipRRect(
@ -263,8 +267,8 @@ class _AttachmentListState extends State<AttachmentList> {
right: 8, right: 8,
bottom: 8, bottom: 8,
child: Chip( child: Chip(
label: label: Text('${idx + 1}/${widget.data.length}'),
Text('${idx + 1}/${widget.data.length}')), ),
), ),
], ],
), ),

View File

@ -65,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(

View File

@ -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) => (ele.nick?.isNotEmpty ?? false)
? ele.nick! ? ele.nick!
: ud.getFromCache(ele.accountId)?.name ?? : ud.getFromCache(ele.accountId)?.nick ??
'unknown') 'unknown',
)
.join(', '), .join(', '),
]), ]),
), ),

View File

@ -16,7 +16,12 @@ class ConnectionIndicator extends StatelessWidget {
final ws = context.watch<WebSocketProvider>(); final ws = context.watch<WebSocketProvider>();
final cfg = context.watch<ConfigProvider>(); final cfg = context.watch<ConfigProvider>();
final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0; final marginLeft =
cfg.drawerIsCollapsed
? 0.0
: cfg.drawerIsExpanded
? 304.0
: 80.0;
return ListenableBuilder( return ListenableBuilder(
listenable: ws, listenable: ws,
@ -32,26 +37,31 @@ class ConnectionIndicator extends StatelessWidget {
elevation: 2, elevation: 2,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
color: Theme.of(context).colorScheme.secondaryContainer, color: Theme.of(context).colorScheme.secondaryContainer,
child: ua.isAuthorized child:
ua.isAuthorized
? Row( ? Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if (ws.isBusy) if (ws.isBusy)
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer) Text(
'serverConnecting',
).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
else if (!ws.isConnected) else if (!ws.isConnected)
Text('serverDisconnected') Text(
.tr() 'serverDisconnected',
.textColor(Theme.of(context).colorScheme.onSecondaryContainer) ).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
else else
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer), Text(
'serverConnected',
).tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
const Gap(8), const Gap(8),
if (ws.isBusy) if (ws.isBusy)
const CircularProgressIndicator(strokeWidth: 2.5) const CircularProgressIndicator(
.width(12) strokeWidth: 2.5,
.height(12) padding: EdgeInsets.zero,
.padding(horizontal: 4, right: 4) ).width(12).height(12).padding(horizontal: 4, right: 4)
else if (!ws.isConnected) else if (!ws.isConnected)
const Icon(Symbols.power_off, size: 18) const Icon(Symbols.power_off, size: 18)
else else
@ -59,10 +69,7 @@ class ConnectionIndicator extends StatelessWidget {
], ],
).padding(horizontal: 8, vertical: 4) ).padding(horizontal: 8, vertical: 4)
: const SizedBox.shrink(), : const SizedBox.shrink(),
).opacity(show ? 1 : 0, animate: true).animate( ).opacity(show ? 1 : 0, animate: true).animate(const Duration(milliseconds: 300), Curves.easeInOut),
const Duration(milliseconds: 300),
Curves.easeInOut,
),
onTap: () { onTap: () {
if (!ws.isConnected && !ws.isBusy) { if (!ws.isConnected && !ws.isBusy) {
ws.connect(); ws.connect();

View File

@ -19,8 +19,7 @@ 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(
@ -101,7 +100,6 @@ class NewsFeedEntry extends StatelessWidget {
), ),
), ),
], ],
),
); );
} }
} }

View File

@ -12,8 +12,7 @@ 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),
@ -21,7 +20,6 @@ class FeedUnknownEntry extends StatelessWidget {
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);
);
} }
} }

View File

@ -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(),

View File

@ -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) {

View File

@ -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(

View File

@ -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);
}, },

View File

@ -1,14 +1,24 @@
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:collection/collection.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 +35,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,17 +52,20 @@ 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, shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(0))),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS) && !cfg.drawerIsExpanded) if (!kIsWeb &&
(Platform.isWindows ||
Platform.isLinux ||
Platform.isMacOS) &&
!cfg.drawerIsExpanded)
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
@ -61,6 +77,124 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
), ),
child: WindowTitleBarBox(), child: WindowTitleBarBox(),
), ),
Gap(MediaQuery.of(context).padding.top),
Expanded(
child: _DrawerContentList(),
),
Row(
spacing: 8,
children:
nav.destinations.where((ele) => ele.isPinned).mapIndexed(
(idx, ele) {
return Expanded(
child: Tooltip(
message: ele.label.tr(),
child: IconButton(
icon: ele.icon,
color: nav.currentIndex == idx
? Theme.of(context).colorScheme.onPrimaryContainer
: Theme.of(context).colorScheme.onSurface,
style: ButtonStyle(
backgroundColor: WidgetStatePropertyAll(
nav.currentIndex == idx
? Theme.of(context)
.colorScheme
.primaryContainer
: Colors.transparent,
),
),
onPressed: () {
GoRouter.of(context).goNamed(ele.screen);
Scaffold.of(context).closeDrawer();
nav.setIndex(idx);
},
),
),
);
},
).toList(),
).padding(horizontal: 16, bottom: 8),
Align(
alignment: Alignment.bottomCenter,
child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 24),
leading: AccountImage(
content: ua.user?.avatar,
fallbackWidget:
ua.isAuthorized ? null : const Icon(Symbols.login),
),
title: ua.isAuthorized
? Text(ua.user?.nick ?? 'unknown'.tr()).fontSize(15)
: Text('screenAuthLogin').tr(),
subtitle: ua.isAuthorized
? Text('@${ua.user?.name ?? 'unknown'.tr()}').fontSize(13)
: Text('navBottomUnauthorizedCaption').fontSize(13).tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (ua.isAuthorized)
IconButton(
icon: const Icon(Symbols.notifications, fill: 1),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
onPressed: () {
GoRouter.of(context).pushNamed('notification');
Scaffold.of(context).closeDrawer();
},
),
IconButton(
icon: const Icon(Symbols.settings, fill: 1),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
onPressed: () {
GoRouter.of(context).pushNamed('settings');
Scaffold.of(context).closeDrawer();
},
),
],
),
onTap: () {
GoRouter.of(context).pushNamed('account');
Scaffold.of(context).closeDrawer();
},
),
),
Gap(MediaQuery.of(context).padding.bottom + 8),
],
),
);
},
);
}
}
class _DrawerContentList extends StatelessWidget {
const _DrawerContentList();
@override
Widget build(BuildContext context) {
final ct = context.read<ChatChannelProvider>();
final sn = context.read<SnNetworkProvider>();
final nav = context.watch<NavigationProvider>();
final rel = context.watch<SnRealmProvider>();
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> primaryAnimation,
Animation<double> secondaryAnimation) {
return SharedAxisTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
transitionType: SharedAxisTransitionType.horizontal,
child: child,
);
},
child: nav.focusedRealm == null
? ListView(
key: const Key('realm-list-view'),
padding: EdgeInsets.zero,
children: [
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -72,27 +206,124 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
horizontal: 32, horizontal: 32,
vertical: 12, vertical: 12,
), ),
...destinations.where((ele) => ele.isPinned).map((ele) { ...rel.availableRealms.map((ele) {
return NavigationDrawerDestination( return ListTile(
icon: ele.icon, minTileHeight: 48,
label: Text(ele.label).tr(), contentPadding: EdgeInsets.symmetric(horizontal: 24),
); leading: AccountImage(
}), content: ele.avatar,
const Divider(), radius: 16,
...destinations.where((ele) => !ele.isPinned).map((ele) { ),
return NavigationDrawerDestination( title: Text(ele.name),
icon: ele.icon, onTap: () {
label: Text(ele.label).tr(), nav.setFocusedRealm(ele);
},
); );
}), }),
ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.only(left: 28, right: 16),
leading: const Icon(Symbols.globe).padding(right: 4),
title: Text('screenRealmDiscovery').tr(),
onTap: () {
GoRouter.of(context).pushNamed('realmDiscovery');
Scaffold.of(context).closeDrawer();
},
),
], ],
onDestinationSelected: (idx) { )
nav.setIndex(idx); : ListView(
GoRouter.of(context).goNamed(destinations[idx].screen); key: ValueKey(nav.focusedRealm),
padding: EdgeInsets.zero,
children: [
if (nav.focusedRealm!.banner != null)
AspectRatio(
aspectRatio: 16 / 9,
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(
nav.focusedRealm!.banner!,
),
fit: BoxFit.cover,
),
),
ListTile(
minTileHeight: 48,
tileColor: Theme.of(context).colorScheme.surfaceContainer,
contentPadding: EdgeInsets.only(
left: 24,
right: 16,
),
leading: AccountImage(
content: nav.focusedRealm!.avatar,
radius: 16,
),
trailing: IconButton(
icon: const Icon(Symbols.close),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
visualDensity: VisualDensity.compact,
onPressed: () {
nav.setFocusedRealm(null);
},
),
title: Text(nav.focusedRealm!.name),
onTap: () {
GoRouter.of(context).goNamed(
'realmDetail',
pathParameters: {
'alias': nav.focusedRealm!.alias,
},
);
Scaffold.of(context).closeDrawer();
},
),
ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.only(
left: 28,
right: 8,
),
leading: const Icon(Symbols.globe),
title: Text('community').tr(),
onTap: () {
GoRouter.of(context).goNamed(
'realmCommunity',
pathParameters: {
'alias': nav.focusedRealm!.alias,
},
);
Scaffold.of(context).closeDrawer();
},
),
if (ct.availableChannels
.where((ele) => ele.realmId == nav.focusedRealm?.id)
.isNotEmpty)
const Divider(height: 1),
...(ct.availableChannels
.where((ele) => ele.realmId == nav.focusedRealm?.id)
.map((ele) {
return ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.only(
left: 28,
right: 8,
),
leading: const Icon(Symbols.tag),
title: Text(ele.name),
onTap: () {
GoRouter.of(context).goNamed(
'chatRoom',
pathParameters: {
'scope': ele.realm?.alias ?? 'global',
'alias': ele.alias,
},
);
Scaffold.of(context).closeDrawer(); Scaffold.of(context).closeDrawer();
}, },
); );
}, }))
],
),
); );
} }
} }

View File

@ -1,10 +1,12 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/navigation.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/widgets/account/account_image.dart';
class AppRailNavigation extends StatefulWidget { class AppRailNavigation extends StatefulWidget {
const AppRailNavigation({super.key}); const AppRailNavigation({super.key});
@ -18,43 +20,59 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
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.watch<UserProvider>();
final nav = context.watch<NavigationProvider>(); final nav = context.watch<NavigationProvider>();
return ListenableBuilder( return ListenableBuilder(
listenable: nav, listenable: nav,
builder: (context, _) { builder: (context, _) {
final destinations = nav.destinations.where((ele) => ele.isPinned).toList(); final destinations = nav.destinations.toList();
return SizedBox( return SizedBox(
width: 80, width: 80,
child: NavigationRail( child: NavigationRail(
selectedIndex: labelType: NavigationRailLabelType.selected,
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null, backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainerLow
.withOpacity(0.5),
selectedIndex: nav.currentIndex != null &&
nav.currentIndex! < nav.destinations.length
? nav.currentIndex
: null,
destinations: [ destinations: [
...destinations.where((ele) => ele.isPinned).map((ele) { ...destinations.map((ele) {
return NavigationRailDestination( return NavigationRailDestination(
icon: ele.icon, icon: ele.icon,
label: Text(ele.label).tr(), label: Text(ele.label).tr(),
); );
}), }),
], ],
leading: const Gap(4),
trailing: Expanded( trailing: Expanded(
child: Align( child: Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: StyledWidget( child: Padding(
IconButton( padding: EdgeInsets.only(bottom: 24),
icon: const Icon(Symbols.menu), child: GestureDetector(
onPressed: () { child: AccountImage(
Scaffold.of(context).openDrawer(); content: ua.user?.avatar,
fallbackWidget:
ua.isAuthorized ? null : const Icon(Symbols.login),
),
onTap: () {
GoRouter.of(context).goNamed('account');
}, },
), ),
).padding(bottom: 16), ),
), ),
), ),
onDestinationSelected: (idx) { onDestinationSelected: (idx) {

View File

@ -13,7 +13,6 @@ import 'package:surface/providers/navigation.dart';
import 'package:surface/widgets/connection_indicator.dart'; import 'package:surface/widgets/connection_indicator.dart';
import 'package:surface/widgets/navigation/app_background.dart'; import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_bottom_navigation.dart'; import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
import 'package:surface/widgets/navigation/app_rail_navigation.dart'; import 'package:surface/widgets/navigation/app_rail_navigation.dart';
import 'package:surface/widgets/notify_indicator.dart'; import 'package:surface/widgets/notify_indicator.dart';
@ -66,7 +65,9 @@ class AppScaffold extends StatelessWidget {
return Scaffold( return Scaffold(
extendBody: true, extendBody: true,
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: noBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor,
body: SizedBox.expand( body: SizedBox.expand(
child: noBackground child: noBackground
? content ? content
@ -107,10 +108,10 @@ 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;
final isExpandedDrawer = cfg.drawerIsExpanded;
final routeName = GoRouter.of(context) final routeName = GoRouter.of(context)
.routerDelegate .routerDelegate
@ -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
@ -130,19 +132,7 @@ class AppRootScaffold extends StatelessWidget {
? body ? body
: Row( : Row(
children: [ children: [
Container( AppRailNavigation(),
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
),
),
),
child: isExpandedDrawer
? AppNavigationDrawer(elevation: 0)
: AppRailNavigation(),
),
Expanded(child: body), Expanded(child: body),
], ],
); );
@ -230,10 +220,71 @@ class AppRootScaffold extends StatelessWidget {
), ),
], ],
), ),
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null, drawerEdgeDragWidth: isPopable ? 0 : null,
bottomNavigationBar: bottomNavigationBar:
isShowBottomNavigation ? AppBottomNavigationBar() : null, isShowBottomNavigation ? AppBottomNavigationBar() : null,
); );
} }
} }
class ResponsiveScaffold extends StatelessWidget {
final Widget aside;
final Widget? child;
final int asideFlex;
final int contentFlex;
const ResponsiveScaffold({
super.key,
required this.aside,
required this.child,
this.asideFlex = 1,
this.contentFlex = 2,
});
static bool getIsExpand(BuildContext context) {
return ResponsiveBreakpoints.of(context).largerOrEqualTo(TABLET);
}
@override
Widget build(BuildContext context) {
if (getIsExpand(context)) {
return AppBackground(
isRoot: true,
child: Row(
children: [
Flexible(
flex: asideFlex,
child: aside,
),
VerticalDivider(width: 1),
if (child != null && child != aside)
Flexible(flex: contentFlex, child: child!)
else
Flexible(
flex: contentFlex,
child: ResponsiveScaffoldLanding(child: null),
),
],
),
);
}
return AppBackground(isRoot: true, child: child ?? aside);
}
}
class ResponsiveScaffoldLanding extends StatelessWidget {
final Widget? child;
const ResponsiveScaffoldLanding({super.key, required this.child});
@override
Widget build(BuildContext context) {
if (ResponsiveScaffold.getIsExpand(context) || child == null) {
return AppScaffold(
noBackground: true,
appBar: AppBar(),
body: const SizedBox.shrink(),
);
}
return child!;
}
}

View File

@ -23,8 +23,6 @@ class FediversePostWidget extends StatelessWidget {
return Center( return Center(
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: maxWidth), constraints: BoxConstraints(maxWidth: maxWidth),
child: Card(
margin: EdgeInsets.zero,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -75,7 +73,6 @@ class FediversePostWidget extends StatelessWidget {
], ],
), ),
), ),
),
); );
} }
} }

View File

@ -4,7 +4,6 @@ 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:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
@ -30,19 +29,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,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE) borderRadius: BorderRadius.zero,
? const BorderRadius.all(Radius.circular(8)) border: Border.symmetric(
: BorderRadius.zero,
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? Border.all(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
)
: Border.symmetric(
horizontal: BorderSide( horizontal: BorderSide(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio, width: 1 / devicePixelRatio,
@ -103,7 +92,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.client await sn.client
.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: { .put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
'publisher': answer.publisherId, 'publisher': widget.parentPost.publisherId,
'answer_id': answer.id, 'answer_id': answer.id,
}); });
if (!mounted) return; if (!mounted) return;
@ -151,6 +140,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()},
@ -225,6 +215,9 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
onPost: () { onPost: () {
_childListKey.currentState!.refresh(); _childListKey.currentState!.refresh();
}, },
onExpand: () {
Navigator.pop(context);
},
), ),
), ),
), ),

View File

@ -1,7 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:animations/animations.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:file_saver/file_saver.dart'; import 'package:file_saver/file_saver.dart';
@ -26,7 +25,6 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/translation.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/screens/post/post_detail.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/types/reaction.dart'; import 'package:surface/types/reaction.dart';
@ -53,6 +51,7 @@ class OpenablePostItem extends StatelessWidget {
final bool showMenu; final bool showMenu;
final bool showFullPost; final bool showFullPost;
final bool showExpandableComments; final bool showExpandableComments;
final bool useReplace;
final double? maxWidth; final double? maxWidth;
final Function(SnPost data)? onChanged; final Function(SnPost data)? onChanged;
final Function()? onDeleted; final Function()? onDeleted;
@ -66,6 +65,7 @@ class OpenablePostItem extends StatelessWidget {
this.showMenu = true, this.showMenu = true,
this.showFullPost = false, this.showFullPost = false,
this.showExpandableComments = false, this.showExpandableComments = false,
this.useReplace = false,
this.maxWidth, this.maxWidth,
this.onChanged, this.onChanged,
this.onDeleted, this.onDeleted,
@ -74,14 +74,10 @@ class OpenablePostItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>();
return Container( return Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Center( child: Center(
child: OpenContainer( child: GestureDetector(
closedBuilder: (_, __) => Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: PostItem( child: PostItem(
data: data, data: data,
maxWidth: maxWidth, maxWidth: maxWidth,
@ -92,22 +88,18 @@ class OpenablePostItem extends StatelessWidget {
onDeleted: onDeleted, onDeleted: onDeleted,
onSelectAnswer: onSelectAnswer, onSelectAnswer: onSelectAnswer,
), ),
), onTap: () {
openBuilder: (_, close) => PostDetailScreen( if (useReplace) {
slug: data.id.toString(), GoRouter.of(context)
preload: data, .pushReplacementNamed('postDetail', pathParameters: {
onBack: close, 'slug': data.id.toString(),
), });
openColor: Colors.transparent, } else {
openElevation: 0, GoRouter.of(context).pushNamed('postDetail', pathParameters: {
transitionType: ContainerTransitionType.fade, 'slug': data.id.toString(),
closedElevation: 0, });
closedColor: Theme.of(context).colorScheme.surface.withOpacity( }
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1, },
),
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
), ),
), ),
); );
@ -122,6 +114,7 @@ class PostItem extends StatefulWidget {
final bool showMenu; final bool showMenu;
final bool showFullPost; final bool showFullPost;
final bool showAvatar; final bool showAvatar;
final bool showCompactAvatar;
final bool showExpandableComments; final bool showExpandableComments;
final double? maxWidth; final double? maxWidth;
final Function(SnPost data)? onChanged; final Function(SnPost data)? onChanged;
@ -137,6 +130,7 @@ class PostItem extends StatefulWidget {
this.showMenu = true, this.showMenu = true,
this.showFullPost = false, this.showFullPost = false,
this.showAvatar = true, this.showAvatar = true,
this.showCompactAvatar = false,
this.showExpandableComments = false, this.showExpandableComments = false,
this.maxWidth, this.maxWidth,
this.onChanged, this.onChanged,
@ -166,6 +160,14 @@ class _PostItemState extends State<PostItem> {
} }
} }
@override
void didUpdateWidget(covariant PostItem oldWidget) {
_displayText = widget.data.body['content'] ?? '';
_displayTitle = widget.data.body['title'] ?? '';
_displayDescription = widget.data.body['description'] ?? '';
super.didUpdateWidget(oldWidget);
}
Future<void> _translateText() async { Future<void> _translateText() async {
final ta = context.read<SnTranslator>(); final ta = context.read<SnTranslator>();
setState(() => _isTranslating = true); setState(() => _isTranslating = true);
@ -269,6 +271,8 @@ class _PostItemState extends State<PostItem> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final isAuthor = final isAuthor =
ua.isAuthorized && widget.data.publisher.accountId == ua.user?.id; ua.isAuthorized && widget.data.publisher.accountId == ua.user?.id;
final isParentAuthor = ua.isAuthorized &&
widget.data.replyTo?.publisher.accountId == ua.user?.id;
final displayableAttachments = widget.data.preload?.attachments final displayableAttachments = widget.data.preload?.attachments
?.where((ele) => ?.where((ele) =>
@ -284,6 +288,244 @@ class _PostItemState extends State<PostItem> {
attachmentSize -= 80; attachmentSize -= 80;
} }
if (widget.showFullPost) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
constraints: BoxConstraints(
maxWidth: widget.maxWidth ?? double.infinity,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
if (widget.showAvatar)
_PostAvatar(
data: widget.data,
isCompact: false,
),
if (widget.showAvatar) const Gap(12),
Expanded(
child: Row(
children: [
if (widget.showCompactAvatar)
_PostAvatar(
data: widget.data,
isCompact: true,
),
if (widget.showAvatar) const Gap(8),
_PostContentHeader(
isRelativeDate: !widget.showFullPost,
isCompact: false,
data: widget.data,
),
],
),
),
_PostActionPopup(
data: widget.data,
isAuthor: isAuthor,
isParentAuthor: isParentAuthor,
onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context),
onSelectAnswer: widget.onSelectAnswer,
onDeleted: () {
widget.onDeleted?.call();
},
onTranslate: () {
_translateText();
},
),
],
),
const Gap(8),
if (widget.data.preload?.thumbnail != null)
Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(
widget.data.preload!.thumbnail!.rid,
),
fit: BoxFit.cover,
),
),
),
),
if (widget.data.preload?.video != null)
_PostVideoPlayer(data: widget.data).padding(bottom: 8),
if (widget.data.type == 'question')
_PostQuestionHint(data: widget.data).padding(bottom: 8),
if (_displayDescription.isNotEmpty || _displayTitle.isNotEmpty)
_PostHeadline(
title: _displayTitle,
description: _displayDescription,
data: widget.data,
isEnlarge:
widget.data.type == 'article' && widget.showFullPost,
).padding(bottom: 8),
if (widget.data.type == 'article' && !widget.showFullPost)
Text('postArticle')
.tr()
.fontSize(13)
.opacity(0.75)
.padding(bottom: 8),
if ((_displayText.isNotEmpty) &&
(widget.showFullPost || widget.data.type != 'article'))
_PostContentBody(
text: _displayText,
data: widget.data,
isSelectable: widget.showFullPost,
isEnlarge:
widget.data.type == 'article' && widget.showFullPost,
).padding(bottom: 6),
if (widget.data.visibility > 0)
_PostVisibilityHint(data: widget.data).padding(
vertical: 4,
),
if (widget.data.body['content_truncated'] == true)
_PostTruncatedHint(data: widget.data).padding(
vertical: 4,
),
if (widget.data.tags.isNotEmpty)
_PostTagsList(data: widget.data).padding(top: 4, bottom: 6),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
if (widget.showViews)
Row(
children: [
Icon(Symbols.play_circle, size: 20),
const Gap(4),
Text('postViews').plural(widget.data.totalViews),
],
).opacity(0.75),
if (_isTranslating)
AnimateWidgetExtensions(Row(
children: [
Icon(Symbols.translate, size: 20),
const Gap(4),
Text('translating').tr(),
],
))
.animate(onPlay: (e) => e.repeat())
.fadeIn(duration: 500.ms, curve: Curves.easeOut)
.then()
.fadeOut(
duration: 500.ms,
delay: 1000.ms,
curve: Curves.easeIn,
),
if (_isTranslated)
InkWell(
child: Row(
children: [
Icon(Symbols.translate, size: 20),
const Gap(4),
Text('translated').tr(),
],
).opacity(0.75),
onTap: () {
setState(() {
_displayText = widget.data.body['content'] ?? '';
_displayTitle = widget.data.body['title'] ?? '';
_displayDescription =
widget.data.body['description'] ?? '';
_isTranslated = false;
});
},
),
if (widget.data.repostTo != null)
_PostQuoteContent(child: widget.data.repostTo!).padding(
top: 4,
bottom: widget.data.preload?.attachments?.isNotEmpty ??
false
? 12
: 0,
),
],
).padding(
bottom: widget.showViews || _isTranslated || _isTranslating
? 8
: 0,
),
],
).padding(horizontal: 12, top: 8),
),
if (displayableAttachments?.isNotEmpty ?? false)
AttachmentList(
data: displayableAttachments!,
bordered: true,
maxHeight: widget.showFullPost ? null : 480,
minWidth: attachmentSize,
maxWidth: attachmentSize,
fit: widget.showFullPost ? BoxFit.cover : BoxFit.contain,
padding: EdgeInsets.only(left: 12, right: 12),
),
if (widget.data.preload?.poll != null)
StyledWidget(Container(
constraints:
BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
child: PostPoll(poll: widget.data.preload!.poll!),
))
.padding(
left: 12,
right: 12,
top: 12,
bottom: 4,
)
.center(),
if (widget.data.body['content'] != null &&
(cfg.prefs.getBool(kAppExpandPostLink) ?? true))
LinkPreviewWidget(
text: widget.data.body['content'],
).padding(left: 12, right: 4),
if (widget.showExpandableComments)
_PostCommentIntent(
data: widget.data,
showAvatar: widget.showAvatar,
maxWidth: widget.maxWidth ?? double.infinity,
).padding(left: 12, right: 12)
else
_PostFeaturedComment(data: widget.data, maxWidth: widget.maxWidth)
.padding(left: 12, right: 12),
if (widget.showReactions)
Center(
child: Container(
constraints: BoxConstraints(
maxWidth: (widget.maxWidth ?? double.infinity) + 24,
),
child: Padding(
padding: const EdgeInsets.only(top: 4),
child: _PostReactionList(
data: widget.data,
padding: EdgeInsets.only(left: 12, right: 12),
onChanged: _onChanged,
),
),
),
),
],
);
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@ -308,6 +550,15 @@ class _PostItemState extends State<PostItem> {
children: [ children: [
Row( Row(
children: [ children: [
Expanded(
child: Row(
children: [
if (widget.showCompactAvatar)
_PostAvatar(
data: widget.data,
isCompact: true,
),
if (widget.showCompactAvatar) const Gap(8),
Expanded( Expanded(
child: _PostContentHeader( child: _PostContentHeader(
isRelativeDate: !widget.showFullPost, isRelativeDate: !widget.showFullPost,
@ -315,9 +566,13 @@ class _PostItemState extends State<PostItem> {
data: widget.data, data: widget.data,
), ),
), ),
],
),
),
_PostActionPopup( _PostActionPopup(
data: widget.data, data: widget.data,
isAuthor: isAuthor, isAuthor: isAuthor,
isParentAuthor: isParentAuthor,
onShare: () => _doShare(context), onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context), onShareImage: () => _doShareViaPicture(context),
onSelectAnswer: widget.onSelectAnswer, onSelectAnswer: widget.onSelectAnswer,
@ -329,7 +584,7 @@ class _PostItemState extends State<PostItem> {
}, },
), ),
], ],
), ).padding(bottom: widget.showCompactAvatar ? 4 : 0),
if (widget.data.preload?.thumbnail != null) if (widget.data.preload?.thumbnail != null)
Container( Container(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
@ -389,15 +644,6 @@ class _PostItemState extends State<PostItem> {
isEnlarge: widget.data.type == 'article' && isEnlarge: widget.data.type == 'article' &&
widget.showFullPost, widget.showFullPost,
).padding(bottom: 6), ).padding(bottom: 6),
if (widget.data.repostTo != null)
_PostQuoteContent(child: widget.data.repostTo!)
.padding(
bottom:
widget.data.preload?.attachments?.isNotEmpty ??
false
? 12
: 0,
),
if (widget.data.visibility > 0) if (widget.data.visibility > 0)
_PostVisibilityHint(data: widget.data).padding( _PostVisibilityHint(data: widget.data).padding(
vertical: 4, vertical: 4,
@ -462,9 +708,22 @@ class _PostItemState extends State<PostItem> {
), ),
], ],
).padding( ).padding(
bottom: widget.showViews || bottom: (widget.showViews ||
_isTranslated || _isTranslated ||
_isTranslating _isTranslating) &&
(widget.data.repostTo != null ||
(widget.data.preload?.attachments
?.isNotEmpty ??
false))
? 8
: 0,
),
if (widget.data.repostTo != null)
_PostQuoteContent(child: widget.data.repostTo!)
.padding(
bottom:
(widget.data.preload?.attachments?.isNotEmpty ??
false)
? 8 ? 8
: 0, : 0,
), ),
@ -502,21 +761,30 @@ class _PostItemState extends State<PostItem> {
if (widget.showExpandableComments) if (widget.showExpandableComments)
_PostCommentIntent( _PostCommentIntent(
data: widget.data, data: widget.data,
maxWidth: (widget.maxWidth ?? double.infinity) -
(widget.showAvatar ? 72 : 24),
showAvatar: widget.showAvatar, showAvatar: widget.showAvatar,
).padding(left: widget.showAvatar ? 60 : 12, right: 12) ).padding(left: widget.showAvatar ? 60 : 12, right: 12)
else else if (widget.showComments)
_PostFeaturedComment(data: widget.data, maxWidth: widget.maxWidth) _PostFeaturedComment(data: widget.data, maxWidth: widget.maxWidth)
.padding(left: widget.showAvatar ? 60 : 12, right: 12), .padding(left: widget.showAvatar ? 60 : 12, right: 12),
if (widget.showReactions) if (widget.showReactions)
Padding( Container(
constraints: BoxConstraints(
maxWidth: widget.maxWidth ?? double.infinity,
),
child: Padding(
padding: const EdgeInsets.only(top: 4), padding: const EdgeInsets.only(top: 4),
child: _PostReactionList( child: _PostReactionList(
data: widget.data, data: widget.data,
padding: padding: EdgeInsets.only(
EdgeInsets.only(left: widget.showAvatar ? 60 : 12, right: 12), left: widget.showAvatar ? 60 : 12,
right: 12,
),
onChanged: _onChanged, onChanged: _onChanged,
), ),
), ),
),
], ],
); );
} }
@ -1045,6 +1313,7 @@ class _PostAvatar extends StatelessWidget {
class _PostActionPopup extends StatelessWidget { class _PostActionPopup extends StatelessWidget {
final SnPost data; final SnPost data;
final bool isAuthor; final bool isAuthor;
final bool isParentAuthor;
final Function onDeleted; final Function onDeleted;
final Function() onShare, onShareImage; final Function() onShare, onShareImage;
final Function()? onSelectAnswer; final Function()? onSelectAnswer;
@ -1052,6 +1321,7 @@ class _PostActionPopup extends StatelessWidget {
const _PostActionPopup({ const _PostActionPopup({
required this.data, required this.data,
required this.isAuthor, required this.isAuthor,
required this.isParentAuthor,
required this.onDeleted, required this.onDeleted,
required this.onShare, required this.onShare,
required this.onShareImage, required this.onShareImage,
@ -1125,7 +1395,7 @@ class _PostActionPopup extends StatelessWidget {
}, },
), ),
if (onTranslate != null) PopupMenuDivider(), if (onTranslate != null) PopupMenuDivider(),
if (isAuthor && onSelectAnswer != null) if (isParentAuthor && onSelectAnswer != null)
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@ -1138,7 +1408,7 @@ class _PostActionPopup extends StatelessWidget {
onSelectAnswer?.call(); onSelectAnswer?.call();
}, },
), ),
if (isAuthor && onSelectAnswer != null) PopupMenuDivider(), if (isParentAuthor && onSelectAnswer != null) PopupMenuDivider(),
if (isAuthor) if (isAuthor)
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
@ -1299,19 +1569,24 @@ class _PostContentHeader extends StatelessWidget {
if (isCompact) { if (isCompact) {
return Row( return Row(
children: [ children: [
Text(data.publisher.nick).bold(), Flexible(
child: Text(
data.publisher.nick,
maxLines: 1,
).bold(),
),
const Gap(4), const Gap(4),
Row( Flexible(
children: [ child: Text(
Text(
isRelativeDate isRelativeDate
? RelativeTime(context) ? RelativeTime(context)
.format((data.publishedAt ?? data.createdAt).toLocal()) .format((data.publishedAt ?? data.createdAt).toLocal())
: DateFormat('y/M/d HH:mm') : DateFormat('y/M/d HH:mm')
.format((data.publishedAt ?? data.createdAt).toLocal()), .format((data.publishedAt ?? data.createdAt).toLocal()),
).fontSize(13), maxLines: 1,
], overflow: TextOverflow.fade,
).opacity(0.8), ).fontSize(13).opacity(0.8),
),
], ],
); );
} else { } else {
@ -1330,7 +1605,10 @@ class _PostContentHeader extends StatelessWidget {
), ),
Row( Row(
children: [ children: [
Text('@${data.publisher.name}').fontSize(13), Text(
'@${data.publisher.name}',
maxLines: 1,
).fontSize(13),
const Gap(4), const Gap(4),
Text( Text(
isRelativeDate isRelativeDate
@ -1338,6 +1616,8 @@ class _PostContentHeader extends StatelessWidget {
.format((data.publishedAt ?? data.createdAt).toLocal()) .format((data.publishedAt ?? data.createdAt).toLocal())
: DateFormat('y/M/d HH:mm') : DateFormat('y/M/d HH:mm')
.format((data.publishedAt ?? data.createdAt).toLocal()), .format((data.publishedAt ?? data.createdAt).toLocal()),
maxLines: 1,
overflow: TextOverflow.fade,
).fontSize(13), ).fontSize(13),
], ],
).opacity(0.8), ).opacity(0.8),
@ -1403,10 +1683,19 @@ class _PostQuoteContent extends StatelessWidget {
children: [ children: [
Column( Column(
children: [ children: [
Row(
children: [
_PostAvatar(
data: child,
isCompact: true,
),
const Gap(8),
_PostContentHeader( _PostContentHeader(
data: child, data: child,
isCompact: true, isCompact: true,
isRelativeDate: isRelativeDate, isRelativeDate: isRelativeDate,
),
],
).padding(bottom: 4), ).padding(bottom: 4),
_PostContentBody( _PostContentBody(
data: child, data: child,
@ -1594,7 +1883,12 @@ class _PostTruncatedHint extends StatelessWidget {
class _PostCommentIntent extends StatefulWidget { class _PostCommentIntent extends StatefulWidget {
final SnPost data; final SnPost data;
final bool showAvatar; final bool showAvatar;
const _PostCommentIntent({required this.data, this.showAvatar = false}); final double maxWidth;
const _PostCommentIntent({
required this.data,
this.showAvatar = false,
required this.maxWidth,
});
@override @override
State<_PostCommentIntent> createState() => _PostCommentIntentState(); State<_PostCommentIntent> createState() => _PostCommentIntentState();
@ -1633,26 +1927,40 @@ class _PostCommentIntentState extends State<_PostCommentIntent> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Container(
constraints: BoxConstraints(maxWidth: widget.maxWidth),
child: Column(
children: [ children: [
if (_comments.isNotEmpty) if (_comments.isNotEmpty)
Card( Card(
elevation: 4,
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: Column( child: Column(
spacing: 8, spacing: 8,
children: [ children: [
for (final ele in _comments) for (final ele in _comments)
PostItem( InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: PostItem(
data: ele, data: ele,
showAvatar: false, showAvatar: false,
showCompactAvatar: true,
showExpandableComments: true, showExpandableComments: true,
showReactions: false, showReactions: false,
showViews: false, showViews: false,
maxWidth: double.infinity, maxWidth: double.infinity,
).padding(vertical: 8, left: 6), ).padding(vertical: 8, left: 6),
onTap: () {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {'slug': ele.id.toString()},
extra: ele,
);
},
),
], ],
), ),
).padding(bottom: 8), ).padding(vertical: 8),
Row( Row(
children: [ children: [
Transform.flip( Transform.flip(
@ -1660,7 +1968,8 @@ class _PostCommentIntentState extends State<_PostCommentIntent> {
child: const Icon(Symbols.comment, size: 20), child: const Icon(Symbols.comment, size: 20),
), ),
const Gap(4), const Gap(4),
Text('postCommentsDetailed'.plural(widget.data.metric.replyCount)), Text(
'postCommentsDetailed'.plural(widget.data.metric.replyCount)),
if (widget.data.metric.replyCount > 0 && !_isAllLoaded) if (widget.data.metric.replyCount > 0 && !_isAllLoaded)
SizedBox( SizedBox(
width: 20, width: 20,
@ -1680,6 +1989,7 @@ class _PostCommentIntentState extends State<_PostCommentIntent> {
], ],
).opacity(0.75).padding(horizontal: widget.showAvatar ? 4 : 0), ).opacity(0.75).padding(horizontal: widget.showAvatar ? 4 : 0),
], ],
),
); );
} }
} }

View File

@ -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: [

View File

@ -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(

View File

@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h> #include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
#include <fast_rsa/fast_rsa_plugin.h> #include <fast_rsa/fast_rsa_plugin.h>
#include <file_saver/file_saver_plugin.h> #include <file_saver/file_saver_plugin.h>
@ -23,6 +24,9 @@
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar = g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin");
bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar); bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar);

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
bitsdojo_window_linux bitsdojo_window_linux
fast_rsa fast_rsa
file_saver file_saver
@ -22,7 +23,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
croppy croppy
media_kit_native_event_loop
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)

View File

@ -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));
} }

View File

@ -5,6 +5,7 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import audioplayers_darwin
import bitsdojo_window_macos import bitsdojo_window_macos
import connectivity_plus import connectivity_plus
import device_info_plus import device_info_plus
@ -29,7 +30,6 @@ import media_kit_video
import package_info_plus import package_info_plus
import pasteboard import pasteboard
import path_provider_foundation import path_provider_foundation
import screen_brightness_macos
import share_plus import share_plus
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
@ -37,9 +37,11 @@ import sqlite3_flutter_libs
import tray_manager import tray_manager
import url_launcher_macos import url_launcher_macos
import video_compress import video_compress
import volume_controller
import wakelock_plus import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
@ -64,7 +66,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
@ -72,5 +73,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
} }

View File

@ -1,4 +1,6 @@
PODS: PODS:
- audioplayers_darwin (0.0.1):
- FlutterMacOS
- bitsdojo_window_macos (0.0.1): - bitsdojo_window_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
@ -154,8 +156,6 @@ PODS:
- FlutterMacOS - FlutterMacOS
- media_kit_libs_macos_video (1.0.4): - media_kit_libs_macos_video (1.0.4):
- FlutterMacOS - FlutterMacOS
- media_kit_native_event_loop (1.0.0):
- FlutterMacOS
- media_kit_video (0.0.1): - media_kit_video (0.0.1):
- FlutterMacOS - FlutterMacOS
- nanopb (3.30910.0): - nanopb (3.30910.0):
@ -173,8 +173,6 @@ PODS:
- FlutterMacOS - FlutterMacOS
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- SAMKeychain (1.5.3) - SAMKeychain (1.5.3)
- screen_brightness_macos (0.1.0):
- FlutterMacOS
- share_plus (0.0.1): - share_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
@ -190,6 +188,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):
@ -200,6 +200,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):
@ -208,11 +209,14 @@ PODS:
- FlutterMacOS - FlutterMacOS
- video_compress (0.3.0): - video_compress (0.3.0):
- FlutterMacOS - FlutterMacOS
- volume_controller (0.0.1):
- FlutterMacOS
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (125.6422.06) - WebRTC-SDK (125.6422.06)
DEPENDENCIES: DEPENDENCIES:
- audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`)
- bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`) - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`)
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`) - croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
@ -235,12 +239,10 @@ DEPENDENCIES:
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
- local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`) - local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`)
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`) - media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`) - pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
@ -248,6 +250,7 @@ DEPENDENCIES:
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`) - video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`)
- volume_controller (from `Flutter/ephemeral/.symlinks/plugins/volume_controller/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
SPEC REPOS: SPEC REPOS:
@ -270,6 +273,8 @@ SPEC REPOS:
- WebRTC-SDK - WebRTC-SDK
EXTERNAL SOURCES: EXTERNAL SOURCES:
audioplayers_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos
bitsdojo_window_macos: bitsdojo_window_macos:
:path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos
connectivity_plus: connectivity_plus:
@ -314,8 +319,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos :path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos
media_kit_libs_macos_video: media_kit_libs_macos_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos :path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos
media_kit_native_event_loop:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos
media_kit_video: media_kit_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos :path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
package_info_plus: package_info_plus:
@ -324,8 +327,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos :path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
path_provider_foundation: path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
screen_brightness_macos:
:path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos
share_plus: share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation: shared_preferences_foundation:
@ -340,61 +341,63 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
video_compress: video_compress:
:path: Flutter/ephemeral/.symlinks/plugins/video_compress/macos :path: Flutter/ephemeral/.symlinks/plugins/video_compress/macos
volume_controller:
:path: Flutter/ephemeral/.symlinks/plugins/volume_controller/macos
wakelock_plus: wakelock_plus:
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 audioplayers_darwin: 761f2948df701d05b5db603220c384fb55720012
connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802 bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9
croppy: 25a638bd7d05411d8c697f481568f261037694fc connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 croppy: d9bfc8c02f3cd1851f669a421df298a474b78f43
fast_rsa: 47a50bec1042c8c01726007dc0590a078418f997 device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af fast_rsa: 940a67b8d8e425f37708189361efc90be7299d66
file_saver: 44e6fbf666677faf097302460e214e977fdd977b file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
firebase_analytics: 75b9d9ea8b21ce77898a3a46910e2051e93db8e1 firebase_analytics: 2c7864ab677e8a178a6dd4126de1d19e9d9a7bf3
firebase_core: 1b573eac37729348cdc472516991dd7e269ae37e firebase_core: 3dcdf8453dfb144a023ee70f49e0463b97177f71
firebase_messaging: 0620038ea399ceae2218c9087fca00a28f576209 firebase_messaging: 96fe41b2f8b5bee4e0f21df8d716cb8c9293448c
FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917 FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8 FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
flutter_timezone: 62400baa441155f2a4144188648f2ff861649747 flutter_timezone: d59eea86178cbd7943cd2431cc2eaa9850f935d8
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52 flutter_udid: d26e455e8c06174e6aff476e147defc6cae38495
flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8 flutter_webrtc: 377dbcebdde6fed0fc40de87bcaaa2bffcec9a88
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 gal: baecd024ebfd13c441269ca7404792a7152fde89
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896 GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160 hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 in_app_review: 0599bccaed5e02f6bed2b0d30d16f86b63ed8638
livekit_client: d03409f83df069a1bb00a4c8dc78c54fb2287262 livekit_client: 35690bf9861be6325a6f7d11bb38d50c7c9fed80
local_notifier: e9506bc66fc70311e8bc7291fb70f743c081e4ff local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b package_info_plus: f0052d280d17aa382b932f399edf32507174e870
pasteboard: 9b69dba6fedbb04866be632205d532fe2f6b1d99 pasteboard: 278d8100149f940fb795d6b3a74f0720c890ecb7
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
share_plus: 1fa619de8392a4398bfaf176d441853922614e89 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f video_compress: 752b161da855df2492dd1a8fa899743cc8fe9534
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd
wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
PODFILE CHECKSUM: c2e95c8c0fe03c5c57e438583cae4cc732296009 PODFILE CHECKSUM: c2e95c8c0fe03c5c57e438583cae4cc732296009

Some files were not shown because too many files have changed in this diff Show More