Compare commits
75 Commits
Author | SHA1 | Date | |
---|---|---|---|
69d5e95565 | |||
3e3442fc89 | |||
8181010b0b | |||
269caf7555 | |||
ae0809ad35 | |||
4005f03cf8 | |||
4bd8ec54f1 | |||
51a387851f | |||
8ed847d870 | |||
dfe13de220 | |||
b02a54c1e9 | |||
55a7e7d900 | |||
3585941ccb | |||
7c6f2cc4ab | |||
61dbf92909 | |||
b69e4002e0 | |||
49aa24b79d | |||
ceb5c53229 | |||
908f0cb59e | |||
7c2b8de931 | |||
ddd0a4c3d3 | |||
99e07de243 | |||
6bb9c21759 | |||
8f2fc55608 | |||
a1c4e5eca0 | |||
10bf0883e5 | |||
595050f89f | |||
0722c99f21 | |||
12d03836f9 | |||
f78d3f4fd5 | |||
e798a8ba76 | |||
c28a664373 | |||
4589722c3b | |||
38e1c51b45 | |||
610ddec05c | |||
d0276f9ac6 | |||
c1e89a2ee6 | |||
ecc79368a1 | |||
e6d732c86a | |||
dd055fb077 | |||
280840c6d8 | |||
bde62a7b2c | |||
5445c570a2 | |||
b2302f5b3c | |||
d7359cfd0d | |||
9cc577adbe | |||
dd196b7754 | |||
16c07c2133 | |||
6bcb658d44 | |||
9311bfc3b5 | |||
8dd6435a30 | |||
21a1d4a2ad | |||
603875b1af | |||
4209a13c84 | |||
55b79bfd8f | |||
6e6c3f42f6 | |||
dc38b46b2c | |||
b4990308e9 | |||
237abe564d | |||
71b41d470a | |||
7052b5b635 | |||
f356e08f79 | |||
152872db65 | |||
dfe117d04f | |||
caf63f0cbe | |||
b8f5cc82f9 | |||
360bc50f21 | |||
2de93a0486 | |||
02227852f8 | |||
ad16de595b | |||
9f8c8923d9 | |||
060bfa4887 | |||
e68ada2d04 | |||
d6013078bd | |||
5976d61997 |
26
.github/workflows/nightly.yml
vendored
26
.github/workflows/nightly.yml
vendored
@ -48,14 +48,15 @@ jobs:
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
cache: true
|
||||
- run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev
|
||||
sudo apt-get install libmpv-dev mpv
|
||||
sudo apt-get install libayatana-appindicator3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
sudo apt-get install libnotify-dev
|
||||
sudo apt-get install -y libmpv-dev mpv
|
||||
sudo apt-get install -y libayatana-appindicator3-dev
|
||||
sudo apt-get install -y keybinder-3.0
|
||||
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 build linux
|
||||
- name: Archive production artifacts
|
||||
@ -63,3 +64,18 @@ jobs:
|
||||
with:
|
||||
name: build-output-linux
|
||||
path: build/linux/x64/release/bundle
|
||||
- name: Build AppImage
|
||||
run: |
|
||||
rm -r Solian.AppDir | true
|
||||
mkdir Solian.AppDir
|
||||
cp -r build/linux/x64/release/bundle/* Solian.AppDir
|
||||
cp -r buildtools/appimage_config/* Solian.AppDir
|
||||
cp assets/icon/icon-light-radius.png Solian.AppDir
|
||||
sudo chmod +x buildtools/appimagetool-x86_64.AppImage
|
||||
sudo chmod +x Solian.AppDir/AppRun
|
||||
./buildtools/appimagetool-x86_64.AppImage Solian.AppDir
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-output-linux-appimage
|
||||
path: './*.AppImage*'
|
||||
|
BIN
assets/audio/notify/metal-pipe.mp3
Normal file
BIN
assets/audio/notify/metal-pipe.mp3
Normal file
Binary file not shown.
BIN
assets/audio/sfx/launch-done.mp3
Normal file
BIN
assets/audio/sfx/launch-done.mp3
Normal file
Binary file not shown.
BIN
assets/audio/sfx/launch-intro.mp3
Normal file
BIN
assets/audio/sfx/launch-intro.mp3
Normal file
Binary file not shown.
BIN
assets/icon/kanban-1st.jpg
Executable file
BIN
assets/icon/kanban-1st.jpg
Executable file
Binary file not shown.
After Width: | Height: | Size: 509 KiB |
@ -130,7 +130,7 @@
|
||||
"accountPublishersSubtitle": "Manage your publish identities.",
|
||||
"accountSettings": "Account Settings",
|
||||
"accountSettingsSubtitle": "Manage your account and make it yours.",
|
||||
"accountProfileEdit": "Edit your profile",
|
||||
"accountProfileEdit": "Edit Profile",
|
||||
"accountProfileEditSubtitle": "Make your Solarpass account more looks like you.",
|
||||
"accountWallet": "Wallet",
|
||||
"accountWalletSubtitle": "View your balance and transactions.",
|
||||
@ -338,6 +338,7 @@
|
||||
"fieldAttachmentRandomId": "Random ID",
|
||||
"fieldAttachmentAlt": "Alternative text",
|
||||
"addAttachmentFromAlbum": "Add from album",
|
||||
"addAttachmentFromFiles": "Add from files",
|
||||
"addAttachmentFromClipboard": "Paste file",
|
||||
"addAttachmentFromCameraPhoto": "Take photo",
|
||||
"addAttachmentFromCameraVideo": "Take video",
|
||||
@ -638,6 +639,7 @@
|
||||
"postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
|
||||
"postQuestionAnswered": "Answered Question",
|
||||
"postQuestionAnswerSelect": "Select as Answer",
|
||||
"postQuestionAnswerTitle": "Selected Question",
|
||||
"postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
|
||||
"postVideoUpload": "Upload Video",
|
||||
"realmJoin": "Join Realm",
|
||||
@ -846,5 +848,105 @@
|
||||
"translating": "Translating…",
|
||||
"translated": "Translated",
|
||||
"settingsAutoTranslate": "Auto Translate",
|
||||
"settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages."
|
||||
"settingsAutoTranslateDescription": "Automatically translate text when viewing posts and messages.",
|
||||
"trayMenuHide": "Hide",
|
||||
"accountSettingsNotify": "Notify Settings",
|
||||
"accountSettingsNotifyDescription": "Adjust the types of notifications you receive.",
|
||||
"accountSettingsSecurity": "Security Settings",
|
||||
"accountSettingsSecurityDescription": "Adjust your account security settings.",
|
||||
"save": "Save",
|
||||
"notificationTopicPostFeedback": "Post Feedback",
|
||||
"notificationTopicPostReply": "Post Replies",
|
||||
"notificationTopicPostSubscription": "Post Subscriptions",
|
||||
"notificationTopicMessaging": "New Messages",
|
||||
"notificationTopicMessagingCall": "Incoming Calls",
|
||||
"notificationTopicGeneral": "General",
|
||||
"authMaximumAuthSteps": "Maximum Authenticate Steps",
|
||||
"authMaximumAuthStepsDescription": {
|
||||
"one": "Maximum ask for {} step authenticate",
|
||||
"other": "Maximum ask for {} steps authenticate"
|
||||
},
|
||||
"authAlwaysRisky": "Always Risky",
|
||||
"authAlwaysRiskyDescription": "Always ask for the highest steps count of authentication when logging in.",
|
||||
"chatUnjoined": "Unjoined Channel",
|
||||
"chatUnjoinedDescription": "You haven't joined this channel, so you can't send messages either view messages in it.",
|
||||
"chatUnjoinedPublicDescription": "Fortunately, this is a public channel, so you can join it as you want.",
|
||||
"chatJoin": "Join the Channel",
|
||||
"appInitStarting": "Starting",
|
||||
"appInitNetwork": "Initializing Network",
|
||||
"appInitUserdata": "Initializing User Data",
|
||||
"appInitWebsocket": "Establishing Solar Link",
|
||||
"appInitNotification": "Initializing Push Notifications",
|
||||
"appInitKeyPair": "Initializing Key Pairs",
|
||||
"appInitStickers": "Initializing Stickers",
|
||||
"appInitUserDirectory": "Initializing User Directory",
|
||||
"appInitRealm": "Initializing Realms",
|
||||
"appInitChat": "Initializing Chat",
|
||||
"appInitDone": "Completed",
|
||||
"community": "Community",
|
||||
"realmCommunity": "{}'s Community",
|
||||
"postTotalCount": {
|
||||
"one": "Total {} post",
|
||||
"other": "Total {} posts"
|
||||
},
|
||||
"settingsHideBottomNav": "Hide Bottom Navigation",
|
||||
"settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.",
|
||||
"reCaptcha": "reCaptcha",
|
||||
"friends": "Friends",
|
||||
"friendsDescription": "Manage your friendships.",
|
||||
"album": "Album",
|
||||
"albumDescription": "View albums and manage attachments.",
|
||||
"stickers": "Stickers",
|
||||
"stickersDescription": "View sticker packs and manage stickers.",
|
||||
"navBottomUnauthorizedCaption": "Or create an account",
|
||||
"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.",
|
||||
"chatDirect": "Direct Messages",
|
||||
"back": "Back",
|
||||
"badgeProgramDeveloper": "Developer Program Member",
|
||||
"badgeProgramStellar": "A Stellar",
|
||||
"badgeProgramModerator": "Community Moderator",
|
||||
"postEditedHint": "edited",
|
||||
"splashScreenServer": "Server",
|
||||
"splashScreenServerName": "Potato",
|
||||
"splashScreenCaption": "Trying to establishing connection with HyperNet™"
|
||||
}
|
||||
|
@ -336,6 +336,7 @@
|
||||
"fieldAttachmentRandomId": "访问 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "从相册中添加附件",
|
||||
"addAttachmentFromFiles": "从文件中添加附件",
|
||||
"addAttachmentFromClipboard": "粘贴附件",
|
||||
"addAttachmentFromCameraPhoto": "拍摄照片",
|
||||
"addAttachmentFromCameraVideo": "拍摄视频",
|
||||
@ -844,5 +845,105 @@
|
||||
"translating": "正在翻译……",
|
||||
"translated": "已翻译",
|
||||
"settingsAutoTranslate": "自动翻译",
|
||||
"settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。"
|
||||
"settingsAutoTranslateDescription": "在查看帖子、消息时自动翻译文本。",
|
||||
"trayMenuHide": "隐藏",
|
||||
"accountSettingsNotify": "通知设置",
|
||||
"accountSettingsNotifyDescription": "调整你所收到的通知种类。",
|
||||
"accountSettingsSecurity": "安全设置",
|
||||
"accountSettingsSecurityDescription": "调整你的帐户安全设置。",
|
||||
"save": "保存",
|
||||
"notificationTopicPostFeedback": "帖子数据反馈",
|
||||
"notificationTopicPostReply": "帖子回复",
|
||||
"notificationTopicPostSubscription": "帖子订阅",
|
||||
"notificationTopicMessaging": "消息",
|
||||
"notificationTopicMessagingCall": "通话",
|
||||
"notificationTopicGeneral": "杂项",
|
||||
"authMaximumAuthSteps": "最大验证步骤",
|
||||
"authMaximumAuthStepsDescription": {
|
||||
"one": "登入时最多要求 {} 步验证",
|
||||
"other": "登入时最多要求 {} 步验证"
|
||||
},
|
||||
"authAlwaysRisky": "总是风险",
|
||||
"authAlwaysRiskyDescription": "在登入时始终按最高标准要求验证。",
|
||||
"chatUnjoined": "未加入频道",
|
||||
"chatUnjoinedDescription": "你没有加入这个频道,所以你也无法发送消息或者查看这个频道中的消息。",
|
||||
"chatUnjoinedPublicDescription": "但幸运的是,这是一个公开频道,所以你可以主动加入。",
|
||||
"chatJoin": "加入频道",
|
||||
"appInitStarting": "启动中",
|
||||
"appInitNetwork": "正在初始化网络",
|
||||
"appInitUserdata": "正在初始化用户数据",
|
||||
"appInitWebsocket": "正在建立 Solar Link",
|
||||
"appInitNotification": "正在初始化推送通知",
|
||||
"appInitKeyPair": "正在初始化密钥对",
|
||||
"appInitStickers": "正在初始化贴图包",
|
||||
"appInitUserDirectory": "正在初始化用户目录",
|
||||
"appInitRealm": "正在初始化领域信息",
|
||||
"appInitChat": "正在初始化聊天",
|
||||
"appInitDone": "完成",
|
||||
"community": "社区",
|
||||
"realmCommunity": "{}的社区",
|
||||
"postTotalCount": {
|
||||
"zero": "没有帖子",
|
||||
"one": "共 {} 条帖子"
|
||||
},
|
||||
"settingsHideBottomNav": "隐藏底部导航栏",
|
||||
"settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。",
|
||||
"reCaptcha": "人机验证",
|
||||
"friends": "好友",
|
||||
"friendsDescription": "管理好友关系。",
|
||||
"album": "相册",
|
||||
"albumDescription": "查看相册与管理上传附件。",
|
||||
"stickers": "贴图",
|
||||
"stickersDescription": "查看贴图包与管理贴图。",
|
||||
"navBottomUnauthorizedCaption": "或者注册一个账号",
|
||||
"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": "重置记忆的窗口大小,以重新设置为默认大小。",
|
||||
"chatDirect": "私信",
|
||||
"back": "返回",
|
||||
"badgeProgramDeveloper": "开发者计划成员",
|
||||
"badgeProgramStellar": "一颗恒星",
|
||||
"badgeProgramModerator": "社区管理员",
|
||||
"postEditedHint": "已编辑",
|
||||
"splashScreenServer": "服务器",
|
||||
"splashScreenServerName": "土豆",
|
||||
"splashScreenCaption": "正在尝试与 HyperNet™ 取得太阳链连接"
|
||||
}
|
||||
|
@ -336,6 +336,7 @@
|
||||
"fieldAttachmentRandomId": "訪問 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||
"addAttachmentFromFiles": "從文件中添加附件",
|
||||
"addAttachmentFromClipboard": "粘貼附件",
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||
@ -844,5 +845,55 @@
|
||||
"translating": "正在翻譯……",
|
||||
"translated": "已翻譯",
|
||||
"settingsAutoTranslate": "自動翻譯",
|
||||
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。"
|
||||
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。",
|
||||
"trayMenuHide": "隱藏",
|
||||
"accountSettingsNotify": "通知設置",
|
||||
"accountSettingsNotifyDescription": "調整你所收到的通知種類。",
|
||||
"accountSettingsSecurity": "安全設置",
|
||||
"accountSettingsSecurityDescription": "調整你的帳户安全設置。",
|
||||
"save": "保存",
|
||||
"notificationTopicPostFeedback": "帖子數據反饋",
|
||||
"notificationTopicPostReply": "帖子回覆",
|
||||
"notificationTopicPostSubscription": "帖子訂閲",
|
||||
"notificationTopicMessaging": "消息",
|
||||
"notificationTopicMessagingCall": "通話",
|
||||
"notificationTopicGeneral": "雜項",
|
||||
"authMaximumAuthSteps": "最大驗證步驟",
|
||||
"authMaximumAuthStepsDescription": {
|
||||
"one": "登入時最多要求 {} 步驗證",
|
||||
"other": "登入時最多要求 {} 步驗證"
|
||||
},
|
||||
"authAlwaysRisky": "總是風險",
|
||||
"authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。",
|
||||
"chatUnjoined": "未加入頻道",
|
||||
"chatUnjoinedDescription": "你沒有加入這個頻道,所以你也無法發送消息或者查看這個頻道中的消息。",
|
||||
"chatUnjoinedPublicDescription": "但幸運的是,這是一個公開頻道,所以你可以主動加入。",
|
||||
"chatJoin": "加入頻道",
|
||||
"appInitStarting": "啓動中",
|
||||
"appInitNetwork": "正在初始化網絡",
|
||||
"appInitUserdata": "正在初始化用户數據",
|
||||
"appInitWebsocket": "正在建立 Solar Link",
|
||||
"appInitNotification": "正在初始化推送通知",
|
||||
"appInitKeyPair": "正在初始化密鑰對",
|
||||
"appInitStickers": "正在初始化貼圖包",
|
||||
"appInitUserDirectory": "正在初始化用户目錄",
|
||||
"appInitRealm": "正在初始化領域信息",
|
||||
"appInitChat": "正在初始化聊天",
|
||||
"appInitDone": "完成",
|
||||
"community": "社區",
|
||||
"realmCommunity": "{}的社區",
|
||||
"postTotalCount": {
|
||||
"zero": "沒有帖子",
|
||||
"one": "共 {} 條帖子"
|
||||
},
|
||||
"settingsHideBottomNav": "隱藏底部導航欄",
|
||||
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
|
||||
"reCaptcha": "人機驗證",
|
||||
"friends": "好友",
|
||||
"friendsDescription": "管理好友關係。",
|
||||
"album": "相冊",
|
||||
"albumDescription": "查看相冊與管理上傳附件。",
|
||||
"stickers": "貼圖",
|
||||
"stickersDescription": "查看貼圖包與管理貼圖。",
|
||||
"navBottomUnauthorizedCaption": "或者註冊一個賬號"
|
||||
}
|
||||
|
@ -336,6 +336,7 @@
|
||||
"fieldAttachmentRandomId": "訪問 ID",
|
||||
"fieldAttachmentAlt": "概述文字",
|
||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||
"addAttachmentFromFiles": "從文件中添加附件",
|
||||
"addAttachmentFromClipboard": "粘貼附件",
|
||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||
@ -844,5 +845,55 @@
|
||||
"translating": "正在翻譯……",
|
||||
"translated": "已翻譯",
|
||||
"settingsAutoTranslate": "自動翻譯",
|
||||
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。"
|
||||
"settingsAutoTranslateDescription": "在查看帖子、消息時自動翻譯文本。",
|
||||
"trayMenuHide": "隱藏",
|
||||
"accountSettingsNotify": "通知設置",
|
||||
"accountSettingsNotifyDescription": "調整你所收到的通知種類。",
|
||||
"accountSettingsSecurity": "安全設置",
|
||||
"accountSettingsSecurityDescription": "調整你的帳戶安全設置。",
|
||||
"save": "保存",
|
||||
"notificationTopicPostFeedback": "帖子數據反饋",
|
||||
"notificationTopicPostReply": "帖子回覆",
|
||||
"notificationTopicPostSubscription": "帖子訂閱",
|
||||
"notificationTopicMessaging": "消息",
|
||||
"notificationTopicMessagingCall": "通話",
|
||||
"notificationTopicGeneral": "雜項",
|
||||
"authMaximumAuthSteps": "最大驗證步驟",
|
||||
"authMaximumAuthStepsDescription": {
|
||||
"one": "登入時最多要求 {} 步驗證",
|
||||
"other": "登入時最多要求 {} 步驗證"
|
||||
},
|
||||
"authAlwaysRisky": "總是風險",
|
||||
"authAlwaysRiskyDescription": "在登入時始終按最高標準要求驗證。",
|
||||
"chatUnjoined": "未加入頻道",
|
||||
"chatUnjoinedDescription": "你沒有加入這個頻道,所以你也無法發送消息或者查看這個頻道中的消息。",
|
||||
"chatUnjoinedPublicDescription": "但幸運的是,這是一個公開頻道,所以你可以主動加入。",
|
||||
"chatJoin": "加入頻道",
|
||||
"appInitStarting": "啟動中",
|
||||
"appInitNetwork": "正在初始化網絡",
|
||||
"appInitUserdata": "正在初始化用戶數據",
|
||||
"appInitWebsocket": "正在建立 Solar Link",
|
||||
"appInitNotification": "正在初始化推送通知",
|
||||
"appInitKeyPair": "正在初始化密鑰對",
|
||||
"appInitStickers": "正在初始化貼圖包",
|
||||
"appInitUserDirectory": "正在初始化用戶目錄",
|
||||
"appInitRealm": "正在初始化領域信息",
|
||||
"appInitChat": "正在初始化聊天",
|
||||
"appInitDone": "完成",
|
||||
"community": "社區",
|
||||
"realmCommunity": "{}的社區",
|
||||
"postTotalCount": {
|
||||
"zero": "沒有帖子",
|
||||
"one": "共 {} 條帖子"
|
||||
},
|
||||
"settingsHideBottomNav": "隱藏底部導航欄",
|
||||
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
|
||||
"reCaptcha": "人機驗證",
|
||||
"friends": "好友",
|
||||
"friendsDescription": "管理好友關係。",
|
||||
"album": "相冊",
|
||||
"albumDescription": "查看相冊與管理上傳附件。",
|
||||
"stickers": "貼圖",
|
||||
"stickersDescription": "查看貼圖包與管理貼圖。",
|
||||
"navBottomUnauthorizedCaption": "或者註冊一個賬號"
|
||||
}
|
||||
|
4
buildtools/appimage_config/AppRun
Executable file
4
buildtools/appimage_config/AppRun
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
exec ./surface
|
8
buildtools/appimage_config/Solian.desktop
Normal file
8
buildtools/appimage_config/Solian.desktop
Normal file
@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Terminal=false
|
||||
Name=Solian
|
||||
Exec=surface %u
|
||||
Icon=icon-light-radius
|
||||
Categories=Network;
|
BIN
buildtools/appimagetool-x86_64.AppImage
Executable file
BIN
buildtools/appimagetool-x86_64.AppImage
Executable file
Binary file not shown.
1
drift_schemas/my_database/drift_schema_v4.json
Normal file
1
drift_schemas/my_database/drift_schema_v4.json
Normal file
File diff suppressed because one or more lines are too long
104
ios/Podfile.lock
104
ios/Podfile.lock
@ -1,5 +1,7 @@
|
||||
PODS:
|
||||
- Alamofire (5.10.2)
|
||||
- audioplayers_darwin (0.0.1):
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- croppy (0.0.1):
|
||||
@ -187,9 +189,12 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
- WebRTC-SDK (= 125.6422.06)
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
- livekit_noise_filter (0.0.1):
|
||||
- Flutter
|
||||
- media_kit_native_event_loop (1.0.0):
|
||||
- flutter_webrtc
|
||||
- LiveKitKrispNoiseFilter (= 0.0.7)
|
||||
- LiveKitKrispNoiseFilter (0.0.7)
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
- Flutter
|
||||
- media_kit_video (0.0.1):
|
||||
- Flutter
|
||||
@ -212,8 +217,6 @@ PODS:
|
||||
- receive_sharing_intent (1.8.1):
|
||||
- Flutter
|
||||
- SAMKeychain (1.5.3)
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- SDWebImage (5.20.1):
|
||||
- SDWebImage/Core (= 5.20.1)
|
||||
- SDWebImage/Core (5.20.1)
|
||||
@ -232,6 +235,8 @@ PODS:
|
||||
- sqlite3/common
|
||||
- sqlite3/fts5 (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/math (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/perf-threadsafe (3.49.1):
|
||||
- sqlite3/common
|
||||
- sqlite3/rtree (3.49.1):
|
||||
@ -242,6 +247,7 @@ PODS:
|
||||
- sqlite3 (~> 3.49.1)
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/math
|
||||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- SwiftyGif (5.4.5)
|
||||
@ -259,6 +265,7 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- Alamofire
|
||||
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- croppy (from `.symlinks/plugins/croppy/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
@ -281,15 +288,14 @@ DEPENDENCIES:
|
||||
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
|
||||
- Kingfisher (~> 8.0)
|
||||
- livekit_client (from `.symlinks/plugins/livekit_client/ios`)
|
||||
- livekit_noise_filter (from `.symlinks/plugins/livekit_noise_filter/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`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/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`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
@ -315,6 +321,7 @@ SPEC REPOS:
|
||||
- GoogleDataTransport
|
||||
- GoogleUtilities
|
||||
- Kingfisher
|
||||
- LiveKitKrispNoiseFilter
|
||||
- nanopb
|
||||
- OrderedSet
|
||||
- PromisesObjC
|
||||
@ -325,6 +332,8 @@ SPEC REPOS:
|
||||
- WebRTC-SDK
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
audioplayers_darwin:
|
||||
:path: ".symlinks/plugins/audioplayers_darwin/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
croppy:
|
||||
@ -367,10 +376,10 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/in_app_review/ios"
|
||||
livekit_client:
|
||||
:path: ".symlinks/plugins/livekit_client/ios"
|
||||
livekit_noise_filter:
|
||||
:path: ".symlinks/plugins/livekit_noise_filter/ios"
|
||||
media_kit_libs_ios_video:
|
||||
: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:
|
||||
:path: ".symlinks/plugins/media_kit_video/ios"
|
||||
package_info_plus:
|
||||
@ -383,8 +392,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
receive_sharing_intent:
|
||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||
screen_brightness_ios:
|
||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
@ -406,65 +413,66 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
croppy: b6199bc8d56bd2e03cc11609d1c47ad9875c1321
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
audioplayers_darwin: ccf9c770ee768abb07e26d90af093f7bab1c12ab
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
fast_rsa: dc48fb05f26bb108863de122b2a9f5554e8e2591
|
||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
fast_rsa: d99f8e1809a4a312fa9216d830186869b2e9eb65
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
|
||||
firebase_analytics: e3b6782e70e32b7fa18f7cd233e3201975dd86aa
|
||||
firebase_core: ac395f994af4e28f6a38b59e05a88ca57abeb874
|
||||
firebase_messaging: 7e223f4ee7ca053bf4ce43748e84a6d774ec9728
|
||||
firebase_analytics: 4e93dbe66872104d28ae9750fec1800e1fd66858
|
||||
firebase_core: 8d552814f6c01ccde5d88939fced4ec26f2f5510
|
||||
firebase_messaging: 8b96a4f09841c15a16b96973ef5c3dcfc1a064e4
|
||||
FirebaseAnalytics: 4fd42def128146e24e480e89f310e3d8534ea42b
|
||||
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
|
||||
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
|
||||
FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917
|
||||
FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
|
||||
flutter_timezone: ac3da59ac941ff1c98a2e1f0293420e020120282
|
||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||
flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
|
||||
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
|
||||
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
|
||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544
|
||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||
flutter_webrtc: 57f32415b8744e806f9c2a96ccdb60c6a627ba33
|
||||
gal: baecd024ebfd13c441269ca7404792a7152fde89
|
||||
GoogleAppMeasurement: fc0817122bd4d4189164f85374e06773b9561896
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
in_app_review: 5596fe56fab799e8edb3561c03d053363ab13457
|
||||
Kingfisher: 323e5c4ec7983aaace12af655a7b51a7f88a599d
|
||||
livekit_client: 170022ce5f7c8c70d7f862ac9c17e11508ad5fbc
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
livekit_client: 08755cabfa4da4ed455642f460cfbb39bc518070
|
||||
livekit_noise_filter: a26aeb1c1eae6db0a023fd2f6ea3ff108c3ecbb0
|
||||
LiveKitKrispNoiseFilter: efe418ceca28163ace0ff222bd2cc02384645d84
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
receive_sharing_intent: 79c848f5b045674ad60b9fea3bafea59962ad2c1
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
SDWebImage: 33d0f23bddeb5d209ae959153883247be6703713
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
||||
sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
video_compress: f2133a07762889d67f0711ac831faa26f956980e
|
||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
workmanager: 01be2de7f184bd15de93a1812936a2b7f42ef07e
|
||||
|
||||
PODFILE CHECKSUM: 9b244e02f87527430136c8d21cbdcf1cd586b6bc
|
||||
|
||||
|
@ -79,6 +79,8 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
@ -241,7 +241,7 @@ class PostWriteController extends ChangeNotifier {
|
||||
contentController.text = post.body['content'] ?? '';
|
||||
aliasController.text = post.alias ?? '';
|
||||
rewardController.text = post.body['reward']?.toString() ?? '';
|
||||
videoAttachment = post.preload?.video;
|
||||
videoAttachment = SnAttachment.fromJson(post.body['video']);
|
||||
publishedAt = post.publishedAt;
|
||||
publishedUntil = post.publishedUntil;
|
||||
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
|
||||
@ -252,17 +252,21 @@ class PostWriteController extends ChangeNotifier {
|
||||
categories =
|
||||
List.from(post.categories.map((ele) => ele.alias), growable: true);
|
||||
attachments.addAll(
|
||||
post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||
poll = post.preload?.poll;
|
||||
post.body['attachments']
|
||||
.where(SnAttachment.fromJson)
|
||||
?.map(PostWriteMedia) ??
|
||||
[],
|
||||
);
|
||||
poll = post.poll;
|
||||
|
||||
editingDraft = post.isDraft;
|
||||
|
||||
if (post.preload?.thumbnail != null &&
|
||||
(post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||
thumbnail = PostWriteMedia(post.preload!.thumbnail);
|
||||
if (post.body['thumbnail'] != null) {
|
||||
thumbnail =
|
||||
PostWriteMedia(SnAttachment.fromJson(post.body['thumbnail']));
|
||||
}
|
||||
if (post.preload?.realm != null) {
|
||||
realm = post.preload!.realm!;
|
||||
if (post.realm != null) {
|
||||
realm = post.realm!;
|
||||
}
|
||||
|
||||
editingPost = post;
|
||||
|
@ -6,10 +6,12 @@ import 'package:surface/database/attachment.dart';
|
||||
import 'package:surface/database/chat.dart';
|
||||
import 'package:surface/database/database.steps.dart';
|
||||
import 'package:surface/database/keypair.dart';
|
||||
import 'package:surface/database/realm.dart';
|
||||
import 'package:surface/database/sticker.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@ -22,12 +24,13 @@ part 'database.g.dart';
|
||||
SnLocalAttachment,
|
||||
SnLocalSticker,
|
||||
SnLocalStickerPack,
|
||||
SnLocalRealm,
|
||||
])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase([QueryExecutor? e]) : super(e ?? _openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 3;
|
||||
int get schemaVersion => 4;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(
|
||||
@ -49,6 +52,10 @@ class AppDatabase extends _$AppDatabase {
|
||||
// Nothing else to do here
|
||||
}, from2To3: (m, schema) async {
|
||||
// Nothing else to do here, too
|
||||
}, from3To4: (m, schema) async {
|
||||
m.createTable(schema.snLocalRealm);
|
||||
m.createIndex(schema.idxRealmAccount);
|
||||
m.createIndex(schema.idxRealmAlias);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -2454,6 +2454,351 @@ class SnLocalStickerPackCompanion
|
||||
}
|
||||
}
|
||||
|
||||
class $SnLocalRealmTable extends SnLocalRealm
|
||||
with TableInfo<$SnLocalRealmTable, SnLocalRealmData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$SnLocalRealmTable(this.attachedDatabase, [this._alias]);
|
||||
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
@override
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
static const VerificationMeta _aliasMeta = const VerificationMeta('alias');
|
||||
@override
|
||||
late final GeneratedColumn<String> alias = GeneratedColumn<String>(
|
||||
'alias', aliasedName, false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
defaultConstraints: GeneratedColumn.constraintIsAlways('UNIQUE'));
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<SnRealm, String> content =
|
||||
GeneratedColumn<String>('content', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true)
|
||||
.withConverter<SnRealm>($SnLocalRealmTable.$convertercontent);
|
||||
static const VerificationMeta _accountIdMeta =
|
||||
const VerificationMeta('accountId');
|
||||
@override
|
||||
late final GeneratedColumn<int> accountId = GeneratedColumn<int>(
|
||||
'account_id', aliasedName, false,
|
||||
type: DriftSqlType.int, requiredDuringInsert: true);
|
||||
static const VerificationMeta _createdAtMeta =
|
||||
const VerificationMeta('createdAt');
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||
'created_at', aliasedName, false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: currentDateAndTime);
|
||||
static const VerificationMeta _cacheExpiredAtMeta =
|
||||
const VerificationMeta('cacheExpiredAt');
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> cacheExpiredAt =
|
||||
GeneratedColumn<DateTime>('cache_expired_at', aliasedName, false,
|
||||
type: DriftSqlType.dateTime, requiredDuringInsert: true);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns =>
|
||||
[id, alias, content, accountId, createdAt, cacheExpiredAt];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'sn_local_realm';
|
||||
@override
|
||||
VerificationContext validateIntegrity(Insertable<SnLocalRealmData> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
if (data.containsKey('alias')) {
|
||||
context.handle(
|
||||
_aliasMeta, alias.isAcceptableOrUnknown(data['alias']!, _aliasMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_aliasMeta);
|
||||
}
|
||||
if (data.containsKey('account_id')) {
|
||||
context.handle(_accountIdMeta,
|
||||
accountId.isAcceptableOrUnknown(data['account_id']!, _accountIdMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_accountIdMeta);
|
||||
}
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||
}
|
||||
if (data.containsKey('cache_expired_at')) {
|
||||
context.handle(
|
||||
_cacheExpiredAtMeta,
|
||||
cacheExpiredAt.isAcceptableOrUnknown(
|
||||
data['cache_expired_at']!, _cacheExpiredAtMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_cacheExpiredAtMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
SnLocalRealmData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return SnLocalRealmData(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
alias: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}alias'])!,
|
||||
content: $SnLocalRealmTable.$convertercontent.fromSql(attachedDatabase
|
||||
.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}content'])!),
|
||||
accountId: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}account_id'])!,
|
||||
createdAt: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||
cacheExpiredAt: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.dateTime, data['${effectivePrefix}cache_expired_at'])!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$SnLocalRealmTable createAlias(String alias) {
|
||||
return $SnLocalRealmTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static JsonTypeConverter2<SnRealm, String, Map<String, Object?>>
|
||||
$convertercontent = const SnRealmConverter();
|
||||
}
|
||||
|
||||
class SnLocalRealmData extends DataClass
|
||||
implements Insertable<SnLocalRealmData> {
|
||||
final int id;
|
||||
final String alias;
|
||||
final SnRealm content;
|
||||
final int accountId;
|
||||
final DateTime createdAt;
|
||||
final DateTime cacheExpiredAt;
|
||||
const SnLocalRealmData(
|
||||
{required this.id,
|
||||
required this.alias,
|
||||
required this.content,
|
||||
required this.accountId,
|
||||
required this.createdAt,
|
||||
required this.cacheExpiredAt});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<int>(id);
|
||||
map['alias'] = Variable<String>(alias);
|
||||
{
|
||||
map['content'] =
|
||||
Variable<String>($SnLocalRealmTable.$convertercontent.toSql(content));
|
||||
}
|
||||
map['account_id'] = Variable<int>(accountId);
|
||||
map['created_at'] = Variable<DateTime>(createdAt);
|
||||
map['cache_expired_at'] = Variable<DateTime>(cacheExpiredAt);
|
||||
return map;
|
||||
}
|
||||
|
||||
SnLocalRealmCompanion toCompanion(bool nullToAbsent) {
|
||||
return SnLocalRealmCompanion(
|
||||
id: Value(id),
|
||||
alias: Value(alias),
|
||||
content: Value(content),
|
||||
accountId: Value(accountId),
|
||||
createdAt: Value(createdAt),
|
||||
cacheExpiredAt: Value(cacheExpiredAt),
|
||||
);
|
||||
}
|
||||
|
||||
factory SnLocalRealmData.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return SnLocalRealmData(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
alias: serializer.fromJson<String>(json['alias']),
|
||||
content: $SnLocalRealmTable.$convertercontent
|
||||
.fromJson(serializer.fromJson<Map<String, Object?>>(json['content'])),
|
||||
accountId: serializer.fromJson<int>(json['accountId']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
cacheExpiredAt: serializer.fromJson<DateTime>(json['cacheExpiredAt']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'alias': serializer.toJson<String>(alias),
|
||||
'content': serializer.toJson<Map<String, Object?>>(
|
||||
$SnLocalRealmTable.$convertercontent.toJson(content)),
|
||||
'accountId': serializer.toJson<int>(accountId),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
'cacheExpiredAt': serializer.toJson<DateTime>(cacheExpiredAt),
|
||||
};
|
||||
}
|
||||
|
||||
SnLocalRealmData copyWith(
|
||||
{int? id,
|
||||
String? alias,
|
||||
SnRealm? content,
|
||||
int? accountId,
|
||||
DateTime? createdAt,
|
||||
DateTime? cacheExpiredAt}) =>
|
||||
SnLocalRealmData(
|
||||
id: id ?? this.id,
|
||||
alias: alias ?? this.alias,
|
||||
content: content ?? this.content,
|
||||
accountId: accountId ?? this.accountId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
cacheExpiredAt: cacheExpiredAt ?? this.cacheExpiredAt,
|
||||
);
|
||||
SnLocalRealmData copyWithCompanion(SnLocalRealmCompanion data) {
|
||||
return SnLocalRealmData(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
alias: data.alias.present ? data.alias.value : this.alias,
|
||||
content: data.content.present ? data.content.value : this.content,
|
||||
accountId: data.accountId.present ? data.accountId.value : this.accountId,
|
||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||
cacheExpiredAt: data.cacheExpiredAt.present
|
||||
? data.cacheExpiredAt.value
|
||||
: this.cacheExpiredAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('SnLocalRealmData(')
|
||||
..write('id: $id, ')
|
||||
..write('alias: $alias, ')
|
||||
..write('content: $content, ')
|
||||
..write('accountId: $accountId, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('cacheExpiredAt: $cacheExpiredAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(id, alias, content, accountId, createdAt, cacheExpiredAt);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is SnLocalRealmData &&
|
||||
other.id == this.id &&
|
||||
other.alias == this.alias &&
|
||||
other.content == this.content &&
|
||||
other.accountId == this.accountId &&
|
||||
other.createdAt == this.createdAt &&
|
||||
other.cacheExpiredAt == this.cacheExpiredAt);
|
||||
}
|
||||
|
||||
class SnLocalRealmCompanion extends UpdateCompanion<SnLocalRealmData> {
|
||||
final Value<int> id;
|
||||
final Value<String> alias;
|
||||
final Value<SnRealm> content;
|
||||
final Value<int> accountId;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<DateTime> cacheExpiredAt;
|
||||
const SnLocalRealmCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.alias = const Value.absent(),
|
||||
this.content = const Value.absent(),
|
||||
this.accountId = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
this.cacheExpiredAt = const Value.absent(),
|
||||
});
|
||||
SnLocalRealmCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
required String alias,
|
||||
required SnRealm content,
|
||||
required int accountId,
|
||||
this.createdAt = const Value.absent(),
|
||||
required DateTime cacheExpiredAt,
|
||||
}) : alias = Value(alias),
|
||||
content = Value(content),
|
||||
accountId = Value(accountId),
|
||||
cacheExpiredAt = Value(cacheExpiredAt);
|
||||
static Insertable<SnLocalRealmData> custom({
|
||||
Expression<int>? id,
|
||||
Expression<String>? alias,
|
||||
Expression<String>? content,
|
||||
Expression<int>? accountId,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<DateTime>? cacheExpiredAt,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (alias != null) 'alias': alias,
|
||||
if (content != null) 'content': content,
|
||||
if (accountId != null) 'account_id': accountId,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
if (cacheExpiredAt != null) 'cache_expired_at': cacheExpiredAt,
|
||||
});
|
||||
}
|
||||
|
||||
SnLocalRealmCompanion copyWith(
|
||||
{Value<int>? id,
|
||||
Value<String>? alias,
|
||||
Value<SnRealm>? content,
|
||||
Value<int>? accountId,
|
||||
Value<DateTime>? createdAt,
|
||||
Value<DateTime>? cacheExpiredAt}) {
|
||||
return SnLocalRealmCompanion(
|
||||
id: id ?? this.id,
|
||||
alias: alias ?? this.alias,
|
||||
content: content ?? this.content,
|
||||
accountId: accountId ?? this.accountId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
cacheExpiredAt: cacheExpiredAt ?? this.cacheExpiredAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<int>(id.value);
|
||||
}
|
||||
if (alias.present) {
|
||||
map['alias'] = Variable<String>(alias.value);
|
||||
}
|
||||
if (content.present) {
|
||||
map['content'] = Variable<String>(
|
||||
$SnLocalRealmTable.$convertercontent.toSql(content.value));
|
||||
}
|
||||
if (accountId.present) {
|
||||
map['account_id'] = Variable<int>(accountId.value);
|
||||
}
|
||||
if (createdAt.present) {
|
||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||
}
|
||||
if (cacheExpiredAt.present) {
|
||||
map['cache_expired_at'] = Variable<DateTime>(cacheExpiredAt.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('SnLocalRealmCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('alias: $alias, ')
|
||||
..write('content: $content, ')
|
||||
..write('accountId: $accountId, ')
|
||||
..write('createdAt: $createdAt, ')
|
||||
..write('cacheExpiredAt: $cacheExpiredAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
_$AppDatabase(QueryExecutor e) : super(e);
|
||||
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
||||
@ -2470,6 +2815,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
late final $SnLocalStickerTable snLocalSticker = $SnLocalStickerTable(this);
|
||||
late final $SnLocalStickerPackTable snLocalStickerPack =
|
||||
$SnLocalStickerPackTable(this);
|
||||
late final $SnLocalRealmTable snLocalRealm = $SnLocalRealmTable(this);
|
||||
late final Index idxChannelAlias = Index('idx_channel_alias',
|
||||
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
|
||||
late final Index idxChatChannel = Index('idx_chat_channel',
|
||||
@ -2480,6 +2826,10 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
|
||||
late final Index idxAttachmentAccount = Index('idx_attachment_account',
|
||||
'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
|
||||
late final Index idxRealmAlias = Index('idx_realm_alias',
|
||||
'CREATE INDEX idx_realm_alias ON sn_local_realm (alias)');
|
||||
late final Index idxRealmAccount = Index('idx_realm_account',
|
||||
'CREATE INDEX idx_realm_account ON sn_local_realm (account_id)');
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@ -2493,11 +2843,14 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
snLocalAttachment,
|
||||
snLocalSticker,
|
||||
snLocalStickerPack,
|
||||
snLocalRealm,
|
||||
idxChannelAlias,
|
||||
idxChatChannel,
|
||||
idxAccountName,
|
||||
idxAttachmentRid,
|
||||
idxAttachmentAccount
|
||||
idxAttachmentAccount,
|
||||
idxRealmAlias,
|
||||
idxRealmAccount
|
||||
];
|
||||
}
|
||||
|
||||
@ -3888,6 +4241,192 @@ typedef $$SnLocalStickerPackTableProcessedTableManager = ProcessedTableManager<
|
||||
),
|
||||
SnLocalStickerPackData,
|
||||
PrefetchHooks Function()>;
|
||||
typedef $$SnLocalRealmTableCreateCompanionBuilder = SnLocalRealmCompanion
|
||||
Function({
|
||||
Value<int> id,
|
||||
required String alias,
|
||||
required SnRealm content,
|
||||
required int accountId,
|
||||
Value<DateTime> createdAt,
|
||||
required DateTime cacheExpiredAt,
|
||||
});
|
||||
typedef $$SnLocalRealmTableUpdateCompanionBuilder = SnLocalRealmCompanion
|
||||
Function({
|
||||
Value<int> id,
|
||||
Value<String> alias,
|
||||
Value<SnRealm> content,
|
||||
Value<int> accountId,
|
||||
Value<DateTime> createdAt,
|
||||
Value<DateTime> cacheExpiredAt,
|
||||
});
|
||||
|
||||
class $$SnLocalRealmTableFilterComposer
|
||||
extends Composer<_$AppDatabase, $SnLocalRealmTable> {
|
||||
$$SnLocalRealmTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnFilters<int> get id => $composableBuilder(
|
||||
column: $table.id, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<String> get alias => $composableBuilder(
|
||||
column: $table.alias, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnWithTypeConverterFilters<SnRealm, SnRealm, String> get content =>
|
||||
$composableBuilder(
|
||||
column: $table.content,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column));
|
||||
|
||||
ColumnFilters<int> get accountId => $composableBuilder(
|
||||
column: $table.accountId, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt, builder: (column) => ColumnFilters(column));
|
||||
|
||||
ColumnFilters<DateTime> get cacheExpiredAt => $composableBuilder(
|
||||
column: $table.cacheExpiredAt,
|
||||
builder: (column) => ColumnFilters(column));
|
||||
}
|
||||
|
||||
class $$SnLocalRealmTableOrderingComposer
|
||||
extends Composer<_$AppDatabase, $SnLocalRealmTable> {
|
||||
$$SnLocalRealmTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
ColumnOrderings<int> get id => $composableBuilder(
|
||||
column: $table.id, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get alias => $composableBuilder(
|
||||
column: $table.alias, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get content => $composableBuilder(
|
||||
column: $table.content, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<int> get accountId => $composableBuilder(
|
||||
column: $table.accountId, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||
column: $table.createdAt, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<DateTime> get cacheExpiredAt => $composableBuilder(
|
||||
column: $table.cacheExpiredAt,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
}
|
||||
|
||||
class $$SnLocalRealmTableAnnotationComposer
|
||||
extends Composer<_$AppDatabase, $SnLocalRealmTable> {
|
||||
$$SnLocalRealmTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
super.$addJoinBuilderToRootComposer,
|
||||
super.$removeJoinBuilderFromRootComposer,
|
||||
});
|
||||
GeneratedColumn<int> get id =>
|
||||
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get alias =>
|
||||
$composableBuilder(column: $table.alias, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<SnRealm, String> get content =>
|
||||
$composableBuilder(column: $table.content, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<int> get accountId =>
|
||||
$composableBuilder(column: $table.accountId, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get createdAt =>
|
||||
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<DateTime> get cacheExpiredAt => $composableBuilder(
|
||||
column: $table.cacheExpiredAt, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$SnLocalRealmTableTableManager extends RootTableManager<
|
||||
_$AppDatabase,
|
||||
$SnLocalRealmTable,
|
||||
SnLocalRealmData,
|
||||
$$SnLocalRealmTableFilterComposer,
|
||||
$$SnLocalRealmTableOrderingComposer,
|
||||
$$SnLocalRealmTableAnnotationComposer,
|
||||
$$SnLocalRealmTableCreateCompanionBuilder,
|
||||
$$SnLocalRealmTableUpdateCompanionBuilder,
|
||||
(
|
||||
SnLocalRealmData,
|
||||
BaseReferences<_$AppDatabase, $SnLocalRealmTable, SnLocalRealmData>
|
||||
),
|
||||
SnLocalRealmData,
|
||||
PrefetchHooks Function()> {
|
||||
$$SnLocalRealmTableTableManager(_$AppDatabase db, $SnLocalRealmTable table)
|
||||
: super(TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
$$SnLocalRealmTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
$$SnLocalRealmTableOrderingComposer($db: db, $table: table),
|
||||
createComputedFieldComposer: () =>
|
||||
$$SnLocalRealmTableAnnotationComposer($db: db, $table: table),
|
||||
updateCompanionCallback: ({
|
||||
Value<int> id = const Value.absent(),
|
||||
Value<String> alias = const Value.absent(),
|
||||
Value<SnRealm> content = const Value.absent(),
|
||||
Value<int> accountId = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<DateTime> cacheExpiredAt = const Value.absent(),
|
||||
}) =>
|
||||
SnLocalRealmCompanion(
|
||||
id: id,
|
||||
alias: alias,
|
||||
content: content,
|
||||
accountId: accountId,
|
||||
createdAt: createdAt,
|
||||
cacheExpiredAt: cacheExpiredAt,
|
||||
),
|
||||
createCompanionCallback: ({
|
||||
Value<int> id = const Value.absent(),
|
||||
required String alias,
|
||||
required SnRealm content,
|
||||
required int accountId,
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
required DateTime cacheExpiredAt,
|
||||
}) =>
|
||||
SnLocalRealmCompanion.insert(
|
||||
id: id,
|
||||
alias: alias,
|
||||
content: content,
|
||||
accountId: accountId,
|
||||
createdAt: createdAt,
|
||||
cacheExpiredAt: cacheExpiredAt,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||
.toList(),
|
||||
prefetchHooksCallback: null,
|
||||
));
|
||||
}
|
||||
|
||||
typedef $$SnLocalRealmTableProcessedTableManager = ProcessedTableManager<
|
||||
_$AppDatabase,
|
||||
$SnLocalRealmTable,
|
||||
SnLocalRealmData,
|
||||
$$SnLocalRealmTableFilterComposer,
|
||||
$$SnLocalRealmTableOrderingComposer,
|
||||
$$SnLocalRealmTableAnnotationComposer,
|
||||
$$SnLocalRealmTableCreateCompanionBuilder,
|
||||
$$SnLocalRealmTableUpdateCompanionBuilder,
|
||||
(
|
||||
SnLocalRealmData,
|
||||
BaseReferences<_$AppDatabase, $SnLocalRealmTable, SnLocalRealmData>
|
||||
),
|
||||
SnLocalRealmData,
|
||||
PrefetchHooks Function()>;
|
||||
|
||||
class $AppDatabaseManager {
|
||||
final _$AppDatabase _db;
|
||||
@ -3908,4 +4447,6 @@ class $AppDatabaseManager {
|
||||
$$SnLocalStickerTableTableManager(_db, _db.snLocalSticker);
|
||||
$$SnLocalStickerPackTableTableManager get snLocalStickerPack =>
|
||||
$$SnLocalStickerPackTableTableManager(_db, _db.snLocalStickerPack);
|
||||
$$SnLocalRealmTableTableManager get snLocalRealm =>
|
||||
$$SnLocalRealmTableTableManager(_db, _db.snLocalRealm);
|
||||
}
|
||||
|
@ -412,9 +412,214 @@ class Shape8 extends i0.VersionedTable {
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
final class Schema4 extends i0.VersionedSchema {
|
||||
Schema4({required super.database}) : super(version: 4);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
snLocalChatChannel,
|
||||
snLocalChatMessage,
|
||||
snLocalChannelMember,
|
||||
snLocalKeyPair,
|
||||
snLocalAccount,
|
||||
snLocalAttachment,
|
||||
snLocalSticker,
|
||||
snLocalStickerPack,
|
||||
snLocalRealm,
|
||||
idxChannelAlias,
|
||||
idxChatChannel,
|
||||
idxAccountName,
|
||||
idxAttachmentRid,
|
||||
idxAttachmentAccount,
|
||||
idxRealmAlias,
|
||||
idxRealmAccount,
|
||||
];
|
||||
late final Shape0 snLocalChatChannel = Shape0(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_chat_channel',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape3 snLocalChatMessage = Shape3(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_chat_message',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_4,
|
||||
_column_10,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape4 snLocalChannelMember = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_channel_member',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_4,
|
||||
_column_6,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_11,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape2 snLocalKeyPair = Shape2(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_key_pair',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [
|
||||
'PRIMARY KEY(id)',
|
||||
],
|
||||
columns: [
|
||||
_column_5,
|
||||
_column_6,
|
||||
_column_7,
|
||||
_column_8,
|
||||
_column_9,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape5 snLocalAccount = Shape5(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_account',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_12,
|
||||
_column_2,
|
||||
_column_3,
|
||||
_column_11,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape6 snLocalAttachment = Shape6(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_attachment',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_2,
|
||||
_column_6,
|
||||
_column_3,
|
||||
_column_11,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape7 snLocalSticker = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_sticker',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_15,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape8 snLocalStickerPack = Shape8(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_sticker_pack',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape9 snLocalRealm = Shape9(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'sn_local_realm',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_16,
|
||||
_column_2,
|
||||
_column_6,
|
||||
_column_3,
|
||||
_column_11,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
final i1.Index idxChannelAlias = i1.Index('idx_channel_alias',
|
||||
'CREATE INDEX idx_channel_alias ON sn_local_chat_channel (alias)');
|
||||
final i1.Index idxChatChannel = i1.Index('idx_chat_channel',
|
||||
'CREATE INDEX idx_chat_channel ON sn_local_chat_message (channel_id)');
|
||||
final i1.Index idxAccountName = i1.Index('idx_account_name',
|
||||
'CREATE INDEX idx_account_name ON sn_local_account (name)');
|
||||
final i1.Index idxAttachmentRid = i1.Index('idx_attachment_rid',
|
||||
'CREATE INDEX idx_attachment_rid ON sn_local_attachment (rid)');
|
||||
final i1.Index idxAttachmentAccount = i1.Index('idx_attachment_account',
|
||||
'CREATE INDEX idx_attachment_account ON sn_local_attachment (account_id)');
|
||||
final i1.Index idxRealmAlias = i1.Index('idx_realm_alias',
|
||||
'CREATE INDEX idx_realm_alias ON sn_local_realm (alias)');
|
||||
final i1.Index idxRealmAccount = i1.Index('idx_realm_account',
|
||||
'CREATE INDEX idx_realm_account ON sn_local_realm (account_id)');
|
||||
}
|
||||
|
||||
class Shape9 extends i0.VersionedTable {
|
||||
Shape9({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get alias =>
|
||||
columnsByName['alias']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get content =>
|
||||
columnsByName['content']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get accountId =>
|
||||
columnsByName['account_id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get cacheExpiredAt =>
|
||||
columnsByName['cache_expired_at']! as i1.GeneratedColumn<DateTime>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_16(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('alias', aliasedName, false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'));
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
@ -428,6 +633,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from2To3(migrator, schema);
|
||||
return 3;
|
||||
case 3:
|
||||
final schema = Schema4(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from3To4(migrator, schema);
|
||||
return 4;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@ -437,9 +647,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||
}) =>
|
||||
i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
from2To3: from2To3,
|
||||
from3To4: from3To4,
|
||||
));
|
||||
|
45
lib/database/realm.dart
Normal file
45
lib/database/realm.dart
Normal file
@ -0,0 +1,45 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
|
||||
class SnRealmConverter extends TypeConverter<SnRealm, String>
|
||||
with JsonTypeConverter2<SnRealm, String, Map<String, Object?>> {
|
||||
const SnRealmConverter();
|
||||
|
||||
@override
|
||||
SnRealm fromSql(String fromDb) {
|
||||
return fromJson(jsonDecode(fromDb) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(SnRealm value) {
|
||||
return jsonEncode(toJson(value));
|
||||
}
|
||||
|
||||
@override
|
||||
SnRealm fromJson(Map<String, Object?> json) {
|
||||
return SnRealm.fromJson(json);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Object?> toJson(SnRealm value) {
|
||||
return value.toJson();
|
||||
}
|
||||
}
|
||||
|
||||
@TableIndex(name: 'idx_realm_alias', columns: {#alias})
|
||||
@TableIndex(name: 'idx_realm_account', columns: {#accountId})
|
||||
class SnLocalRealm extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
|
||||
TextColumn get alias => text().unique()();
|
||||
|
||||
TextColumn get content => text().map(const SnRealmConverter())();
|
||||
|
||||
IntColumn get accountId => integer()();
|
||||
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
DateTimeColumn get cacheExpiredAt => dateTime()();
|
||||
}
|
450
lib/main.dart
450
lib/main.dart
@ -3,6 +3,7 @@ import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:croppy/croppy.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
@ -12,13 +13,16 @@ import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/firebase_options.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
@ -46,6 +50,8 @@ import 'package:surface/router.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/menu_bar.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/version_label.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
@ -53,6 +59,7 @@ import 'package:in_app_review/in_app_review.dart';
|
||||
import 'package:image_picker_android/image_picker_android.dart';
|
||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||
import 'package:local_notifier/local_notifier.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void appBackgroundDispatcher() {
|
||||
@ -71,13 +78,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 {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||
final Size savedSize = await _getSavedWindowSize();
|
||||
doWhenWindowReady(() {
|
||||
appWindow.minSize = Size(480, 640);
|
||||
appWindow.size = Size(1280, 720);
|
||||
appWindow.size = savedSize;
|
||||
appWindow.alignment = Alignment.center;
|
||||
appWindow.show();
|
||||
});
|
||||
@ -87,18 +121,15 @@ void main() async {
|
||||
|
||||
if (!kIsWeb && !Platform.isLinux) {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform,
|
||||
);
|
||||
options: DefaultFirebaseOptions.currentPlatform);
|
||||
}
|
||||
|
||||
GoRouter.optionURLReflectsImperativeAPIs = true;
|
||||
usePathUrlStrategy();
|
||||
|
||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||
Workmanager().initialize(
|
||||
appBackgroundDispatcher,
|
||||
isInDebugMode: kDebugMode,
|
||||
);
|
||||
Workmanager()
|
||||
.initialize(appBackgroundDispatcher, isInDebugMode: kDebugMode);
|
||||
if (Platform.isAndroid) {
|
||||
Workmanager().registerPeriodicTask(
|
||||
"widget-update-random-post",
|
||||
@ -133,7 +164,7 @@ class SolianApp extends StatelessWidget {
|
||||
Locale('en', 'US'),
|
||||
Locale('zh', 'CN'),
|
||||
Locale('zh', 'TW'),
|
||||
Locale('zh', 'HK'),
|
||||
Locale('zh', 'HK')
|
||||
],
|
||||
fallbackLocale: Locale('en', 'US'),
|
||||
useFallbackTranslations: true,
|
||||
@ -157,7 +188,7 @@ class SolianApp extends StatelessWidget {
|
||||
Provider(create: (ctx) => SnNetworkProvider(ctx)),
|
||||
Provider(create: (ctx) => UserDirectoryProvider(ctx)),
|
||||
Provider(create: (ctx) => SnAttachmentProvider(ctx)),
|
||||
Provider(create: (ctx) => SnRealmProvider(ctx)),
|
||||
ChangeNotifierProvider(create: (ctx) => SnRealmProvider(ctx)),
|
||||
Provider(create: (ctx) => SnPostContentProvider(ctx)),
|
||||
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
|
||||
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
|
||||
@ -228,6 +259,10 @@ class _AppSplashScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
bool _isBusy = false;
|
||||
double _initPercentage = 0;
|
||||
String _phaseText = 'appInitStarting';
|
||||
|
||||
void _tryRequestRating() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.containsKey('first_boot_time')) {
|
||||
@ -256,12 +291,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
final localVersionString = '${info.version}+${info.buildNumber}';
|
||||
final resp = await Dio(
|
||||
BaseOptions(
|
||||
sendTimeout: const Duration(seconds: 60),
|
||||
receiveTimeout: const Duration(seconds: 60),
|
||||
),
|
||||
sendTimeout: const Duration(seconds: 60),
|
||||
receiveTimeout: const Duration(seconds: 60)),
|
||||
).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 remoteVersion = Version.parse(remoteVersionString.split('+').first);
|
||||
final localVersion = Version.parse(localVersionString.split('+').first);
|
||||
@ -276,9 +309,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
mounted) {
|
||||
final config = context.read<ConfigProvider>();
|
||||
config.setUpdate(
|
||||
remoteVersionString,
|
||||
resp.data?['body'] ?? 'No changelog',
|
||||
);
|
||||
remoteVersionString, resp.data?['body'] ?? 'No changelog');
|
||||
logging.info("[Update] Update available: $remoteVersionString");
|
||||
}
|
||||
} catch (e) {
|
||||
@ -287,6 +318,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
}
|
||||
}
|
||||
|
||||
void _setPhaseText(String text) {
|
||||
_phaseText = 'appInit${text.capitalize()}'.tr();
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
try {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
@ -299,31 +335,57 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
// The Network initialization must be done after the HomeWidget initialization
|
||||
// The Network initialization will save the server url to the HomeWidget
|
||||
// The Network initialization will also save initialize the Config, so it not need to be initialized again
|
||||
_initPercentage = 0.1;
|
||||
_setPhaseText('network');
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.initializeUserAgent();
|
||||
await sn.setConfigWithNative();
|
||||
if (!mounted) return;
|
||||
_initPercentage = 0.2;
|
||||
_setPhaseText('userdata');
|
||||
final ua = context.read<UserProvider>();
|
||||
await ua.initialize();
|
||||
if (!mounted) return;
|
||||
_initPercentage = 0.3;
|
||||
_setPhaseText('websocket');
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
await ws.tryConnect();
|
||||
if (!mounted) return;
|
||||
final notify = context.read<NotificationProvider>();
|
||||
notify.listen();
|
||||
await notify.registerPushNotifications();
|
||||
if (!mounted) return;
|
||||
final kp = context.read<KeyPairProvider>();
|
||||
await kp.reloadActive();
|
||||
kp.listen();
|
||||
if (!mounted) return;
|
||||
final sticker = context.read<SnStickerProvider>();
|
||||
await sticker.listSticker();
|
||||
if (!mounted) return;
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final userCacheSize = await ud.loadAccountCache();
|
||||
logging.info('[Users] Loaded local user cache, size: $userCacheSize');
|
||||
logging.info('[Bootstrap] Everything initialized!');
|
||||
try {
|
||||
if (!mounted) return;
|
||||
_initPercentage = 0.9;
|
||||
_setPhaseText('keyPair');
|
||||
final kp = context.read<KeyPairProvider>();
|
||||
kp.reloadActive();
|
||||
kp.listen();
|
||||
} catch (_) {}
|
||||
if (ua.isAuthorized) {
|
||||
if (!mounted) return;
|
||||
_setPhaseText('notification');
|
||||
final notify = context.read<NotificationProvider>();
|
||||
notify.listen();
|
||||
try {
|
||||
notify.registerPushNotifications();
|
||||
if (!mounted) return;
|
||||
_setPhaseText('stickers');
|
||||
final sticker = context.read<SnStickerProvider>();
|
||||
await sticker.listSticker();
|
||||
if (!mounted) return;
|
||||
_setPhaseText('userDirectory');
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
await ud.loadAccountCache();
|
||||
if (!mounted) return;
|
||||
_setPhaseText('realm');
|
||||
final rm = context.read<SnRealmProvider>();
|
||||
await rm.refreshAvailableRealms();
|
||||
if (!mounted) return;
|
||||
_setPhaseText('chat');
|
||||
final ct = context.read<ChatChannelProvider>();
|
||||
await ct.refreshAvailableChannels();
|
||||
_initPercentage = 1;
|
||||
_setPhaseText('done');
|
||||
} catch (_) {}
|
||||
_playIntro();
|
||||
}
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
await context.showErrorDialog(err);
|
||||
@ -339,28 +401,42 @@ 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.
|
||||
}
|
||||
|
||||
void _playIntro() async {
|
||||
final cfg = context.read<ConfigProvider>();
|
||||
if (!cfg.soundEffects) return;
|
||||
|
||||
final date = DateTime.now();
|
||||
final player = AudioPlayer(playerId: 'launch-done-player');
|
||||
await player.play(
|
||||
(cfg.aprilFoolFeatures && date.month == 4 && date.day == 1)
|
||||
? AssetSource('audio/sfx/launch-intro.mp3')
|
||||
: AssetSource('audio/sfx/launch-done.mp3'),
|
||||
volume: 0.8,
|
||||
ctx: AudioContext(
|
||||
android: AudioContextAndroid(
|
||||
contentType: AndroidContentType.sonification,
|
||||
usageType: AndroidUsageType.notificationEvent,
|
||||
),
|
||||
iOS: AudioContextIOS(category: AVAudioSessionCategory.ambient),
|
||||
),
|
||||
mode: PlayerMode.lowLatency,
|
||||
);
|
||||
player.onPlayerComplete.listen((_) {
|
||||
player.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
final Menu _appTrayMenu = Menu(
|
||||
items: [
|
||||
MenuItem(
|
||||
key: 'version_label',
|
||||
label: 'Solian',
|
||||
disabled: true,
|
||||
),
|
||||
MenuItem(key: 'version_label', label: 'Solian', disabled: true),
|
||||
MenuItem.separator(),
|
||||
MenuItem.checkbox(
|
||||
checked: false,
|
||||
key: 'mute_notification',
|
||||
label: 'trayMenuMuteNotification'.tr(),
|
||||
),
|
||||
checked: false,
|
||||
key: 'mute_notification',
|
||||
label: 'trayMenuMuteNotification'.tr()),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
key: 'window_show',
|
||||
label: 'trayMenuShow'.tr(),
|
||||
),
|
||||
MenuItem(
|
||||
key: 'exit',
|
||||
label: 'trayMenuExit'.tr(),
|
||||
),
|
||||
MenuItem(key: 'window_show', label: 'trayMenuShow'.tr()),
|
||||
MenuItem(key: 'exit', label: 'trayMenuExit'.tr()),
|
||||
],
|
||||
);
|
||||
|
||||
@ -388,9 +464,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
|
||||
|
||||
await localNotifier.setup(
|
||||
appName: 'Solian',
|
||||
shortcutPolicy: ShortcutPolicy.requireCreate,
|
||||
);
|
||||
appName: 'Solian', shortcutPolicy: ShortcutPolicy.requireCreate);
|
||||
}
|
||||
|
||||
AppLifecycleListener? _appLifecycleListener;
|
||||
@ -399,20 +473,28 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_isBusy = true;
|
||||
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
|
||||
_appLifecycleListener = AppLifecycleListener(
|
||||
onExitRequested: _onExitRequested,
|
||||
);
|
||||
_appLifecycleListener =
|
||||
AppLifecycleListener(onExitRequested: _onExitRequested);
|
||||
}
|
||||
|
||||
_trayInitialization();
|
||||
_hotkeyInitialization();
|
||||
_notifyInitialization();
|
||||
_initialize().then((_) {
|
||||
_postInitialization();
|
||||
_tryRequestRating();
|
||||
_checkForUpdate();
|
||||
});
|
||||
try {
|
||||
_trayInitialization();
|
||||
_hotkeyInitialization();
|
||||
_notifyInitialization();
|
||||
_initialize().then((_) {
|
||||
_postInitialization();
|
||||
_tryRequestRating();
|
||||
_checkForUpdate();
|
||||
setState(() => _isBusy = false);
|
||||
}).catchError((err) {
|
||||
logging.error('[Bootstrap] Unable to initialize app', err);
|
||||
setState(() => _isBusy = false);
|
||||
});
|
||||
} catch (err) {
|
||||
logging.error('[Bootstrap] Unable to initialize (pre-stage) app', err);
|
||||
}
|
||||
}
|
||||
|
||||
Future<AppExitResponse> _onExitRequested() async {
|
||||
@ -421,6 +503,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
}
|
||||
|
||||
void _quitApp() {
|
||||
_saveWindowSize();
|
||||
_appLifecycleListener?.dispose();
|
||||
if (Platform.isWindows) {
|
||||
appWindow.close();
|
||||
@ -501,7 +584,13 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
}
|
||||
});
|
||||
return SizeChangedLayoutNotifier(
|
||||
child: widget.child,
|
||||
child: _isBusy
|
||||
? _AppLoadingScreen(
|
||||
isBusy: _isBusy,
|
||||
initPercentage: _initPercentage,
|
||||
phaseText: _phaseText,
|
||||
)
|
||||
: widget.child,
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -509,3 +598,234 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppLoadingScreen extends StatelessWidget {
|
||||
const _AppLoadingScreen({
|
||||
required this.isBusy,
|
||||
required this.initPercentage,
|
||||
required this.phaseText,
|
||||
});
|
||||
|
||||
final bool isBusy;
|
||||
final double initPercentage;
|
||||
final String phaseText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (ResponsiveScaffold.getIsExpand(context)) {
|
||||
return 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: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: initPercentage),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${(value * 100).toStringAsFixed(0)}%')
|
||||
.padding(left: 32, bottom: 4),
|
||||
LinearProgressIndicator(
|
||||
value: value,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(0),
|
||||
),
|
||||
stopIndicatorColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
const Gap(24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: initPercentage),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('${(value * 100).toStringAsFixed(0)}%')
|
||||
.padding(right: 32, bottom: 4),
|
||||
Transform.flip(
|
||||
flipX: true,
|
||||
child: LinearProgressIndicator(
|
||||
value: value,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(0),
|
||||
),
|
||||
stopIndicatorColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 240, minWidth: 160),
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surface.withOpacity(0.85),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 3,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'splashScreenServer',
|
||||
style: GoogleFonts.notoSerifHk(height: 1, fontSize: 11),
|
||||
textAlign: TextAlign.center,
|
||||
).tr().opacity(0.85),
|
||||
Text(
|
||||
'splashScreenServerName',
|
||||
style: GoogleFonts.notoSerifHk(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
).tr().opacity(0.85),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: '#',
|
||||
style: GoogleFonts.notoSerifHk(),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '0',
|
||||
style: GoogleFonts.notoSerifHk(
|
||||
fontSize: 80,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
).padding(vertical: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: MediaQuery.of(context).size.height * 0.2,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
phaseText,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
AnimateWidgetExtensions(Text(
|
||||
'splashScreenCaption',
|
||||
textAlign: TextAlign.center,
|
||||
).tr())
|
||||
.animate(onPlay: (e) => e.repeat())
|
||||
.fadeIn(duration: 500.ms, curve: Curves.easeOut)
|
||||
.then()
|
||||
.fadeOut(
|
||||
duration: 500.ms,
|
||||
delay: 1000.ms,
|
||||
curve: Curves.easeIn,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/icon/icon.png',
|
||||
width: 40,
|
||||
height: 40,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
).padding(all: 4),
|
||||
const Gap(4),
|
||||
Text('Solar Network').bold(),
|
||||
Expanded(child: const SizedBox()),
|
||||
AppVersionLabel(),
|
||||
const Gap(12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return 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),
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: initPercentage),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, value, _) =>
|
||||
LinearProgressIndicator(value: value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,24 @@ class ChatChannelProvider extends ChangeNotifier {
|
||||
_rels = context.read<SnRealmProvider>();
|
||||
}
|
||||
|
||||
final List<SnChannel> _availableChannels = List.empty(growable: true);
|
||||
|
||||
List<SnChannel> get availableChannels => _availableChannels;
|
||||
|
||||
Future<void> refreshAvailableChannels() async {
|
||||
final stream = fetchChannels();
|
||||
stream.listen((ele) {
|
||||
_availableChannels.clear();
|
||||
_availableChannels.addAll(ele);
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
void addAvailableChannel(SnChannel channel) {
|
||||
_availableChannels.add(channel);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
|
||||
await Future.wait(
|
||||
channels.map(
|
||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:livekit_noise_filter/livekit_noise_filter.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
@ -131,10 +132,14 @@ class ChatCallProvider extends ChangeNotifier {
|
||||
|
||||
void initRoom() {
|
||||
initHardware();
|
||||
final timeout = const Duration(seconds: 60);
|
||||
_room = Room(
|
||||
roomOptions: const RoomOptions(
|
||||
roomOptions: RoomOptions(
|
||||
dynacast: true,
|
||||
adaptiveStream: true,
|
||||
defaultAudioCaptureOptions: AudioCaptureOptions(
|
||||
processor: LiveKitNoiseFilter(),
|
||||
),
|
||||
defaultAudioPublishOptions: AudioPublishOptions(
|
||||
name: 'call_voice',
|
||||
stream: 'call_stream',
|
||||
@ -154,6 +159,16 @@ class ChatCallProvider extends ChangeNotifier {
|
||||
params: VideoParametersPresets.h1080_169,
|
||||
),
|
||||
),
|
||||
connectOptions: ConnectOptions(
|
||||
autoSubscribe: true,
|
||||
timeouts: Timeouts(
|
||||
connection: timeout,
|
||||
debounce: timeout,
|
||||
publish: timeout,
|
||||
peerConnection: timeout,
|
||||
iceRestart: timeout,
|
||||
),
|
||||
),
|
||||
);
|
||||
_listener = _room.createListener();
|
||||
WakelockPlus.enable();
|
||||
|
@ -13,7 +13,6 @@ const kNetworkServerStoreKey = 'app_server_url';
|
||||
const kAppbarTransparentStoreKey = 'app_bar_transparent';
|
||||
const kAppBackgroundStoreKey = 'app_has_background';
|
||||
const kAppColorSchemeStoreKey = 'app_color_scheme';
|
||||
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
|
||||
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
|
||||
const kAppExpandPostLink = 'app_expand_post_link';
|
||||
const kAppExpandChatLink = 'app_expand_chat_link';
|
||||
@ -21,6 +20,10 @@ const kAppRealmCompactView = 'app_realm_compact_view';
|
||||
const kAppCustomFonts = 'app_custom_fonts';
|
||||
const kAppMixedFeed = 'app_mixed_feed';
|
||||
const kAppAutoTranslate = 'app_auto_translate';
|
||||
const kAppHideBottomNav = 'app_hide_bottom_nav';
|
||||
const kAppSoundEffects = 'app_sound_effects';
|
||||
const kAppAprilFoolFeatures = 'app_april_fool_features';
|
||||
const kAppWindowSize = 'app_window_size';
|
||||
|
||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||
'settingsImageQualityLowest': FilterQuality.none,
|
||||
@ -43,27 +46,17 @@ class ConfigProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
bool drawerIsCollapsed = false;
|
||||
bool drawerIsExpanded = false;
|
||||
|
||||
void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
|
||||
bool newDrawerIsCollapsed = false;
|
||||
bool newDrawerIsExpanded = false;
|
||||
if (withMediaQuery) {
|
||||
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600;
|
||||
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 601;
|
||||
} else {
|
||||
final rpb = ResponsiveBreakpoints.of(context);
|
||||
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
|
||||
newDrawerIsExpanded = rpb.largerThan(TABLET)
|
||||
? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
|
||||
? false
|
||||
: true
|
||||
: false;
|
||||
}
|
||||
|
||||
if (newDrawerIsExpanded != drawerIsExpanded ||
|
||||
newDrawerIsCollapsed != drawerIsCollapsed) {
|
||||
drawerIsExpanded = newDrawerIsExpanded;
|
||||
if (newDrawerIsCollapsed != drawerIsCollapsed) {
|
||||
drawerIsCollapsed = newDrawerIsCollapsed;
|
||||
notifyListeners();
|
||||
}
|
||||
@ -91,6 +84,33 @@ class ConfigProvider extends ChangeNotifier {
|
||||
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) {
|
||||
prefs.setBool(kAppAutoTranslate, value);
|
||||
notifyListeners();
|
||||
|
@ -5,6 +5,20 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:shared_preferences/shared_preferences.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 {
|
||||
final String label;
|
||||
final String screen;
|
||||
@ -24,13 +38,10 @@ class NavigationProvider extends ChangeNotifier {
|
||||
|
||||
int? get currentIndex => _currentIndex;
|
||||
|
||||
static const List<String> kShowBottomNavScreen = [
|
||||
'home',
|
||||
'explore',
|
||||
'account',
|
||||
'album',
|
||||
'chat',
|
||||
];
|
||||
List<String> get showBottomNavScreen => destinations
|
||||
.where((ele) => ele.isPinned)
|
||||
.map((ele) => ele.screen)
|
||||
.toList();
|
||||
|
||||
static const List<AppNavDestination> kAllDestination = [
|
||||
AppNavDestination(
|
||||
@ -48,11 +59,6 @@ class NavigationProvider extends ChangeNotifier {
|
||||
screen: 'chat',
|
||||
label: 'screenChat',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
|
||||
screen: 'account',
|
||||
label: 'screenAccount',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.group, weight: 400, opticalSize: 20),
|
||||
screen: 'realm',
|
||||
@ -64,31 +70,16 @@ class NavigationProvider extends ChangeNotifier {
|
||||
label: 'screenNews',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.emoji_emotions, weight: 400, opticalSize: 20),
|
||||
screen: 'stickers',
|
||||
label: 'screenStickers',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
|
||||
screen: 'album',
|
||||
label: 'screenAlbum',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.diversity_4, weight: 400, opticalSize: 20),
|
||||
screen: 'friend',
|
||||
label: 'screenFriend',
|
||||
),
|
||||
AppNavDestination(
|
||||
icon: Icon(Symbols.notifications, weight: 400, opticalSize: 20),
|
||||
screen: 'notification',
|
||||
label: 'screenNotification',
|
||||
icon: Icon(Symbols.settings, weight: 400, opticalSize: 20),
|
||||
screen: 'settings',
|
||||
label: 'screenSettings',
|
||||
),
|
||||
];
|
||||
static const List<String> kDefaultPinnedDestination = [
|
||||
'home',
|
||||
'explore',
|
||||
'chat',
|
||||
'account',
|
||||
'realm',
|
||||
];
|
||||
|
||||
List<AppNavDestination> destinations = [];
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -22,6 +23,8 @@ class NotificationProvider extends ChangeNotifier {
|
||||
late final WebSocketProvider _ws;
|
||||
late final ConfigProvider _cfg;
|
||||
|
||||
final AudioPlayer _notifySoundPlayer = AudioPlayer(playerId: 'notify-sound');
|
||||
|
||||
NotificationProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
_ua = context.read<UserProvider>();
|
||||
@ -66,14 +69,19 @@ class NotificationProvider extends ChangeNotifier {
|
||||
}
|
||||
logging.info('[Push Notification] Device Push Token is $token');
|
||||
|
||||
await _sn.client.post(
|
||||
'/cgi/id/notifications/subscription',
|
||||
data: {
|
||||
'provider': provider,
|
||||
'device_token': token,
|
||||
'device_id': deviceUuid,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await _sn.client.post(
|
||||
'/cgi/id/notifications/subscription',
|
||||
data: {
|
||||
'provider': provider,
|
||||
'device_token': token,
|
||||
'device_id': deviceUuid
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
logging.error(
|
||||
'[Push Notification] Unable to register push notifications: $err');
|
||||
}
|
||||
}
|
||||
|
||||
int showingCount = 0;
|
||||
@ -91,6 +99,25 @@ class NotificationProvider extends ChangeNotifier {
|
||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||
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'),
|
||||
volume: 0.6,
|
||||
ctx: AudioContext(
|
||||
android: AudioContextAndroid(
|
||||
contentType: AndroidContentType.sonification,
|
||||
usageType: AndroidUsageType.notificationEvent,
|
||||
),
|
||||
iOS: AudioContextIOS(category: AVAudioSessionCategory.ambient),
|
||||
),
|
||||
mode: PlayerMode.lowLatency,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (notification.topic == 'messaging.message' &&
|
||||
skippableNotifyChannel != null) {
|
||||
if (notification.metadata['channel_id'] != null &&
|
||||
|
@ -1,144 +1,31 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_attachment.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_realm.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
|
||||
class SnPostContentProvider {
|
||||
late final SnNetworkProvider _sn;
|
||||
late final UserDirectoryProvider _ud;
|
||||
late final SnAttachmentProvider _attach;
|
||||
late final SnRealmProvider _realm;
|
||||
|
||||
SnPostContentProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
_ud = context.read<UserDirectoryProvider>();
|
||||
_attach = context.read<SnAttachmentProvider>();
|
||||
_realm = context.read<SnRealmProvider>();
|
||||
}
|
||||
|
||||
Future<SnPoll> _fetchPoll(int id) async {
|
||||
final resp = await _sn.client.get('/cgi/co/polls/$id');
|
||||
return SnPoll.fromJson(resp.data);
|
||||
}
|
||||
|
||||
Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async {
|
||||
Set<String> rids = {};
|
||||
Set<int> uids = {};
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
rids.addAll(out[i].body['attachments']?.cast<String>() ?? []);
|
||||
if (out[i].body['thumbnail'] != null) {
|
||||
rids.add(out[i].body['thumbnail']);
|
||||
}
|
||||
if (out[i].body['video'] != null) {
|
||||
rids.add(out[i].body['video']);
|
||||
}
|
||||
if (out[i].repostTo != null) {
|
||||
out[i] = out[i].copyWith(
|
||||
repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
|
||||
);
|
||||
}
|
||||
if (out[i].publisher.type == 0) {
|
||||
uids.add(out[i].publisher.accountId);
|
||||
}
|
||||
}
|
||||
|
||||
final attachments = await _attach.getMultiple(rids.toList());
|
||||
for (var i = 0; i < out.length; i++) {
|
||||
SnPoll? poll;
|
||||
SnRealm? realm;
|
||||
if (out[i].pollId != null) {
|
||||
poll = await _fetchPoll(out[i].pollId!);
|
||||
}
|
||||
if (out[i].realmId != null) {
|
||||
realm = await _realm.getRealm(out[i].realmId!);
|
||||
}
|
||||
|
||||
out[i] = out[i].copyWith(
|
||||
preload: SnPostPreload(
|
||||
thumbnail: attachments
|
||||
.where((ele) => ele?.rid == out[i].body['thumbnail'])
|
||||
.firstOrNull,
|
||||
attachments: attachments
|
||||
.where((ele) =>
|
||||
out[i].body['attachments']?.contains(ele?.rid) ?? false)
|
||||
.toList(),
|
||||
video: attachments
|
||||
.where((ele) => ele?.rid == out[i].body['video'])
|
||||
.firstOrNull,
|
||||
poll: poll,
|
||||
realm: realm,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
uids.addAll(
|
||||
attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
|
||||
await _ud.listAccount(uids);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
Future<SnPost> _preloadRelatedDataSingle(SnPost out) async {
|
||||
Set<String> rids = {};
|
||||
Set<int> uids = {};
|
||||
rids.addAll(out.body['attachments']?.cast<String>() ?? []);
|
||||
if (out.body['thumbnail'] != null) {
|
||||
rids.add(out.body['thumbnail']);
|
||||
}
|
||||
if (out.body['video'] != null) {
|
||||
rids.add(out.body['video']);
|
||||
}
|
||||
if (out.repostTo != null) {
|
||||
out = out.copyWith(
|
||||
repostTo: await _preloadRelatedDataSingle(out.repostTo!),
|
||||
);
|
||||
}
|
||||
if (out.publisher.type == 0) {
|
||||
uids.add(out.publisher.accountId);
|
||||
}
|
||||
|
||||
final attachments = await _attach.getMultiple(rids.toList());
|
||||
|
||||
SnPoll? poll;
|
||||
SnRealm? realm;
|
||||
if (out.pollId != null) {
|
||||
poll = await _fetchPoll(out.pollId!);
|
||||
}
|
||||
if (out.realmId != null) {
|
||||
realm = await _realm.getRealm(out.realmId!);
|
||||
}
|
||||
|
||||
out = out.copyWith(
|
||||
preload: SnPostPreload(
|
||||
thumbnail: attachments
|
||||
.where((ele) => ele?.rid == out.body['thumbnail'])
|
||||
.firstOrNull,
|
||||
attachments: attachments
|
||||
.where(
|
||||
(ele) => out.body['attachments']?.contains(ele?.rid) ?? false)
|
||||
.toList(),
|
||||
video: attachments
|
||||
.where((ele) => ele?.rid == out.body['video'])
|
||||
.firstOrNull,
|
||||
poll: poll,
|
||||
realm: realm,
|
||||
),
|
||||
);
|
||||
|
||||
uids.addAll(
|
||||
attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
|
||||
await _ud.listAccount(uids);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
Future<List<SnPost>> listRecommendations() async {
|
||||
final resp = await _sn.client.get('/cgi/co/recommendations');
|
||||
final resp = await _sn.client.get(
|
||||
'/cgi/co/recommendations',
|
||||
options: Options(headers: {
|
||||
'X-API-Version': '2',
|
||||
}),
|
||||
);
|
||||
final out = _preloadRelatedDataInBatch(
|
||||
List.from(resp.data.map((ele) => SnPost.fromJson(ele))),
|
||||
);
|
||||
@ -202,6 +89,9 @@ class SnPostContentProvider {
|
||||
if (realm != null) 'realm': realm,
|
||||
if (channel != null) 'channel': channel,
|
||||
},
|
||||
options: Options(headers: {
|
||||
'X-API-Version': '2',
|
||||
}),
|
||||
);
|
||||
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
||||
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
||||
@ -215,11 +105,16 @@ class SnPostContentProvider {
|
||||
int take = 10,
|
||||
int offset = 0,
|
||||
}) async {
|
||||
final resp = await _sn.client
|
||||
.get('/cgi/co/posts/$parentId/replies', queryParameters: {
|
||||
'take': take,
|
||||
'offset': offset,
|
||||
});
|
||||
final resp = await _sn.client.get(
|
||||
'/cgi/co/posts/$parentId/replies',
|
||||
queryParameters: {
|
||||
'take': take,
|
||||
'offset': offset,
|
||||
},
|
||||
options: Options(headers: {
|
||||
'X-API-Version': '2',
|
||||
}),
|
||||
);
|
||||
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
||||
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
||||
);
|
||||
@ -234,13 +129,20 @@ class SnPostContentProvider {
|
||||
Iterable<String>? tags,
|
||||
Iterable<String>? categories,
|
||||
}) async {
|
||||
final resp = await _sn.client.get('/cgi/co/posts/search', queryParameters: {
|
||||
'take': take,
|
||||
'offset': offset,
|
||||
'probe': searchTerm,
|
||||
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
||||
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
|
||||
});
|
||||
final resp = await _sn.client.get(
|
||||
'/cgi/co/posts/search',
|
||||
queryParameters: {
|
||||
'take': take,
|
||||
'offset': offset,
|
||||
'probe': searchTerm,
|
||||
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
||||
if (categories?.isNotEmpty ?? false)
|
||||
'categories': categories!.join(','),
|
||||
},
|
||||
options: Options(headers: {
|
||||
'X-API-Version': '2',
|
||||
}),
|
||||
);
|
||||
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
||||
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
||||
);
|
||||
@ -249,7 +151,12 @@ class SnPostContentProvider {
|
||||
}
|
||||
|
||||
Future<SnPost> getPost(dynamic id) async {
|
||||
final resp = await _sn.client.get('/cgi/co/posts/$id');
|
||||
final resp = await _sn.client.get(
|
||||
'/cgi/co/posts/$id',
|
||||
options: Options(headers: {
|
||||
'X-API-Version': '2',
|
||||
}),
|
||||
);
|
||||
final out = _preloadRelatedDataSingle(
|
||||
SnPost.fromJson(resp.data),
|
||||
);
|
||||
|
@ -321,13 +321,13 @@ class SnAttachmentProvider {
|
||||
uuid: ele.uuid,
|
||||
content: ele,
|
||||
accountId: ele.accountId,
|
||||
cacheExpiredAt: DateTime.now().add(const Duration(days: 7)),
|
||||
cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
|
||||
),
|
||||
onConflict: DoUpdate(
|
||||
(_) => SnLocalAttachmentCompanion.custom(
|
||||
content: Constant(jsonEncode(ele.toJson())),
|
||||
cacheExpiredAt:
|
||||
Constant(DateTime.now().add(const Duration(days: 7))),
|
||||
Constant(DateTime.now().add(const Duration(hours: 1))),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -1,16 +1,30 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/database/database.dart';
|
||||
import 'package:surface/providers/database.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
|
||||
class SnRealmProvider {
|
||||
class SnRealmProvider extends ChangeNotifier {
|
||||
late final SnNetworkProvider _sn;
|
||||
late final DatabaseProvider _dt;
|
||||
|
||||
SnRealmProvider(BuildContext context) {
|
||||
_sn = context.read<SnNetworkProvider>();
|
||||
_dt = context.read<DatabaseProvider>();
|
||||
}
|
||||
|
||||
final Map<String, SnRealm> _cache = {};
|
||||
List<SnRealm> _availableRealms = List.empty(growable: true);
|
||||
|
||||
Future<void> refreshAvailableRealms() async {
|
||||
_availableRealms = await listAvailableRealms();
|
||||
}
|
||||
|
||||
List<SnRealm> get availableRealms => _availableRealms;
|
||||
|
||||
Future<List<SnRealm>> listAvailableRealms() async {
|
||||
final resp = await _sn.client.get('/cgi/id/realms/me/available');
|
||||
@ -21,17 +35,56 @@ class SnRealmProvider {
|
||||
_cache[realm.alias] = realm;
|
||||
_cache[realm.id.toString()] = realm;
|
||||
}
|
||||
_saveToLocal(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
void addAvailableRealm(SnRealm realm) {
|
||||
_availableRealms.add(realm);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<SnRealm> getRealm(dynamic aliasOrId) async {
|
||||
if (_cache.containsKey(aliasOrId.toString())) {
|
||||
return _cache[aliasOrId.toString()]!;
|
||||
}
|
||||
final localResp = await (_dt.db.snLocalRealm.select()
|
||||
..where((e) =>
|
||||
e.id.equals(aliasOrId is int ? aliasOrId : 0) |
|
||||
e.alias.equals(aliasOrId.toString()))
|
||||
..where((e) => e.cacheExpiredAt.isBiggerThanValue(DateTime.now())))
|
||||
.getSingleOrNull();
|
||||
if (localResp != null) {
|
||||
_cache[localResp.content.id.toString()] = localResp.content;
|
||||
_cache[localResp.content.alias] = localResp.content;
|
||||
return localResp.content;
|
||||
}
|
||||
final resp = await _sn.client.get('/cgi/id/realms/$aliasOrId');
|
||||
final out = SnRealm.fromJson(resp.data);
|
||||
_cache[out.alias] = out;
|
||||
_cache[out.id.toString()] = out;
|
||||
_saveToLocal([out]);
|
||||
return out;
|
||||
}
|
||||
|
||||
Future<void> _saveToLocal(Iterable<SnRealm> out) async {
|
||||
for (final ele in out) {
|
||||
await _dt.db.snLocalRealm.insertOne(
|
||||
SnLocalRealmCompanion.insert(
|
||||
id: Value(ele.id),
|
||||
alias: ele.alias,
|
||||
content: ele,
|
||||
accountId: ele.accountId,
|
||||
cacheExpiredAt: DateTime.now().add(const Duration(hours: 1)),
|
||||
),
|
||||
onConflict: DoUpdate(
|
||||
(_) => SnLocalRealmCompanion.custom(
|
||||
content: Constant(jsonEncode(ele.toJson())),
|
||||
cacheExpiredAt:
|
||||
Constant(DateTime.now().add(const Duration(hours: 1))),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,7 @@ import 'package:crypto/crypto.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
|
||||
// TODO self host translate api
|
||||
const kTranslateApiBaseUrl = 'https://translate.disroot.org';
|
||||
const kTranslateApiBaseUrl = 'https://translate.solsynth.dev';
|
||||
|
||||
class SnTranslator {
|
||||
final Dio client = Dio(
|
||||
|
@ -64,6 +64,7 @@ class UserProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<SnAccount?> refreshUser() async {
|
||||
if (!isAuthorized) return null;
|
||||
final resp = await _sn.client.get('/cgi/id/users/me');
|
||||
final out = SnAccount.fromJson(resp.data);
|
||||
|
||||
|
320
lib/router.dart
320
lib/router.dart
@ -1,16 +1,19 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:surface/screens/abuse_report.dart';
|
||||
import 'package:surface/screens/account.dart';
|
||||
import 'package:surface/screens/account/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/badges.dart';
|
||||
import 'package:surface/screens/account/contact_methods.dart';
|
||||
import 'package:surface/screens/account/factor_settings.dart';
|
||||
import 'package:surface/screens/account/keypairs.dart';
|
||||
import 'package:surface/screens/account/prefs/notify.dart';
|
||||
import 'package:surface/screens/account/prefs/security.dart';
|
||||
import 'package:surface/screens/account/profile_page.dart';
|
||||
import 'package:surface/screens/account/profile_edit.dart';
|
||||
import 'package:surface/screens/account/programs.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/publishers.dart';
|
||||
@ -37,6 +40,7 @@ import 'package:surface/screens/post/post_shuffle.dart';
|
||||
import 'package:surface/screens/post/publisher_page.dart';
|
||||
import 'package:surface/screens/post/post_search.dart';
|
||||
import 'package:surface/screens/realm.dart';
|
||||
import 'package:surface/screens/realm/community.dart';
|
||||
import 'package:surface/screens/realm/manage.dart';
|
||||
import 'package:surface/screens/realm/realm_detail.dart';
|
||||
import 'package:surface/screens/realm/realm_discovery.dart';
|
||||
@ -49,16 +53,6 @@ import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/about.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
Widget _fadeThroughTransition(BuildContext context, Animation<double> animation,
|
||||
Animation<double> secondaryAnimation, Widget child) {
|
||||
return FadeThroughTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
final _appRoutes = [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
@ -67,8 +61,8 @@ final _appRoutes = [
|
||||
),
|
||||
GoRoute(
|
||||
path: '/posts',
|
||||
name: 'explore',
|
||||
builder: (context, state) => const ExploreScreen(),
|
||||
name: 'posts',
|
||||
builder: (_, __) => const SizedBox.shrink(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/draft',
|
||||
@ -106,145 +100,209 @@ final _appRoutes = [
|
||||
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(
|
||||
path: '/publishers/:name',
|
||||
name: 'postPublisher',
|
||||
builder: (context, state) =>
|
||||
PostPublisherScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => ResponsiveScaffold(
|
||||
aside: const AccountScreen(),
|
||||
child: child,
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:slug',
|
||||
name: 'postDetail',
|
||||
builder: (context, state) => PostDetailScreen(
|
||||
slug: state.pathParameters['slug']!,
|
||||
preload: state.extra as SnPost?,
|
||||
),
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
builder: (context, state) =>
|
||||
const ResponsiveScaffoldLanding(child: AccountScreen()),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/punishments',
|
||||
name: 'accountPunishments',
|
||||
builder: (context, state) => const PunishmentsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/programs',
|
||||
name: 'accountProgram',
|
||||
builder: (context, state) => const AccountProgramScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/contacts',
|
||||
name: 'accountContactMethods',
|
||||
builder: (context, state) => const AccountContactMethod(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/events',
|
||||
name: 'accountActionEvents',
|
||||
builder: (context, state) => const ActionEventScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/tickets',
|
||||
name: 'accountAuthTickets',
|
||||
builder: (context, state) => const AccountAuthTicket(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/badges',
|
||||
name: 'accountBadges',
|
||||
builder: (context, state) => const AccountBadgesScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/wallet',
|
||||
name: 'accountWallet',
|
||||
builder: (context, state) => const WalletScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/keypairs',
|
||||
name: 'accountKeyPairs',
|
||||
builder: (context, state) => const KeyPairScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'accountSettings',
|
||||
builder: (context, state) => AccountSettingsScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/notify',
|
||||
name: 'accountSettingsNotify',
|
||||
builder: (context, state) => const AccountNotifyPrefsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth',
|
||||
name: 'accountSettingsSecurity',
|
||||
builder: (context, state) => const AccountSecurityPrefsScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings/factors',
|
||||
name: 'factorSettings',
|
||||
builder: (context, state) => FactorSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile/edit',
|
||||
name: 'accountProfileEdit',
|
||||
builder: (context, state) => ProfileEditScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers',
|
||||
name: 'accountPublishers',
|
||||
builder: (context, state) => PublisherScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/new',
|
||||
name: 'accountPublisherNew',
|
||||
builder: (context, state) => AccountPublisherNewScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/edit/:name',
|
||||
name: 'accountPublisherEdit',
|
||||
builder: (context, state) => AccountPublisherEditScreen(
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
builder: (context, state) => const AccountScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/contacts',
|
||||
name: 'accountContactMethods',
|
||||
builder: (context, state) => const AccountContactMethod(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/events',
|
||||
name: 'accountActionEvents',
|
||||
builder: (context, state) => const ActionEventScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/tickets',
|
||||
name: 'accountAuthTickets',
|
||||
builder: (context, state) => const AccountAuthTicket(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/badges',
|
||||
name: 'accountBadges',
|
||||
builder: (context, state) => const AccountBadgesScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/wallet',
|
||||
name: 'accountWallet',
|
||||
builder: (context, state) => const WalletScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/keypairs',
|
||||
name: 'accountKeyPairs',
|
||||
builder: (context, state) => const KeyPairScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
name: 'accountSettings',
|
||||
builder: (context, state) => AccountSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings/factors',
|
||||
name: 'factorSettings',
|
||||
builder: (context, state) => FactorSettingsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile/edit',
|
||||
name: 'accountProfileEdit',
|
||||
builder: (context, state) => ProfileEditScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers',
|
||||
name: 'accountPublishers',
|
||||
builder: (context, state) => PublisherScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/new',
|
||||
name: 'accountPublisherNew',
|
||||
builder: (context, state) => AccountPublisherNewScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/publishers/edit/:name',
|
||||
name: 'accountPublisherEdit',
|
||||
builder: (context, state) => AccountPublisherEditScreen(
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile/:name',
|
||||
name: 'accountProfilePage',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: UserScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
),
|
||||
],
|
||||
path: '/accounts/:name',
|
||||
name: 'accountProfilePage',
|
||||
pageBuilder: (context, state) => NoTransitionPage(
|
||||
child: UserScreen(name: state.pathParameters['name']!),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
builder: (context, state) => const ChatScreen(),
|
||||
ShellRoute(
|
||||
builder: (context, state, child) =>
|
||||
ResponsiveScaffold(aside: const ChatScreen(), child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:scope/:alias',
|
||||
name: 'chatRoom',
|
||||
builder: (context, state) => ChatRoomScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
extra: state.extra as ChatRoomScreenExtra?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:scope/:alias/call',
|
||||
name: 'chatCallRoom',
|
||||
builder: (context, state) => CallRoomScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:scope/:alias/detail',
|
||||
name: 'channelDetail',
|
||||
builder: (context, state) => ChannelDetailScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/manage',
|
||||
name: 'chatManage',
|
||||
builder: (context, state) => ChatManageScreen(
|
||||
editingChannelAlias: state.uri.queryParameters['editing'],
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
builder: (context, state) => const ResponsiveScaffoldLanding(
|
||||
child: ChatScreen(),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:scope/:alias',
|
||||
name: 'chatRoom',
|
||||
builder: (context, state) => ChatRoomScreen(
|
||||
key: ValueKey(
|
||||
'${state.pathParameters['scope']!}:${state.pathParameters['alias']!}',
|
||||
),
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
extra: state.extra as ChatRoomScreenExtra?,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:scope/:alias/call',
|
||||
name: 'chatCallRoom',
|
||||
builder: (context, state) => CallRoomScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/:scope/:alias/detail',
|
||||
name: 'channelDetail',
|
||||
builder: (context, state) => ChannelDetailScreen(
|
||||
scope: state.pathParameters['scope']!,
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/manage',
|
||||
name: 'chatManage',
|
||||
builder: (context, state) => ChatManageScreen(
|
||||
editingChannelAlias: state.uri.queryParameters['editing'],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/realm',
|
||||
name: 'realm',
|
||||
pageBuilder: (context, state) => CustomTransitionPage(
|
||||
transitionsBuilder: _fadeThroughTransition,
|
||||
child: const RealmScreen(),
|
||||
),
|
||||
builder: (context, state) => const RealmScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:alias/community',
|
||||
name: 'realmCommunity',
|
||||
builder: (context, state) => RealmCommunityScreen(
|
||||
alias: state.pathParameters['alias']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/manage',
|
||||
name: 'realmManage',
|
||||
|
@ -8,6 +8,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/database.dart';
|
||||
import 'package:surface/providers/navigation.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/websocket.dart';
|
||||
@ -22,27 +23,97 @@ import 'package:surface/widgets/universal_image.dart';
|
||||
class AccountScreen extends StatelessWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.watch<UserProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text(
|
||||
"screenAccount",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
offset: Offset(1, 1),
|
||||
blurRadius: 5.0,
|
||||
color: Color.fromARGB(255, 0, 0, 0),
|
||||
),
|
||||
],
|
||||
),
|
||||
).tr(),
|
||||
title: Text("screenAccount").tr(),
|
||||
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
|
||||
? Stack(
|
||||
fit: StackFit.expand,
|
||||
@ -71,15 +142,6 @@ class AccountScreen extends StatelessWidget {
|
||||
],
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.settings, fill: 1),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed('settings');
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: ua.isAuthorized
|
||||
@ -118,7 +180,18 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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!),
|
||||
],
|
||||
),
|
||||
@ -147,115 +220,42 @@ class _AuthorizedAccountScreen extends StatelessWidget {
|
||||
);
|
||||
}).padding(all: 20),
|
||||
).padding(horizontal: 8, top: 16, bottom: 4),
|
||||
ListTile(
|
||||
title: Text('accountPublishers').tr(),
|
||||
subtitle: Text('accountPublishersSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.face),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountPublishers');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('abuseReport').tr(),
|
||||
subtitle: Text('abuseReportActionDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.flag),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('abuseReport');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('factorSettings').tr(),
|
||||
subtitle: Text('factorSettingsSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.lock),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('factorSettings');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('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(),
|
||||
subtitle: Text('accountLogoutSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.logout),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () async {
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'accountLogoutConfirmTitle'.tr(),
|
||||
'accountLogoutConfirm'.tr(),
|
||||
);
|
||||
for (final item in AccountScreen.kNavList)
|
||||
Tooltip(
|
||||
message: item.subtitle.tr(),
|
||||
child: ListTile(
|
||||
minTileHeight: 48,
|
||||
title: Text(item.title).tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: Icon(item.icon),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(item.screen);
|
||||
},
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: 'accountLogoutSubtitle'.tr(),
|
||||
child: ListTile(
|
||||
title: Text('accountLogout').tr(),
|
||||
minTileHeight: 48,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.logout),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () async {
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'accountLogoutConfirmTitle'.tr(),
|
||||
'accountLogoutConfirm'.tr(),
|
||||
);
|
||||
|
||||
if (!confirm) return;
|
||||
if (!context.mounted) return;
|
||||
ua.logoutUser();
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
ws.disconnect();
|
||||
context.read<DatabaseProvider>().removeDatabase();
|
||||
},
|
||||
if (!confirm) return;
|
||||
if (!context.mounted) return;
|
||||
ua.logoutUser();
|
||||
final ws = context.read<WebSocketProvider>();
|
||||
ws.disconnect();
|
||||
context.read<DatabaseProvider>().removeDatabase();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -298,9 +298,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
|
||||
GoRouter.of(context).pushNamed('authLogin').then((value) {
|
||||
if (value == true && context.mounted) {
|
||||
final ua = context.read<UserProvider>();
|
||||
context.showSnackbar('loginSuccess'.tr(args: [
|
||||
'@${ua.user?.name} (${ua.user?.nick})',
|
||||
]));
|
||||
ua.refreshUser();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -59,6 +59,7 @@ class _ActionEventScreenState extends State<ActionEventScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('accountActionEvent').tr(),
|
||||
|
@ -91,6 +91,7 @@ class _AccountAuthTicketState extends State<AccountAuthTicket> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('accountAuthTickets').tr(),
|
||||
|
@ -70,6 +70,7 @@ class _AccountBadgesScreenState extends State<AccountBadgesScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
title: Text('screenAccountBadges').tr(),
|
||||
),
|
||||
|
@ -69,6 +69,7 @@ class _AccountContactMethodState extends State<AccountContactMethod> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('accountContactMethods').tr(),
|
||||
|
@ -16,7 +16,11 @@ final Map<int, (String, String, IconData)> kFactorTypes = {
|
||||
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
|
||||
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
|
||||
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
|
||||
3: ('authFactorInAppNotify', 'authFactorInAppNotifyDescription', Symbols.notifications_active),
|
||||
3: (
|
||||
'authFactorInAppNotify',
|
||||
'authFactorInAppNotifyDescription',
|
||||
Symbols.notifications_active
|
||||
),
|
||||
};
|
||||
|
||||
class FactorSettingsScreen extends StatefulWidget {
|
||||
@ -36,7 +40,10 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/me/factors');
|
||||
_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) {
|
||||
if (!mounted) return;
|
||||
@ -55,6 +62,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenFactorSettings').tr(),
|
||||
@ -96,7 +104,8 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
||||
return ListTile(
|
||||
title: Text(kFactorTypes[ele.type]!.$1).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),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
@ -105,14 +114,17 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
|
||||
context
|
||||
.showConfirmDialog(
|
||||
'authFactorDelete'.tr(),
|
||||
'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]),
|
||||
'authFactorDeleteDescription'.tr(
|
||||
args: [kFactorTypes[ele.type]!.$1.tr()]),
|
||||
)
|
||||
.then((val) async {
|
||||
if (!val) return;
|
||||
try {
|
||||
if (!context.mounted) return;
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.delete('/cgi/id/users/me/factors/${ele.id}');
|
||||
final sn =
|
||||
context.read<SnNetworkProvider>();
|
||||
await sn.client.delete(
|
||||
'/cgi/id/users/me/factors/${ele.id}');
|
||||
_fetchFactors();
|
||||
} catch (err) {
|
||||
if (!context.mounted) return;
|
||||
@ -191,7 +203,9 @@ class _FactorNewDialogState extends State<_FactorNewDialog> {
|
||||
value: _factorType,
|
||||
items: kFactorTypes.entries.map(
|
||||
(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>(
|
||||
enabled: !contains,
|
||||
value: ele.key,
|
||||
|
@ -37,6 +37,7 @@ class _KeyPairScreenState extends State<KeyPairScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
title: Text('screenKeyPairs').tr(),
|
||||
),
|
||||
|
@ -1,11 +1,123 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
|
||||
class AccountNotifyPrefsScreen extends StatelessWidget {
|
||||
final Map<String, String> kNotifyTopicMap = {
|
||||
'interactive.reply': 'notificationTopicPostReply'.tr(),
|
||||
'interactive.feedback': 'notificationTopicPostFeedback'.tr(),
|
||||
'interactive.subscription': 'notificationTopicPostSubscription'.tr(),
|
||||
'messaging.message': 'notificationTopicMessaging'.tr(),
|
||||
'messaging.call': 'notificationTopicMessagingCall'.tr(),
|
||||
'general': 'notificationTopicGeneral'.tr(),
|
||||
};
|
||||
|
||||
class AccountNotifyPrefsScreen extends StatefulWidget {
|
||||
const AccountNotifyPrefsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AccountNotifyPrefsScreen> createState() =>
|
||||
_AccountNotifyPrefsScreenState();
|
||||
}
|
||||
|
||||
class _AccountNotifyPrefsScreenState extends State<AccountNotifyPrefsScreen> {
|
||||
bool _isBusy = true;
|
||||
|
||||
Map<String, bool> _config = {};
|
||||
|
||||
Future<void> _getPreferences() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
try {
|
||||
final resp = await sn.client.get('/cgi/id/preferences/notifications');
|
||||
_config = resp.data['config']
|
||||
.map((k, v) => MapEntry(k, v as bool))
|
||||
.cast<String, bool>();
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _savePreferences() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
try {
|
||||
await sn.client.put(
|
||||
'/cgi/id/preferences/notifications',
|
||||
data: {
|
||||
'config': _config,
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('accountSettingsApplied'.tr());
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getPreferences();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold();
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
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;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
148
lib/screens/account/prefs/security.dart
Normal file
148
lib/screens/account/prefs/security.dart
Normal 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: ResponsiveScaffold.getIsExpand(context),
|
||||
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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -66,37 +66,40 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
_locationController.text = prof.profile!.location;
|
||||
_avatar = prof.avatar;
|
||||
_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();
|
||||
if (_birthday != null) {
|
||||
_birthdayController.text = DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal());
|
||||
_birthdayController.text =
|
||||
DateFormat(_kDateFormat).format(prof.profile!.birthday!.toLocal());
|
||||
}
|
||||
}
|
||||
|
||||
void _selectBirthday() async {
|
||||
await showCupertinoModalPopup<DateTime?>(
|
||||
context: context,
|
||||
builder:
|
||||
(BuildContext context) => Container(
|
||||
height: 216,
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: CupertinoDatePicker(
|
||||
initialDateTime: _birthday?.toLocal(),
|
||||
mode: CupertinoDatePickerMode.date,
|
||||
use24hFormat: true,
|
||||
onDateTimeChanged: (DateTime newDate) {
|
||||
setState(() {
|
||||
_birthday = newDate;
|
||||
_birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
builder: (BuildContext context) => Container(
|
||||
height: 216,
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
margin:
|
||||
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: CupertinoDatePicker(
|
||||
initialDateTime: _birthday?.toLocal(),
|
||||
mode: CupertinoDatePickerMode.date,
|
||||
use24hFormat: true,
|
||||
onDateTimeChanged: (DateTime newDate) {
|
||||
setState(() {
|
||||
_birthday = newDate;
|
||||
_birthdayController.text =
|
||||
DateFormat(_kDateFormat).format(_birthday!);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -109,29 +112,32 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
|
||||
Uint8List? rawBytes;
|
||||
if (!skipCrop) {
|
||||
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios =
|
||||
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
|
||||
final result =
|
||||
(!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||
? await showCupertinoImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
context,
|
||||
allowedAspectRatios: aspectRatios,
|
||||
imageProvider: imageProvider,
|
||||
)
|
||||
: await showMaterialImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
context,
|
||||
allowedAspectRatios: aspectRatios,
|
||||
imageProvider: imageProvider,
|
||||
);
|
||||
final ImageProvider imageProvider =
|
||||
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios = place == 'banner'
|
||||
? [CropAspectRatio(width: 16, height: 7)]
|
||||
: [CropAspectRatio(width: 1, height: 1)];
|
||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||
? await showCupertinoImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
context,
|
||||
allowedAspectRatios: aspectRatios,
|
||||
imageProvider: imageProvider,
|
||||
)
|
||||
: await showMaterialImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
context,
|
||||
allowedAspectRatios: aspectRatios,
|
||||
imageProvider: imageProvider,
|
||||
);
|
||||
|
||||
if (result == null) return;
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _isBusy = true);
|
||||
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
||||
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||
.buffer
|
||||
.asUint8List();
|
||||
} else {
|
||||
if (!mounted) return;
|
||||
setState(() => _isBusy = true);
|
||||
@ -152,7 +158,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
|
||||
if (!mounted) return;
|
||||
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;
|
||||
final ua = context.read<UserProvider>();
|
||||
@ -188,7 +195,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
'location': _locationController.value.text,
|
||||
'birthday': _birthday?.toUtc().toIso8601String(),
|
||||
'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>();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(leading: const PageBackButton(), title: Text('screenAccountProfileEdit').tr()),
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountProfileEdit').tr()),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -251,13 +263,16 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
aspectRatio: 16 / 7,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child:
|
||||
_banner != null
|
||||
? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover)
|
||||
: const SizedBox.shrink(),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(_banner!),
|
||||
fit: BoxFit.cover)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -294,12 +309,16 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
labelText: 'fieldUsername'.tr(),
|
||||
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextField(
|
||||
controller: _nicknameController,
|
||||
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldNickname'.tr()),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldNickname'.tr()),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
@ -311,7 +330,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldFirstName'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
@ -323,7 +343,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
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(),
|
||||
labelText: 'fieldGender'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
@ -350,7 +372,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
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,
|
||||
maxLines: null,
|
||||
minLines: 3,
|
||||
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldDescription'.tr()),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldDescription'.tr()),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@ -373,18 +399,21 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldTimeZone'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
StyledWidget(
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.calendar_month),
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
visualDensity:
|
||||
VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () async {
|
||||
_timezoneController.text = await FlutterTimezone.getLocalTimezone();
|
||||
_timezoneController.text =
|
||||
await FlutterTimezone.getLocalTimezone();
|
||||
},
|
||||
),
|
||||
).padding(top: 6),
|
||||
@ -392,7 +421,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
StyledWidget(
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.clear),
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
visualDensity:
|
||||
VisualDensity(horizontal: -4, vertical: -4),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
onPressed: () {
|
||||
@ -404,13 +434,18 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
),
|
||||
TextField(
|
||||
controller: _locationController,
|
||||
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldLocation'.tr()),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldLocation'.tr()),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextField(
|
||||
controller: _birthdayController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(border: const UnderlineInputBorder(), labelText: 'fieldBirthday'.tr()),
|
||||
decoration: InputDecoration(
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldBirthday'.tr()),
|
||||
onTap: () => _selectBirthday(),
|
||||
),
|
||||
if (_links != null)
|
||||
@ -418,7 +453,8 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
margin: const EdgeInsets.only(top: 16, bottom: 4),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -427,13 +463,17 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
'fieldLinks'.tr(),
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 17),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 17),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
visualDensity:
|
||||
VisualDensity(horizontal: -4, vertical: -4),
|
||||
icon: const Icon(Symbols.add),
|
||||
onPressed: () {
|
||||
setState(() => _links!.add(('', '')));
|
||||
@ -457,7 +497,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
onChanged: (value) {
|
||||
_links![idx] = (value, _links![idx].$2);
|
||||
},
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager
|
||||
.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
@ -473,7 +515,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
|
||||
onChanged: (value) {
|
||||
_links![idx] = (_links![idx].$1, value);
|
||||
},
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) => FocusManager
|
||||
.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -60,6 +61,21 @@ final Map<String, (String, IconData, Color)> kBadgesMeta = {
|
||||
Symbols.thumb_up,
|
||||
Colors.lightGreen,
|
||||
),
|
||||
'programs.developers': (
|
||||
'badgeProgramDeveloper',
|
||||
Symbols.code,
|
||||
Colors.blue,
|
||||
),
|
||||
'programs.stellar': (
|
||||
'badgeProgramStellar',
|
||||
Symbols.family_star,
|
||||
Colors.orange,
|
||||
),
|
||||
'programs.moderator': (
|
||||
'badgeProgramModerator',
|
||||
Symbols.sword_rose,
|
||||
Colors.blue,
|
||||
),
|
||||
};
|
||||
|
||||
class UserScreen extends StatefulWidget {
|
||||
@ -227,7 +243,7 @@ class _UserScreenState extends State<UserScreen>
|
||||
|
||||
late final _appBarWidth = MediaQuery.of(context).size.width;
|
||||
late final _appBarHeight =
|
||||
(_appBarWidth * kBannerAspectRatio).roundToDouble();
|
||||
math.min((_appBarWidth * kBannerAspectRatio), 360).roundToDouble();
|
||||
|
||||
void _updateAppBarBlur() {
|
||||
if (_scrollController.offset > _appBarHeight) return;
|
||||
@ -489,10 +505,10 @@ class _UserScreenState extends State<UserScreen>
|
||||
),
|
||||
const Gap(8),
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: _account!.badges
|
||||
.map(
|
||||
(ele) => AccountBadge(badge: ele),
|
||||
)
|
||||
.map((ele) => AccountBadge(badge: ele))
|
||||
.toList(),
|
||||
).padding(horizontal: 8),
|
||||
const Gap(8),
|
||||
|
291
lib/screens/account/programs.dart
Normal file
291
lib/screens/account/programs.dart
Normal 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: ResponsiveScaffold.getIsExpand(context),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -27,10 +27,12 @@ class AccountPublisherEditScreen extends StatefulWidget {
|
||||
const AccountPublisherEditScreen({super.key, required this.name});
|
||||
|
||||
@override
|
||||
State<AccountPublisherEditScreen> createState() => _AccountPublisherEditScreenState();
|
||||
State<AccountPublisherEditScreen> createState() =>
|
||||
_AccountPublisherEditScreenState();
|
||||
}
|
||||
|
||||
class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> {
|
||||
class _AccountPublisherEditScreenState
|
||||
extends State<AccountPublisherEditScreen> {
|
||||
bool _isBusy = false;
|
||||
|
||||
SnPublisher? _publisher;
|
||||
@ -115,29 +117,32 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
||||
|
||||
Uint8List? rawBytes;
|
||||
if (!skipCrop) {
|
||||
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios =
|
||||
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
|
||||
final result =
|
||||
(!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||
? await showCupertinoImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
context,
|
||||
allowedAspectRatios: aspectRatios,
|
||||
imageProvider: imageProvider,
|
||||
)
|
||||
: await showMaterialImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
context,
|
||||
allowedAspectRatios: aspectRatios,
|
||||
imageProvider: imageProvider,
|
||||
);
|
||||
final ImageProvider imageProvider =
|
||||
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||
final aspectRatios = place == 'banner'
|
||||
? [CropAspectRatio(width: 16, height: 7)]
|
||||
: [CropAspectRatio(width: 1, height: 1)];
|
||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||
? await showCupertinoImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
context,
|
||||
allowedAspectRatios: aspectRatios,
|
||||
imageProvider: imageProvider,
|
||||
)
|
||||
: await showMaterialImageCropper(
|
||||
// ignore: use_build_context_synchronously
|
||||
context,
|
||||
allowedAspectRatios: aspectRatios,
|
||||
imageProvider: imageProvider,
|
||||
);
|
||||
|
||||
if (result == null) return;
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _isBusy = true);
|
||||
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
||||
rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
||||
.buffer
|
||||
.asUint8List();
|
||||
} else {
|
||||
if (!mounted) return;
|
||||
setState(() => _isBusy = true);
|
||||
@ -191,7 +196,10 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(leading: PageBackButton(), title: Text('screenAccountPublisherEdit').tr()),
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountPublisherEdit').tr()),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
@ -206,13 +214,16 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
aspectRatio: 16 / 7,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child:
|
||||
_banner != null
|
||||
? AutoResizeUniversalImage(sn.getAttachmentUrl(_banner!), fit: BoxFit.cover)
|
||||
: const SizedBox.shrink(),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerHigh,
|
||||
child: _banner != null
|
||||
? AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(_banner!),
|
||||
fit: BoxFit.cover)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -245,13 +256,15 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
||||
labelText: 'fieldUsername'.tr(),
|
||||
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
controller: _nickController,
|
||||
decoration: InputDecoration(labelText: 'fieldNickname'.tr()),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
@ -259,7 +272,8 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
|
||||
maxLines: null,
|
||||
minLines: 3,
|
||||
decoration: InputDecoration(labelText: 'fieldDescription'.tr()),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
Row(
|
||||
|
@ -25,7 +25,8 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountPublisherNew').tr(),
|
||||
|
@ -33,7 +33,8 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
|
||||
try {
|
||||
final resp = await sn.client.get('/cgi/co/publishers/me');
|
||||
final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
||||
final List<SnPublisher> out = List<SnPublisher>.from(
|
||||
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
@ -81,6 +82,7 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: const PageBackButton(),
|
||||
title: Text('screenAccountPublishers').tr(),
|
||||
@ -93,7 +95,9 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.add_circle),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
|
||||
GoRouter.of(context)
|
||||
.pushNamed('accountPublisherNew')
|
||||
.then((value) {
|
||||
if (value == true) {
|
||||
_publishers.clear();
|
||||
_fetchPublishers();
|
||||
@ -119,7 +123,8 @@ class _PublisherScreenState extends State<PublisherScreen> {
|
||||
return ListTile(
|
||||
title: Text(publisher.nick),
|
||||
subtitle: Text('@${publisher.name}'),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: AccountImage(content: publisher.avatar),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (BuildContext context) => [
|
||||
|
187
lib/screens/account/punishments.dart
Normal file
187
lib/screens/account/punishments.dart
Normal 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: ResponsiveScaffold.getIsExpand(context),
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -37,6 +37,7 @@ class AccountSettingsScreen extends StatelessWidget {
|
||||
final ua = context.watch<UserProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountSettings').tr(),
|
||||
@ -97,6 +98,36 @@ class AccountSettingsScreen extends StatelessWidget {
|
||||
GoRouter.of(context).pushNamed('accountContactMethods');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountSettingsNotify').tr(),
|
||||
subtitle: Text('accountSettingsNotifyDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.notifications),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountSettingsNotify');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountSettingsSecurity').tr(),
|
||||
subtitle: Text('accountSettingsSecurityDescription').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.shield),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('accountSettingsSecurity');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('factorSettings').tr(),
|
||||
subtitle: Text('factorSettingsSubtitle').tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.lock),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('factorSettings');
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('accountProfileEdit').tr(),
|
||||
subtitle: Text('accountProfileEditSubtitle').tr(),
|
@ -8,9 +8,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
@ -54,7 +52,6 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': _attachments.length,
|
||||
@ -65,8 +62,6 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
_attachments.addAll(attachments);
|
||||
_heroTags.addAll(_attachments.map((_) => uuid.v4()));
|
||||
|
||||
await ud.listAccount(attachments.map((e) => e.accountId).toSet());
|
||||
|
||||
_totalCount = resp.data['count'] as int?;
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
@ -106,7 +101,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAlbum').tr(),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
@ -119,7 +114,8 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
child: CircularProgressIndicator(
|
||||
value: _billing?.includedRatio ?? 0,
|
||||
strokeWidth: 8,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
).padding(all: 12),
|
||||
const Gap(24),
|
||||
@ -129,7 +125,8 @@ class _AlbumScreenState extends State<AlbumScreen> {
|
||||
children: [
|
||||
Text('attachmentBillingUploaded').tr().bold(),
|
||||
Text(
|
||||
(_billing?.currentBytes ?? 0).formatBytes(decimals: 4),
|
||||
(_billing?.currentBytes ?? 0)
|
||||
.formatBytes(decimals: 4),
|
||||
style: GoogleFonts.robotoMono(),
|
||||
),
|
||||
Text('attachmentBillingDiscount').tr().bold(),
|
||||
|
@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/captcha/captcha.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
@ -33,10 +34,20 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
final username = _usernameController.value.text;
|
||||
final nickname = _nicknameController.value.text;
|
||||
final password = _passwordController.value.text;
|
||||
if (email.isEmpty || username.isEmpty || nickname.isEmpty || password.isEmpty) {
|
||||
if (email.isEmpty ||
|
||||
username.isEmpty ||
|
||||
nickname.isEmpty ||
|
||||
password.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CaptchaScreen(),
|
||||
),
|
||||
);
|
||||
if (captchaTk == null) return;
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/id/users', data: {
|
||||
@ -45,6 +56,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
'email': email,
|
||||
'password': password,
|
||||
'language': EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
'captcha_token': captchaTk,
|
||||
});
|
||||
|
||||
if (!context.mounted) return;
|
||||
@ -91,8 +103,11 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
children: [
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (value == null || value.length < 4 || value.length > 32) {
|
||||
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
||||
if (value == null ||
|
||||
value.length < 4 ||
|
||||
value.length > 32) {
|
||||
return 'fieldUsernameLengthLimit'
|
||||
.tr(args: [4.toString(), 32.toString()]);
|
||||
}
|
||||
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
||||
return 'fieldUsernameAlphanumOnly'.tr();
|
||||
@ -108,13 +123,17 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldUsername'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextFormField(
|
||||
validator: (value) {
|
||||
if (value == null || value.length < 4 || value.length > 32) {
|
||||
return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
|
||||
if (value == null ||
|
||||
value.length < 4 ||
|
||||
value.length > 32) {
|
||||
return 'fieldNicknameLengthLimit'
|
||||
.tr(args: [4.toString(), 32.toString()]);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@ -127,7 +146,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldNickname'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextFormField(
|
||||
@ -149,7 +169,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldEmail'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextFormField(
|
||||
@ -169,7 +190,8 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldPassword'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 7),
|
||||
@ -186,9 +208,13 @@ class _RegisterScreenState extends State<RegisterScreen> {
|
||||
Text(
|
||||
'termAcceptNextWithAgree'.tr(),
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
||||
),
|
||||
style:
|
||||
Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withAlpha((255 * 0.75).round()),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
|
3
lib/screens/captcha/captcha.dart
Normal file
3
lib/screens/captcha/captcha.dart
Normal file
@ -0,0 +1,3 @@
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
|
||||
export 'captcha_native.dart' if (kIsWeb) 'captcha_web.dart';
|
37
lib/screens/captcha/captcha_native.dart
Normal file
37
lib/screens/captcha/captcha_native.dart
Normal 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;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
54
lib/screens/captcha/captcha_web.dart
Normal file
54
lib/screens/captcha/captcha_web.dart
Normal 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'),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||
@ -6,21 +8,22 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:responsive_framework/responsive_framework.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_realm.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/screens/chat/room.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/account/account_image.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/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_background.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/unauthorized_hint.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
@ -38,6 +41,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
List<SnChannel>? _channels;
|
||||
Map<int, SnChatMessage>? _lastMessages;
|
||||
Map<int, int>? _unreadCounts;
|
||||
Map<int, int>? _unreadCountsGrouped;
|
||||
|
||||
Future<void> _fetchWhatsNew() async {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
@ -45,19 +49,48 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
if (resp.data == null) return;
|
||||
final List<dynamic> out = resp.data;
|
||||
setState(() {
|
||||
_unreadCounts = {for (var v in out) v['channel_id']: v['count']};
|
||||
_unreadCounts ??= {};
|
||||
_unreadCountsGrouped ??= {};
|
||||
for (var v in out) {
|
||||
_unreadCounts![v['channel_id']] = v['count'];
|
||||
final channel =
|
||||
_channels?.firstWhereOrNull((ele) => ele.id == v['channel_id']);
|
||||
if (channel != null) {
|
||||
if (channel.realmId != null) {
|
||||
_unreadCountsGrouped![channel.realmId!] ??= 0;
|
||||
_unreadCountsGrouped![channel.realmId!] =
|
||||
(_unreadCountsGrouped![channel.realmId!]! + v['count']).toInt();
|
||||
}
|
||||
if (channel.type == 1) {
|
||||
_unreadCountsGrouped![0] ??= 0;
|
||||
_unreadCountsGrouped![0] =
|
||||
(_unreadCountsGrouped![0]! + v['count']).toInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _refreshChannels({bool noRemote = false}) {
|
||||
void _refreshChannels({bool withBoost = false, bool noRemote = false}) {
|
||||
final ct = context.read<ChatChannelProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
if (!ua.isAuthorized) {
|
||||
setState(() => _isBusy = false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!withBoost) {
|
||||
if (!noRemote) {
|
||||
ct.refreshAvailableChannels();
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_channels = ct.availableChannels;
|
||||
});
|
||||
}
|
||||
|
||||
final chan = context.read<ChatChannelProvider>();
|
||||
chan.fetchChannels(noRemote: noRemote).listen((channels) async {
|
||||
chan.fetchChannels(noRemote: true).listen((channels) async {
|
||||
final lastMessages = await chan.getLastMessages(channels);
|
||||
_lastMessages = {for (final val in lastMessages) val.channelId: val};
|
||||
channels.sort((a, b) {
|
||||
@ -99,6 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
..onDone(() {
|
||||
if (!mounted) return;
|
||||
setState(() => _isBusy = false);
|
||||
_fetchWhatsNew();
|
||||
});
|
||||
}
|
||||
|
||||
@ -130,40 +164,51 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
SnChannel? _focusChannel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_refreshChannels();
|
||||
_fetchWhatsNew();
|
||||
_refreshChannels(withBoost: true);
|
||||
}
|
||||
|
||||
void _onTapChannel(SnChannel channel) {
|
||||
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP);
|
||||
|
||||
if (doExpand) {
|
||||
setState(() => _focusChannel = channel);
|
||||
return;
|
||||
setState(() => _unreadCounts?[channel.id] = 0);
|
||||
if (ResponsiveScaffold.getIsExpand(context)) {
|
||||
GoRouter.of(context).pushReplacementNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {
|
||||
'scope': channel.realm?.alias ?? 'global',
|
||||
'alias': channel.alias,
|
||||
},
|
||||
).then((value) {
|
||||
if (mounted) {
|
||||
setState(() => _unreadCounts?[channel.id] = 0);
|
||||
_refreshChannels(noRemote: true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {
|
||||
'scope': channel.realm?.alias ?? 'global',
|
||||
'alias': channel.alias,
|
||||
},
|
||||
).then((value) {
|
||||
if (mounted) {
|
||||
setState(() => _unreadCounts?[channel.id] = 0);
|
||||
_refreshChannels(noRemote: true);
|
||||
}
|
||||
});
|
||||
}
|
||||
GoRouter.of(context).pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {
|
||||
'scope': channel.realm?.alias ?? 'global',
|
||||
'alias': channel.alias,
|
||||
},
|
||||
).then((value) {
|
||||
if (mounted) {
|
||||
_unreadCounts?[channel.id] = 0;
|
||||
setState(() => _unreadCounts?[channel.id] = 0);
|
||||
_refreshChannels(noRemote: true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SnRealm? _focusedRealm;
|
||||
bool _isDirect = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ua = context.read<UserProvider>();
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final rel = context.read<SnRealmProvider>();
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return AppScaffold(
|
||||
@ -177,10 +222,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP);
|
||||
|
||||
final chatList = AppScaffold(
|
||||
noBackground: doExpand,
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenChat').tr(),
|
||||
@ -248,64 +291,198 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
if (_channels != null && ResponsiveScaffold.getIsExpand(context))
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.wait([
|
||||
Future.sync(() => _refreshChannels()),
|
||||
_fetchWhatsNew(),
|
||||
]),
|
||||
child: ListView.builder(
|
||||
itemCount: _channels?.length ?? 0,
|
||||
itemBuilder: (context, idx) {
|
||||
final channel = _channels![idx];
|
||||
final lastMessage = _lastMessages?[channel.id];
|
||||
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||
child: Builder(builder: (context) {
|
||||
final scopeList = ListView(
|
||||
key: const Key('realm-list-view'),
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading:
|
||||
const Icon(Symbols.inbox_text).padding(right: 4),
|
||||
contentPadding: EdgeInsets.only(left: 24, right: 24),
|
||||
title: Text('chatDirect').tr(),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (_unreadCountsGrouped?[0] != null &&
|
||||
(_unreadCountsGrouped?[0] ?? 0) > 0)
|
||||
Badge(
|
||||
label: Text(
|
||||
_unreadCountsGrouped![0].toString(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
setState(() => _isDirect = true);
|
||||
},
|
||||
),
|
||||
...rel.availableRealms.map((ele) {
|
||||
return ListTile(
|
||||
minTileHeight: 48,
|
||||
contentPadding: EdgeInsets.only(left: 20, right: 24),
|
||||
leading: AccountImage(
|
||||
content: ele.avatar,
|
||||
radius: 16,
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (_unreadCountsGrouped?[ele.id] != null &&
|
||||
(_unreadCountsGrouped?[ele.id] ?? 0) > 0)
|
||||
Badge(
|
||||
label: Text(
|
||||
_unreadCountsGrouped![ele.id].toString(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Text(ele.name),
|
||||
onTap: () {
|
||||
setState(() => _focusedRealm = ele);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
return _ChatChannelEntry(
|
||||
channel: channel,
|
||||
lastMessage: lastMessage,
|
||||
unreadCount: _unreadCounts?[channel.id],
|
||||
onTap: () {
|
||||
if (doExpand) {
|
||||
_unreadCounts?[channel.id] = 0;
|
||||
setState(() => _focusChannel = channel);
|
||||
return;
|
||||
}
|
||||
_onTapChannel(channel);
|
||||
},
|
||||
);
|
||||
},
|
||||
final directChatList = ListView(
|
||||
key: Key('direct-chat-list-view'),
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.arrow_left_alt),
|
||||
contentPadding: EdgeInsets.only(left: 24),
|
||||
title: Text('back').tr(),
|
||||
onTap: () {
|
||||
setState(() => _isDirect = false);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1),
|
||||
..._channels!.where((ele) => ele.type == 1).map(
|
||||
(ele) {
|
||||
return _ChatChannelEntry(
|
||||
channel: ele,
|
||||
unreadCount: _unreadCounts?[ele.id],
|
||||
lastMessage: _lastMessages?[ele.id],
|
||||
isCompact: true,
|
||||
onTap: () => _onTapChannel(ele),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
final realmScopedChatList = _focusedRealm == null
|
||||
? const SizedBox.shrink()
|
||||
: ListView(
|
||||
key: ValueKey(_focusedRealm),
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
if (_focusedRealm!.banner != null)
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: AutoResizeUniversalImage(
|
||||
sn.getAttachmentUrl(
|
||||
_focusedRealm!.banner!,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
tileColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer,
|
||||
leading: AccountImage(
|
||||
content: _focusedRealm!.avatar,
|
||||
radius: 16,
|
||||
),
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 20,
|
||||
right: 16,
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () {
|
||||
setState(() => _focusedRealm = null);
|
||||
},
|
||||
),
|
||||
title: Text(_focusedRealm!.name),
|
||||
),
|
||||
...(_channels!
|
||||
.where(
|
||||
(ele) => ele.realmId == _focusedRealm?.id)
|
||||
.map(
|
||||
(ele) {
|
||||
return _ChatChannelEntry(
|
||||
channel: ele,
|
||||
unreadCount: _unreadCounts?[ele.id],
|
||||
lastMessage: _lastMessages?[ele.id],
|
||||
onTap: () => _onTapChannel(ele),
|
||||
isCompact: true,
|
||||
);
|
||||
},
|
||||
))
|
||||
],
|
||||
);
|
||||
|
||||
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: (_focusedRealm == null && !_isDirect)
|
||||
? scopeList
|
||||
: _isDirect
|
||||
? directChatList
|
||||
: realmScopedChatList,
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
else if (_channels != null)
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.sync(() => _refreshChannels()),
|
||||
child: ListView(
|
||||
key: const Key('chat-list-view'),
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
...(_channels!.map((ele) {
|
||||
return _ChatChannelEntry(
|
||||
channel: ele,
|
||||
unreadCount: _unreadCounts?[ele.id],
|
||||
lastMessage: _lastMessages?[ele.id],
|
||||
onTap: () => _onTapChannel(ele),
|
||||
);
|
||||
}))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -314,11 +491,13 @@ class _ChatChannelEntry extends StatelessWidget {
|
||||
final int? unreadCount;
|
||||
final SnChatMessage? lastMessage;
|
||||
final Function? onTap;
|
||||
final bool isCompact;
|
||||
const _ChatChannelEntry({
|
||||
required this.channel,
|
||||
this.unreadCount,
|
||||
this.lastMessage,
|
||||
this.onTap,
|
||||
this.isCompact = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -337,6 +516,34 @@ class _ChatChannelEntry extends StatelessWidget {
|
||||
? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name
|
||||
: channel.name;
|
||||
|
||||
if (isCompact) {
|
||||
return ListTile(
|
||||
minTileHeight: 48,
|
||||
contentPadding:
|
||||
EdgeInsets.only(left: otherMember != null ? 20 : 24, right: 24),
|
||||
leading: otherMember != null
|
||||
? AccountImage(
|
||||
content: ud.getFromCache(otherMember.accountId)?.avatar,
|
||||
radius: 16,
|
||||
)
|
||||
: const Icon(Symbols.tag),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (unreadCount != null && (unreadCount ?? 0) > 0)
|
||||
Badge(
|
||||
label: Text(unreadCount.toString()),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Text(title),
|
||||
onTap: () {
|
||||
onTap?.call();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
@ -399,7 +606,7 @@ class _ChatChannelEntry extends StatelessWidget {
|
||||
content: otherMember != null
|
||||
? ud.getFromCache(otherMember.accountId)?.avatar
|
||||
: channel.realm?.avatar,
|
||||
fallbackWidget: const Icon(Symbols.chat, size: 20),
|
||||
fallbackWidget: const Icon(Symbols.tag, size: 20),
|
||||
),
|
||||
onTap: () => onTap?.call(),
|
||||
);
|
||||
|
@ -32,17 +32,16 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildListLayout() {
|
||||
Widget _buildMeetLayout() {
|
||||
final call = context.read<ChatCallProvider>();
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75),
|
||||
child: call.focusTrack != null
|
||||
? InteractiveParticipantWidget(
|
||||
isFixedAvatar: false,
|
||||
participant: call.focusTrack!,
|
||||
onTap: () {},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
@ -61,22 +60,18 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
return Container();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8, left: 8),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: InteractiveParticipantWidget(
|
||||
isFixedAvatar: true,
|
||||
width: 120,
|
||||
height: 120,
|
||||
color: Theme.of(context).cardColor,
|
||||
participant: track,
|
||||
onTap: () {
|
||||
if (track.participant.sid != call.focusTrack?.participant.sid) {
|
||||
call.setFocusTrack(track);
|
||||
}
|
||||
},
|
||||
),
|
||||
return SizedBox(
|
||||
height: 128,
|
||||
width: 128,
|
||||
child: InteractiveParticipantWidget(
|
||||
participant: track,
|
||||
avatarSize: 32,
|
||||
onTap: () {
|
||||
if (track.participant.sid !=
|
||||
call.focusTrack?.participant.sid) {
|
||||
call.setFocusTrack(track);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -87,46 +82,26 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGridLayout() {
|
||||
Widget _buildListLayout() {
|
||||
final call = context.read<ChatCallProvider>();
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
double screenWidth = constraints.maxWidth;
|
||||
double screenHeight = constraints.maxHeight;
|
||||
|
||||
int columns = (math.sqrt(call.participantTracks.length)).ceil();
|
||||
int rows = (call.participantTracks.length / columns).ceil();
|
||||
|
||||
double tileWidth = screenWidth / columns;
|
||||
double tileHeight = screenHeight / rows;
|
||||
|
||||
return StyledWidget(GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
childAspectRatio: tileWidth / tileHeight,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemCount: math.max(0, call.participantTracks.length),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final track = call.participantTracks[index];
|
||||
return Card(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: InteractiveParticipantWidget(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
|
||||
participant: track,
|
||||
onTap: () {
|
||||
if (track.participant.sid != call.focusTrack?.participant.sid) {
|
||||
call.setFocusTrack(track);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)).padding(all: 8);
|
||||
});
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: math.max(0, call.participantTracks.length),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final track = call.participantTracks[index];
|
||||
return InteractiveParticipantWidget(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
isList: true,
|
||||
avatarSize: 24,
|
||||
participant: track,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -149,6 +124,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
listenable: call,
|
||||
builder: (context, _) {
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
title: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
@ -169,117 +145,129 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
|
||||
]),
|
||||
),
|
||||
),
|
||||
body: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Column(
|
||||
children: [
|
||||
body: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: 64,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Builder(builder: (context) {
|
||||
final call = context.read<ChatCallProvider>();
|
||||
final connectionQuality =
|
||||
call.room.localParticipant?.connectionQuality ??
|
||||
livekit.ConnectionQuality.unknown;
|
||||
return Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
call.channel?.name ?? 'unknown'.tr(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Gap(6),
|
||||
Text(call.lastDuration.toString())
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
{
|
||||
livekit.ConnectionState.disconnected:
|
||||
'callStatusDisconnected'.tr(),
|
||||
livekit.ConnectionState.connected:
|
||||
'callStatusConnected'.tr(),
|
||||
livekit.ConnectionState.connecting:
|
||||
'callStatusConnecting'.tr(),
|
||||
livekit.ConnectionState.reconnecting:
|
||||
'callStatusReconnecting'.tr(),
|
||||
}[call.room.connectionState]!,
|
||||
),
|
||||
const Gap(6),
|
||||
if (connectionQuality !=
|
||||
livekit.ConnectionQuality.unknown)
|
||||
Icon(
|
||||
{
|
||||
livekit.ConnectionQuality.excellent:
|
||||
Icons.signal_cellular_alt,
|
||||
livekit.ConnectionQuality.good:
|
||||
Icons.signal_cellular_alt_2_bar,
|
||||
livekit.ConnectionQuality.poor:
|
||||
Icons.signal_cellular_alt_1_bar,
|
||||
}[connectionQuality],
|
||||
color: {
|
||||
livekit.ConnectionQuality.excellent:
|
||||
Colors.green,
|
||||
livekit.ConnectionQuality.good:
|
||||
Colors.orange,
|
||||
livekit.ConnectionQuality.poor:
|
||||
Colors.red,
|
||||
}[connectionQuality],
|
||||
size: 16,
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
).padding(all: 3),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: _layoutMode == 0
|
||||
? const Icon(Icons.view_list)
|
||||
: const Icon(Icons.grid_view),
|
||||
onPressed: () {
|
||||
_switchLayout();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(left: 20, right: 16),
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
switch (_layoutMode) {
|
||||
case 1:
|
||||
return _buildListLayout();
|
||||
default:
|
||||
return _buildMeetLayout();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (call.room.localParticipant != null)
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: 64,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Builder(builder: (context) {
|
||||
final call = context.read<ChatCallProvider>();
|
||||
final connectionQuality =
|
||||
call.room.localParticipant?.connectionQuality ?? livekit.ConnectionQuality.unknown;
|
||||
return Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
call.channel?.name ?? 'unknown'.tr(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Gap(6),
|
||||
Text(call.lastDuration.toString())
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
{
|
||||
livekit.ConnectionState.disconnected: 'callStatusDisconnected'.tr(),
|
||||
livekit.ConnectionState.connected: 'callStatusConnected'.tr(),
|
||||
livekit.ConnectionState.connecting: 'callStatusConnecting'.tr(),
|
||||
livekit.ConnectionState.reconnecting: 'callStatusReconnecting'.tr(),
|
||||
}[call.room.connectionState]!,
|
||||
),
|
||||
const Gap(6),
|
||||
if (connectionQuality != livekit.ConnectionQuality.unknown)
|
||||
Icon(
|
||||
{
|
||||
livekit.ConnectionQuality.excellent: Icons.signal_cellular_alt,
|
||||
livekit.ConnectionQuality.good: Icons.signal_cellular_alt_2_bar,
|
||||
livekit.ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar,
|
||||
}[connectionQuality],
|
||||
color: {
|
||||
livekit.ConnectionQuality.excellent: Colors.green,
|
||||
livekit.ConnectionQuality.good: Colors.orange,
|
||||
livekit.ConnectionQuality.poor: Colors.red,
|
||||
}[connectionQuality],
|
||||
size: 16,
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
).padding(all: 3),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: _layoutMode == 0 ? const Icon(Icons.view_list) : const Icon(Icons.grid_view),
|
||||
onPressed: () {
|
||||
_switchLayout();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).padding(left: 20, right: 16),
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
switch (_layoutMode) {
|
||||
case 1:
|
||||
return _buildGridLayout();
|
||||
default:
|
||||
return _buildListLayout();
|
||||
}
|
||||
},
|
||||
),
|
||||
child: ControlsWidget(
|
||||
call.room,
|
||||
call.room.localParticipant!,
|
||||
),
|
||||
),
|
||||
if (call.room.localParticipant != null)
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: ControlsWidget(
|
||||
call.room,
|
||||
call.room.localParticipant!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {},
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
@ -220,6 +220,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
final isOwned = ua.isAuthorized && _channel?.accountId == ua.user?.id;
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
|
||||
),
|
||||
|
@ -49,7 +49,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
resp.data?.map((e) => SnRealm.fromJson(e)) ?? [],
|
||||
);
|
||||
if (_editingChannel != null) {
|
||||
_belongToRealm = _realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
|
||||
_belongToRealm =
|
||||
_realms?.firstWhereOrNull((e) => e.id == _editingChannel!.realmId);
|
||||
}
|
||||
} catch (err) {
|
||||
if (mounted) context.showErrorDialog(err);
|
||||
@ -97,7 +98,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
'is_community': _isCommunity,
|
||||
if (_editingChannel != null && _belongToRealm == null)
|
||||
'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,
|
||||
};
|
||||
|
||||
@ -139,8 +141,11 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
title: widget.editingChannelAlias != null ? Text('screenChatManage').tr() : Text('screenChatNew').tr(),
|
||||
title: widget.editingChannelAlias != null
|
||||
? Text('screenChatManage').tr()
|
||||
: Text('screenChatNew').tr(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
@ -152,7 +157,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
leadingPadding: const EdgeInsets.only(left: 10, right: 20),
|
||||
dividerColor: Colors.transparent,
|
||||
content: Text(
|
||||
'channelEditingNotice'.tr(args: ['#${_editingChannel!.alias}']),
|
||||
'channelEditingNotice'
|
||||
.tr(args: ['#${_editingChannel!.alias}']),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
@ -192,12 +198,15 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.name).textStyle(Theme.of(context).textTheme.bodyMedium!),
|
||||
Text(item.name).textStyle(Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!),
|
||||
Text(
|
||||
item.description,
|
||||
maxLines: 1,
|
||||
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(
|
||||
radius: 16,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onSurface,
|
||||
child: const Icon(Symbols.clear),
|
||||
),
|
||||
const Gap(12),
|
||||
@ -222,7 +232,9 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('fieldChatBelongToRealmUnset').tr().textStyle(
|
||||
Text('fieldChatBelongToRealmUnset')
|
||||
.tr()
|
||||
.textStyle(
|
||||
Theme.of(context).textTheme.bodyMedium!,
|
||||
),
|
||||
],
|
||||
@ -257,7 +269,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
helperText: 'fieldChatAliasHint'.tr(),
|
||||
helperMaxLines: 2,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
@ -266,7 +279,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldChatName'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(4),
|
||||
TextField(
|
||||
@ -277,7 +291,8 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
|
||||
border: const UnderlineInputBorder(),
|
||||
labelText: 'fieldChatDescription'.tr(),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
CheckboxListTile(
|
||||
|
@ -52,8 +52,10 @@ class ChatRoomScreen extends StatefulWidget {
|
||||
class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
bool _isBusy = false;
|
||||
bool _isCalling = false;
|
||||
bool _isJoining = false;
|
||||
|
||||
SnChannel? _channel;
|
||||
SnChannelMember? _currentMember;
|
||||
SnChannelMember? _otherMember;
|
||||
SnChatCall? _ongoingCall;
|
||||
|
||||
@ -67,7 +69,24 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
|
||||
StreamSubscription? _wsSubscription;
|
||||
|
||||
// TODO fetch user identity and ask them to join the channel or not
|
||||
Future<void> _joinChannel() async {
|
||||
try {
|
||||
setState(() => _isJoining = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
await sn.client
|
||||
.post('/cgi/im/channels/${_channel!.keyPath}/members', data: {
|
||||
'related': ua.user?.name,
|
||||
});
|
||||
_initializeChat();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isJoining = true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchChannel() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
@ -76,6 +95,12 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
_channel = await chan.getChannel('${widget.scope}:${widget.alias}');
|
||||
|
||||
if (!mounted || _channel == null) return;
|
||||
final ct = context.read<ChatChannelProvider>();
|
||||
try {
|
||||
_currentMember = await ct.getChannelProfile(_channel!);
|
||||
} catch (_) {}
|
||||
|
||||
if (!mounted || _currentMember == null) return;
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
if (_channel!.type == 1) {
|
||||
@ -204,11 +229,9 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
return a.createdAt.difference(b.createdAt).inMinutes <= 3;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_messageController = ChatMessageController(context);
|
||||
Future<void> _initializeChat() async {
|
||||
_fetchChannel().then((_) async {
|
||||
if (_currentMember == null) return;
|
||||
await _messageController.initialize(_channel!);
|
||||
|
||||
if (widget.extra != null) {
|
||||
@ -230,6 +253,13 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
_fetchOngoingCall(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_messageController = ChatMessageController(context);
|
||||
_initializeChat();
|
||||
|
||||
_wsSubscription = _ws.pk.stream.listen((event) {
|
||||
switch (event.method) {
|
||||
@ -274,6 +304,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_channel?.type == 1
|
||||
@ -281,25 +312,27 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
: _channel?.name ?? 'loading'.tr(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() => _isEncrypted = !_isEncrypted);
|
||||
_inputGlobalKey.currentState?.setEncrypted(_isEncrypted);
|
||||
},
|
||||
icon: _isEncrypted
|
||||
? const Icon(Symbols.lock)
|
||||
: const Icon(Symbols.no_encryption),
|
||||
),
|
||||
IconButton(
|
||||
icon: _ongoingCall == null
|
||||
? const Icon(Symbols.call)
|
||||
: const Icon(Symbols.call_end),
|
||||
onPressed: _isCalling
|
||||
? null
|
||||
: _ongoingCall == null
|
||||
? _makeCall
|
||||
: _endCall,
|
||||
),
|
||||
if (_currentMember != null)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() => _isEncrypted = !_isEncrypted);
|
||||
_inputGlobalKey.currentState?.setEncrypted(_isEncrypted);
|
||||
},
|
||||
icon: _isEncrypted
|
||||
? const Icon(Symbols.lock)
|
||||
: const Icon(Symbols.no_encryption),
|
||||
),
|
||||
if (_currentMember != null)
|
||||
IconButton(
|
||||
icon: _ongoingCall == null
|
||||
? const Icon(Symbols.call)
|
||||
: const Icon(Symbols.call_end),
|
||||
onPressed: _isCalling
|
||||
? null
|
||||
: _ongoingCall == null
|
||||
? _makeCall
|
||||
: _endCall,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.more_vert),
|
||||
onPressed: () {
|
||||
@ -348,7 +381,41 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
).height(_ongoingCall != null ? 54 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.fastLinearToSlowEaseIn),
|
||||
if (_messageController.isPending)
|
||||
if (_currentMember == null && !_isBusy)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Symbols.person_remove, size: 40, fill: 1),
|
||||
const Gap(8),
|
||||
Text('chatUnjoined'.tr(), textAlign: TextAlign.center)
|
||||
.fontSize(16)
|
||||
.bold(),
|
||||
Text('chatUnjoinedDescription'.tr(),
|
||||
textAlign: TextAlign.center)
|
||||
.fontSize(13),
|
||||
if (_channel!.isPublic)
|
||||
Text('chatUnjoinedPublicDescription'.tr(),
|
||||
textAlign: TextAlign.center)
|
||||
.fontSize(13)
|
||||
.padding(top: 8),
|
||||
if (_channel!.isPublic)
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
onPressed: _isJoining ? null : _joinChannel,
|
||||
child: Text('chatJoin').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (_messageController.isPending)
|
||||
Expanded(
|
||||
child: const CircularProgressIndicator().center(),
|
||||
)
|
||||
@ -403,7 +470,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!_messageController.isPending)
|
||||
if (!_messageController.isPending && _currentMember != null)
|
||||
Material(
|
||||
elevation: 2,
|
||||
child: Column(
|
||||
|
@ -157,6 +157,7 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
Widget build(BuildContext context) {
|
||||
final cfg = context.watch<ConfigProvider>();
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
floatingActionButtonLocation: ExpandableFab.location,
|
||||
floatingActionButton: ExpandableFab(
|
||||
key: _fabKey,
|
||||
@ -243,6 +244,8 @@ class _ExploreScreenState extends State<ExploreScreen>
|
||||
GoRouter.of(context).pushNamed('postShuffle');
|
||||
},
|
||||
),
|
||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE))
|
||||
const Gap(48),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: IconButton(
|
||||
@ -449,7 +452,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
data: ele.toJson(),
|
||||
createdAt: ele.createdAt)),
|
||||
);
|
||||
_hasLoadedAll = postCount >= _feed.length;
|
||||
_hasLoadedAll = _feed.length >= postCount;
|
||||
|
||||
if (mounted) setState(() => _isBusy = false);
|
||||
}
|
||||
@ -534,6 +537,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
switch (ele.type) {
|
||||
case 'interactive.post':
|
||||
return OpenablePostItem(
|
||||
useReplace: true,
|
||||
data: SnPost.fromJson(ele.data),
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
@ -551,9 +555,11 @@ class _PostListWidgetState extends State<_PostListWidget> {
|
||||
maxWidth: 640,
|
||||
);
|
||||
case 'reader.news':
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: NewsFeedEntry(data: ele),
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: NewsFeedEntry(data: ele),
|
||||
),
|
||||
);
|
||||
default:
|
||||
return Container(
|
||||
|
@ -10,7 +10,6 @@ import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/widgets/account/account_image.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/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
@ -47,8 +46,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/me/relations?status=1');
|
||||
_relations = List<SnRelationship>.from(
|
||||
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
|
||||
);
|
||||
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -67,8 +65,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/me/relations?status=0,3');
|
||||
_requests = List<SnRelationship>.from(
|
||||
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
|
||||
);
|
||||
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -87,8 +84,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/users/me/relations?status=2');
|
||||
_blocks = List<SnRelationship>.from(
|
||||
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? [],
|
||||
);
|
||||
resp.data?.map((e) => SnRelationship.fromJson(e)) ?? []);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -105,10 +101,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
try {
|
||||
final rel = context.read<SnRelationshipProvider>();
|
||||
await rel.updateRelationship(
|
||||
relation.relatedId,
|
||||
dstStatus,
|
||||
relation.permNodes,
|
||||
);
|
||||
relation.relatedId, dstStatus, relation.permNodes);
|
||||
if (!mounted) return;
|
||||
_fetchRelations();
|
||||
} catch (err) {
|
||||
@ -122,9 +115,8 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
Future<void> _deleteRelation(SnRelationship relation) async {
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
|
||||
'friendDeleteDescription'.tr(args: [
|
||||
relation.related?.nick ?? 'unknown'.tr(),
|
||||
]),
|
||||
'friendDeleteDescription'
|
||||
.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
|
||||
);
|
||||
if (!confirm) return;
|
||||
if (!mounted) return;
|
||||
@ -146,9 +138,11 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
|
||||
void _showRequests() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _FriendshipListWidget(relations: _requests),
|
||||
).then((value) {
|
||||
context: context,
|
||||
builder: (context) => _FriendshipListWidget(relations: _requests))
|
||||
.then((
|
||||
value,
|
||||
) {
|
||||
if (value != null) {
|
||||
_fetchRequests();
|
||||
_fetchRelations();
|
||||
@ -158,9 +152,10 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
|
||||
void _showBlocks() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => _FriendshipListWidget(relations: _blocks),
|
||||
).then((value) {
|
||||
context: context,
|
||||
builder: (context) => _FriendshipListWidget(relations: _blocks)).then((
|
||||
value,
|
||||
) {
|
||||
if (value != null) {
|
||||
_fetchBlocks();
|
||||
_fetchRelations();
|
||||
@ -173,9 +168,8 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/id/users/me/relations', data: {
|
||||
'related': user.name,
|
||||
});
|
||||
await sn.client
|
||||
.post('/cgi/id/users/me/relations', data: {'related': user.name});
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('friendRequestSent'.tr());
|
||||
} catch (err) {
|
||||
@ -201,18 +195,16 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
if (!ua.isAuthorized) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenFriend').tr(),
|
||||
),
|
||||
body: Center(
|
||||
child: UnauthorizedHint(),
|
||||
),
|
||||
body: Center(child: UnauthorizedHint()),
|
||||
);
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenFriend').tr(),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
@ -220,9 +212,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
onPressed: () async {
|
||||
final user = await showModalBottomSheet<SnAccount?>(
|
||||
context: context,
|
||||
builder: (context) => AccountSelect(
|
||||
title: 'friendNew'.tr(),
|
||||
),
|
||||
builder: (context) => AccountSelect(title: 'friendNew'.tr()),
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (user == null) return;
|
||||
@ -235,9 +225,8 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
if (_requests.isNotEmpty)
|
||||
ListTile(
|
||||
title: Text('friendRequests').tr(),
|
||||
subtitle: Text(
|
||||
'friendRequestsDescription',
|
||||
).plural(_requests.length),
|
||||
subtitle:
|
||||
Text('friendRequestsDescription').plural(_requests.length),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.group_add),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
@ -246,31 +235,30 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
if (_blocks.isNotEmpty)
|
||||
ListTile(
|
||||
title: Text('friendBlocklist').tr(),
|
||||
subtitle: Text(
|
||||
'friendBlocklistDescription',
|
||||
).plural(_blocks.length),
|
||||
subtitle:
|
||||
Text('friendBlocklistDescription').plural(_blocks.length),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Symbols.block),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: _showBlocks,
|
||||
),
|
||||
if (_requests.isNotEmpty || _blocks.isNotEmpty) const Divider(height: 1),
|
||||
if (_requests.isNotEmpty || _blocks.isNotEmpty)
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => Future.wait([
|
||||
_fetchRelations(),
|
||||
_fetchRequests(),
|
||||
]),
|
||||
onRefresh: () =>
|
||||
Future.wait([_fetchRelations(), _fetchRequests()]),
|
||||
child: ListView.builder(
|
||||
itemCount: _relations.length,
|
||||
itemBuilder: (context, index) {
|
||||
final relation = _relations[index];
|
||||
final other = relation.related;
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.only(right: 24, left: 16),
|
||||
contentPadding:
|
||||
const EdgeInsets.only(right: 24, left: 16),
|
||||
leading: AccountImage(content: other?.avatar),
|
||||
title: Text(other?.nick ?? 'unknown'),
|
||||
subtitle: Text(other?.nick ?? 'unknown'),
|
||||
@ -286,12 +274,16 @@ class _FriendScreenState extends State<FriendScreen> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: _isUpdating ? null : () => _changeRelation(relation, 2),
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _changeRelation(relation, 2),
|
||||
child: Text('friendBlock').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
InkWell(
|
||||
onTap: _isUpdating ? null : () => _deleteRelation(relation),
|
||||
onTap: _isUpdating
|
||||
? null
|
||||
: () => _deleteRelation(relation),
|
||||
child: Text('friendDeleteAction').tr(),
|
||||
),
|
||||
],
|
||||
@ -361,10 +353,7 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
||||
try {
|
||||
final rel = context.read<SnRelationshipProvider>();
|
||||
await rel.updateRelationship(
|
||||
relation.relatedId,
|
||||
dstStatus,
|
||||
relation.permNodes,
|
||||
);
|
||||
relation.relatedId, dstStatus, relation.permNodes);
|
||||
if (!mounted) return;
|
||||
Navigator.pop(context, true);
|
||||
} catch (err) {
|
||||
@ -378,9 +367,8 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
||||
Future<void> _deleteRelation(SnRelationship relation) async {
|
||||
final confirm = await context.showConfirmDialog(
|
||||
'friendDelete'.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
|
||||
'friendDeleteDescription'.tr(args: [
|
||||
relation.related?.nick ?? 'unknown'.tr(),
|
||||
]),
|
||||
'friendDeleteDescription'
|
||||
.tr(args: [relation.related?.nick ?? 'unknown'.tr()]),
|
||||
);
|
||||
if (!confirm) return;
|
||||
if (!mounted) return;
|
||||
@ -420,7 +408,9 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(kFriendStatus[relation.status] ?? 'unknown').tr().opacity(0.75),
|
||||
Text(kFriendStatus[relation.status] ?? 'unknown')
|
||||
.tr()
|
||||
.opacity(0.75),
|
||||
if (relation.status == 0)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
@ -441,7 +431,8 @@ class _FriendshipListWidgetState extends State<_FriendshipListWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: _isBusy ? null : () => _changeRelation(relation, 1),
|
||||
onTap:
|
||||
_isBusy ? null : () => _changeRelation(relation, 1),
|
||||
child: Text('friendUnblock').tr(),
|
||||
),
|
||||
const Gap(8),
|
||||
|
@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/special_day.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/providers/widget.dart';
|
||||
import 'package:surface/screens/captcha/captcha.dart';
|
||||
import 'package:surface/types/check_in.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
@ -389,41 +390,50 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
|
||||
size: 20,
|
||||
),
|
||||
const Gap(10),
|
||||
Text('serviceStatusOperational').tr(),
|
||||
Text('loading').tr(),
|
||||
],
|
||||
)
|
||||
: switch (_serviceStatus) {
|
||||
ServiceStatus.operational => Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icon(
|
||||
Symbols.check,
|
||||
size: 20,
|
||||
color: Colors.green[900],
|
||||
),
|
||||
const Gap(10),
|
||||
Text('serviceStatusOperational').tr(),
|
||||
Text('serviceStatusOperational')
|
||||
.tr()
|
||||
.textColor(Colors.green[900]),
|
||||
],
|
||||
),
|
||||
ServiceStatus.failed => Tooltip(
|
||||
message: 'serviceStatusFailedDescription'.tr(),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icon(
|
||||
Symbols.dangerous,
|
||||
size: 20,
|
||||
color: Colors.red[900],
|
||||
),
|
||||
const Gap(10),
|
||||
Text('serviceStatusFailed').tr(),
|
||||
Text('serviceStatusFailed')
|
||||
.tr()
|
||||
.textColor(Colors.red[900]),
|
||||
],
|
||||
),
|
||||
),
|
||||
_ => Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icon(
|
||||
Symbols.error,
|
||||
size: 20,
|
||||
color: Colors.orange[900],
|
||||
),
|
||||
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),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final entry in _statuses!.entries)
|
||||
Tooltip(
|
||||
@ -441,6 +452,8 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
|
||||
? 'serviceName${kServicesName[entry.key]}'.tr()
|
||||
: 'unknown'.tr(),
|
||||
child: Chip(
|
||||
visualDensity:
|
||||
VisualDensity(horizontal: -4, vertical: -4),
|
||||
avatar: entry.value
|
||||
? const Icon(
|
||||
Symbols.circle,
|
||||
@ -505,11 +518,20 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
||||
}
|
||||
|
||||
Future<void> _doCheckIn() async {
|
||||
final captchaTk = await Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CaptchaScreen(),
|
||||
),
|
||||
);
|
||||
if (captchaTk == null) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final home = context.read<HomeWidgetProvider>();
|
||||
final resp = await sn.client.post('/cgi/id/check-in');
|
||||
final resp = await sn.client.post('/cgi/id/check-in', data: {
|
||||
'captcha_token': captchaTk,
|
||||
});
|
||||
_todayRecord = SnCheckInRecord.fromJson(resp.data);
|
||||
await home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
|
||||
} catch (err) {
|
||||
@ -793,7 +815,7 @@ class _HomeDashNotificationWidgetState
|
||||
child: IconButton(
|
||||
icon: const Icon(Symbols.arrow_right_alt),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).goNamed('notification');
|
||||
GoRouter.of(context).pushNamed('notification');
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -877,8 +899,10 @@ class _HomeDashRecommendationPostWidgetState
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
Text('${_currentPage + 1}/${_posts?.length ?? 0}',
|
||||
style: GoogleFonts.robotoMono())
|
||||
Text(
|
||||
'${_currentPage + 1}/${_posts?.length ?? 0}',
|
||||
style: GoogleFonts.robotoMono(),
|
||||
)
|
||||
],
|
||||
).padding(horizontal: 18, top: 12, bottom: 8),
|
||||
Expanded(
|
||||
@ -896,6 +920,7 @@ class _HomeDashRecommendationPostWidgetState
|
||||
child: PostItem(
|
||||
data: _posts![index],
|
||||
showMenu: false,
|
||||
showFullPost: true,
|
||||
).padding(bottom: 8),
|
||||
onTap: () {
|
||||
GoRouter.of(context)
|
||||
|
@ -11,13 +11,11 @@ import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/notification.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/notification.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/markdown_content.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
import '../providers/userinfo.dart';
|
||||
@ -63,7 +61,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
queryParameters: {'take': 10, 'offset': _notifications.length},
|
||||
);
|
||||
_totalCount = resp.data['count'];
|
||||
_notifications.addAll(resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? []);
|
||||
_notifications.addAll(resp.data['data']
|
||||
?.map((e) => SnNotification.fromJson(e))
|
||||
.cast<SnNotification>() ??
|
||||
[]);
|
||||
nty.updateTray();
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
@ -98,7 +99,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
nty.clear();
|
||||
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('notificationMarkAllReadPrompt'.plural(resp.data['count']));
|
||||
context.showSnackbar(
|
||||
'notificationMarkAllReadPrompt'.plural(resp.data['count']));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -122,7 +124,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
_fetchNotifications();
|
||||
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']));
|
||||
context.showSnackbar(
|
||||
'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -143,7 +146,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
|
||||
if (!ua.isAuthorized) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(leading: AutoAppBarLeading(), title: Text('screenNotification').tr()),
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenNotification').tr(),
|
||||
),
|
||||
body: Center(child: UnauthorizedHint()),
|
||||
);
|
||||
}
|
||||
@ -153,7 +159,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
leading: AutoAppBarLeading(),
|
||||
title: Text('screenNotification').tr(),
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Symbols.checklist), onPressed: _isSubmitting ? null : _markAllAsRead),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.checklist),
|
||||
onPressed: _isSubmitting ? null : _markAllAsRead),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
@ -167,13 +175,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
return _fetchNotifications();
|
||||
},
|
||||
child: InfiniteList(
|
||||
padding: EdgeInsets.only(top: 16, bottom: math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
bottom:
|
||||
math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
itemCount: _notifications.length,
|
||||
onFetchData: () {
|
||||
_fetchNotifications();
|
||||
},
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!,
|
||||
hasReachedMax: _totalCount != null &&
|
||||
_notifications.length >= _totalCount!,
|
||||
itemBuilder: (context, idx) {
|
||||
final nty = _notifications[idx];
|
||||
return Row(
|
||||
@ -186,46 +198,55 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (nty.readAt == null)
|
||||
StyledWidget(Badge(label: Text('notificationUnread').tr())).padding(bottom: 4),
|
||||
Text(nty.title, style: Theme.of(context).textTheme.titleMedium),
|
||||
StyledWidget(Badge(
|
||||
label: Text('notificationUnread').tr()))
|
||||
.padding(bottom: 4),
|
||||
Text(nty.title,
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
if (nty.subtitle != null)
|
||||
Text(nty.subtitle!, style: Theme.of(context).textTheme.titleSmall),
|
||||
Text(nty.subtitle!,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleSmall),
|
||||
if (nty.subtitle != null) const Gap(4),
|
||||
SelectionArea(child: MarkdownTextContent(content: nty.body, isAutoWarp: true)),
|
||||
SelectionArea(
|
||||
child: MarkdownTextContent(
|
||||
content: nty.body, isAutoWarp: true)),
|
||||
if ([
|
||||
'interactive.reply',
|
||||
'interactive.feedback',
|
||||
'interactive.subscription',
|
||||
].contains(nty.topic) &&
|
||||
nty.metadata['related_post'] != null)
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
border: Border.all(color: Theme.of(context).dividerColor, width: 1),
|
||||
),
|
||||
child: PostItem(
|
||||
data: SnPost.fromJson(nty.metadata['related_post']!),
|
||||
showComments: false,
|
||||
showReactions: false,
|
||||
showMenu: false,
|
||||
TextButton(
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStatePropertyAll(
|
||||
EdgeInsets.zero,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
onTap: () {
|
||||
child: Text('postReadMore').tr(),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postDetail',
|
||||
pathParameters: {'slug': nty.metadata['related_post']!['id'].toString()},
|
||||
pathParameters: {
|
||||
'slug': nty.metadata['related_post']['id']
|
||||
.toString(),
|
||||
},
|
||||
);
|
||||
},
|
||||
).padding(top: 8),
|
||||
),
|
||||
const Gap(8),
|
||||
Row(
|
||||
children: [
|
||||
Text(DateFormat('yy/MM/dd').format(nty.createdAt)).fontSize(12),
|
||||
Text(DateFormat('yy/MM/dd')
|
||||
.format(nty.createdAt))
|
||||
.fontSize(12),
|
||||
const Gap(4),
|
||||
Text('·', style: TextStyle(fontSize: 12)),
|
||||
const Gap(4),
|
||||
Text(RelativeTime(context).format(nty.createdAt)).fontSize(12),
|
||||
Text(RelativeTime(context)
|
||||
.format(nty.createdAt))
|
||||
.fontSize(12),
|
||||
],
|
||||
).opacity(0.75),
|
||||
],
|
||||
@ -235,8 +256,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.check),
|
||||
padding: EdgeInsets.all(0),
|
||||
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed: _isSubmitting ? null : () => _markOneAsRead(nty),
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -4, vertical: -4),
|
||||
onPressed:
|
||||
_isSubmitting ? null : () => _markOneAsRead(nty),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16);
|
||||
|
@ -12,7 +12,6 @@ import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
import 'package:surface/widgets/navigation/app_background.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_comment_list.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
@ -22,7 +21,8 @@ class PostDetailScreen extends StatefulWidget {
|
||||
final SnPost? preload;
|
||||
final Function? onBack;
|
||||
|
||||
const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack});
|
||||
const PostDetailScreen(
|
||||
{super.key, required this.slug, this.preload, this.onBack});
|
||||
|
||||
@override
|
||||
State<PostDetailScreen> createState() => _PostDetailScreenState();
|
||||
@ -65,108 +65,111 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
|
||||
final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
|
||||
|
||||
return AppBackground(
|
||||
isRoot: widget.onBack != null,
|
||||
child: AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (widget.onBack != null) {
|
||||
widget.onBack!.call();
|
||||
}
|
||||
if (GoRouter.of(context).canPop()) {
|
||||
GoRouter.of(context).pop(context);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).replaceNamed('explore');
|
||||
},
|
||||
),
|
||||
title: _data?.body['title'] != null
|
||||
? RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: 'postDetail'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
]),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text('postDetail').tr(),
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: BackButton(
|
||||
onPressed: () {
|
||||
if (widget.onBack != null) {
|
||||
widget.onBack!.call();
|
||||
}
|
||||
if (GoRouter.of(context).canPop()) {
|
||||
GoRouter.of(context).pop(context);
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).replaceNamed('explore');
|
||||
},
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: LoadingIndicator(isActive: _isBusy),
|
||||
),
|
||||
if (_data != null)
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
data: _data!,
|
||||
maxWidth: maxWidth,
|
||||
showComments: false,
|
||||
showFullPost: true,
|
||||
onChanged: (data) {
|
||||
setState(() => _data = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_data != null && _data!.type != 'video') const SliverToBoxAdapter(child: Divider(height: 1)),
|
||||
if (_data != null && _data!.type != 'video')
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.comment, size: 24),
|
||||
const Gap(16),
|
||||
Text('postCommentsDetailed')
|
||||
.plural(_data!.metric.replyCount)
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 12).center(),
|
||||
),
|
||||
),
|
||||
if (_data != null && ua.isAuthorized && _data!.type != 'video')
|
||||
SliverToBoxAdapter(
|
||||
child: PostCommentQuickAction(
|
||||
parentPost: _data!,
|
||||
maxWidth: maxWidth,
|
||||
onPosted: () {
|
||||
setState(() {
|
||||
_data = _data!.copyWith(
|
||||
metric: _data!.metric.copyWith(
|
||||
replyCount: _data!.metric.replyCount + 1,
|
||||
title: _data?.body['title'] != null
|
||||
? RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: _data?.body['title'] ?? 'postNoun'.tr(),
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
);
|
||||
});
|
||||
_childListKey.currentState!.refresh();
|
||||
},
|
||||
),
|
||||
),
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: 'postDetail'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
]),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text('postDetail').tr(),
|
||||
),
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: LoadingIndicator(isActive: _isBusy),
|
||||
),
|
||||
if (_data != null)
|
||||
SliverToBoxAdapter(
|
||||
child: PostItem(
|
||||
data: _data!,
|
||||
maxWidth: maxWidth,
|
||||
showComments: false,
|
||||
showFullPost: true,
|
||||
onChanged: (data) {
|
||||
setState(() => _data = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
if (_data != null && _data!.type != 'video')
|
||||
PostCommentSliverList(
|
||||
key: _childListKey,
|
||||
),
|
||||
if (_data != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Divider(height: 1).padding(top: 8),
|
||||
),
|
||||
if (_data != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.comment, size: 24),
|
||||
const Gap(16),
|
||||
Text('postCommentsDetailed')
|
||||
.plural(_data!.metric.replyCount)
|
||||
.textStyle(Theme.of(context).textTheme.titleLarge!),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 12).center(),
|
||||
),
|
||||
),
|
||||
if (_data != null && ua.isAuthorized)
|
||||
SliverToBoxAdapter(
|
||||
child: PostCommentQuickAction(
|
||||
parentPost: _data!,
|
||||
maxWidth: maxWidth,
|
||||
onPosted: () {
|
||||
setState(() {
|
||||
_data = _data!.copyWith(
|
||||
metric: _data!.metric.copyWith(
|
||||
replyCount: _data!.metric.replyCount + 1,
|
||||
),
|
||||
);
|
||||
});
|
||||
_childListKey.currentState!.refresh();
|
||||
},
|
||||
),
|
||||
if (_data != null && _data!.type == 'video') SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_data != null) SliverGap(8),
|
||||
if (_data != null)
|
||||
PostCommentSliverList(
|
||||
key: _childListKey,
|
||||
parentPost: _data!,
|
||||
maxWidth: maxWidth,
|
||||
),
|
||||
if (_data != null)
|
||||
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
@ -77,7 +77,8 @@ class _PostDraftBoxState extends State<PostDraftBox> {
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
separatorBuilder: (_, __) =>
|
||||
const Divider().padding(vertical: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -45,12 +45,14 @@ class PostEditorExtra {
|
||||
final String? title;
|
||||
final String? description;
|
||||
final List<PostWriteMedia>? attachments;
|
||||
final SnRealm? realm;
|
||||
|
||||
const PostEditorExtra({
|
||||
this.text,
|
||||
this.title,
|
||||
this.description,
|
||||
this.attachments,
|
||||
this.realm,
|
||||
});
|
||||
}
|
||||
|
||||
@ -263,6 +265,7 @@ class _PostEditorScreenState extends State<PostEditorScreen>
|
||||
_writeController.descriptionController.text =
|
||||
widget.extraProps!.description ?? '';
|
||||
_writeController.addAttachments(widget.extraProps!.attachments ?? []);
|
||||
_writeController.setRealm(widget.extraProps!.realm);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
}
|
||||
|
||||
Future<void> _fetchPosts() async {
|
||||
if (_searchTerm.isEmpty && _searchCategories.isEmpty && _searchTags.isEmpty) return;
|
||||
if (_searchTerm.isEmpty && _searchCategories.isEmpty && _searchTags.isEmpty)
|
||||
return;
|
||||
if (_postCount != null && _posts.length >= _postCount!) return;
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
@ -152,7 +153,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
|
||||
),
|
||||
Positioned(
|
||||
top: 16,
|
||||
@ -166,7 +167,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
||||
padding: const WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(horizontal: 24),
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onChanged: (value) {
|
||||
_searchTerm = value;
|
||||
},
|
||||
|
@ -28,11 +28,8 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final pt = context.read<SnPostContentProvider>();
|
||||
final result = await pt.listPosts(
|
||||
take: 10,
|
||||
offset: _posts.length,
|
||||
isShuffle: true,
|
||||
);
|
||||
final result =
|
||||
await pt.listPosts(take: 10, offset: _posts.length, isShuffle: true);
|
||||
_posts.addAll(result.$1);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
@ -57,19 +54,14 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('postShuffle').tr(),
|
||||
),
|
||||
appBar: AppBar(title: Text('postShuffle').tr()),
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
if (_isBusy || _posts.isEmpty)
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
child: Center(child: CircularProgressIndicator()))
|
||||
else
|
||||
Expanded(
|
||||
child: CardSwiper(
|
||||
@ -81,17 +73,21 @@ class _PostShuffleScreenState extends State<PostShuffleScreen> {
|
||||
final ele = _posts[idx];
|
||||
return SingleChildScrollView(
|
||||
child: Center(
|
||||
child: OpenablePostItem(
|
||||
key: ValueKey(ele),
|
||||
data: ele,
|
||||
maxWidth: 640,
|
||||
onChanged: (ele) {
|
||||
_posts[idx] = ele;
|
||||
setState(() {});
|
||||
},
|
||||
onDeleted: () {
|
||||
_fetchPosts();
|
||||
},
|
||||
child: Card(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: OpenablePostItem(
|
||||
key: ValueKey(ele),
|
||||
data: ele,
|
||||
maxWidth: 640,
|
||||
useReplace: true,
|
||||
onChanged: (ele) {
|
||||
_posts[idx] = ele;
|
||||
setState(() {});
|
||||
},
|
||||
onDeleted: () {
|
||||
_fetchPosts();
|
||||
},
|
||||
).padding(all: 8),
|
||||
).padding(
|
||||
all: 24,
|
||||
bottom:
|
||||
|
@ -34,9 +34,11 @@ class PostPublisherScreen extends StatefulWidget {
|
||||
State<PostPublisherScreen> createState() => _PostPublisherScreenState();
|
||||
}
|
||||
|
||||
class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTickerProviderStateMixin {
|
||||
class _PostPublisherScreenState extends State<PostPublisherScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final ScrollController _scrollController = ScrollController();
|
||||
late final TabController _tabController = TabController(length: 3, vsync: this);
|
||||
late final TabController _tabController =
|
||||
TabController(length: 5, vsync: this);
|
||||
|
||||
SnPublisher? _publisher;
|
||||
SnAccount? _account;
|
||||
@ -66,7 +68,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
_account = await ud.getAccount(_publisher?.accountId);
|
||||
_accountRelationship = await rel.getRelationship(_account!.id);
|
||||
if (_publisher?.realmId != null && _publisher!.realmId != 0) {
|
||||
final resp = await sn.client.get('/cgi/id/realms/${_publisher!.realmId}');
|
||||
final resp =
|
||||
await sn.client.get('/cgi/id/realms/${_publisher!.realmId}');
|
||||
_realm = SnRealm.fromJson(resp.data);
|
||||
}
|
||||
} catch (_) {
|
||||
@ -133,12 +136,14 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
double _appBarBlur = 0.0;
|
||||
|
||||
late final _appBarWidth = MediaQuery.of(context).size.width;
|
||||
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
|
||||
late final _appBarHeight =
|
||||
math.min((_appBarWidth * kBannerAspectRatio), 360).roundToDouble();
|
||||
|
||||
void _updateAppBarBlur() {
|
||||
if (_scrollController.offset > _appBarHeight) return;
|
||||
setState(() {
|
||||
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
||||
_appBarBlur =
|
||||
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
|
||||
});
|
||||
}
|
||||
|
||||
@ -160,6 +165,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
type: switch (_tabController.index) {
|
||||
1 => 'story',
|
||||
2 => 'article',
|
||||
3 => 'question',
|
||||
4 => 'video',
|
||||
_ => null,
|
||||
},
|
||||
);
|
||||
@ -193,7 +200,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
'related': _account!.name,
|
||||
});
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
||||
context.showSnackbar(
|
||||
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -209,9 +217,11 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
|
||||
try {
|
||||
final rel = context.read<SnRelationshipProvider>();
|
||||
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
||||
await rel.updateRelationship(
|
||||
_account!.id, 1, _accountRelationship?.permNodes ?? {});
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
||||
context.showSnackbar(
|
||||
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'.tr()}']));
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -276,6 +286,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
body: NestedScrollView(
|
||||
controller: _scrollController,
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
@ -299,7 +310,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
text: TextSpan(children: [
|
||||
TextSpan(
|
||||
text: _publisher!.nick,
|
||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge!
|
||||
.copyWith(
|
||||
color: Colors.white,
|
||||
shadows: labelShadows,
|
||||
),
|
||||
@ -307,7 +321,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
const TextSpan(text: '\n'),
|
||||
TextSpan(
|
||||
text: '@${_publisher!.name}',
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(
|
||||
color: Colors.white,
|
||||
shadows: labelShadows,
|
||||
),
|
||||
@ -330,13 +347,16 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
)
|
||||
else
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer,
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 56 + MediaQuery.of(context).padding.top,
|
||||
height:
|
||||
56 + MediaQuery.of(context).padding.top,
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
@ -345,7 +365,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(
|
||||
clampDouble(_appBarBlur * 0.1, 0, 0.5),
|
||||
clampDouble(
|
||||
_appBarBlur * 0.1, 0, 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -372,11 +393,14 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
const Gap(16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_publisher!.nick,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium,
|
||||
).bold(),
|
||||
Text('@${_publisher!.name}').fontSize(13),
|
||||
],
|
||||
@ -387,7 +411,9 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
style: ButtonStyle(
|
||||
elevation: WidgetStatePropertyAll(0),
|
||||
),
|
||||
onPressed: _isSubscribing ? null : _toggleSubscription,
|
||||
onPressed: _isSubscribing
|
||||
? null
|
||||
: _toggleSubscription,
|
||||
label: Text('subscribe').tr(),
|
||||
icon: const Icon(Symbols.add),
|
||||
)
|
||||
@ -396,14 +422,17 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
style: ButtonStyle(
|
||||
elevation: WidgetStatePropertyAll(0),
|
||||
),
|
||||
onPressed: _isSubscribing ? null : _toggleSubscription,
|
||||
onPressed: _isSubscribing
|
||||
? null
|
||||
: _toggleSubscription,
|
||||
label: Text('unsubscribe').tr(),
|
||||
icon: const Icon(Symbols.remove),
|
||||
),
|
||||
PopupMenuButton(
|
||||
padding: EdgeInsets.zero,
|
||||
style: ButtonStyle(
|
||||
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -4, vertical: -4),
|
||||
),
|
||||
itemBuilder: (BuildContext context) => [
|
||||
PopupMenuItem(
|
||||
@ -443,7 +472,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
Text(_publisher!.description).padding(horizontal: 8),
|
||||
Text(_publisher!.description)
|
||||
.padding(horizontal: 8),
|
||||
const Gap(12),
|
||||
Column(
|
||||
children: [
|
||||
@ -451,8 +481,10 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
children: [
|
||||
const Icon(Symbols.calendar_add_on),
|
||||
const Gap(8),
|
||||
Text('publisherJoinedAt')
|
||||
.tr(args: [DateFormat('y/M/d').format(_publisher!.createdAt)]),
|
||||
Text('publisherJoinedAt').tr(args: [
|
||||
DateFormat('y/M/d')
|
||||
.format(_publisher!.createdAt)
|
||||
]),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
@ -460,7 +492,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
const Icon(Symbols.trending_up),
|
||||
const Gap(8),
|
||||
Text('publisherSocialPointTotal').plural(
|
||||
_publisher!.totalUpvote - _publisher!.totalDownvote,
|
||||
_publisher!.totalUpvote -
|
||||
_publisher!.totalDownvote,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -470,18 +503,22 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
const Icon(Symbols.group_work),
|
||||
const Gap(8),
|
||||
InkWell(
|
||||
child: Text('publisherAffiliatedBy').tr(args: [
|
||||
child: Text('publisherAffiliatedBy')
|
||||
.tr(args: [
|
||||
'@${_realm?.alias ?? 'unknown'}',
|
||||
]),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'realmDetail',
|
||||
pathParameters: {'alias': _realm!.alias},
|
||||
pathParameters: {
|
||||
'alias': _realm!.alias
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
AccountImage(content: _realm?.avatar, radius: 8),
|
||||
AccountImage(
|
||||
content: _realm?.avatar, radius: 8),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
@ -502,7 +539,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
||||
},
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
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)),
|
||||
@ -606,7 +656,7 @@ class _PublisherPostList extends StatelessWidget {
|
||||
onDeleted: onDeleted,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
149
lib/screens/realm/community.dart
Normal file
149
lib/screens/realm/community.dart
Normal file
@ -0,0 +1,149 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/post.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/screens/post/post_editor.dart';
|
||||
import 'package:surface/types/post.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/navigation/app_scaffold.dart';
|
||||
import 'package:surface/widgets/post/post_item.dart';
|
||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||
|
||||
class RealmCommunityScreen extends StatefulWidget {
|
||||
final String alias;
|
||||
const RealmCommunityScreen({super.key, required this.alias});
|
||||
|
||||
@override
|
||||
State<RealmCommunityScreen> createState() => _RealmCommunityScreenState();
|
||||
}
|
||||
|
||||
class _RealmCommunityScreenState extends State<RealmCommunityScreen> {
|
||||
SnRealm? _realm;
|
||||
|
||||
Future<void> _fetchRealm() async {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/id/realms/${widget.alias}');
|
||||
_realm = SnRealm.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
rethrow;
|
||||
} finally {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
bool _isBusy = false;
|
||||
int? _totalCount;
|
||||
final List<SnPost> _posts = List.empty(growable: true);
|
||||
|
||||
Future<void> _fetchPosts() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final pt = context.read<SnPostContentProvider>();
|
||||
final out = await pt.listPosts(
|
||||
take: 10,
|
||||
offset: _posts.length,
|
||||
realm: _realm?.id.toString(),
|
||||
);
|
||||
_totalCount = out.$2;
|
||||
_posts.addAll(out.$1);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchRealm();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_realm?.name ?? 'loading'.tr()),
|
||||
),
|
||||
floatingActionButton: _realm != null
|
||||
? FloatingActionButton(
|
||||
child: const Icon(Symbols.edit),
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'postEditor',
|
||||
extra: PostEditorExtra(realm: _realm!),
|
||||
);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_realm == null)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator().center(),
|
||||
),
|
||||
),
|
||||
if (_realm != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('realmCommunity'.tr(args: [_realm!.name]))
|
||||
.fontSize(17)
|
||||
.padding(horizontal: 20, bottom: 4),
|
||||
Text('postTotalCount'.plural(_totalCount ?? 0))
|
||||
.fontSize(13)
|
||||
.opacity(0.8)
|
||||
.padding(horizontal: 20, bottom: 4),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 16),
|
||||
const Divider(height: 1),
|
||||
if (_realm != null)
|
||||
Expanded(
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _fetchPosts,
|
||||
child: InfiniteList(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
itemCount: _posts.length,
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax:
|
||||
_totalCount != null && _posts.length >= _totalCount!,
|
||||
onFetchData: _fetchPosts,
|
||||
itemBuilder: (context, idx) {
|
||||
final post = _posts[idx];
|
||||
return OpenablePostItem(
|
||||
data: post,
|
||||
maxWidth: 640,
|
||||
onChanged: (data) {
|
||||
setState(() => _posts[idx] = data);
|
||||
},
|
||||
onDeleted: () {
|
||||
setState(() => _posts.removeAt(idx));
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) =>
|
||||
const Divider().padding(vertical: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -318,7 +318,7 @@ class _RealmPostListWidgetState extends State<_RealmPostListWidget> {
|
||||
},
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, __) => const Gap(8),
|
||||
separatorBuilder: (_, __) => const Divider().padding(vertical: 2),
|
||||
),
|
||||
),
|
||||
).padding(top: 8);
|
||||
|
@ -4,8 +4,10 @@ import 'package:gap/gap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/channel.dart';
|
||||
import 'package:surface/providers/config.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_realm.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
@ -57,7 +59,9 @@ class _RealmDiscoveryScreenState extends State<RealmDiscoveryScreen> {
|
||||
title: Text('screenRealmDiscovery').tr(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: _isCompactView ? const Icon(Symbols.view_list) : const Icon(Symbols.view_module),
|
||||
icon: _isCompactView
|
||||
? const Icon(Symbols.view_list)
|
||||
: const Icon(Symbols.view_module),
|
||||
onPressed: () {
|
||||
setState(() => _isCompactView = !_isCompactView);
|
||||
context.read<ConfigProvider>().realmCompactView = _isCompactView;
|
||||
@ -117,7 +121,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
|
||||
final resp =
|
||||
await sn.client.get('/cgi/im/channels/${widget.realm.alias}/public');
|
||||
final out = List<SnChannel>.from(
|
||||
resp.data.map((e) => SnChannel.fromJson(e)).cast<SnChannel>(),
|
||||
);
|
||||
@ -135,10 +140,13 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
||||
setState(() => _isJoining = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
await sn.client.post('/cgi/id/realms/${widget.realm.alias}/members', data: {
|
||||
final rel = context.read<SnRealmProvider>();
|
||||
await sn.client
|
||||
.post('/cgi/id/realms/${widget.realm.alias}/members', data: {
|
||||
'related': ua.user?.name,
|
||||
});
|
||||
await _joinSelectedChannels();
|
||||
rel.addAvailableRealm(widget.realm);
|
||||
if (!mounted) return;
|
||||
context.showSnackbar('realmJoined'.tr(args: [widget.realm.name]));
|
||||
Navigator.pop(context);
|
||||
@ -156,13 +164,20 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
||||
try {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final ua = context.read<UserProvider>();
|
||||
await sn.client.post('/cgi/im/channels/${widget.realm.alias}/$channel/members', data: {
|
||||
'related': ua.user?.name,
|
||||
});
|
||||
await sn.client.post(
|
||||
'/cgi/im/channels/${widget.realm.alias}/$channel/members',
|
||||
data: {
|
||||
'related': ua.user?.name,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
}
|
||||
final ct = context.read<ChatChannelProvider>();
|
||||
for (final channel
|
||||
in _channels!.where((ele) => _planJoinChannels.contains(ele.alias))) {
|
||||
ct.addAvailableChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,7 +197,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
||||
children: [
|
||||
const Icon(Symbols.group_add, size: 24),
|
||||
const Gap(16),
|
||||
Text('realmJoin', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
Text('realmJoin', style: Theme.of(context).textTheme.titleLarge)
|
||||
.tr(),
|
||||
],
|
||||
).padding(horizontal: 20, top: 16, bottom: 12),
|
||||
Row(
|
||||
@ -216,7 +232,8 @@ class _RealmJoinPopupState extends State<_RealmJoinPopup> {
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Text('realmCommunityPublicChannelsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium)
|
||||
child: Text('realmCommunityPublicChannelsHint'.tr(),
|
||||
style: Theme.of(context).textTheme.bodyMedium)
|
||||
.padding(horizontal: 24, vertical: 8),
|
||||
),
|
||||
Expanded(
|
||||
|
@ -80,6 +80,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final dt = context.read<DatabaseProvider>();
|
||||
final cfg = context.watch<ConfigProvider>();
|
||||
|
||||
final now = DateTime.now();
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
@ -323,19 +326,43 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
},
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.left_panel_close),
|
||||
title: Text('settingsDrawerPreferCollapse').tr(),
|
||||
subtitle:
|
||||
Text('settingsDrawerPreferCollapseDescription').tr(),
|
||||
secondary: const Icon(Symbols.hide),
|
||||
title: Text('settingsHideBottomNav').tr(),
|
||||
subtitle: Text('settingsHideBottomNavDescription').tr(),
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||
value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
|
||||
value: _prefs.getBool(kAppHideBottomNav) ?? false,
|
||||
onChanged: (value) {
|
||||
_prefs.setBool(kAppDrawerPreferCollapse, value ?? false);
|
||||
_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(
|
||||
leading: const Icon(Symbols.font_download),
|
||||
title: Text('settingsCustomFonts').tr(),
|
||||
@ -728,6 +755,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
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(() {});
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -50,6 +50,7 @@ class _AppSharingListenerState extends State<AppSharingListener> {
|
||||
Card(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(width: double.infinity),
|
||||
ListTile(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 24),
|
||||
|
@ -9,7 +9,6 @@ import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/sn_sticker.dart';
|
||||
import 'package:surface/providers/userinfo.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/app_bar_leading.dart';
|
||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
import 'package:surface/widgets/loading_indicator.dart';
|
||||
@ -134,7 +133,7 @@ class _StickerScreenState extends State<StickerScreen>
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: AutoAppBarLeading(),
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenStickers').tr(),
|
||||
actions: [
|
||||
IconButton(
|
||||
|
@ -45,10 +45,9 @@ class _WalletScreenState extends State<WalletScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
noBackground: ResponsiveScaffold.getIsExpand(context),
|
||||
appBar: AppBar(
|
||||
leading: PageBackButton(),
|
||||
title: Text('screenAccountWallet').tr(),
|
||||
),
|
||||
leading: PageBackButton(), title: Text('screenAccountWallet').tr()),
|
||||
body: Column(
|
||||
children: [
|
||||
LoadingIndicator(isActive: _isBusy),
|
||||
@ -66,25 +65,36 @@ class _WalletScreenState extends State<WalletScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
child: Icon(Symbols.wallet, size: 28),
|
||||
),
|
||||
const Gap(12),
|
||||
SizedBox(width: double.infinity),
|
||||
Text(
|
||||
NumberFormat.compactCurrency(
|
||||
locale: EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
locale: EasyLocalization.of(context)!
|
||||
.currentLocale
|
||||
.toString(),
|
||||
symbol: '${'walletCurrencyShort'.tr()} ',
|
||||
decimalDigits: 2,
|
||||
).format(double.parse(_wallet!.balance)),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
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: 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 {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final resp = await sn.client.get('/cgi/wa/transactions/me', queryParameters: {
|
||||
'take': 10,
|
||||
'offset': _transactions.length,
|
||||
});
|
||||
_totalCount = resp.data['count'];
|
||||
_transactions.addAll(
|
||||
resp.data['data']?.map((e) => SnTransaction.fromJson(e)).cast<SnTransaction>() ?? [],
|
||||
final resp = await sn.client.get(
|
||||
'/cgi/wa/transactions/me',
|
||||
queryParameters: {'take': 10, 'offset': _transactions.length},
|
||||
);
|
||||
_totalCount = resp.data['count'];
|
||||
_transactions.addAll(resp.data['data']
|
||||
?.map((e) => SnTransaction.fromJson(e))
|
||||
.cast<SnTransaction>() ??
|
||||
[]);
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -141,7 +152,8 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
|
||||
child: InfiniteList(
|
||||
itemCount: _transactions.length,
|
||||
isLoading: _isBusy,
|
||||
hasReachedMax: _totalCount != null && _transactions.length >= _totalCount!,
|
||||
hasReachedMax:
|
||||
_totalCount != null && _transactions.length >= _totalCount!,
|
||||
onFetchData: () {
|
||||
_fetchTransactions();
|
||||
},
|
||||
@ -149,7 +161,9 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
|
||||
final ele = _transactions[idx];
|
||||
final isIncoming = ele.payeeId == widget.myself.id;
|
||||
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(
|
||||
'${isIncoming ? '+' : '-'}${ele.amount} ${'walletCurrencyShort'.tr()}',
|
||||
style: TextStyle(color: isIncoming ? Colors.green : Colors.red),
|
||||
@ -159,12 +173,26 @@ class _WalletTransactionListState extends State<_WalletTransactionList> {
|
||||
children: [
|
||||
Text(ele.remark),
|
||||
const Gap(2),
|
||||
Text(
|
||||
DateFormat(
|
||||
null,
|
||||
EasyLocalization.of(context)!.currentLocale.toString(),
|
||||
).format(ele.createdAt),
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
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(
|
||||
DateFormat(
|
||||
null,
|
||||
EasyLocalization.of(context)!
|
||||
.currentLocale
|
||||
.toString())
|
||||
.format(ele.createdAt),
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -205,17 +233,14 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
controller: passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'fieldPassword'.tr(),
|
||||
),
|
||||
decoration: InputDecoration(labelText: 'fieldPassword'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text('cancel').tr(),
|
||||
),
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text('cancel').tr()),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop(passwordController.text);
|
||||
@ -234,9 +259,7 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
|
||||
try {
|
||||
setState(() => _isBusy = true);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
await sn.client.post('/cgi/wa/wallets/me', data: {
|
||||
'password': password,
|
||||
});
|
||||
await sn.client.post('/cgi/wa/wallets/me', data: {'password': password});
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
@ -255,20 +278,20 @@ class _CreateWalletWidgetState extends State<_CreateWalletWidget> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
child: Icon(Symbols.add, size: 28),
|
||||
),
|
||||
CircleAvatar(radius: 28, child: Icon(Symbols.add, size: 28)),
|
||||
const Gap(12),
|
||||
Text('walletCreate', style: Theme.of(context).textTheme.titleLarge).tr(),
|
||||
Text('walletCreateSubtitle', style: Theme.of(context).textTheme.bodyMedium).tr(),
|
||||
Text('walletCreate',
|
||||
style: Theme.of(context).textTheme.titleLarge)
|
||||
.tr(),
|
||||
Text('walletCreateSubtitle',
|
||||
style: Theme.of(context).textTheme.bodyMedium)
|
||||
.tr(),
|
||||
const Gap(8),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: _isBusy ? null : () => _createWallet(),
|
||||
child: Text('next').tr(),
|
||||
),
|
||||
onPressed: _isBusy ? null : () => _createWallet(),
|
||||
child: Text('next').tr()),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 24),
|
||||
|
@ -88,6 +88,8 @@ Future<ThemeData> createAppTheme(
|
||||
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
progressIndicatorTheme: ProgressIndicatorThemeData(year2023: false),
|
||||
sliderTheme: SliderThemeData(year2023: false),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -184,3 +184,63 @@ abstract class SnActionEvent with _$SnActionEvent {
|
||||
factory SnActionEvent.fromJson(Map<String, Object?> 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
@ -319,3 +319,104 @@ Map<String, dynamic> _$SnActionEventToJson(_SnActionEvent instance) =>
|
||||
'account': instance.account.toJson(),
|
||||
'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,
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
|
||||
part 'attachment.freezed.dart';
|
||||
|
||||
@ -39,6 +40,7 @@ abstract class SnAttachment with _$SnAttachment {
|
||||
required int? refId,
|
||||
required SnAttachmentPool? pool,
|
||||
required int? poolId,
|
||||
required SnAccount? account,
|
||||
required int accountId,
|
||||
int? thumbnailId,
|
||||
SnAttachment? thumbnail,
|
||||
@ -49,7 +51,8 @@ abstract class SnAttachment with _$SnAttachment {
|
||||
@Default({}) Map<String, dynamic> metadata,
|
||||
}) = _SnAttachment;
|
||||
|
||||
factory SnAttachment.fromJson(Map<String, Object?> json) => _$SnAttachmentFromJson(json);
|
||||
factory SnAttachment.fromJson(Map<String, Object?> json) =>
|
||||
_$SnAttachmentFromJson(json);
|
||||
|
||||
Map<String, dynamic> get data => {
|
||||
...metadata,
|
||||
@ -85,7 +88,8 @@ abstract class SnAttachmentFragment with _$SnAttachmentFragment {
|
||||
@Default([]) List<String> fileChunksMissing,
|
||||
}) = _SnAttachmentFragment;
|
||||
|
||||
factory SnAttachmentFragment.fromJson(Map<String, Object?> json) => _$SnAttachmentFragmentFromJson(json);
|
||||
factory SnAttachmentFragment.fromJson(Map<String, Object?> json) =>
|
||||
_$SnAttachmentFragmentFromJson(json);
|
||||
|
||||
SnMediaType get mediaType => switch (mimetype.split('/').firstOrNull) {
|
||||
'image' => SnMediaType.image,
|
||||
@ -109,7 +113,8 @@ abstract class SnAttachmentPool with _$SnAttachmentPool {
|
||||
required int? accountId,
|
||||
}) = _SnAttachmentPool;
|
||||
|
||||
factory SnAttachmentPool.fromJson(Map<String, Object?> json) => _$SnAttachmentPoolFromJson(json);
|
||||
factory SnAttachmentPool.fromJson(Map<String, Object?> json) =>
|
||||
_$SnAttachmentPoolFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -122,7 +127,8 @@ abstract class SnAttachmentDestination with _$SnAttachmentDestination {
|
||||
required bool isBoost,
|
||||
}) = _SnAttachmentDestination;
|
||||
|
||||
factory SnAttachmentDestination.fromJson(Map<String, Object?> json) => _$SnAttachmentDestinationFromJson(json);
|
||||
factory SnAttachmentDestination.fromJson(Map<String, Object?> json) =>
|
||||
_$SnAttachmentDestinationFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -139,7 +145,8 @@ abstract class SnAttachmentBoost with _$SnAttachmentBoost {
|
||||
required int account,
|
||||
}) = _SnAttachmentBoost;
|
||||
|
||||
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) => _$SnAttachmentBoostFromJson(json);
|
||||
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) =>
|
||||
_$SnAttachmentBoostFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -158,7 +165,8 @@ abstract class SnSticker with _$SnSticker {
|
||||
required int accountId,
|
||||
}) = _SnSticker;
|
||||
|
||||
factory SnSticker.fromJson(Map<String, Object?> json) => _$SnStickerFromJson(json);
|
||||
factory SnSticker.fromJson(Map<String, Object?> json) =>
|
||||
_$SnStickerFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -175,7 +183,8 @@ abstract class SnStickerPack with _$SnStickerPack {
|
||||
required int accountId,
|
||||
}) = _SnStickerPack;
|
||||
|
||||
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json);
|
||||
factory SnStickerPack.fromJson(Map<String, Object?> json) =>
|
||||
_$SnStickerPackFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -186,5 +195,6 @@ abstract class SnAttachmentBilling with _$SnAttachmentBilling {
|
||||
required double includedRatio,
|
||||
}) = _SnAttachmentBilling;
|
||||
|
||||
factory SnAttachmentBilling.fromJson(Map<String, Object?> json) => _$SnAttachmentBillingFromJson(json);
|
||||
factory SnAttachmentBilling.fromJson(Map<String, Object?> json) =>
|
||||
_$SnAttachmentBillingFromJson(json);
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ mixin _$SnAttachment {
|
||||
int? get refId;
|
||||
SnAttachmentPool? get pool;
|
||||
int? get poolId;
|
||||
SnAccount? get account;
|
||||
int get accountId;
|
||||
int? get thumbnailId;
|
||||
SnAttachment? get thumbnail;
|
||||
@ -98,6 +99,7 @@ mixin _$SnAttachment {
|
||||
(identical(other.refId, refId) || other.refId == refId) &&
|
||||
(identical(other.pool, pool) || other.pool == pool) &&
|
||||
(identical(other.poolId, poolId) || other.poolId == poolId) &&
|
||||
(identical(other.account, account) || other.account == account) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId) &&
|
||||
(identical(other.thumbnailId, thumbnailId) ||
|
||||
@ -140,6 +142,7 @@ mixin _$SnAttachment {
|
||||
refId,
|
||||
pool,
|
||||
poolId,
|
||||
account,
|
||||
accountId,
|
||||
thumbnailId,
|
||||
thumbnail,
|
||||
@ -152,7 +155,7 @@ mixin _$SnAttachment {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
|
||||
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,6 +189,7 @@ abstract mixin class $SnAttachmentCopyWith<$Res> {
|
||||
int? refId,
|
||||
SnAttachmentPool? pool,
|
||||
int? poolId,
|
||||
SnAccount? account,
|
||||
int accountId,
|
||||
int? thumbnailId,
|
||||
SnAttachment? thumbnail,
|
||||
@ -197,6 +201,7 @@ abstract mixin class $SnAttachmentCopyWith<$Res> {
|
||||
|
||||
$SnAttachmentCopyWith<$Res>? get ref;
|
||||
$SnAttachmentPoolCopyWith<$Res>? get pool;
|
||||
$SnAccountCopyWith<$Res>? get account;
|
||||
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
||||
$SnAttachmentCopyWith<$Res>? get compressed;
|
||||
}
|
||||
@ -236,6 +241,7 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
||||
Object? refId = freezed,
|
||||
Object? pool = freezed,
|
||||
Object? poolId = freezed,
|
||||
Object? account = freezed,
|
||||
Object? accountId = null,
|
||||
Object? thumbnailId = freezed,
|
||||
Object? thumbnail = freezed,
|
||||
@ -338,6 +344,10 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
||||
? _self.poolId
|
||||
: poolId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
account: freezed == account
|
||||
? _self.account
|
||||
: account // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccount?,
|
||||
accountId: null == accountId
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
@ -401,6 +411,20 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnAttachment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountCopyWith<$Res>? get account {
|
||||
if (_self.account == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||
return _then(_self.copyWith(account: value));
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnAttachment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@ -457,6 +481,7 @@ class _SnAttachment extends SnAttachment {
|
||||
required this.refId,
|
||||
required this.pool,
|
||||
required this.poolId,
|
||||
required this.account,
|
||||
required this.accountId,
|
||||
this.thumbnailId,
|
||||
this.thumbnail,
|
||||
@ -521,6 +546,8 @@ class _SnAttachment extends SnAttachment {
|
||||
@override
|
||||
final int? poolId;
|
||||
@override
|
||||
final SnAccount? account;
|
||||
@override
|
||||
final int accountId;
|
||||
@override
|
||||
final int? thumbnailId;
|
||||
@ -612,6 +639,7 @@ class _SnAttachment extends SnAttachment {
|
||||
(identical(other.refId, refId) || other.refId == refId) &&
|
||||
(identical(other.pool, pool) || other.pool == pool) &&
|
||||
(identical(other.poolId, poolId) || other.poolId == poolId) &&
|
||||
(identical(other.account, account) || other.account == account) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId) &&
|
||||
(identical(other.thumbnailId, thumbnailId) ||
|
||||
@ -654,6 +682,7 @@ class _SnAttachment extends SnAttachment {
|
||||
refId,
|
||||
pool,
|
||||
poolId,
|
||||
account,
|
||||
accountId,
|
||||
thumbnailId,
|
||||
thumbnail,
|
||||
@ -666,7 +695,7 @@ class _SnAttachment extends SnAttachment {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
|
||||
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -702,6 +731,7 @@ abstract mixin class _$SnAttachmentCopyWith<$Res>
|
||||
int? refId,
|
||||
SnAttachmentPool? pool,
|
||||
int? poolId,
|
||||
SnAccount? account,
|
||||
int accountId,
|
||||
int? thumbnailId,
|
||||
SnAttachment? thumbnail,
|
||||
@ -716,6 +746,8 @@ abstract mixin class _$SnAttachmentCopyWith<$Res>
|
||||
@override
|
||||
$SnAttachmentPoolCopyWith<$Res>? get pool;
|
||||
@override
|
||||
$SnAccountCopyWith<$Res>? get account;
|
||||
@override
|
||||
$SnAttachmentCopyWith<$Res>? get thumbnail;
|
||||
@override
|
||||
$SnAttachmentCopyWith<$Res>? get compressed;
|
||||
@ -757,6 +789,7 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
||||
Object? refId = freezed,
|
||||
Object? pool = freezed,
|
||||
Object? poolId = freezed,
|
||||
Object? account = freezed,
|
||||
Object? accountId = null,
|
||||
Object? thumbnailId = freezed,
|
||||
Object? thumbnail = freezed,
|
||||
@ -859,6 +892,10 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
||||
? _self.poolId
|
||||
: poolId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
account: freezed == account
|
||||
? _self.account
|
||||
: account // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccount?,
|
||||
accountId: null == accountId
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
@ -922,6 +959,20 @@ class __$SnAttachmentCopyWithImpl<$Res>
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnAttachment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountCopyWith<$Res>? get account {
|
||||
if (_self.account == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||
return _then(_self.copyWith(account: value));
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnAttachment
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
|
@ -39,6 +39,9 @@ _SnAttachment _$SnAttachmentFromJson(Map<String, dynamic> json) =>
|
||||
? null
|
||||
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
|
||||
poolId: (json['pool_id'] as num?)?.toInt(),
|
||||
account: json['account'] == null
|
||||
? null
|
||||
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
|
||||
thumbnail: json['thumbnail'] == null
|
||||
@ -82,6 +85,7 @@ Map<String, dynamic> _$SnAttachmentToJson(_SnAttachment instance) =>
|
||||
'ref_id': instance.refId,
|
||||
'pool': instance.pool?.toJson(),
|
||||
'pool_id': instance.poolId,
|
||||
'account': instance.account?.toJson(),
|
||||
'account_id': instance.accountId,
|
||||
'thumbnail_id': instance.thumbnailId,
|
||||
'thumbnail': instance.thumbnail?.toJson(),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/types/poll.dart';
|
||||
import 'package:surface/types/realm.dart';
|
||||
@ -26,6 +27,7 @@ abstract class SnPost with _$SnPost {
|
||||
required int? replyId,
|
||||
required int? repostId,
|
||||
required int? realmId,
|
||||
required SnRealm? realm,
|
||||
required SnPost? replyTo,
|
||||
required SnPost? repostTo,
|
||||
required List<int>? visibleUsersList,
|
||||
@ -43,9 +45,9 @@ abstract class SnPost with _$SnPost {
|
||||
@Default(0) int totalAggregatedViews,
|
||||
required int publisherId,
|
||||
required int? pollId,
|
||||
required SnPoll? poll,
|
||||
required SnPublisher publisher,
|
||||
required SnMetric metric,
|
||||
SnPostPreload? preload,
|
||||
}) = _SnPost;
|
||||
|
||||
factory SnPost.fromJson(Map<String, Object?> json) => _$SnPostFromJson(json);
|
||||
@ -146,6 +148,7 @@ abstract class SnPublisher with _$SnPublisher {
|
||||
required int totalDownvote,
|
||||
required int? realmId,
|
||||
required int accountId,
|
||||
required SnAccount? account,
|
||||
}) = _SnPublisher;
|
||||
|
||||
factory SnPublisher.fromJson(Map<String, Object?> json) =>
|
||||
|
@ -30,6 +30,7 @@ mixin _$SnPost {
|
||||
int? get replyId;
|
||||
int? get repostId;
|
||||
int? get realmId;
|
||||
SnRealm? get realm;
|
||||
SnPost? get replyTo;
|
||||
SnPost? get repostTo;
|
||||
List<int>? get visibleUsersList;
|
||||
@ -47,9 +48,9 @@ mixin _$SnPost {
|
||||
int get totalAggregatedViews;
|
||||
int get publisherId;
|
||||
int? get pollId;
|
||||
SnPoll? get poll;
|
||||
SnPublisher get publisher;
|
||||
SnMetric get metric;
|
||||
SnPostPreload? get preload;
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -88,6 +89,7 @@ mixin _$SnPost {
|
||||
(identical(other.repostId, repostId) ||
|
||||
other.repostId == repostId) &&
|
||||
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
||||
(identical(other.realm, realm) || other.realm == realm) &&
|
||||
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
|
||||
(identical(other.repostTo, repostTo) ||
|
||||
other.repostTo == repostTo) &&
|
||||
@ -119,10 +121,10 @@ mixin _$SnPost {
|
||||
(identical(other.publisherId, publisherId) ||
|
||||
other.publisherId == publisherId) &&
|
||||
(identical(other.pollId, pollId) || other.pollId == pollId) &&
|
||||
(identical(other.poll, poll) || other.poll == poll) &&
|
||||
(identical(other.publisher, publisher) ||
|
||||
other.publisher == publisher) &&
|
||||
(identical(other.metric, metric) || other.metric == metric) &&
|
||||
(identical(other.preload, preload) || other.preload == preload));
|
||||
(identical(other.metric, metric) || other.metric == metric));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@ -144,6 +146,7 @@ mixin _$SnPost {
|
||||
replyId,
|
||||
repostId,
|
||||
realmId,
|
||||
realm,
|
||||
replyTo,
|
||||
repostTo,
|
||||
const DeepCollectionEquality().hash(visibleUsersList),
|
||||
@ -161,14 +164,14 @@ mixin _$SnPost {
|
||||
totalAggregatedViews,
|
||||
publisherId,
|
||||
pollId,
|
||||
poll,
|
||||
publisher,
|
||||
metric,
|
||||
preload
|
||||
metric
|
||||
]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)';
|
||||
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, realm: $realm, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, poll: $poll, publisher: $publisher, metric: $metric)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,6 +196,7 @@ abstract mixin class $SnPostCopyWith<$Res> {
|
||||
int? replyId,
|
||||
int? repostId,
|
||||
int? realmId,
|
||||
SnRealm? realm,
|
||||
SnPost? replyTo,
|
||||
SnPost? repostTo,
|
||||
List<int>? visibleUsersList,
|
||||
@ -210,15 +214,16 @@ abstract mixin class $SnPostCopyWith<$Res> {
|
||||
int totalAggregatedViews,
|
||||
int publisherId,
|
||||
int? pollId,
|
||||
SnPoll? poll,
|
||||
SnPublisher publisher,
|
||||
SnMetric metric,
|
||||
SnPostPreload? preload});
|
||||
SnMetric metric});
|
||||
|
||||
$SnRealmCopyWith<$Res>? get realm;
|
||||
$SnPostCopyWith<$Res>? get replyTo;
|
||||
$SnPostCopyWith<$Res>? get repostTo;
|
||||
$SnPollCopyWith<$Res>? get poll;
|
||||
$SnPublisherCopyWith<$Res> get publisher;
|
||||
$SnMetricCopyWith<$Res> get metric;
|
||||
$SnPostPreloadCopyWith<$Res>? get preload;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -248,6 +253,7 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
||||
Object? replyId = freezed,
|
||||
Object? repostId = freezed,
|
||||
Object? realmId = freezed,
|
||||
Object? realm = freezed,
|
||||
Object? replyTo = freezed,
|
||||
Object? repostTo = freezed,
|
||||
Object? visibleUsersList = freezed,
|
||||
@ -265,9 +271,9 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
||||
Object? totalAggregatedViews = null,
|
||||
Object? publisherId = null,
|
||||
Object? pollId = freezed,
|
||||
Object? poll = freezed,
|
||||
Object? publisher = null,
|
||||
Object? metric = null,
|
||||
Object? preload = freezed,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
@ -330,6 +336,10 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
||||
? _self.realmId
|
||||
: realmId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
realm: freezed == realm
|
||||
? _self.realm
|
||||
: realm // ignore: cast_nullable_to_non_nullable
|
||||
as SnRealm?,
|
||||
replyTo: freezed == replyTo
|
||||
? _self.replyTo
|
||||
: replyTo // ignore: cast_nullable_to_non_nullable
|
||||
@ -398,6 +408,10 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
||||
? _self.pollId
|
||||
: pollId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
poll: freezed == poll
|
||||
? _self.poll
|
||||
: poll // ignore: cast_nullable_to_non_nullable
|
||||
as SnPoll?,
|
||||
publisher: null == publisher
|
||||
? _self.publisher
|
||||
: publisher // ignore: cast_nullable_to_non_nullable
|
||||
@ -406,13 +420,23 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
||||
? _self.metric
|
||||
: metric // ignore: cast_nullable_to_non_nullable
|
||||
as SnMetric,
|
||||
preload: freezed == preload
|
||||
? _self.preload
|
||||
: preload // ignore: cast_nullable_to_non_nullable
|
||||
as SnPostPreload?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnRealmCopyWith<$Res>? get realm {
|
||||
if (_self.realm == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnRealmCopyWith<$Res>(_self.realm!, (value) {
|
||||
return _then(_self.copyWith(realm: value));
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@ -441,6 +465,20 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPollCopyWith<$Res>? get poll {
|
||||
if (_self.poll == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPollCopyWith<$Res>(_self.poll!, (value) {
|
||||
return _then(_self.copyWith(poll: value));
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@ -460,20 +498,6 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
|
||||
return _then(_self.copyWith(metric: value));
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPostPreloadCopyWith<$Res>? get preload {
|
||||
if (_self.preload == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPostPreloadCopyWith<$Res>(_self.preload!, (value) {
|
||||
return _then(_self.copyWith(preload: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -495,6 +519,7 @@ class _SnPost extends SnPost {
|
||||
required this.replyId,
|
||||
required this.repostId,
|
||||
required this.realmId,
|
||||
required this.realm,
|
||||
required this.replyTo,
|
||||
required this.repostTo,
|
||||
required final List<int>? visibleUsersList,
|
||||
@ -512,9 +537,9 @@ class _SnPost extends SnPost {
|
||||
this.totalAggregatedViews = 0,
|
||||
required this.publisherId,
|
||||
required this.pollId,
|
||||
required this.poll,
|
||||
required this.publisher,
|
||||
required this.metric,
|
||||
this.preload})
|
||||
required this.metric})
|
||||
: _body = body,
|
||||
_tags = tags,
|
||||
_categories = categories,
|
||||
@ -583,6 +608,8 @@ class _SnPost extends SnPost {
|
||||
@override
|
||||
final int? realmId;
|
||||
@override
|
||||
final SnRealm? realm;
|
||||
@override
|
||||
final SnPost? replyTo;
|
||||
@override
|
||||
final SnPost? repostTo;
|
||||
@ -637,11 +664,11 @@ class _SnPost extends SnPost {
|
||||
@override
|
||||
final int? pollId;
|
||||
@override
|
||||
final SnPoll? poll;
|
||||
@override
|
||||
final SnPublisher publisher;
|
||||
@override
|
||||
final SnMetric metric;
|
||||
@override
|
||||
final SnPostPreload? preload;
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -685,6 +712,7 @@ class _SnPost extends SnPost {
|
||||
(identical(other.repostId, repostId) ||
|
||||
other.repostId == repostId) &&
|
||||
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
||||
(identical(other.realm, realm) || other.realm == realm) &&
|
||||
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
|
||||
(identical(other.repostTo, repostTo) ||
|
||||
other.repostTo == repostTo) &&
|
||||
@ -716,10 +744,10 @@ class _SnPost extends SnPost {
|
||||
(identical(other.publisherId, publisherId) ||
|
||||
other.publisherId == publisherId) &&
|
||||
(identical(other.pollId, pollId) || other.pollId == pollId) &&
|
||||
(identical(other.poll, poll) || other.poll == poll) &&
|
||||
(identical(other.publisher, publisher) ||
|
||||
other.publisher == publisher) &&
|
||||
(identical(other.metric, metric) || other.metric == metric) &&
|
||||
(identical(other.preload, preload) || other.preload == preload));
|
||||
(identical(other.metric, metric) || other.metric == metric));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@ -741,6 +769,7 @@ class _SnPost extends SnPost {
|
||||
replyId,
|
||||
repostId,
|
||||
realmId,
|
||||
realm,
|
||||
replyTo,
|
||||
repostTo,
|
||||
const DeepCollectionEquality().hash(_visibleUsersList),
|
||||
@ -758,14 +787,14 @@ class _SnPost extends SnPost {
|
||||
totalAggregatedViews,
|
||||
publisherId,
|
||||
pollId,
|
||||
poll,
|
||||
publisher,
|
||||
metric,
|
||||
preload
|
||||
metric
|
||||
]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)';
|
||||
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, realm: $realm, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, poll: $poll, publisher: $publisher, metric: $metric)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -791,6 +820,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
||||
int? replyId,
|
||||
int? repostId,
|
||||
int? realmId,
|
||||
SnRealm? realm,
|
||||
SnPost? replyTo,
|
||||
SnPost? repostTo,
|
||||
List<int>? visibleUsersList,
|
||||
@ -808,20 +838,22 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
||||
int totalAggregatedViews,
|
||||
int publisherId,
|
||||
int? pollId,
|
||||
SnPoll? poll,
|
||||
SnPublisher publisher,
|
||||
SnMetric metric,
|
||||
SnPostPreload? preload});
|
||||
SnMetric metric});
|
||||
|
||||
@override
|
||||
$SnRealmCopyWith<$Res>? get realm;
|
||||
@override
|
||||
$SnPostCopyWith<$Res>? get replyTo;
|
||||
@override
|
||||
$SnPostCopyWith<$Res>? get repostTo;
|
||||
@override
|
||||
$SnPollCopyWith<$Res>? get poll;
|
||||
@override
|
||||
$SnPublisherCopyWith<$Res> get publisher;
|
||||
@override
|
||||
$SnMetricCopyWith<$Res> get metric;
|
||||
@override
|
||||
$SnPostPreloadCopyWith<$Res>? get preload;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -851,6 +883,7 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
||||
Object? replyId = freezed,
|
||||
Object? repostId = freezed,
|
||||
Object? realmId = freezed,
|
||||
Object? realm = freezed,
|
||||
Object? replyTo = freezed,
|
||||
Object? repostTo = freezed,
|
||||
Object? visibleUsersList = freezed,
|
||||
@ -868,9 +901,9 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
||||
Object? totalAggregatedViews = null,
|
||||
Object? publisherId = null,
|
||||
Object? pollId = freezed,
|
||||
Object? poll = freezed,
|
||||
Object? publisher = null,
|
||||
Object? metric = null,
|
||||
Object? preload = freezed,
|
||||
}) {
|
||||
return _then(_SnPost(
|
||||
id: null == id
|
||||
@ -933,6 +966,10 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
||||
? _self.realmId
|
||||
: realmId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
realm: freezed == realm
|
||||
? _self.realm
|
||||
: realm // ignore: cast_nullable_to_non_nullable
|
||||
as SnRealm?,
|
||||
replyTo: freezed == replyTo
|
||||
? _self.replyTo
|
||||
: replyTo // ignore: cast_nullable_to_non_nullable
|
||||
@ -1001,6 +1038,10 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
||||
? _self.pollId
|
||||
: pollId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
poll: freezed == poll
|
||||
? _self.poll
|
||||
: poll // ignore: cast_nullable_to_non_nullable
|
||||
as SnPoll?,
|
||||
publisher: null == publisher
|
||||
? _self.publisher
|
||||
: publisher // ignore: cast_nullable_to_non_nullable
|
||||
@ -1009,13 +1050,23 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
||||
? _self.metric
|
||||
: metric // ignore: cast_nullable_to_non_nullable
|
||||
as SnMetric,
|
||||
preload: freezed == preload
|
||||
? _self.preload
|
||||
: preload // ignore: cast_nullable_to_non_nullable
|
||||
as SnPostPreload?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnRealmCopyWith<$Res>? get realm {
|
||||
if (_self.realm == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnRealmCopyWith<$Res>(_self.realm!, (value) {
|
||||
return _then(_self.copyWith(realm: value));
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@ -1044,6 +1095,20 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPollCopyWith<$Res>? get poll {
|
||||
if (_self.poll == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPollCopyWith<$Res>(_self.poll!, (value) {
|
||||
return _then(_self.copyWith(poll: value));
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@ -1063,20 +1128,6 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
|
||||
return _then(_self.copyWith(metric: value));
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPostPreloadCopyWith<$Res>? get preload {
|
||||
if (_self.preload == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnPostPreloadCopyWith<$Res>(_self.preload!, (value) {
|
||||
return _then(_self.copyWith(preload: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -2465,6 +2516,7 @@ mixin _$SnPublisher {
|
||||
int get totalDownvote;
|
||||
int? get realmId;
|
||||
int get accountId;
|
||||
SnAccount? get account;
|
||||
|
||||
/// Create a copy of SnPublisher
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -2501,7 +2553,8 @@ mixin _$SnPublisher {
|
||||
other.totalDownvote == totalDownvote) &&
|
||||
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId));
|
||||
other.accountId == accountId) &&
|
||||
(identical(other.account, account) || other.account == account));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@ -2521,11 +2574,12 @@ mixin _$SnPublisher {
|
||||
totalUpvote,
|
||||
totalDownvote,
|
||||
realmId,
|
||||
accountId);
|
||||
accountId,
|
||||
account);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId)';
|
||||
return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId, account: $account)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -2549,7 +2603,10 @@ abstract mixin class $SnPublisherCopyWith<$Res> {
|
||||
int totalUpvote,
|
||||
int totalDownvote,
|
||||
int? realmId,
|
||||
int accountId});
|
||||
int accountId,
|
||||
SnAccount? account});
|
||||
|
||||
$SnAccountCopyWith<$Res>? get account;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -2578,6 +2635,7 @@ class _$SnPublisherCopyWithImpl<$Res> implements $SnPublisherCopyWith<$Res> {
|
||||
Object? totalDownvote = null,
|
||||
Object? realmId = freezed,
|
||||
Object? accountId = null,
|
||||
Object? account = freezed,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id
|
||||
@ -2636,8 +2694,26 @@ class _$SnPublisherCopyWithImpl<$Res> implements $SnPublisherCopyWith<$Res> {
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
account: freezed == account
|
||||
? _self.account
|
||||
: account // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccount?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnPublisher
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountCopyWith<$Res>? get account {
|
||||
if (_self.account == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||
return _then(_self.copyWith(account: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -2657,7 +2733,8 @@ class _SnPublisher implements SnPublisher {
|
||||
required this.totalUpvote,
|
||||
required this.totalDownvote,
|
||||
required this.realmId,
|
||||
required this.accountId});
|
||||
required this.accountId,
|
||||
required this.account});
|
||||
factory _SnPublisher.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnPublisherFromJson(json);
|
||||
|
||||
@ -2689,6 +2766,8 @@ class _SnPublisher implements SnPublisher {
|
||||
final int? realmId;
|
||||
@override
|
||||
final int accountId;
|
||||
@override
|
||||
final SnAccount? account;
|
||||
|
||||
/// Create a copy of SnPublisher
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@ -2730,7 +2809,8 @@ class _SnPublisher implements SnPublisher {
|
||||
other.totalDownvote == totalDownvote) &&
|
||||
(identical(other.realmId, realmId) || other.realmId == realmId) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
other.accountId == accountId));
|
||||
other.accountId == accountId) &&
|
||||
(identical(other.account, account) || other.account == account));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@ -2750,11 +2830,12 @@ class _SnPublisher implements SnPublisher {
|
||||
totalUpvote,
|
||||
totalDownvote,
|
||||
realmId,
|
||||
accountId);
|
||||
accountId,
|
||||
account);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId)';
|
||||
return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId, account: $account)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -2780,7 +2861,11 @@ abstract mixin class _$SnPublisherCopyWith<$Res>
|
||||
int totalUpvote,
|
||||
int totalDownvote,
|
||||
int? realmId,
|
||||
int accountId});
|
||||
int accountId,
|
||||
SnAccount? account});
|
||||
|
||||
@override
|
||||
$SnAccountCopyWith<$Res>? get account;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@ -2809,6 +2894,7 @@ class __$SnPublisherCopyWithImpl<$Res> implements _$SnPublisherCopyWith<$Res> {
|
||||
Object? totalDownvote = null,
|
||||
Object? realmId = freezed,
|
||||
Object? accountId = null,
|
||||
Object? account = freezed,
|
||||
}) {
|
||||
return _then(_SnPublisher(
|
||||
id: null == id
|
||||
@ -2867,8 +2953,26 @@ class __$SnPublisherCopyWithImpl<$Res> implements _$SnPublisherCopyWith<$Res> {
|
||||
? _self.accountId
|
||||
: accountId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
account: freezed == account
|
||||
? _self.account
|
||||
: account // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccount?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnPublisher
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountCopyWith<$Res>? get account {
|
||||
if (_self.account == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||
return _then(_self.copyWith(account: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
@ -32,6 +32,9 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
|
||||
replyId: (json['reply_id'] as num?)?.toInt(),
|
||||
repostId: (json['repost_id'] as num?)?.toInt(),
|
||||
realmId: (json['realm_id'] as num?)?.toInt(),
|
||||
realm: json['realm'] == null
|
||||
? null
|
||||
: SnRealm.fromJson(json['realm'] as Map<String, dynamic>),
|
||||
replyTo: json['reply_to'] == null
|
||||
? null
|
||||
: SnPost.fromJson(json['reply_to'] as Map<String, dynamic>),
|
||||
@ -68,12 +71,12 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
|
||||
(json['total_aggregated_views'] as num?)?.toInt() ?? 0,
|
||||
publisherId: (json['publisher_id'] as num).toInt(),
|
||||
pollId: (json['poll_id'] as num?)?.toInt(),
|
||||
poll: json['poll'] == null
|
||||
? null
|
||||
: SnPoll.fromJson(json['poll'] as Map<String, dynamic>),
|
||||
publisher:
|
||||
SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
|
||||
metric: SnMetric.fromJson(json['metric'] as Map<String, dynamic>),
|
||||
preload: json['preload'] == null
|
||||
? null
|
||||
: SnPostPreload.fromJson(json['preload'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
|
||||
@ -92,6 +95,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
|
||||
'reply_id': instance.replyId,
|
||||
'repost_id': instance.repostId,
|
||||
'realm_id': instance.realmId,
|
||||
'realm': instance.realm?.toJson(),
|
||||
'reply_to': instance.replyTo?.toJson(),
|
||||
'repost_to': instance.repostTo?.toJson(),
|
||||
'visible_users_list': instance.visibleUsersList,
|
||||
@ -109,9 +113,9 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
|
||||
'total_aggregated_views': instance.totalAggregatedViews,
|
||||
'publisher_id': instance.publisherId,
|
||||
'poll_id': instance.pollId,
|
||||
'poll': instance.poll?.toJson(),
|
||||
'publisher': instance.publisher.toJson(),
|
||||
'metric': instance.metric.toJson(),
|
||||
'preload': instance.preload?.toJson(),
|
||||
};
|
||||
|
||||
_SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) => _SnPostTag(
|
||||
@ -241,6 +245,9 @@ _SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
|
||||
totalDownvote: (json['total_downvote'] as num).toInt(),
|
||||
realmId: (json['realm_id'] as num?)?.toInt(),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
account: json['account'] == null
|
||||
? null
|
||||
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
|
||||
@ -259,6 +266,7 @@ Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
|
||||
'total_downvote': instance.totalDownvote,
|
||||
'realm_id': instance.realmId,
|
||||
'account_id': instance.accountId,
|
||||
'account': instance.account?.toJson(),
|
||||
};
|
||||
|
||||
_SnSubscription _$SnSubscriptionFromJson(Map<String, dynamic> json) =>
|
||||
|
@ -17,4 +17,5 @@ const Map<String, ReactInfo> kTemplateReactions = {
|
||||
'party': ReactInfo(icon: '🎉', attitude: 1),
|
||||
'joy': ReactInfo(icon: '🤣', attitude: 1),
|
||||
'pray': ReactInfo(icon: '🙏', attitude: 1),
|
||||
'heart': ReactInfo(icon: '❤️', attitude: 1),
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ abstract class SnWallet with _$SnWallet {
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required String balance,
|
||||
required String goldenBalance,
|
||||
required String password,
|
||||
required int accountId,
|
||||
}) = _SnWallet;
|
||||
@ -27,6 +28,7 @@ abstract class SnTransaction with _$SnTransaction {
|
||||
required DateTime? deletedAt,
|
||||
required String remark,
|
||||
required String amount,
|
||||
required String currency,
|
||||
required SnWallet? payer,
|
||||
required SnWallet? payee,
|
||||
required int? payerId,
|
||||
|
@ -20,6 +20,7 @@ mixin _$SnWallet {
|
||||
DateTime get updatedAt;
|
||||
DateTime? get deletedAt;
|
||||
String get balance;
|
||||
String get goldenBalance;
|
||||
String get password;
|
||||
int get accountId;
|
||||
|
||||
@ -46,6 +47,8 @@ mixin _$SnWallet {
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.balance, balance) || other.balance == balance) &&
|
||||
(identical(other.goldenBalance, goldenBalance) ||
|
||||
other.goldenBalance == goldenBalance) &&
|
||||
(identical(other.password, password) ||
|
||||
other.password == password) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
@ -55,11 +58,11 @@ mixin _$SnWallet {
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||
deletedAt, balance, password, accountId);
|
||||
deletedAt, balance, goldenBalance, password, accountId);
|
||||
|
||||
@override
|
||||
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? deletedAt,
|
||||
String balance,
|
||||
String goldenBalance,
|
||||
String password,
|
||||
int accountId});
|
||||
}
|
||||
@ -95,6 +99,7 @@ class _$SnWalletCopyWithImpl<$Res> implements $SnWalletCopyWith<$Res> {
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? balance = null,
|
||||
Object? goldenBalance = null,
|
||||
Object? password = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
@ -119,6 +124,10 @@ class _$SnWalletCopyWithImpl<$Res> implements $SnWalletCopyWith<$Res> {
|
||||
? _self.balance
|
||||
: balance // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
goldenBalance: null == goldenBalance
|
||||
? _self.goldenBalance
|
||||
: goldenBalance // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
password: null == password
|
||||
? _self.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
@ -140,6 +149,7 @@ class _SnWallet implements SnWallet {
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.balance,
|
||||
required this.goldenBalance,
|
||||
required this.password,
|
||||
required this.accountId});
|
||||
factory _SnWallet.fromJson(Map<String, dynamic> json) =>
|
||||
@ -156,6 +166,8 @@ class _SnWallet implements SnWallet {
|
||||
@override
|
||||
final String balance;
|
||||
@override
|
||||
final String goldenBalance;
|
||||
@override
|
||||
final String password;
|
||||
@override
|
||||
final int accountId;
|
||||
@ -188,6 +200,8 @@ class _SnWallet implements SnWallet {
|
||||
(identical(other.deletedAt, deletedAt) ||
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.balance, balance) || other.balance == balance) &&
|
||||
(identical(other.goldenBalance, goldenBalance) ||
|
||||
other.goldenBalance == goldenBalance) &&
|
||||
(identical(other.password, password) ||
|
||||
other.password == password) &&
|
||||
(identical(other.accountId, accountId) ||
|
||||
@ -197,11 +211,11 @@ class _SnWallet implements SnWallet {
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, id, createdAt, updatedAt,
|
||||
deletedAt, balance, password, accountId);
|
||||
deletedAt, balance, goldenBalance, password, accountId);
|
||||
|
||||
@override
|
||||
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? deletedAt,
|
||||
String balance,
|
||||
String goldenBalance,
|
||||
String password,
|
||||
int accountId});
|
||||
}
|
||||
@ -239,6 +254,7 @@ class __$SnWalletCopyWithImpl<$Res> implements _$SnWalletCopyWith<$Res> {
|
||||
Object? updatedAt = null,
|
||||
Object? deletedAt = freezed,
|
||||
Object? balance = null,
|
||||
Object? goldenBalance = null,
|
||||
Object? password = null,
|
||||
Object? accountId = null,
|
||||
}) {
|
||||
@ -263,6 +279,10 @@ class __$SnWalletCopyWithImpl<$Res> implements _$SnWalletCopyWith<$Res> {
|
||||
? _self.balance
|
||||
: balance // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
goldenBalance: null == goldenBalance
|
||||
? _self.goldenBalance
|
||||
: goldenBalance // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
password: null == password
|
||||
? _self.password
|
||||
: password // ignore: cast_nullable_to_non_nullable
|
||||
@ -283,6 +303,7 @@ mixin _$SnTransaction {
|
||||
DateTime? get deletedAt;
|
||||
String get remark;
|
||||
String get amount;
|
||||
String get currency;
|
||||
SnWallet? get payer;
|
||||
SnWallet? get payee;
|
||||
int? get payerId;
|
||||
@ -313,6 +334,8 @@ mixin _$SnTransaction {
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.remark, remark) || other.remark == remark) &&
|
||||
(identical(other.amount, amount) || other.amount == amount) &&
|
||||
(identical(other.currency, currency) ||
|
||||
other.currency == currency) &&
|
||||
(identical(other.payer, payer) || other.payer == payer) &&
|
||||
(identical(other.payee, payee) || other.payee == payee) &&
|
||||
(identical(other.payerId, payerId) || other.payerId == payerId) &&
|
||||
@ -322,11 +345,11 @@ mixin _$SnTransaction {
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
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
|
||||
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,
|
||||
String remark,
|
||||
String amount,
|
||||
String currency,
|
||||
SnWallet? payer,
|
||||
SnWallet? payee,
|
||||
int? payerId,
|
||||
@ -371,6 +395,7 @@ class _$SnTransactionCopyWithImpl<$Res>
|
||||
Object? deletedAt = freezed,
|
||||
Object? remark = null,
|
||||
Object? amount = null,
|
||||
Object? currency = null,
|
||||
Object? payer = freezed,
|
||||
Object? payee = freezed,
|
||||
Object? payerId = freezed,
|
||||
@ -401,6 +426,10 @@ class _$SnTransactionCopyWithImpl<$Res>
|
||||
? _self.amount
|
||||
: amount // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
currency: null == currency
|
||||
? _self.currency
|
||||
: currency // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
payer: freezed == payer
|
||||
? _self.payer
|
||||
: payer // ignore: cast_nullable_to_non_nullable
|
||||
@ -459,6 +488,7 @@ class _SnTransaction implements SnTransaction {
|
||||
required this.deletedAt,
|
||||
required this.remark,
|
||||
required this.amount,
|
||||
required this.currency,
|
||||
required this.payer,
|
||||
required this.payee,
|
||||
required this.payerId,
|
||||
@ -479,6 +509,8 @@ class _SnTransaction implements SnTransaction {
|
||||
@override
|
||||
final String amount;
|
||||
@override
|
||||
final String currency;
|
||||
@override
|
||||
final SnWallet? payer;
|
||||
@override
|
||||
final SnWallet? payee;
|
||||
@ -516,6 +548,8 @@ class _SnTransaction implements SnTransaction {
|
||||
other.deletedAt == deletedAt) &&
|
||||
(identical(other.remark, remark) || other.remark == remark) &&
|
||||
(identical(other.amount, amount) || other.amount == amount) &&
|
||||
(identical(other.currency, currency) ||
|
||||
other.currency == currency) &&
|
||||
(identical(other.payer, payer) || other.payer == payer) &&
|
||||
(identical(other.payee, payee) || other.payee == payee) &&
|
||||
(identical(other.payerId, payerId) || other.payerId == payerId) &&
|
||||
@ -525,11 +559,11 @@ class _SnTransaction implements SnTransaction {
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
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
|
||||
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,
|
||||
String remark,
|
||||
String amount,
|
||||
String currency,
|
||||
SnWallet? payer,
|
||||
SnWallet? payee,
|
||||
int? payerId,
|
||||
@ -578,6 +613,7 @@ class __$SnTransactionCopyWithImpl<$Res>
|
||||
Object? deletedAt = freezed,
|
||||
Object? remark = null,
|
||||
Object? amount = null,
|
||||
Object? currency = null,
|
||||
Object? payer = freezed,
|
||||
Object? payee = freezed,
|
||||
Object? payerId = freezed,
|
||||
@ -608,6 +644,10 @@ class __$SnTransactionCopyWithImpl<$Res>
|
||||
? _self.amount
|
||||
: amount // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
currency: null == currency
|
||||
? _self.currency
|
||||
: currency // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
payer: freezed == payer
|
||||
? _self.payer
|
||||
: payer // ignore: cast_nullable_to_non_nullable
|
||||
|
@ -14,6 +14,7 @@ _SnWallet _$SnWalletFromJson(Map<String, dynamic> json) => _SnWallet(
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
balance: json['balance'] as String,
|
||||
goldenBalance: json['golden_balance'] as String,
|
||||
password: json['password'] as String,
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
@ -24,6 +25,7 @@ Map<String, dynamic> _$SnWalletToJson(_SnWallet instance) => <String, dynamic>{
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'balance': instance.balance,
|
||||
'golden_balance': instance.goldenBalance,
|
||||
'password': instance.password,
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
@ -38,6 +40,7 @@ _SnTransaction _$SnTransactionFromJson(Map<String, dynamic> json) =>
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
remark: json['remark'] as String,
|
||||
amount: json['amount'] as String,
|
||||
currency: json['currency'] as String,
|
||||
payer: json['payer'] == null
|
||||
? null
|
||||
: SnWallet.fromJson(json['payer'] as Map<String, dynamic>),
|
||||
@ -56,6 +59,7 @@ Map<String, dynamic> _$SnTransactionToJson(_SnTransaction instance) =>
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'remark': instance.remark,
|
||||
'amount': instance.amount,
|
||||
'currency': instance.currency,
|
||||
'payer': instance.payer?.toJson(),
|
||||
'payee': instance.payee?.toJson(),
|
||||
'payer_id': instance.payerId,
|
||||
|
@ -54,11 +54,15 @@ class AccountImage extends StatelessWidget {
|
||||
))
|
||||
.center(),
|
||||
)
|
||||
: AutoResizeUniversalImage(
|
||||
: UniversalImage(
|
||||
sn.getAttachmentUrl(url),
|
||||
filterQuality: filterQuality,
|
||||
key: Key('attachment-${content.hashCode}'),
|
||||
fit: BoxFit.cover,
|
||||
width: (radius != null ? radius! : 20) * 2,
|
||||
height: (radius != null ? radius! : 20) * 2,
|
||||
cacheWidth: (radius != null ? radius! : 20) * 2,
|
||||
cacheHeight: (radius != null ? radius! : 20) * 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -5,6 +5,7 @@ import 'dart:math' as math;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.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:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/logger.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/universal_image.dart';
|
||||
@ -222,20 +224,71 @@ class _AttachmentItemContentVideoState
|
||||
: sn.getAttachmentUrl(widget.data.compressed!.rid);
|
||||
_videoPlayer = Player();
|
||||
_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;
|
||||
}
|
||||
|
||||
_videoPlayer!.open(Media(uri), play: !widget.isAutoload);
|
||||
}
|
||||
|
||||
void _toggleOriginal() {
|
||||
void _toggleOriginal() async {
|
||||
if (!mounted) return;
|
||||
if (widget.data.compressedId == null) return;
|
||||
setState(() => _showOriginal = !_showOriginal);
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final url = _showOriginal
|
||||
? sn.getAttachmentUrl(widget.data.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(
|
||||
_showOriginal
|
||||
? sn.getAttachmentUrl(widget.data.rid)
|
||||
: sn.getAttachmentUrl(widget.data.compressed!.rid),
|
||||
),
|
||||
Media(uri),
|
||||
play: true,
|
||||
);
|
||||
}
|
||||
@ -439,7 +492,33 @@ class _AttachmentItemContentAudioState
|
||||
final sn = context.read<SnNetworkProvider>();
|
||||
final url = sn.getAttachmentUrl(widget.data.rid);
|
||||
_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.position.listen((v) => setState(() => _position = v));
|
||||
_audioPlayer!.stream.duration.listen((v) => setState(() => _duration = v));
|
||||
@ -567,6 +646,7 @@ class _AttachmentItemContentAudioState
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
constraints: const BoxConstraints(maxWidth: 320),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
@ -224,8 +224,10 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
(widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.data[idx]?.mediaType != SnMediaType.image)
|
||||
if (widget.data[idx]?.mediaType !=
|
||||
SnMediaType.image) {
|
||||
return;
|
||||
}
|
||||
context.pushTransparentRoute(
|
||||
AttachmentZoomView(
|
||||
data: widget.data
|
||||
@ -246,8 +248,10 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
border:
|
||||
Border(top: borderSide, bottom: borderSide),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Theme.of(context).dividerColor,
|
||||
),
|
||||
borderRadius: AttachmentList.kDefaultRadius,
|
||||
),
|
||||
child: ClipRRect(
|
||||
@ -263,8 +267,8 @@ class _AttachmentListState extends State<AttachmentList> {
|
||||
right: 8,
|
||||
bottom: 8,
|
||||
child: Chip(
|
||||
label:
|
||||
Text('${idx + 1}/${widget.data.length}')),
|
||||
label: Text('${idx + 1}/${widget.data.length}'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -16,7 +16,6 @@ import 'package:photo_view/photo_view_gallery.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/providers/sn_network.dart';
|
||||
import 'package:surface/providers/user_directory.dart';
|
||||
import 'package:surface/types/attachment.dart';
|
||||
import 'package:surface/widgets/account/account_image.dart';
|
||||
import 'package:surface/widgets/dialog.dart';
|
||||
@ -418,8 +417,7 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ud = context.read<UserDirectoryProvider>();
|
||||
final account = ud.getFromCache(data.accountId);
|
||||
final account = data.account!;
|
||||
|
||||
const tableGap = TableRow(
|
||||
children: [
|
||||
@ -461,12 +459,12 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
|
||||
children: [
|
||||
if (data.accountId > 0)
|
||||
AccountImage(
|
||||
content: account?.avatar,
|
||||
content: account.avatar,
|
||||
radius: 8,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(data.accountId > 0
|
||||
? account?.nick ?? 'unknown'.tr()
|
||||
? account.nick
|
||||
: 'unknown'.tr()),
|
||||
const Gap(8),
|
||||
Text('#${data.accountId}',
|
||||
|
@ -8,12 +8,12 @@ import 'package:surface/widgets/account/account_image.dart';
|
||||
class NoContentWidget extends StatefulWidget {
|
||||
final SnAccount? userinfo;
|
||||
final bool isSpeaking;
|
||||
final bool isFixed;
|
||||
final double? avatarSize;
|
||||
|
||||
const NoContentWidget({
|
||||
super.key,
|
||||
this.userinfo,
|
||||
this.isFixed = false,
|
||||
this.avatarSize,
|
||||
required this.isSpeaking,
|
||||
});
|
||||
|
||||
@ -45,41 +45,35 @@ class _NoContentWidgetState extends State<NoContentWidget>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double radius = widget.isFixed
|
||||
? 32
|
||||
: math.min(
|
||||
MediaQuery.of(context).size.width * 0.1,
|
||||
MediaQuery.of(context).size.height * 0.1,
|
||||
);
|
||||
final double radius = widget.avatarSize ??
|
||||
math.min(
|
||||
MediaQuery.of(context).size.width * 0.1,
|
||||
MediaQuery.of(context).size.height * 0.1,
|
||||
);
|
||||
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
child: Center(
|
||||
child: Animate(
|
||||
autoPlay: false,
|
||||
controller: _animationController,
|
||||
effects: [
|
||||
CustomEffect(
|
||||
begin: widget.isSpeaking ? 2 : 0,
|
||||
end: 8,
|
||||
curve: Curves.easeInOut,
|
||||
duration: 1250.ms,
|
||||
builder: (context, value, child) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(radius + 8)),
|
||||
border: value > 0
|
||||
? Border.all(color: Colors.green, width: value)
|
||||
: null,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
)
|
||||
],
|
||||
child: AccountImage(
|
||||
content: widget.userinfo?.avatar,
|
||||
radius: radius,
|
||||
return Animate(
|
||||
autoPlay: false,
|
||||
controller: _animationController,
|
||||
effects: [
|
||||
CustomEffect(
|
||||
begin: widget.isSpeaking ? 2 : 0,
|
||||
end: 8,
|
||||
curve: Curves.easeInOut,
|
||||
duration: 1250.ms,
|
||||
builder: (context, value, child) => Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(radius + 8)),
|
||||
border: value > 0
|
||||
? Border.all(color: Colors.green, width: value)
|
||||
: null,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
child: AccountImage(
|
||||
content: widget.userinfo?.avatar,
|
||||
radius: radius,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:surface/types/account.dart';
|
||||
import 'package:surface/types/chat.dart';
|
||||
import 'package:surface/widgets/chat/call/call_no_content.dart';
|
||||
@ -11,23 +13,32 @@ import 'package:surface/widgets/chat/call/call_participant_menu.dart';
|
||||
import 'package:surface/widgets/chat/call/call_participant_stats.dart';
|
||||
|
||||
abstract class ParticipantWidget extends StatefulWidget {
|
||||
static ParticipantWidget widgetFor(ParticipantTrack participantTrack,
|
||||
{bool isFixed = false, bool showStatsLayer = false}) {
|
||||
static ParticipantWidget widgetFor(
|
||||
ParticipantTrack participantTrack, {
|
||||
double? avatarSize,
|
||||
EdgeInsets? padding,
|
||||
bool showStatsLayer = false,
|
||||
bool isList = false,
|
||||
}) {
|
||||
if (participantTrack.participant is LocalParticipant) {
|
||||
return LocalParticipantWidget(
|
||||
participantTrack.participant as LocalParticipant,
|
||||
participantTrack.videoTrack,
|
||||
isFixed,
|
||||
avatarSize,
|
||||
participantTrack.isScreenShare,
|
||||
showStatsLayer,
|
||||
isList,
|
||||
padding,
|
||||
);
|
||||
} else if (participantTrack.participant is RemoteParticipant) {
|
||||
return RemoteParticipantWidget(
|
||||
participantTrack.participant as RemoteParticipant,
|
||||
participantTrack.videoTrack,
|
||||
isFixed,
|
||||
avatarSize,
|
||||
participantTrack.isScreenShare,
|
||||
showStatsLayer,
|
||||
isList,
|
||||
padding,
|
||||
);
|
||||
}
|
||||
throw UnimplementedError('Unknown participant type');
|
||||
@ -36,8 +47,10 @@ abstract class ParticipantWidget extends StatefulWidget {
|
||||
abstract final Participant participant;
|
||||
abstract final VideoTrack? videoTrack;
|
||||
abstract final bool isScreenShare;
|
||||
abstract final bool isFixed;
|
||||
abstract final double? avatarSize;
|
||||
abstract final bool showStatsLayer;
|
||||
abstract final bool isList;
|
||||
abstract final EdgeInsets? padding;
|
||||
final VideoQuality quality;
|
||||
|
||||
const ParticipantWidget({
|
||||
@ -52,18 +65,24 @@ class LocalParticipantWidget extends ParticipantWidget {
|
||||
@override
|
||||
final VideoTrack? videoTrack;
|
||||
@override
|
||||
final bool isFixed;
|
||||
final double? avatarSize;
|
||||
@override
|
||||
final bool isScreenShare;
|
||||
@override
|
||||
final bool showStatsLayer;
|
||||
@override
|
||||
final bool isList;
|
||||
@override
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const LocalParticipantWidget(
|
||||
this.participant,
|
||||
this.videoTrack,
|
||||
this.isFixed,
|
||||
this.avatarSize,
|
||||
this.isScreenShare,
|
||||
this.showStatsLayer, {
|
||||
this.showStatsLayer,
|
||||
this.isList,
|
||||
this.padding, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -77,18 +96,24 @@ class RemoteParticipantWidget extends ParticipantWidget {
|
||||
@override
|
||||
final VideoTrack? videoTrack;
|
||||
@override
|
||||
final bool isFixed;
|
||||
final double? avatarSize;
|
||||
@override
|
||||
final bool isScreenShare;
|
||||
@override
|
||||
final bool showStatsLayer;
|
||||
@override
|
||||
final bool isList;
|
||||
@override
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const RemoteParticipantWidget(
|
||||
this.participant,
|
||||
this.videoTrack,
|
||||
this.isFixed,
|
||||
this.avatarSize,
|
||||
this.isScreenShare,
|
||||
this.showStatsLayer, {
|
||||
this.showStatsLayer,
|
||||
this.isList,
|
||||
this.padding, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -136,19 +161,82 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext ctx) {
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.isList) {
|
||||
return Padding(
|
||||
padding: widget.padding ?? EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: (widget.avatarSize ?? 32) * 2,
|
||||
height: (widget.avatarSize ?? 32) * 2,
|
||||
child: Center(
|
||||
child: NoContentWidget(
|
||||
userinfo: _userinfoMetadata,
|
||||
avatarSize: widget.avatarSize,
|
||||
isSpeaking: widget.participant.isSpeaking,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: (widget.avatarSize ?? 32) * 2,
|
||||
child: ParticipantInfoWidget(
|
||||
isList: true,
|
||||
title: widget.participant.name.isNotEmpty
|
||||
? widget.participant.name
|
||||
: widget.participant.identity,
|
||||
audioAvailable: _firstAudioPublication?.muted == false &&
|
||||
_firstAudioPublication?.subscribed == true,
|
||||
connectionQuality: widget.participant.connectionQuality,
|
||||
isScreenShare: widget.isScreenShare,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_activeVideoTrack != null && !_activeVideoTrack!.muted)
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Material(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainer
|
||||
.withOpacity(0.75),
|
||||
child: VideoTrackRenderer(
|
||||
_activeVideoTrack!,
|
||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
),
|
||||
),
|
||||
).padding(top: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
_activeVideoTrack != null && !_activeVideoTrack!.muted
|
||||
? VideoTrackRenderer(
|
||||
_activeVideoTrack!,
|
||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
)
|
||||
: NoContentWidget(
|
||||
userinfo: _userinfoMetadata,
|
||||
isFixed: widget.isFixed,
|
||||
isSpeaking: widget.participant.isSpeaking,
|
||||
),
|
||||
if (_activeVideoTrack != null && !_activeVideoTrack!.muted)
|
||||
VideoTrackRenderer(
|
||||
_activeVideoTrack!,
|
||||
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
)
|
||||
else
|
||||
Center(
|
||||
child: NoContentWidget(
|
||||
userinfo: _userinfoMetadata,
|
||||
avatarSize: widget.avatarSize,
|
||||
isSpeaking: widget.participant.isSpeaking,
|
||||
),
|
||||
),
|
||||
if (widget.showStatsLayer)
|
||||
Positioned(
|
||||
top: 30,
|
||||
@ -199,44 +287,51 @@ class _RemoteParticipantWidgetState
|
||||
}
|
||||
|
||||
class InteractiveParticipantWidget extends StatelessWidget {
|
||||
final double? width;
|
||||
final double? height;
|
||||
final Color? color;
|
||||
final bool isFixedAvatar;
|
||||
final double? avatarSize;
|
||||
final bool isList;
|
||||
final ParticipantTrack participant;
|
||||
final Function() onTap;
|
||||
final Function? onTap;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
const InteractiveParticipantWidget({
|
||||
super.key,
|
||||
this.width,
|
||||
this.height,
|
||||
this.color,
|
||||
this.isFixedAvatar = false,
|
||||
this.avatarSize,
|
||||
this.isList = false,
|
||||
this.padding,
|
||||
required this.participant,
|
||||
required this.onTap,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
child: Container(
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
child: ParticipantWidget.widgetFor(participant, isFixed: isFixedAvatar),
|
||||
),
|
||||
onTap: () => onTap(),
|
||||
onLongPress: () {
|
||||
if (participant.participant is LocalParticipant) return;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => ParticipantMenu(
|
||||
participant: participant.participant as RemoteParticipant,
|
||||
videoTrack: participant.videoTrack,
|
||||
isScreenShare: participant.isScreenShare,
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap != null
|
||||
? () {
|
||||
onTap?.call();
|
||||
}
|
||||
: null,
|
||||
onLongPress: () {
|
||||
if (participant.participant is LocalParticipant) return;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => ParticipantMenu(
|
||||
participant: participant.participant as RemoteParticipant,
|
||||
videoTrack: participant.videoTrack,
|
||||
isScreenShare: participant.isScreenShare,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
child: ParticipantWidget.widgetFor(
|
||||
participant,
|
||||
avatarSize: avatarSize,
|
||||
isList: isList,
|
||||
padding: padding,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ class ParticipantInfoWidget extends StatelessWidget {
|
||||
final bool audioAvailable;
|
||||
final ConnectionQuality connectionQuality;
|
||||
final bool isScreenShare;
|
||||
final bool isList;
|
||||
|
||||
const ParticipantInfoWidget({
|
||||
super.key,
|
||||
@ -16,64 +17,124 @@ class ParticipantInfoWidget extends StatelessWidget {
|
||||
this.audioAvailable = true,
|
||||
this.connectionQuality = ConnectionQuality.unknown,
|
||||
this.isScreenShare = false,
|
||||
this.isList = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 7,
|
||||
horizontal: 10,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (title != null)
|
||||
Flexible(
|
||||
child: Text(
|
||||
title!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
Widget build(BuildContext context) {
|
||||
if (isList) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null)
|
||||
Text(
|
||||
title!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const Gap(5),
|
||||
isScreenShare
|
||||
? const Icon(
|
||||
Symbols.monitor,
|
||||
).padding(left: 2),
|
||||
Row(
|
||||
children: [
|
||||
isScreenShare
|
||||
? const Icon(
|
||||
Symbols.monitor,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
)
|
||||
: Icon(
|
||||
audioAvailable ? Symbols.mic : Symbols.mic_off,
|
||||
color: audioAvailable ? Colors.white : Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
const Gap(3),
|
||||
if (connectionQuality != ConnectionQuality.unknown)
|
||||
Icon(
|
||||
{
|
||||
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
|
||||
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
|
||||
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
|
||||
}[connectionQuality],
|
||||
color: {
|
||||
ConnectionQuality.excellent: Colors.green,
|
||||
ConnectionQuality.good: Colors.orange,
|
||||
ConnectionQuality.poor: Colors.red,
|
||||
}[connectionQuality],
|
||||
size: 16,
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
)
|
||||
: Icon(
|
||||
audioAvailable ? Symbols.mic : Symbols.mic_off,
|
||||
color: audioAvailable ? Colors.white : Colors.red,
|
||||
size: 16,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
const Gap(3),
|
||||
if (connectionQuality != ConnectionQuality.unknown)
|
||||
Icon(
|
||||
{
|
||||
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
|
||||
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
|
||||
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
|
||||
}[connectionQuality],
|
||||
color: {
|
||||
ConnectionQuality.excellent: Colors.green,
|
||||
ConnectionQuality.good: Colors.orange,
|
||||
ConnectionQuality.poor: Colors.red,
|
||||
}[connectionQuality],
|
||||
size: 16,
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
).padding(all: 3),
|
||||
],
|
||||
),
|
||||
).padding(all: 3),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 7,
|
||||
horizontal: 10,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (title != null)
|
||||
Flexible(
|
||||
child: Text(
|
||||
title!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
const Gap(5),
|
||||
isScreenShare
|
||||
? const Icon(
|
||||
Symbols.monitor,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
)
|
||||
: Icon(
|
||||
audioAvailable ? Symbols.mic : Symbols.mic_off,
|
||||
color: audioAvailable ? Colors.white : Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
const Gap(3),
|
||||
if (connectionQuality != ConnectionQuality.unknown)
|
||||
Icon(
|
||||
{
|
||||
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
|
||||
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
|
||||
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
|
||||
}[connectionQuality],
|
||||
color: {
|
||||
ConnectionQuality.excellent: Colors.green,
|
||||
ConnectionQuality.good: Colors.orange,
|
||||
ConnectionQuality.poor: Colors.red,
|
||||
}[connectionQuality],
|
||||
size: 16,
|
||||
)
|
||||
else
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
).padding(all: 3),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ class ChatMessage extends StatelessWidget {
|
||||
key: Key('chat-message-${data.id}'),
|
||||
iconOnLeftSwipe: Symbols.reply,
|
||||
iconOnRightSwipe: Symbols.edit,
|
||||
swipeSensitivity: 20,
|
||||
swipeSensitivity: 10,
|
||||
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
|
||||
onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
|
||||
child: ContextMenuArea(
|
||||
|
@ -36,10 +36,12 @@ class ChatTypingIndicator extends StatelessWidget {
|
||||
'messageTyping'
|
||||
.plural(controller.typingMembers.length, args: [
|
||||
controller.typingMembers
|
||||
.map((ele) => (ele.nick?.isNotEmpty ?? false)
|
||||
? ele.nick!
|
||||
: ud.getFromCache(ele.accountId)?.name ??
|
||||
'unknown')
|
||||
.map(
|
||||
(ele) => (ele.nick?.isNotEmpty ?? false)
|
||||
? ele.nick!
|
||||
: ud.getFromCache(ele.accountId)?.nick ??
|
||||
'unknown',
|
||||
)
|
||||
.join(', '),
|
||||
]),
|
||||
),
|
||||
|
@ -16,7 +16,7 @@ class ConnectionIndicator extends StatelessWidget {
|
||||
final ws = context.watch<WebSocketProvider>();
|
||||
final cfg = context.watch<ConfigProvider>();
|
||||
|
||||
final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0;
|
||||
final marginLeft = cfg.drawerIsCollapsed ? 0.0 : 80.0;
|
||||
|
||||
return ListenableBuilder(
|
||||
listenable: ws,
|
||||
@ -30,7 +30,8 @@ class ConnectionIndicator extends StatelessWidget {
|
||||
child: GestureDetector(
|
||||
child: Material(
|
||||
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,
|
||||
child: ua.isAuthorized
|
||||
? Row(
|
||||
@ -39,16 +40,29 @@ class ConnectionIndicator extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (ws.isBusy)
|
||||
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
Text(
|
||||
'serverConnecting',
|
||||
).tr().textColor(Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer)
|
||||
else if (!ws.isConnected)
|
||||
Text('serverDisconnected')
|
||||
.tr()
|
||||
.textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||
Text(
|
||||
'serverDisconnected',
|
||||
).tr().textColor(Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer)
|
||||
else
|
||||
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
|
||||
Text(
|
||||
'serverConnected',
|
||||
).tr().textColor(Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer),
|
||||
const Gap(8),
|
||||
if (ws.isBusy)
|
||||
const CircularProgressIndicator(strokeWidth: 2.5)
|
||||
const CircularProgressIndicator(
|
||||
strokeWidth: 2.5,
|
||||
padding: EdgeInsets.zero,
|
||||
)
|
||||
.width(12)
|
||||
.height(12)
|
||||
.padding(horizontal: 4, right: 4)
|
||||
@ -59,10 +73,9 @@ class ConnectionIndicator extends StatelessWidget {
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 4)
|
||||
: const SizedBox.shrink(),
|
||||
).opacity(show ? 1 : 0, animate: true).animate(
|
||||
const Duration(milliseconds: 300),
|
||||
Curves.easeInOut,
|
||||
),
|
||||
)
|
||||
.opacity(show ? 1 : 0, animate: true)
|
||||
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
|
||||
onTap: () {
|
||||
if (!ws.isConnected && !ws.isBusy) {
|
||||
ws.connect();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user