Compare commits

..

34 Commits

Author SHA1 Message Date
908f0cb59e 🐛 Fix some issues 2025-03-28 00:54:51 +08:00
7c2b8de931 Desktop chat list
🍱 Update launch sfx
2025-03-28 00:52:19 +08:00
6bb9c21759 Rollback drawer style on mobile
🗑️ Remove drawer prefer collapse & expand
2025-03-28 00:00:39 +08:00
8f2fc55608 User ear healthy 2025-03-27 23:52:37 +08:00
a1c4e5eca0 ♻️ Refactored large screen user experience 2025-03-27 23:18:40 +08:00
595050f89f ♻️ Explore two column 2025-03-27 22:58:06 +08:00
0722c99f21 ♻️ Openable Post Item now push pages 2025-03-27 22:46:36 +08:00
12d03836f9 ♻️ Updated nav & account page two column design 2025-03-27 22:42:44 +08:00
f78d3f4fd5 🔀 Merge pull request #18 from Texas0295/master
Fix workflow
2025-03-27 22:04:02 +08:00
e798a8ba76 fix workflow 2025-03-27 21:24:38 +08:00
c28a664373 Memorable window size 2025-03-27 00:37:45 +08:00
4589722c3b Weird boot sound effects 2025-03-27 00:22:41 +08:00
38e1c51b45 🐛 Fix linux compile issue 2025-03-26 23:32:23 +08:00
610ddec05c Sound effects on notify 2025-03-26 23:16:55 +08:00
d0276f9ac6 🐛 Fix some date issue 2025-03-26 22:51:09 +08:00
c1e89a2ee6 Punishments 2025-03-26 22:43:27 +08:00
ecc79368a1 🐛 Fix attachment border in list 2025-03-26 00:29:00 +08:00
e6d732c86a 💄 Optimize status text 2025-03-26 00:26:37 +08:00
dd055fb077 💄 Optimization and bug fixes 2025-03-26 00:24:07 +08:00
280840c6d8 ⬆️ Upgrade deps 2025-03-25 21:33:58 +08:00
bde62a7b2c Add cache for audio and video (experimental) 2025-03-25 00:33:39 +08:00
5445c570a2 Add deps for google_mobile_ads 2025-03-24 23:12:49 +08:00
b2302f5b3c 🐛 Make initialize for push notification no longer waited 2025-03-24 20:55:55 +08:00
d7359cfd0d 🐛 Fixes and optimization in programs 2025-03-24 00:09:36 +08:00
9cc577adbe Programs, members
🐛 Fix web assets redirecting issue
2025-03-23 22:34:58 +08:00
dd196b7754 Golden points 2025-03-23 18:23:18 +08:00
16c07c2133 🐛 Fix deps 2025-03-23 17:01:21 +08:00
6bcb658d44 🐛 Fix platform specific captcha solution cause build failed. 2025-03-23 16:47:06 +08:00
9311bfc3b5 ⬆️ Upgrade deps & replace to own translation api 2025-03-23 16:26:41 +08:00
8dd6435a30 🐛 Fix some issues on Android and Web 2025-03-23 16:24:53 +08:00
21a1d4a2ad 🐛 Fix unable select answer 2025-03-23 00:01:48 +08:00
603875b1af 🐛 Fix styling issue 2025-03-22 23:07:13 +08:00
4209a13c84 🐛 Fix no nav to use 2025-03-22 22:51:50 +08:00
55b79bfd8f 🐛 Finish bug fixes 2025-03-22 21:50:01 +08:00
82 changed files with 4015 additions and 1443 deletions

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

View File

@ -639,6 +639,7 @@
"postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}", "postQuestionUnansweredWithReward": "Unanswered Question, reward source points {}",
"postQuestionAnswered": "Answered Question", "postQuestionAnswered": "Answered Question",
"postQuestionAnswerSelect": "Select as Answer", "postQuestionAnswerSelect": "Select as Answer",
"postQuestionAnswerTitle": "Selected Question",
"postQuestionAnswerSelected": "Answer has been selected, reward has been applied.", "postQuestionAnswerSelected": "Answer has been selected, reward has been applied.",
"postVideoUpload": "Upload Video", "postVideoUpload": "Upload Video",
"realmJoin": "Join Realm", "realmJoin": "Join Realm",
@ -890,5 +891,55 @@
}, },
"settingsHideBottomNav": "Hide Bottom Navigation", "settingsHideBottomNav": "Hide Bottom Navigation",
"settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.", "settingsHideBottomNavDescription": "Hide the bottom navigation bar, and show the navigation buttons in the drawer.",
"reCaptcha": "reCaptcha" "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": "返回"
} }

View File

@ -888,5 +888,55 @@
}, },
"settingsHideBottomNav": "隐藏底部导航栏", "settingsHideBottomNav": "隐藏底部导航栏",
"settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。", "settingsHideBottomNavDescription": "隐藏底部导航栏,在侧边栏抽屉显示导航按钮。",
"reCaptcha": "人机验证" "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": "返回"
} }

View File

@ -888,5 +888,12 @@
}, },
"settingsHideBottomNav": "隱藏底部導航欄", "settingsHideBottomNav": "隱藏底部導航欄",
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。", "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
"reCaptcha": "人機驗證" "reCaptcha": "人機驗證",
"friends": "好友",
"friendsDescription": "管理好友關係。",
"album": "相冊",
"albumDescription": "查看相冊與管理上傳附件。",
"stickers": "貼圖",
"stickersDescription": "查看貼圖包與管理貼圖。",
"navBottomUnauthorizedCaption": "或者註冊一個賬號"
} }

View File

@ -888,5 +888,12 @@
}, },
"settingsHideBottomNav": "隱藏底部導航欄", "settingsHideBottomNav": "隱藏底部導航欄",
"settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。", "settingsHideBottomNavDescription": "隱藏底部導航欄,在側邊欄抽屜顯示導航按鈕。",
"reCaptcha": "人機驗證" "reCaptcha": "人機驗證",
"friends": "好友",
"friendsDescription": "管理好友關係。",
"album": "相冊",
"albumDescription": "查看相冊與管理上傳附件。",
"stickers": "貼圖",
"stickersDescription": "查看貼圖包與管理貼圖。",
"navBottomUnauthorizedCaption": "或者註冊一個賬號"
} }

View File

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

View File

@ -1,9 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'dart:math' hide log;
import 'dart:ui'; import 'dart:ui';
import 'package:audioplayers/audioplayers.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:croppy/croppy.dart'; import 'package:croppy/croppy.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@ -75,13 +75,40 @@ void appBackgroundDispatcher() {
}); });
} }
// Desktop size tools
Future<Size> _getSavedWindowSize() async {
final prefs = await SharedPreferences.getInstance();
String? sizeString = prefs.getString(kAppWindowSize);
if (sizeString != null) {
List<String> parts = sizeString.split('x');
if (parts.length == 2) {
double? width = double.tryParse(parts[0]);
double? height = double.tryParse(parts[1]);
if (width != null && height != null) {
return Size(width, height);
}
}
}
return const Size(1280, 720); // Default size
}
Future<void> _saveWindowSize() async {
final prefs = await SharedPreferences.getInstance();
final size = appWindow.size;
await prefs.setString(kAppWindowSize, '${size.width}x${size.height}');
}
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) { if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
final Size savedSize = await _getSavedWindowSize();
doWhenWindowReady(() { doWhenWindowReady(() {
appWindow.minSize = Size(480, 640); appWindow.minSize = Size(480, 640);
appWindow.size = Size(1280, 720); appWindow.size = savedSize;
appWindow.alignment = Alignment.center; appWindow.alignment = Alignment.center;
appWindow.show(); appWindow.show();
}); });
@ -91,18 +118,15 @@ void main() async {
if (!kIsWeb && !Platform.isLinux) { if (!kIsWeb && !Platform.isLinux) {
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform);
);
} }
GoRouter.optionURLReflectsImperativeAPIs = true; GoRouter.optionURLReflectsImperativeAPIs = true;
usePathUrlStrategy(); usePathUrlStrategy();
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Workmanager().initialize( Workmanager()
appBackgroundDispatcher, .initialize(appBackgroundDispatcher, isInDebugMode: kDebugMode);
isInDebugMode: kDebugMode,
);
if (Platform.isAndroid) { if (Platform.isAndroid) {
Workmanager().registerPeriodicTask( Workmanager().registerPeriodicTask(
"widget-update-random-post", "widget-update-random-post",
@ -137,7 +161,7 @@ class SolianApp extends StatelessWidget {
Locale('en', 'US'), Locale('en', 'US'),
Locale('zh', 'CN'), Locale('zh', 'CN'),
Locale('zh', 'TW'), Locale('zh', 'TW'),
Locale('zh', 'HK'), Locale('zh', 'HK')
], ],
fallbackLocale: Locale('en', 'US'), fallbackLocale: Locale('en', 'US'),
useFallbackTranslations: true, useFallbackTranslations: true,
@ -161,7 +185,7 @@ class SolianApp extends StatelessWidget {
Provider(create: (ctx) => SnNetworkProvider(ctx)), Provider(create: (ctx) => SnNetworkProvider(ctx)),
Provider(create: (ctx) => UserDirectoryProvider(ctx)), Provider(create: (ctx) => UserDirectoryProvider(ctx)),
Provider(create: (ctx) => SnAttachmentProvider(ctx)), Provider(create: (ctx) => SnAttachmentProvider(ctx)),
Provider(create: (ctx) => SnRealmProvider(ctx)), ChangeNotifierProvider(create: (ctx) => SnRealmProvider(ctx)),
Provider(create: (ctx) => SnPostContentProvider(ctx)), Provider(create: (ctx) => SnPostContentProvider(ctx)),
Provider(create: (ctx) => SnRelationshipProvider(ctx)), Provider(create: (ctx) => SnRelationshipProvider(ctx)),
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)), Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
@ -264,11 +288,9 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
final resp = await Dio( final resp = await Dio(
BaseOptions( BaseOptions(
sendTimeout: const Duration(seconds: 60), sendTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60), receiveTimeout: const Duration(seconds: 60)),
),
).get( ).get(
'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest', 'https://api.github.com/repos/Solsynth/HyperNet.Surface/releases/latest');
);
final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0'; final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0';
final remoteVersion = Version.parse(remoteVersionString.split('+').first); final remoteVersion = Version.parse(remoteVersionString.split('+').first);
final localVersion = Version.parse(localVersionString.split('+').first); final localVersion = Version.parse(localVersionString.split('+').first);
@ -283,9 +305,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
mounted) { mounted) {
final config = context.read<ConfigProvider>(); final config = context.read<ConfigProvider>();
config.setUpdate( config.setUpdate(
remoteVersionString, remoteVersionString, resp.data?['body'] ?? 'No changelog');
resp.data?['body'] ?? 'No changelog',
);
logging.info("[Update] Update available: $remoteVersionString"); logging.info("[Update] Update available: $remoteVersionString");
} }
} catch (e) { } catch (e) {
@ -323,16 +343,21 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
_setPhaseText('websocket'); _setPhaseText('websocket');
final ws = context.read<WebSocketProvider>(); final ws = context.read<WebSocketProvider>();
await ws.tryConnect(); await ws.tryConnect();
if (!mounted) return; try {
_setPhaseText('notification');
final notify = context.read<NotificationProvider>();
notify.listen();
await notify.registerPushNotifications();
if (!mounted) return; if (!mounted) return;
_setPhaseText('keyPair'); _setPhaseText('keyPair');
final kp = context.read<KeyPairProvider>(); final kp = context.read<KeyPairProvider>();
await kp.reloadActive(); await kp.reloadActive();
kp.listen(); kp.listen();
} catch (_) {}
if (ua.isAuthorized) {
if (!mounted) return;
_setPhaseText('notification');
final notify = context.read<NotificationProvider>();
notify.listen();
try {
notify.registerPushNotifications();
} catch (_) {}
if (!mounted) return; if (!mounted) return;
_setPhaseText('stickers'); _setPhaseText('stickers');
final sticker = context.read<SnStickerProvider>(); final sticker = context.read<SnStickerProvider>();
@ -350,6 +375,8 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
final ct = context.read<ChatChannelProvider>(); final ct = context.read<ChatChannelProvider>();
await ct.refreshAvailableChannels(); await ct.refreshAvailableChannels();
_setPhaseText('done'); _setPhaseText('done');
_playIntro();
}
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
await context.showErrorDialog(err); await context.showErrorDialog(err);
@ -365,28 +392,28 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
// The quit key has been removed, and the logic of the quit key is moved to system menu bar activator. // The quit key has been removed, and the logic of the quit key is moved to system menu bar activator.
} }
void _playIntro() async {
final cfg = context.read<ConfigProvider>();
if (!cfg.soundEffects) return;
final player = AudioPlayer(playerId: 'launch-done-player');
await player.play(AssetSource('audio/sfx/launch-done.mp3'), volume: 0.8);
player.onPlayerComplete.listen((_) {
player.dispose();
});
}
final Menu _appTrayMenu = Menu( final Menu _appTrayMenu = Menu(
items: [ items: [
MenuItem( MenuItem(key: 'version_label', label: 'Solian', disabled: true),
key: 'version_label',
label: 'Solian',
disabled: true,
),
MenuItem.separator(), MenuItem.separator(),
MenuItem.checkbox( MenuItem.checkbox(
checked: false, checked: false,
key: 'mute_notification', key: 'mute_notification',
label: 'trayMenuMuteNotification'.tr(), label: 'trayMenuMuteNotification'.tr()),
),
MenuItem.separator(), MenuItem.separator(),
MenuItem( MenuItem(key: 'window_show', label: 'trayMenuShow'.tr()),
key: 'window_show', MenuItem(key: 'exit', label: 'trayMenuExit'.tr()),
label: 'trayMenuShow'.tr(),
),
MenuItem(
key: 'exit',
label: 'trayMenuExit'.tr(),
),
], ],
); );
@ -414,9 +441,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return; if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
await localNotifier.setup( await localNotifier.setup(
appName: 'Solian', appName: 'Solian', shortcutPolicy: ShortcutPolicy.requireCreate);
shortcutPolicy: ShortcutPolicy.requireCreate,
);
} }
AppLifecycleListener? _appLifecycleListener; AppLifecycleListener? _appLifecycleListener;
@ -427,9 +452,8 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
_isBusy = true; _isBusy = true;
if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) { if (!kIsWeb && !(Platform.isIOS || Platform.isAndroid)) {
_appLifecycleListener = AppLifecycleListener( _appLifecycleListener =
onExitRequested: _onExitRequested, AppLifecycleListener(onExitRequested: _onExitRequested);
);
} }
_trayInitialization(); _trayInitialization();
@ -449,6 +473,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
} }
void _quitApp() { void _quitApp() {
_saveWindowSize();
_appLifecycleListener?.dispose(); _appLifecycleListener?.dispose();
if (Platform.isWindows) { if (Platform.isWindows) {
appWindow.close(); appWindow.close();
@ -534,12 +559,20 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
key: Key('app-splash-screen-$_isBusy'), key: Key('app-splash-screen-$_isBusy'),
child: Stack( child: Stack(
children: [ children: [
CustomPaint(painter: GraphPainter()), 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( Center(
child: Container( child: Container(
constraints: const BoxConstraints( constraints: const BoxConstraints(maxWidth: 240),
maxWidth: 240,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -553,10 +586,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
Text('Solar Network').bold(), Text('Solar Network').bold(),
AppVersionLabel(), AppVersionLabel(),
Gap(8), Gap(8),
Text( Text(_phaseText, textAlign: TextAlign.center),
_phaseText,
textAlign: TextAlign.center,
),
Gap(16), Gap(16),
const LinearProgressIndicator(), const LinearProgressIndicator(),
], ],
@ -574,44 +604,3 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
); );
} }
} }
class GraphPainter extends CustomPainter {
final Random random = Random();
final int numNodes = 20;
final double maxDistance = 100; // Max distance to draw a line
@override
void paint(Canvas canvas, Size size) {
final paintNode = Paint()..color = Colors.white;
final paintEdge = Paint()
..color = Colors.white.withOpacity(0.3)
..strokeWidth = 1;
// Generate random points
List<Offset> nodes = List.generate(
numNodes,
(_) => Offset(
random.nextDouble() * size.width,
random.nextDouble() * size.height,
),
);
// Draw edges between close nodes
for (var i = 0; i < nodes.length; i++) {
for (var j = i + 1; j < nodes.length; j++) {
double distance = (nodes[i] - nodes[j]).distance;
if (distance < maxDistance) {
canvas.drawLine(nodes[i], nodes[j], paintEdge);
}
}
}
// Draw nodes
for (var node in nodes) {
canvas.drawCircle(node, 4, paintNode);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

View File

@ -41,6 +41,11 @@ class ChatChannelProvider extends ChangeNotifier {
}); });
} }
void addAvailableChannel(SnChannel channel) {
_availableChannels.add(channel);
notifyListeners();
}
Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async { Future<void> _saveChannelToLocal(Iterable<SnChannel> channels) async {
await Future.wait( await Future.wait(
channels.map( channels.map(

View File

@ -13,7 +13,6 @@ const kNetworkServerStoreKey = 'app_server_url';
const kAppbarTransparentStoreKey = 'app_bar_transparent'; const kAppbarTransparentStoreKey = 'app_bar_transparent';
const kAppBackgroundStoreKey = 'app_has_background'; const kAppBackgroundStoreKey = 'app_has_background';
const kAppColorSchemeStoreKey = 'app_color_scheme'; const kAppColorSchemeStoreKey = 'app_color_scheme';
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
const kAppNotifyWithHaptic = 'app_notify_with_haptic'; const kAppNotifyWithHaptic = 'app_notify_with_haptic';
const kAppExpandPostLink = 'app_expand_post_link'; const kAppExpandPostLink = 'app_expand_post_link';
const kAppExpandChatLink = 'app_expand_chat_link'; const kAppExpandChatLink = 'app_expand_chat_link';
@ -22,6 +21,9 @@ const kAppCustomFonts = 'app_custom_fonts';
const kAppMixedFeed = 'app_mixed_feed'; const kAppMixedFeed = 'app_mixed_feed';
const kAppAutoTranslate = 'app_auto_translate'; const kAppAutoTranslate = 'app_auto_translate';
const kAppHideBottomNav = 'app_hide_bottom_nav'; const kAppHideBottomNav = 'app_hide_bottom_nav';
const kAppSoundEffects = 'app_sound_effects';
const kAppAprilFoolFeatures = 'app_april_fool_features';
const kAppWindowSize = 'app_window_size';
const Map<String, FilterQuality> kImageQualityLevel = { const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none, 'settingsImageQualityLowest': FilterQuality.none,
@ -44,27 +46,17 @@ class ConfigProvider extends ChangeNotifier {
} }
bool drawerIsCollapsed = false; bool drawerIsCollapsed = false;
bool drawerIsExpanded = false;
void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) { void calcDrawerSize(BuildContext context, {bool withMediaQuery = false}) {
bool newDrawerIsCollapsed = false; bool newDrawerIsCollapsed = false;
bool newDrawerIsExpanded = false;
if (withMediaQuery) { if (withMediaQuery) {
newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600; newDrawerIsCollapsed = MediaQuery.of(context).size.width < 600;
newDrawerIsExpanded = MediaQuery.of(context).size.width >= 601;
} else { } else {
final rpb = ResponsiveBreakpoints.of(context); final rpb = ResponsiveBreakpoints.of(context);
newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE); newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
newDrawerIsExpanded = rpb.largerThan(TABLET)
? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
? false
: true
: false;
} }
if (newDrawerIsExpanded != drawerIsExpanded || if (newDrawerIsCollapsed != drawerIsCollapsed) {
newDrawerIsCollapsed != drawerIsCollapsed) {
drawerIsExpanded = newDrawerIsExpanded;
drawerIsCollapsed = newDrawerIsCollapsed; drawerIsCollapsed = newDrawerIsCollapsed;
notifyListeners(); notifyListeners();
} }
@ -96,6 +88,24 @@ class ConfigProvider extends ChangeNotifier {
return prefs.getBool(kAppHideBottomNav) ?? false; 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) { set hideBottomNav(bool value) {
prefs.setBool(kAppHideBottomNav, value); prefs.setBool(kAppHideBottomNav, value);
notifyListeners(); notifyListeners();

View File

@ -4,7 +4,20 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/types/realm.dart';
class AppNavListItem {
final String title;
final String subtitle;
final String screen;
final IconData icon;
const AppNavListItem({
required this.title,
required this.subtitle,
required this.screen,
required this.icon,
});
}
class AppNavDestination { class AppNavDestination {
final String label; final String label;
@ -46,11 +59,6 @@ class NavigationProvider extends ChangeNotifier {
screen: 'chat', screen: 'chat',
label: 'screenChat', label: 'screenChat',
), ),
AppNavDestination(
icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20),
screen: 'account',
label: 'screenAccount',
),
AppNavDestination( AppNavDestination(
icon: Icon(Symbols.group, weight: 400, opticalSize: 20), icon: Icon(Symbols.group, weight: 400, opticalSize: 20),
screen: 'realm', screen: 'realm',
@ -62,24 +70,9 @@ class NavigationProvider extends ChangeNotifier {
label: 'screenNews', label: 'screenNews',
), ),
AppNavDestination( AppNavDestination(
icon: Icon(Symbols.emoji_emotions, weight: 400, opticalSize: 20), icon: Icon(Symbols.settings, weight: 400, opticalSize: 20),
screen: 'stickers', screen: 'settings',
label: 'screenStickers', label: 'screenSettings',
),
AppNavDestination(
icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20),
screen: 'album',
label: 'screenAlbum',
),
AppNavDestination(
icon: Icon(Symbols.diversity_4, weight: 400, opticalSize: 20),
screen: 'friend',
label: 'screenFriend',
),
AppNavDestination(
icon: Icon(Symbols.notifications, weight: 400, opticalSize: 20),
screen: 'notification',
label: 'screenNotification',
), ),
]; ];
static const List<String> kDefaultPinnedDestination = [ static const List<String> kDefaultPinnedDestination = [
@ -141,11 +134,4 @@ class NavigationProvider extends ChangeNotifier {
_currentIndex = idx; _currentIndex = idx;
notifyListeners(); notifyListeners();
} }
SnRealm? focusedRealm;
void setFocusedRealm(SnRealm? realm) {
focusedRealm = realm;
notifyListeners();
}
} }

View File

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

View File

@ -8,7 +8,7 @@ import 'package:surface/providers/database.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
class SnRealmProvider { class SnRealmProvider extends ChangeNotifier {
late final SnNetworkProvider _sn; late final SnNetworkProvider _sn;
late final DatabaseProvider _dt; late final DatabaseProvider _dt;
@ -39,6 +39,11 @@ class SnRealmProvider {
return out; return out;
} }
void addAvailableRealm(SnRealm realm) {
_availableRealms.add(realm);
notifyListeners();
}
Future<SnRealm> getRealm(dynamic aliasOrId) async { Future<SnRealm> getRealm(dynamic aliasOrId) async {
if (_cache.containsKey(aliasOrId.toString())) { if (_cache.containsKey(aliasOrId.toString())) {
return _cache[aliasOrId.toString()]!; return _cache[aliasOrId.toString()]!;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -70,6 +70,7 @@ class _AccountSecurityPrefsScreenState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
leading: const PageBackButton(), leading: const PageBackButton(),
title: Text('accountSettingsSecurity').tr(), title: Text('accountSettingsSecurity').tr(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/screens/captcha.dart'; import 'package:surface/screens/captcha/captcha.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -43,7 +43,7 @@ class _RegisterScreenState extends State<RegisterScreen> {
final captchaTk = await Navigator.of(context, rootNavigator: true).push( final captchaTk = await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => TurnstileScreen(), builder: (context) => CaptchaScreen(),
), ),
); );
if (captchaTk == null) return; if (captchaTk == null) return;

View File

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

View File

@ -5,19 +5,18 @@ import 'package:provider/provider.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
class TurnstileScreen extends StatefulWidget { class CaptchaScreen extends StatefulWidget {
const TurnstileScreen({ const CaptchaScreen({super.key});
super.key,
});
@override @override
State<TurnstileScreen> createState() => _TurnstileScreenState(); State<CaptchaScreen> createState() => _CaptchaScreenState();
} }
class _TurnstileScreenState extends State<TurnstileScreen> { class _CaptchaScreenState extends State<CaptchaScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>(); final cfg = context.read<ConfigProvider>();
return AppScaffold( return AppScaffold(
appBar: AppBar(title: Text("reCaptcha").tr()), appBar: AppBar(title: Text("reCaptcha").tr()),
body: InAppWebView( body: InAppWebView(

View File

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

View File

@ -1,3 +1,5 @@
import 'package:animations/animations.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.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:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/chat/room.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/types/realm.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_select.dart'; import 'package:surface/widgets/account/account_select.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ChatScreen extends StatefulWidget { class ChatScreen extends StatefulWidget {
@ -38,6 +41,7 @@ class _ChatScreenState extends State<ChatScreen> {
List<SnChannel>? _channels; List<SnChannel>? _channels;
Map<int, SnChatMessage>? _lastMessages; Map<int, SnChatMessage>? _lastMessages;
Map<int, int>? _unreadCounts; Map<int, int>? _unreadCounts;
Map<int, int>? _unreadCountsGrouped;
Future<void> _fetchWhatsNew() async { Future<void> _fetchWhatsNew() async {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
@ -45,19 +49,48 @@ class _ChatScreenState extends State<ChatScreen> {
if (resp.data == null) return; if (resp.data == null) return;
final List<dynamic> out = resp.data; final List<dynamic> out = resp.data;
setState(() { 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>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
setState(() => _isBusy = false); setState(() => _isBusy = false);
return; return;
} }
if (!withBoost) {
if (!noRemote) {
ct.refreshAvailableChannels();
}
} else {
setState(() {
_channels = ct.availableChannels;
});
}
final chan = context.read<ChatChannelProvider>(); 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); final lastMessages = await chan.getLastMessages(channels);
_lastMessages = {for (final val in lastMessages) val.channelId: val}; _lastMessages = {for (final val in lastMessages) val.channelId: val};
channels.sort((a, b) { channels.sort((a, b) {
@ -99,6 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
..onDone(() { ..onDone(() {
if (!mounted) return; if (!mounted) return;
setState(() => _isBusy = false); setState(() => _isBusy = false);
_fetchWhatsNew();
}); });
} }
@ -130,22 +164,28 @@ class _ChatScreenState extends State<ChatScreen> {
} }
} }
SnChannel? _focusChannel;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_refreshChannels(); _refreshChannels(withBoost: true);
_fetchWhatsNew();
} }
void _onTapChannel(SnChannel channel) { void _onTapChannel(SnChannel channel) {
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP); setState(() => _unreadCounts?[channel.id] = 0);
if (ResponsiveScaffold.getIsExpand(context)) {
if (doExpand) { GoRouter.of(context).pushReplacementNamed(
setState(() => _focusChannel = channel); 'chatRoom',
return; 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( GoRouter.of(context).pushNamed(
'chatRoom', 'chatRoom',
pathParameters: { pathParameters: {
@ -154,16 +194,21 @@ class _ChatScreenState extends State<ChatScreen> {
}, },
).then((value) { ).then((value) {
if (mounted) { if (mounted) {
_unreadCounts?[channel.id] = 0;
setState(() => _unreadCounts?[channel.id] = 0); setState(() => _unreadCounts?[channel.id] = 0);
_refreshChannels(noRemote: true); _refreshChannels(noRemote: true);
} }
}); });
} }
}
SnRealm? _focusedRealm;
bool _isDirect = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final sn = context.read<SnNetworkProvider>();
final rel = context.read<SnRealmProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return AppScaffold( return AppScaffold(
@ -177,10 +222,8 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
final doExpand = ResponsiveBreakpoints.of(context).largerOrEqualTo(DESKTOP); return AppScaffold(
noBackground: true,
final chatList = AppScaffold(
noBackground: doExpand,
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenChat').tr(), title: Text('screenChat').tr(),
@ -248,65 +291,199 @@ class _ChatScreenState extends State<ChatScreen> {
body: Column( body: Column(
children: [ children: [
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
if (_channels != null && ResponsiveScaffold.getIsExpand(context))
Expanded( Expanded(
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () => Future.wait([ onRefresh: () => Future.sync(() => _refreshChannels()),
Future.sync(() => _refreshChannels()), child: Builder(builder: (context) {
_fetchWhatsNew(), final scopeList = ListView(
]), key: const Key('realm-list-view'),
child: ListView.builder( padding: EdgeInsets.zero,
itemCount: _channels?.length ?? 0,
itemBuilder: (context, idx) {
final channel = _channels![idx];
final lastMessage = _lastMessages?[channel.id];
return _ChatChannelEntry(
channel: channel,
lastMessage: lastMessage,
unreadCount: _unreadCounts?[channel.id],
onTap: () {
if (doExpand) {
_unreadCounts?[channel.id] = 0;
setState(() => _focusChannel = channel);
return;
}
_onTapChannel(channel);
},
);
},
),
),
),
),
],
),
);
if (doExpand) {
return AppBackground(
isRoot: true,
child: Row(
children: [ children: [
SizedBox(width: 340, child: chatList), ListTile(
const VerticalDivider(width: 1), minTileHeight: 48,
if (_focusChannel != null) 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);
},
);
}),
],
);
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 / 9,
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( Expanded(
child: ChatRoomScreen( child: RefreshIndicator(
key: ValueKey(_focusChannel!.id), onRefresh: () => Future.sync(() => _refreshChannels()),
scope: _focusChannel!.realm?.alias ?? 'global', child: ListView(
alias: _focusChannel!.alias, 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),
);
}))
],
),
), ),
), ),
], ],
), ),
); );
} }
return chatList;
}
} }
class _ChatChannelEntry extends StatelessWidget { class _ChatChannelEntry extends StatelessWidget {
@ -314,11 +491,13 @@ class _ChatChannelEntry extends StatelessWidget {
final int? unreadCount; final int? unreadCount;
final SnChatMessage? lastMessage; final SnChatMessage? lastMessage;
final Function? onTap; final Function? onTap;
final bool isCompact;
const _ChatChannelEntry({ const _ChatChannelEntry({
required this.channel, required this.channel,
this.unreadCount, this.unreadCount,
this.lastMessage, this.lastMessage,
this.onTap, this.onTap,
this.isCompact = false,
}); });
@override @override
@ -337,6 +516,34 @@ class _ChatChannelEntry extends StatelessWidget {
? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name ? ud.getFromCache(otherMember.accountId)?.nick ?? channel.name
: 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( return ListTile(
title: Row( title: Row(
children: [ children: [
@ -399,7 +606,7 @@ class _ChatChannelEntry extends StatelessWidget {
content: otherMember != null content: otherMember != null
? ud.getFromCache(otherMember.accountId)?.avatar ? ud.getFromCache(otherMember.accountId)?.avatar
: channel.realm?.avatar, : channel.realm?.avatar,
fallbackWidget: const Icon(Symbols.chat, size: 20), fallbackWidget: const Icon(Symbols.tag, size: 20),
), ),
onTap: () => onTap?.call(), onTap: () => onTap?.call(),
); );

View File

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

View File

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

View File

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

View File

@ -304,6 +304,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
final ud = context.read<UserDirectoryProvider>(); final ud = context.read<UserDirectoryProvider>();
return AppScaffold( return AppScaffold(
noBackground: true,
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
_channel?.type == 1 _channel?.type == 1

View File

@ -157,6 +157,7 @@ class _ExploreScreenState extends State<ExploreScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.watch<ConfigProvider>(); final cfg = context.watch<ConfigProvider>();
return AppScaffold( return AppScaffold(
noBackground: true,
floatingActionButtonLocation: ExpandableFab.location, floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab( floatingActionButton: ExpandableFab(
key: _fabKey, key: _fabKey,
@ -243,6 +244,8 @@ class _ExploreScreenState extends State<ExploreScreen>
GoRouter.of(context).pushNamed('postShuffle'); GoRouter.of(context).pushNamed('postShuffle');
}, },
), ),
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE))
const Gap(48),
Expanded( Expanded(
child: Center( child: Center(
child: IconButton( child: IconButton(
@ -534,6 +537,7 @@ class _PostListWidgetState extends State<_PostListWidget> {
switch (ele.type) { switch (ele.type) {
case 'interactive.post': case 'interactive.post':
return OpenablePostItem( return OpenablePostItem(
useReplace: true,
data: SnPost.fromJson(ele.data), data: SnPost.fromJson(ele.data),
maxWidth: 640, maxWidth: 640,
onChanged: (data) { onChanged: (data) {

View File

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

View File

@ -18,7 +18,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/special_day.dart'; import 'package:surface/providers/special_day.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/widget.dart'; import 'package:surface/providers/widget.dart';
import 'package:surface/screens/captcha.dart'; import 'package:surface/screens/captcha/captcha.dart';
import 'package:surface/types/check_in.dart'; import 'package:surface/types/check_in.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/app_bar_leading.dart';
@ -396,35 +396,44 @@ class _HomeDashServiceStatusState extends State<_HomeDashServiceStatus> {
: switch (_serviceStatus) { : switch (_serviceStatus) {
ServiceStatus.operational => Row( ServiceStatus.operational => Row(
children: [ children: [
const Icon( Icon(
Symbols.check, Symbols.check,
size: 20, size: 20,
color: Colors.green[900],
), ),
const Gap(10), const Gap(10),
Text('serviceStatusOperational').tr(), Text('serviceStatusOperational')
.tr()
.textColor(Colors.green[900]),
], ],
), ),
ServiceStatus.failed => Tooltip( ServiceStatus.failed => Tooltip(
message: 'serviceStatusFailedDescription'.tr(), message: 'serviceStatusFailedDescription'.tr(),
child: Row( child: Row(
children: [ children: [
const Icon( Icon(
Symbols.dangerous, Symbols.dangerous,
size: 20, size: 20,
color: Colors.red[900],
), ),
const Gap(10), const Gap(10),
Text('serviceStatusFailed').tr(), Text('serviceStatusFailed')
.tr()
.textColor(Colors.red[900]),
], ],
), ),
), ),
_ => Row( _ => Row(
children: [ children: [
const Icon( Icon(
Symbols.error, Symbols.error,
size: 20, size: 20,
color: Colors.orange[900],
), ),
const Gap(10), const Gap(10),
Text('serviceStatusDowngraded').tr(), Text('serviceStatusDowngraded')
.tr()
.textColor(Colors.orange[900]),
], ],
), ),
}, },
@ -511,7 +520,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
Future<void> _doCheckIn() async { Future<void> _doCheckIn() async {
final captchaTk = await Navigator.of(context, rootNavigator: true).push( final captchaTk = await Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => TurnstileScreen(), builder: (context) => CaptchaScreen(),
), ),
); );
if (captchaTk == null) return; if (captchaTk == null) return;
@ -806,7 +815,7 @@ class _HomeDashNotificationWidgetState
child: IconButton( child: IconButton(
icon: const Icon(Symbols.arrow_right_alt), icon: const Icon(Symbols.arrow_right_alt),
onPressed: () { onPressed: () {
GoRouter.of(context).goNamed('notification'); GoRouter.of(context).pushNamed('notification');
}, },
), ),
), ),

View File

@ -149,8 +149,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: PageBackButton(),
title: Text('screenNotification').tr()), title: Text('screenNotification').tr(),
),
body: Center(child: UnauthorizedHint()), body: Center(child: UnauthorizedHint()),
); );
} }

View File

@ -12,7 +12,6 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_comment_list.dart'; import 'package:surface/widgets/post/post_comment_list.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
@ -66,9 +65,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
final double maxWidth = _data?.type == 'video' ? double.infinity : 640; final double maxWidth = _data?.type == 'video' ? double.infinity : 640;
return AppBackground( return AppScaffold(
isRoot: widget.onBack != null, noBackground: true,
child: AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: BackButton( leading: BackButton(
onPressed: () { onPressed: () {
@ -89,16 +87,14 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
TextSpan( TextSpan(
text: _data?.body['title'] ?? 'postNoun'.tr(), text: _data?.body['title'] ?? 'postNoun'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith( style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: color: Theme.of(context).appBarTheme.foregroundColor!,
Theme.of(context).appBarTheme.foregroundColor!,
), ),
), ),
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: 'postDetail'.tr(), text: 'postDetail'.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: color: Theme.of(context).appBarTheme.foregroundColor!,
Theme.of(context).appBarTheme.foregroundColor!,
), ),
), ),
]), ]),
@ -175,7 +171,6 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)), SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
], ],
), ),
),
); );
} }
} }

View File

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

View File

@ -38,7 +38,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final ScrollController _scrollController = ScrollController(); late final ScrollController _scrollController = ScrollController();
late final TabController _tabController = late final TabController _tabController =
TabController(length: 3, vsync: this); TabController(length: 5, vsync: this);
SnPublisher? _publisher; SnPublisher? _publisher;
SnAccount? _account; SnAccount? _account;
@ -137,7 +137,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
late final _appBarWidth = MediaQuery.of(context).size.width; late final _appBarWidth = MediaQuery.of(context).size.width;
late final _appBarHeight = late final _appBarHeight =
(_appBarWidth * kBannerAspectRatio).roundToDouble(); math.min((_appBarWidth * kBannerAspectRatio), 360).roundToDouble();
void _updateAppBarBlur() { void _updateAppBarBlur() {
if (_scrollController.offset > _appBarHeight) return; if (_scrollController.offset > _appBarHeight) return;
@ -165,6 +165,8 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
type: switch (_tabController.index) { type: switch (_tabController.index) {
1 => 'story', 1 => 'story',
2 => 'article', 2 => 'article',
3 => 'question',
4 => 'video',
_ => null, _ => null,
}, },
); );
@ -284,6 +286,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return AppScaffold( return AppScaffold(
noBackground: true,
body: NestedScrollView( body: NestedScrollView(
controller: _scrollController, controller: _scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
@ -568,6 +571,18 @@ class _PostPublisherScreenState extends State<PostPublisherScreen>
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), ),
), ),
Tab(
icon: Icon(
Symbols.help,
color: Theme.of(context).colorScheme.onSurface,
),
),
Tab(
icon: Icon(
Symbols.video_call,
color: Theme.of(context).colorScheme.onSurface,
),
),
], ],
), ),
SliverToBoxAdapter(child: const Divider(height: 1)), SliverToBoxAdapter(child: const Divider(height: 1)),

View File

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

View File

@ -80,6 +80,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final dt = context.read<DatabaseProvider>(); final dt = context.read<DatabaseProvider>();
final cfg = context.watch<ConfigProvider>();
final now = DateTime.now();
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
@ -322,20 +325,6 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {}); setState(() {});
}, },
), ),
CheckboxListTile(
secondary: const Icon(Symbols.left_panel_close),
title: Text('settingsDrawerPreferCollapse').tr(),
subtitle:
Text('settingsDrawerPreferCollapseDescription').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
onChanged: (value) {
_prefs.setBool(kAppDrawerPreferCollapse, value ?? false);
final cfg = context.read<ConfigProvider>();
cfg.calcDrawerSize(context);
setState(() {});
},
),
CheckboxListTile( CheckboxListTile(
secondary: const Icon(Symbols.hide), secondary: const Icon(Symbols.hide),
title: Text('settingsHideBottomNav').tr(), title: Text('settingsHideBottomNav').tr(),
@ -349,6 +338,31 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {}); setState(() {});
}, },
), ),
CheckboxListTile(
value: cfg.soundEffects,
onChanged: (value) {
cfg.soundEffects = value ?? false;
setState(() {});
},
contentPadding: const EdgeInsets.only(left: 24, right: 17),
title: Text('settingsSoundEffects').tr(),
subtitle: Text('settingsSoundEffectsDescription').tr(),
secondary: const Icon(Symbols.sound_sampler),
),
if (!kIsWeb && !(Platform.isAndroid || Platform.isIOS))
ListTile(
leading: const Icon(Symbols.window),
title: Text('settingsResetMemorizedWindowSize').tr(),
subtitle:
Text('settingsResetMemorizedWindowSizeDescription')
.tr(),
trailing: const Icon(Symbols.chevron_right),
contentPadding: const EdgeInsets.only(left: 24, right: 24),
onTap: () {
final prefs = context.read<ConfigProvider>().prefs;
prefs.remove(kAppWindowSize);
},
),
ListTile( ListTile(
leading: const Icon(Symbols.font_download), leading: const Icon(Symbols.font_download),
title: Text('settingsCustomFonts').tr(), title: Text('settingsCustomFonts').tr(),
@ -741,6 +755,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
GoRouter.of(context).pushNamed('about'); GoRouter.of(context).pushNamed('about');
}, },
), ),
if (now.day == 1 && now.month == 4)
CheckboxListTile(
title: Text('settingsAprilFoolFeatures').tr(),
subtitle: Text('settingsAprilFoolFeaturesDescription').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
secondary: const Icon(Symbols.new_releases),
value: cfg.aprilFoolFeatures,
onChanged: (value) {
cfg.aprilFoolFeatures = value ?? false;
setState(() {});
},
)
], ],
), ),
], ],

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,9 +26,7 @@ class ContextMenuArea extends StatelessWidget {
final cfg = context.read<ConfigProvider>(); final cfg = context.read<ConfigProvider>();
if (!cfg.drawerIsCollapsed) { if (!cfg.drawerIsCollapsed) {
// Leave padding for side navigation // Leave padding for side navigation
mousePosition = cfg.drawerIsExpanded mousePosition = mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
: mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
} }
}, },
child: GestureDetector( child: GestureDetector(
@ -40,7 +38,8 @@ class ContextMenuArea extends StatelessWidget {
} }
void _showMenu(BuildContext context, Offset mousePosition) async { void _showMenu(BuildContext context, Offset mousePosition) async {
final menu = contextMenu.copyWith(position: contextMenu.position ?? mousePosition); final menu =
contextMenu.copyWith(position: contextMenu.position ?? mousePosition);
final value = await showContextMenu(context, contextMenu: menu); final value = await showContextMenu(context, contextMenu: menu);
onItemSelected?.call(value); onItemSelected?.call(value);
} }

View File

@ -1,7 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:animations/animations.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -10,14 +10,9 @@ import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/channel.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/navigation.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:surface/widgets/version_label.dart'; import 'package:surface/widgets/version_label.dart';
class AppNavigationDrawer extends StatefulWidget { class AppNavigationDrawer extends StatefulWidget {
@ -44,25 +39,18 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final nav = context.watch<NavigationProvider>(); final nav = context.watch<NavigationProvider>();
final cfg = context.watch<ConfigProvider>();
final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
return ListenableBuilder( return ListenableBuilder(
listenable: nav, listenable: nav,
builder: (context, _) { builder: (context, _) {
return Drawer( return Drawer(
elevation: widget.elevation, elevation: widget.elevation,
backgroundColor: backgroundColor,
child: Column( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (!kIsWeb && if (!kIsWeb &&
(Platform.isWindows || (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
Platform.isLinux ||
Platform.isMacOS) &&
!cfg.drawerIsExpanded)
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
@ -75,43 +63,55 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
child: WindowTitleBarBox(), child: WindowTitleBarBox(),
), ),
Gap(MediaQuery.of(context).padding.top), Gap(MediaQuery.of(context).padding.top),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Solar Network').bold(),
AppVersionLabel(),
],
).padding(
horizontal: 32,
vertical: 12,
),
Expanded( Expanded(
child: _DrawerContentList(), child: ListView(
), padding: EdgeInsets.zero,
if (cfg.hideBottomNav) children: [
Row( ...nav.destinations.mapIndexed((idx, ele) {
spacing: 8, return ListTile(
children: nav.destinations.where((ele) => ele.isPinned).map( leading: ele.icon,
(ele) { title: Text(ele.label).tr(),
return Expanded( contentPadding: EdgeInsets.symmetric(horizontal: 24),
child: Tooltip( selected: nav.currentIndex == idx,
message: ele.label.tr(), onTap: () {
child: IconButton.filledTonal( GoRouter.of(context).pushNamed(ele.screen);
icon: ele.icon, nav.setIndex(idx);
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
onPressed: () {
GoRouter.of(context).goNamed(ele.screen);
Scaffold.of(context).closeDrawer();
}, },
),
),
); );
}, })
).toList(), ],
).padding(horizontal: 16), ),
),
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: ListTile( child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
leading: AccountImage(content: ua.user?.avatar), leading: AccountImage(
title: Text(ua.user?.nick ?? 'unknown'.tr()).fontSize(15), content: ua.user?.avatar,
subtitle: fallbackWidget:
Text('@${ua.user?.name ?? 'unknown'.tr()}').fontSize(13), ua.isAuthorized ? null : const Icon(Symbols.login),
),
title: ua.isAuthorized
? Text(ua.user?.nick ?? 'unknown'.tr()).fontSize(15)
: Text('screenAuthLogin').tr(),
subtitle: ua.isAuthorized
? Text('@${ua.user?.name ?? 'unknown'.tr()}').fontSize(13)
: Text('navBottomUnauthorizedCaption').fontSize(13).tr(),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (ua.isAuthorized)
IconButton( IconButton(
icon: const Icon(Symbols.notifications, fill: 1), icon: const Icon(Symbols.notifications, fill: 1),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
@ -138,7 +138,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
}, },
), ),
), ),
Gap(MediaQuery.of(context).padding.bottom), Gap(MediaQuery.of(context).padding.bottom + 8),
], ],
), ),
); );
@ -146,163 +146,3 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
); );
} }
} }
class _DrawerContentList extends StatelessWidget {
const _DrawerContentList();
@override
Widget build(BuildContext context) {
final ct = context.read<ChatChannelProvider>();
final sn = context.read<SnNetworkProvider>();
final nav = context.watch<NavigationProvider>();
final rel = context.read<SnRealmProvider>();
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child, Animation<double> primaryAnimation,
Animation<double> secondaryAnimation) {
return SharedAxisTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
transitionType: SharedAxisTransitionType.horizontal,
child: child,
);
},
child: nav.focusedRealm == null
? ListView(
key: const Key('realm-list-view'),
padding: EdgeInsets.zero,
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Solar Network').bold(),
AppVersionLabel(),
],
).padding(
horizontal: 32,
vertical: 12,
),
ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.only(left: 28, right: 16),
leading: const Icon(Symbols.home),
title: Text('screenHome').tr(),
onTap: () {
GoRouter.of(context).goNamed('home');
Scaffold.of(context).closeDrawer();
},
),
...rel.availableRealms.map((ele) {
return ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.symmetric(horizontal: 24),
leading: AccountImage(
content: ele.avatar,
radius: 16,
),
title: Text(ele.name),
onTap: () {
nav.setFocusedRealm(ele);
},
);
}),
],
)
: ListView(
key: ValueKey(nav.focusedRealm),
padding: EdgeInsets.zero,
children: [
if (nav.focusedRealm!.banner != null)
AspectRatio(
aspectRatio: 16 / 9,
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(
nav.focusedRealm!.banner!,
),
fit: BoxFit.cover,
),
),
ListTile(
minTileHeight: 48,
tileColor: Theme.of(context).colorScheme.surfaceContainer,
contentPadding: EdgeInsets.only(
left: 24,
right: 16,
),
leading: AccountImage(
content: nav.focusedRealm!.avatar,
radius: 16,
),
trailing: IconButton(
icon: const Icon(Symbols.close),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
visualDensity: VisualDensity.compact,
onPressed: () {
nav.setFocusedRealm(null);
},
),
title: Text(nav.focusedRealm!.name),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {
'alias': nav.focusedRealm!.alias,
},
);
Scaffold.of(context).closeDrawer();
},
),
ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.only(
left: 28,
right: 8,
),
leading: const Icon(Symbols.globe),
title: Text('community').tr(),
onTap: () {
GoRouter.of(context).pushNamed(
'realmCommunity',
pathParameters: {
'alias': nav.focusedRealm!.alias,
},
);
Scaffold.of(context).closeDrawer();
},
),
if (ct.availableChannels
.where((ele) => ele.realmId == nav.focusedRealm?.id)
.isNotEmpty)
const Divider(height: 1),
...(ct.availableChannels
.where((ele) => ele.realmId == nav.focusedRealm?.id)
.map((ele) {
return ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.only(
left: 28,
right: 8,
),
leading: const Icon(Symbols.tag),
title: Text(ele.name),
onTap: () {
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
'scope': ele.realm?.alias ?? 'global',
'alias': ele.alias,
},
);
Scaffold.of(context).closeDrawer();
},
);
}))
],
),
);
}
}

View File

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

View File

@ -66,7 +66,9 @@ class AppScaffold extends StatelessWidget {
return Scaffold( return Scaffold(
extendBody: true, extendBody: true,
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: noBackground
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor,
body: SizedBox.expand( body: SizedBox.expand(
child: noBackground child: noBackground
? content ? content
@ -111,7 +113,6 @@ class AppRootScaffold extends StatelessWidget {
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final isCollapseDrawer = cfg.drawerIsCollapsed; final isCollapseDrawer = cfg.drawerIsCollapsed;
final isExpandedDrawer = cfg.drawerIsExpanded;
final routeName = GoRouter.of(context) final routeName = GoRouter.of(context)
.routerDelegate .routerDelegate
@ -132,19 +133,7 @@ class AppRootScaffold extends StatelessWidget {
? body ? body
: Row( : Row(
children: [ children: [
Container( AppRailNavigation(),
decoration: BoxDecoration(
border: Border(
right: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
),
),
),
child: isExpandedDrawer
? AppNavigationDrawer(elevation: 0)
: AppRailNavigation(),
),
Expanded(child: body), Expanded(child: body),
], ],
); );
@ -232,10 +221,72 @@ class AppRootScaffold extends StatelessWidget {
), ),
], ],
), ),
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null, drawerEdgeDragWidth: isPopable ? 0 : null,
drawer: isCollapseDrawer ? const AppNavigationDrawer() : null,
bottomNavigationBar: bottomNavigationBar:
isShowBottomNavigation ? AppBottomNavigationBar() : null, isShowBottomNavigation ? AppBottomNavigationBar() : null,
); );
} }
} }
class ResponsiveScaffold extends StatelessWidget {
final Widget aside;
final Widget? child;
final int asideFlex;
final int contentFlex;
const ResponsiveScaffold({
super.key,
required this.aside,
required this.child,
this.asideFlex = 1,
this.contentFlex = 2,
});
static bool getIsExpand(BuildContext context) {
return ResponsiveBreakpoints.of(context).largerOrEqualTo(TABLET);
}
@override
Widget build(BuildContext context) {
if (getIsExpand(context)) {
return AppBackground(
isRoot: true,
child: Row(
children: [
Flexible(
flex: asideFlex,
child: aside,
),
VerticalDivider(width: 1),
if (child != null && child != aside)
Flexible(flex: contentFlex, child: child!)
else
Flexible(
flex: contentFlex,
child: ResponsiveScaffoldLanding(child: null),
),
],
),
);
}
return AppBackground(isRoot: true, child: child ?? aside);
}
}
class ResponsiveScaffoldLanding extends StatelessWidget {
final Widget? child;
const ResponsiveScaffoldLanding({super.key, required this.child});
@override
Widget build(BuildContext context) {
if (ResponsiveScaffold.getIsExpand(context) || child == null) {
return AppScaffold(
noBackground: true,
appBar: AppBar(),
body: const SizedBox.shrink(),
);
}
return child!;
}
}

View File

@ -4,7 +4,6 @@ import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
@ -30,19 +29,9 @@ class PostCommentQuickAction extends StatelessWidget {
return Container( return Container(
height: 240, height: 240,
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
margin: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? const EdgeInsets.symmetric(vertical: 8)
: EdgeInsets.zero,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE) borderRadius: BorderRadius.zero,
? const BorderRadius.all(Radius.circular(8)) border: Border.symmetric(
: BorderRadius.zero,
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? Border.all(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
)
: Border.symmetric(
horizontal: BorderSide( horizontal: BorderSide(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio, width: 1 / devicePixelRatio,
@ -103,7 +92,7 @@ class PostCommentSliverListState extends State<PostCommentSliverList> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.client await sn.client
.put('/cgi/co/questions/${widget.parentPost.id}/answer', data: { .put('/cgi/co/questions/${widget.parentPost.id}/answer', data: {
'publisher': answer.publisherId, 'publisher': widget.parentPost.publisherId,
'answer_id': answer.id, 'answer_id': answer.id,
}); });
if (!mounted) return; if (!mounted) return;

View File

@ -1,7 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:animations/animations.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:file_saver/file_saver.dart'; import 'package:file_saver/file_saver.dart';
@ -26,7 +25,6 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/translation.dart'; import 'package:surface/providers/translation.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/post/post_detail.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/types/reaction.dart'; import 'package:surface/types/reaction.dart';
@ -53,6 +51,7 @@ class OpenablePostItem extends StatelessWidget {
final bool showMenu; final bool showMenu;
final bool showFullPost; final bool showFullPost;
final bool showExpandableComments; final bool showExpandableComments;
final bool useReplace;
final double? maxWidth; final double? maxWidth;
final Function(SnPost data)? onChanged; final Function(SnPost data)? onChanged;
final Function()? onDeleted; final Function()? onDeleted;
@ -66,6 +65,7 @@ class OpenablePostItem extends StatelessWidget {
this.showMenu = true, this.showMenu = true,
this.showFullPost = false, this.showFullPost = false,
this.showExpandableComments = false, this.showExpandableComments = false,
this.useReplace = false,
this.maxWidth, this.maxWidth,
this.onChanged, this.onChanged,
this.onDeleted, this.onDeleted,
@ -74,14 +74,10 @@ class OpenablePostItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>();
return Container( return Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity), constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Center( child: Center(
child: OpenContainer( child: GestureDetector(
closedBuilder: (_, __) => Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: PostItem( child: PostItem(
data: data, data: data,
maxWidth: maxWidth, maxWidth: maxWidth,
@ -92,22 +88,18 @@ class OpenablePostItem extends StatelessWidget {
onDeleted: onDeleted, onDeleted: onDeleted,
onSelectAnswer: onSelectAnswer, onSelectAnswer: onSelectAnswer,
), ),
), onTap: () {
openBuilder: (_, close) => PostDetailScreen( if (useReplace) {
slug: data.id.toString(), GoRouter.of(context)
preload: data, .pushReplacementNamed('postDetail', pathParameters: {
onBack: close, 'slug': data.id.toString(),
), });
openColor: Colors.transparent, } else {
openElevation: 0, GoRouter.of(context).pushNamed('postDetail', pathParameters: {
transitionType: ContainerTransitionType.fade, 'slug': data.id.toString(),
closedElevation: 0, });
closedColor: Theme.of(context).colorScheme.surface.withOpacity( }
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0 : 1, },
),
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
), ),
), ),
); );
@ -279,6 +271,8 @@ class _PostItemState extends State<PostItem> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final isAuthor = final isAuthor =
ua.isAuthorized && widget.data.publisher.accountId == ua.user?.id; ua.isAuthorized && widget.data.publisher.accountId == ua.user?.id;
final isParentAuthor = ua.isAuthorized &&
widget.data.replyTo?.publisher.accountId == ua.user?.id;
final displayableAttachments = widget.data.preload?.attachments final displayableAttachments = widget.data.preload?.attachments
?.where((ele) => ?.where((ele) =>
@ -333,6 +327,7 @@ class _PostItemState extends State<PostItem> {
_PostActionPopup( _PostActionPopup(
data: widget.data, data: widget.data,
isAuthor: isAuthor, isAuthor: isAuthor,
isParentAuthor: isParentAuthor,
onShare: () => _doShare(context), onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context), onShareImage: () => _doShareViaPicture(context),
onSelectAnswer: widget.onSelectAnswer, onSelectAnswer: widget.onSelectAnswer,
@ -577,6 +572,7 @@ class _PostItemState extends State<PostItem> {
_PostActionPopup( _PostActionPopup(
data: widget.data, data: widget.data,
isAuthor: isAuthor, isAuthor: isAuthor,
isParentAuthor: isParentAuthor,
onShare: () => _doShare(context), onShare: () => _doShare(context),
onShareImage: () => _doShareViaPicture(context), onShareImage: () => _doShareViaPicture(context),
onSelectAnswer: widget.onSelectAnswer, onSelectAnswer: widget.onSelectAnswer,
@ -1317,6 +1313,7 @@ class _PostAvatar extends StatelessWidget {
class _PostActionPopup extends StatelessWidget { class _PostActionPopup extends StatelessWidget {
final SnPost data; final SnPost data;
final bool isAuthor; final bool isAuthor;
final bool isParentAuthor;
final Function onDeleted; final Function onDeleted;
final Function() onShare, onShareImage; final Function() onShare, onShareImage;
final Function()? onSelectAnswer; final Function()? onSelectAnswer;
@ -1324,6 +1321,7 @@ class _PostActionPopup extends StatelessWidget {
const _PostActionPopup({ const _PostActionPopup({
required this.data, required this.data,
required this.isAuthor, required this.isAuthor,
required this.isParentAuthor,
required this.onDeleted, required this.onDeleted,
required this.onShare, required this.onShare,
required this.onShareImage, required this.onShareImage,
@ -1397,7 +1395,7 @@ class _PostActionPopup extends StatelessWidget {
}, },
), ),
if (onTranslate != null) PopupMenuDivider(), if (onTranslate != null) PopupMenuDivider(),
if (isAuthor && onSelectAnswer != null) if (isParentAuthor && onSelectAnswer != null)
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(
children: [ children: [
@ -1410,7 +1408,7 @@ class _PostActionPopup extends StatelessWidget {
onSelectAnswer?.call(); onSelectAnswer?.call();
}, },
), ),
if (isAuthor && onSelectAnswer != null) PopupMenuDivider(), if (isParentAuthor && onSelectAnswer != null) PopupMenuDivider(),
if (isAuthor) if (isAuthor)
PopupMenuItem( PopupMenuItem(
child: Row( child: Row(

View File

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

View File

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

View File

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

View File

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

View File

@ -45,10 +45,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
sha256: "0c64e928dcbefddecd234205422bcfc2b5e6d31be0b86fef0d0dd48d7b4c9742" sha256: "7dcbd0f87fe5f61cb28da39a1a8b70dbc106e2fe0516f7836eb7bb2948481a12"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.4" version: "4.0.5"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -65,6 +65,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.12.0" version: "2.12.0"
audioplayers:
dependency: "direct main"
description:
name: audioplayers
sha256: a5341380a4f1d3a10a4edde5bb75de5127fe31e0faa8c4d860e64d2f91ad84c7
url: "https://pub.dev"
source: hosted
version: "6.4.0"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
sha256: f8c90823a45b475d2c129f85bbda9c029c8d4450b172f62e066564c6e170f69a
url: "https://pub.dev"
source: hosted
version: "5.2.0"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: "405cdbd53ebdb4623f1c5af69f275dad4f930ce895512d5261c07cd95d23e778"
url: "https://pub.dev"
source: hosted
version: "6.2.0"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: "7e0d081a6a527c53aef9539691258a08ff69a7dc15ef6335fbea1b4b03ebbef0"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
sha256: "77e5fa20fb4a64709158391c75c1cca69a481d35dc879b519e350a05ff520373"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: bd99d8821114747682a2be0adcdb70233d4697af989b549d3a20a0f49f6c9b13
url: "https://pub.dev"
source: hosted
version: "5.1.0"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: "871d3831c25cd2408ddc552600fd4b32fba675943e319a41284704ee038ad563"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
bitsdojo_window: bitsdojo_window:
dependency: "direct main" dependency: "direct main"
description: description:
@ -365,10 +421,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dart_webrtc name: dart_webrtc
sha256: b34e90bc82f33c1023cf98661369c37bccd648c8a4cf882a875d9f5d8bbef694 sha256: "8565f1f1f412b8a6fd862f3a157560811e61eeeac26741c735a5d2ff409a0202"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.2+hotfix.1" version: "1.5.3"
dbus: dbus:
dependency: transitive dependency: transitive
description: description:
@ -746,10 +802,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_expandable_fab name: flutter_expandable_fab
sha256: b14caf78720a48f650e6e1a38d724e33b1f5348d646fa1c266570c31a7f87ef3 sha256: "4d03f54e5384897e32606e9959cef5e7857e5a203e24684f95dfbb5f7fb9b88e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.1"
flutter_highlight: flutter_highlight:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1137,10 +1193,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: "13d3349ace88f12f4a0d175eb5c12dcdd39d35c4c109a8a13dfeb6d0bd9e31c3" sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.3" version: "4.5.4"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1241,10 +1297,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: js name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.7" version: "0.7.2"
json_annotation: json_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1393,18 +1449,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: media_kit name: media_kit
sha256: "1f1deee148533d75129a6f38251ff8388e33ee05fc2d20a6a80e57d6051b7b62" sha256: "48c10c3785df5d88f0eef970743f8c99b2e5da2b34b9d8f9876e598f62d9e776"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.11" version: "1.2.0"
media_kit_libs_android_video: media_kit_libs_android_video:
dependency: transitive dependency: transitive
description: description:
name: media_kit_libs_android_video name: media_kit_libs_android_video
sha256: "9dd8012572e4aff47516e55f2597998f0a378e3d588d0fad0ca1f11a53ae090c" sha256: adff9b571b8ead0867f9f91070f8df39562078c0eb3371d88b9029a2d547d7b7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.6" version: "1.3.7"
media_kit_libs_ios_video: media_kit_libs_ios_video:
dependency: transitive dependency: transitive
description: description:
@ -1417,10 +1473,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: media_kit_libs_linux name: media_kit_libs_linux
sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310 sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.3" version: "1.2.1"
media_kit_libs_macos_video: media_kit_libs_macos_video:
dependency: transitive dependency: transitive
description: description:
@ -1433,34 +1489,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: media_kit_libs_video name: media_kit_libs_video
sha256: "20bb4aefa8fece282b59580e1cd8528117297083a6640c98c2e98cfc96b93288" sha256: "958cc55e7065d9d01f52a2842dab2a0812a92add18489f1006d864fb5e42a3ef"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.6"
media_kit_libs_windows_video: media_kit_libs_windows_video:
dependency: transitive dependency: transitive
description: description:
name: media_kit_libs_windows_video name: media_kit_libs_windows_video
sha256: "32654572167825c42c55466f5d08eee23ea11061c84aa91b09d0e0f69bdd0887" sha256: dff76da2778729ab650229e6b4ec6ec111eb5151431002cbd7ea304ff1f112ab
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.10" version: "1.0.11"
media_kit_native_event_loop:
dependency: transitive
description:
name: media_kit_native_event_loop
sha256: "7d82e3b3e9ded5c35c3146c5ba1da3118d1dd8ac3435bac7f29f458181471b40"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
media_kit_video: media_kit_video:
dependency: "direct main" dependency: "direct main"
description: description:
name: media_kit_video name: media_kit_video
sha256: "2cc3b966679963ba25a4ce5b771e532a521ebde7c6aa20e9802bec95d9916c8f" sha256: a656a9463298c1adc64c57f2d012874f7f2900f0c614d9545a3e7b8bb9e2137b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.5" version: "1.3.0"
menu_base: menu_base:
dependency: transitive dependency: transitive
description: description:
@ -1833,58 +1881,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: safe_local_storage name: safe_local_storage
sha256: ede4eb6cb7d88a116b3d3bf1df70790b9e2038bc37cb19112e381217c74d9440 sha256: e9a21b6fec7a8aa62cc2585ff4c1b127df42f3185adbd2aca66b47abe2e80236
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "2.0.1"
screen_brightness:
dependency: transitive
description:
name: screen_brightness
sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
screen_brightness_android: screen_brightness_android:
dependency: transitive dependency: transitive
description: description:
name: screen_brightness_android name: screen_brightness_android
sha256: "3df10961e3a9e968a5e076fe27e7f4741fa8a1d3950bdeb48cf121ed529d0caf" sha256: "6ba1b5812f66c64e9e4892be2d36ecd34210f4e0da8bdec6a2ea34f1aa42683e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.0+2" version: "2.1.1"
screen_brightness_ios:
dependency: transitive
description:
name: screen_brightness_ios
sha256: "99adc3ca5490b8294284aad5fcc87f061ad685050e03cf45d3d018fe398fd9a2"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
screen_brightness_macos:
dependency: transitive
description:
name: screen_brightness_macos
sha256: "64b34e7e3f4900d7687c8e8fb514246845a73ecec05ab53483ed025bd4a899fd"
url: "https://pub.dev"
source: hosted
version: "0.1.0+1"
screen_brightness_platform_interface: screen_brightness_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: screen_brightness_platform_interface name: screen_brightness_platform_interface
sha256: b211d07f0c96637a15fb06f6168617e18030d5d74ad03795dd8547a52717c171 sha256: "737bd47b57746bc4291cab1b8a5843ee881af499514881b0247ec77447ee769c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.0" version: "2.1.0"
screen_brightness_windows:
dependency: transitive
description:
name: screen_brightness_windows
sha256: "9261bf33d0fc2707d8cf16339ce25768100a65e70af0fcabaf032fc12408ba86"
url: "https://pub.dev"
source: hosted
version: "0.1.3"
screenshot: screenshot:
dependency: "direct main" dependency: "direct main"
description: description:
@ -2238,10 +2254,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: tray_manager name: tray_manager
sha256: "80be6c508159a6f3c57983de795209ac13453e9832fd574143b06dceee188ed2" sha256: c2da0f0f1ddb455e721cf68d05d1281fec75cf5df0a1d3cb67b6ca0bdfd5709d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.2" version: "0.4.0"
tuple: tuple:
dependency: transitive dependency: transitive
description: description:
@ -2294,10 +2310,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: uri_parser name: uri_parser
sha256: "6543c9fd86d2862fac55d800a43e67c0dcd1a41677cb69c2f8edfe73bbcf1835" sha256: ff4d2c720aca3f4f7d5445e23b11b2d15ef8af5ddce5164643f38ff962dcb270
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" version: "3.0.0"
url_launcher: url_launcher:
dependency: "direct main" dependency: "direct main"
description: description:
@ -2438,10 +2454,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: volume_controller name: volume_controller
sha256: c71d4c62631305df63b72da79089e078af2659649301807fa746088f365cb48e sha256: "4c2a873c242da6ce69ae1d17c256c5626e0c481be1824d6c5fc95e68c31f3b36"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.8" version: "3.3.2"
wakelock_plus: wakelock_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -2494,10 +2510,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: webrtc_interface name: webrtc_interface
sha256: e05f00091c9c70a15bab4ccb1b6c46d9a16a6075002f02cfac3641eccb05e25d sha256: e92afec11152a9ccb5c9f35482754edd99696e886ab6acaf90c06dd2d09f09eb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1+hotfix.1" version: "1.2.2+hotfix.1"
win32: win32:
dependency: transitive dependency: transitive
description: description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 2.4.2+83 version: 2.4.2+84
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@ -59,7 +59,7 @@ dependencies:
relative_time: ^5.0.0 relative_time: ^5.0.0
image_picker: ^1.1.2 image_picker: ^1.1.2
cross_file: ^0.3.4+2 cross_file: ^0.3.4+2
file_picker: ^9.0.0 # pinned due to compile failed on android, https://github.com/miguelpruivo/flutter_file_picker/issues/1643 file_picker: ^9.2.1
croppy: ^1.3.1 croppy: ^1.3.1
flutter_expandable_fab: ^2.3.0 flutter_expandable_fab: ^2.3.0
dropdown_button2: ^2.3.9 dropdown_button2: ^2.3.9
@ -103,7 +103,7 @@ dependencies:
flutter_svg: ^2.0.16 flutter_svg: ^2.0.16
home_widget: ^0.7.0 home_widget: ^0.7.0
receive_sharing_intent: ^1.8.1 receive_sharing_intent: ^1.8.1
workmanager: workmanager: # use git due to: https://github.com/fluttercommunity/flutter_workmanager/issues/588#issuecomment-2660871645
git: git:
url: https://github.com/fluttercommunity/flutter_workmanager.git url: https://github.com/fluttercommunity/flutter_workmanager.git
path: workmanager path: workmanager
@ -120,7 +120,7 @@ dependencies:
flutter_inappwebview: ^6.1.5 flutter_inappwebview: ^6.1.5
html: ^0.15.5 html: ^0.15.5
xml: ^6.5.0 xml: ^6.5.0
tray_manager: ^0.3.2 tray_manager: ^0.4.0
hotkey_manager: ^0.2.3 hotkey_manager: ^0.2.3
image_picker_android: ^0.8.12+20 image_picker_android: ^0.8.12+20
cached_network_image_platform_interface: ^4.1.1 cached_network_image_platform_interface: ^4.1.1
@ -143,6 +143,7 @@ dependencies:
timelines_plus: ^1.0.6 timelines_plus: ^1.0.6
latlong2: ^0.9.1 latlong2: ^0.9.1
crypto: ^3.0.6 crypto: ^3.0.6
audioplayers: ^6.4.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -179,6 +180,9 @@ flutter:
- assets/icon/icon-light-radius.png - assets/icon/icon-light-radius.png
- assets/icon/tray-icon.ico - assets/icon/tray-icon.ico
- assets/icon/tray-icon.png - assets/icon/tray-icon.png
- assets/icon/kanban-1st.jpg
- assets/audio/sfx/
- assets/audio/notify/
- assets/translations/ - assets/translations/
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see

2
web/_redirects Normal file
View File

@ -0,0 +1,2 @@
/assets/assets/translations/en.json /assets/assets/translations/en-US.json 301
/assets/assets/translations/zh.json /assets/assets/translations/zh-CN.json 301

View File

@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <audioplayers_windows/audioplayers_windows_plugin.h>
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h> #include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h> #include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <fast_rsa/fast_rsa_plugin.h> #include <fast_rsa/fast_rsa_plugin.h>
@ -24,13 +25,15 @@
#include <media_kit_video/media_kit_video_plugin_c_api.h> #include <media_kit_video/media_kit_video_plugin_c_api.h>
#include <pasteboard/pasteboard_plugin.h> #include <pasteboard/pasteboard_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_brightness_windows/screen_brightness_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h> #include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h> #include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <tray_manager/tray_manager_plugin.h> #include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
#include <volume_controller/volume_controller_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
AudioplayersWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
BitsdojoWindowPluginRegisterWithRegistrar( BitsdojoWindowPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar( ConnectivityPlusWindowsPluginRegisterWithRegistrar(
@ -67,8 +70,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("PasteboardPlugin")); registry->GetRegistrarForPlugin("PasteboardPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
ScreenBrightnessWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar( SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar( Sqlite3FlutterLibsPluginRegisterWithRegistrar(
@ -77,4 +78,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("TrayManagerPlugin")); registry->GetRegistrarForPlugin("TrayManagerPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
VolumeControllerPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows
bitsdojo_window_windows bitsdojo_window_windows
connectivity_plus connectivity_plus
fast_rsa fast_rsa
@ -21,16 +22,15 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_video media_kit_video
pasteboard pasteboard
permission_handler_windows permission_handler_windows
screen_brightness_windows
share_plus share_plus
sqlite3_flutter_libs sqlite3_flutter_libs
tray_manager tray_manager
url_launcher_windows url_launcher_windows
volume_controller
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
croppy croppy
media_kit_native_event_loop
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)