Compare commits

..

55 Commits

Author SHA1 Message Date
69d5e95565 🚀 Launch 2.4.2+86 March Update 2025-03-31 23:05:36 +08:00
3e3442fc89 💄 Enable new sfx on special days 2025-03-31 23:00:13 +08:00
8181010b0b 💄 New desktop loading animation 2025-03-31 22:50:08 +08:00
269caf7555 💄 Some improvements
🐛 Bug fixes
 The heart reaction
2025-03-31 01:27:45 +08:00
ae0809ad35 💄 Optimize background color 2025-03-31 00:51:37 +08:00
4005f03cf8 🐛 Fix notification 2025-03-30 23:25:38 +08:00
4bd8ec54f1 Optimize initialization 2025-03-30 20:43:47 +08:00
51a387851f 🐛 Fix infinite starting up 2025-03-30 20:37:04 +08:00
8ed847d870 ♻️ Use API Version 2 to load post 2025-03-30 15:31:02 +08:00
dfe13de220 Program Badges 2025-03-29 17:00:17 +08:00
b02a54c1e9 🐛 Fix sound mode 2025-03-29 16:41:23 +08:00
55a7e7d900 🐛 Fix app drawer did not close after selected 2025-03-29 01:04:37 +08:00
3585941ccb 🐛 Optimize noise cancellation 2025-03-29 01:03:11 +08:00
7c6f2cc4ab ♻️ Refactored call view 2025-03-29 00:58:13 +08:00
61dbf92909 🌐 Merge pull request #19 from Texas0295/master
Add AppImage build tools & Update workflow
2025-03-28 18:37:55 +08:00
b69e4002e0 Add AppImage build tools & Update workflow 2025-03-28 02:01:45 +08:00
49aa24b79d Merge branch 'Solsynth:master' into master 2025-03-28 01:59:19 +08:00
ceb5c53229 🚀 Launch 2.4.2+85 2025-03-28 01:01:34 +08:00
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
ddd0a4c3d3 remove cache=true in build-linux 2025-03-28 00:41:58 +08:00
99e07de243 upload appimage file 2025-03-28 00:04:44 +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
10bf0883e5 add appimage build 2025-03-27 23:11:15 +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
101 changed files with 5243 additions and 2149 deletions

View File

@ -48,14 +48,15 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: stable channel: stable
cache: true
- 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
@ -63,3 +64,18 @@ jobs:
with: with:
name: build-output-linux name: build-output-linux
path: build/linux/x64/release/bundle path: build/linux/x64/release/bundle
- name: Build AppImage
run: |
rm -r Solian.AppDir | true
mkdir Solian.AppDir
cp -r build/linux/x64/release/bundle/* Solian.AppDir
cp -r buildtools/appimage_config/* Solian.AppDir
cp assets/icon/icon-light-radius.png Solian.AppDir
sudo chmod +x buildtools/appimagetool-x86_64.AppImage
sudo chmod +x Solian.AppDir/AppRun
./buildtools/appimagetool-x86_64.AppImage Solian.AppDir
- name: Archive production artifacts
uses: actions/upload-artifact@v4
with:
name: build-output-linux-appimage
path: './*.AppImage*'

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,62 @@
}, },
"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": "Back",
"badgeProgramDeveloper": "Developer Program Member",
"badgeProgramStellar": "A Stellar",
"badgeProgramModerator": "Community Moderator",
"postEditedHint": "edited",
"splashScreenServer": "Server",
"splashScreenServerName": "Potato",
"splashScreenCaption": "Trying to establishing connection with HyperNet™"
} }

View File

@ -888,5 +888,62 @@
}, },
"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": "返回",
"badgeProgramDeveloper": "开发者计划成员",
"badgeProgramStellar": "一颗恒星",
"badgeProgramModerator": "社区管理员",
"postEditedHint": "已编辑",
"splashScreenServer": "服务器",
"splashScreenServerName": "土豆",
"splashScreenCaption": "正在尝试与 HyperNet™ 取得太阳链连接"
} }

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

@ -0,0 +1,4 @@
#!/bin/sh
cd "$(dirname "$0")"
exec ./surface

View File

@ -0,0 +1,8 @@
[Desktop Entry]
Version=1.0
Type=Application
Terminal=false
Name=Solian
Exec=surface %u
Icon=icon-light-radius
Categories=Network;

Binary file not shown.

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):
@ -187,9 +189,12 @@ PODS:
- Flutter - Flutter
- flutter_webrtc - flutter_webrtc
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
- media_kit_libs_ios_video (1.0.4): - livekit_noise_filter (0.0.1):
- Flutter - Flutter
- media_kit_native_event_loop (1.0.0): - flutter_webrtc
- LiveKitKrispNoiseFilter (= 0.0.7)
- LiveKitKrispNoiseFilter (0.0.7)
- media_kit_libs_ios_video (1.0.4):
- Flutter - Flutter
- media_kit_video (0.0.1): - media_kit_video (0.0.1):
- Flutter - Flutter
@ -212,8 +217,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 +265,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`)
@ -284,15 +288,14 @@ DEPENDENCIES:
- in_app_review (from `.symlinks/plugins/in_app_review/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- Kingfisher (~> 8.0) - Kingfisher (~> 8.0)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`) - livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- livekit_noise_filter (from `.symlinks/plugins/livekit_noise_filter/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - media_kit_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`)
@ -318,6 +321,7 @@ SPEC REPOS:
- GoogleDataTransport - GoogleDataTransport
- GoogleUtilities - GoogleUtilities
- Kingfisher - Kingfisher
- LiveKitKrispNoiseFilter
- nanopb - nanopb
- OrderedSet - OrderedSet
- PromisesObjC - PromisesObjC
@ -328,6 +332,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:
@ -370,10 +376,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/in_app_review/ios" :path: ".symlinks/plugins/in_app_review/ios"
livekit_client: livekit_client:
:path: ".symlinks/plugins/livekit_client/ios" :path: ".symlinks/plugins/livekit_client/ios"
livekit_noise_filter:
:path: ".symlinks/plugins/livekit_noise_filter/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 +392,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 +413,66 @@ 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 livekit_noise_filter: a26aeb1c1eae6db0a023fd2f6ea3ff108c3ecbb0
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a LiveKitKrispNoiseFilter: efe418ceca28163ace0ff222bd2cc02384645d84
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
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

@ -241,7 +241,7 @@ class PostWriteController extends ChangeNotifier {
contentController.text = post.body['content'] ?? ''; contentController.text = post.body['content'] ?? '';
aliasController.text = post.alias ?? ''; aliasController.text = post.alias ?? '';
rewardController.text = post.body['reward']?.toString() ?? ''; rewardController.text = post.body['reward']?.toString() ?? '';
videoAttachment = post.preload?.video; videoAttachment = SnAttachment.fromJson(post.body['video']);
publishedAt = post.publishedAt; publishedAt = post.publishedAt;
publishedUntil = post.publishedUntil; publishedUntil = post.publishedUntil;
visibleUsers = List.from(post.visibleUsersList ?? [], growable: true); visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
@ -252,17 +252,21 @@ class PostWriteController extends ChangeNotifier {
categories = categories =
List.from(post.categories.map((ele) => ele.alias), growable: true); List.from(post.categories.map((ele) => ele.alias), growable: true);
attachments.addAll( attachments.addAll(
post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []); post.body['attachments']
poll = post.preload?.poll; .where(SnAttachment.fromJson)
?.map(PostWriteMedia) ??
[],
);
poll = post.poll;
editingDraft = post.isDraft; editingDraft = post.isDraft;
if (post.preload?.thumbnail != null && if (post.body['thumbnail'] != null) {
(post.preload?.thumbnail?.rid.isNotEmpty ?? false)) { thumbnail =
thumbnail = PostWriteMedia(post.preload!.thumbnail); PostWriteMedia(SnAttachment.fromJson(post.body['thumbnail']));
} }
if (post.preload?.realm != null) { if (post.realm != null) {
realm = post.preload!.realm!; realm = post.realm!;
} }
editingPost = post; editingPost = post;

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';
@ -15,6 +15,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -49,6 +50,7 @@ import 'package:surface/router.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/menu_bar.dart'; import 'package:surface/widgets/menu_bar.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/version_label.dart'; import 'package:surface/widgets/version_label.dart';
import 'package:tray_manager/tray_manager.dart'; import 'package:tray_manager/tray_manager.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
@ -57,6 +59,7 @@ import 'package:in_app_review/in_app_review.dart';
import 'package:image_picker_android/image_picker_android.dart'; import 'package:image_picker_android/image_picker_android.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:local_notifier/local_notifier.dart'; import 'package:local_notifier/local_notifier.dart';
import 'package:flutter_animate/flutter_animate.dart';
@pragma('vm:entry-point') @pragma('vm:entry-point')
void appBackgroundDispatcher() { void appBackgroundDispatcher() {
@ -75,13 +78,40 @@ void appBackgroundDispatcher() {
}); });
} }
// Desktop size tools
Future<Size> _getSavedWindowSize() async {
final prefs = await SharedPreferences.getInstance();
String? sizeString = prefs.getString(kAppWindowSize);
if (sizeString != null) {
List<String> parts = sizeString.split('x');
if (parts.length == 2) {
double? width = double.tryParse(parts[0]);
double? height = double.tryParse(parts[1]);
if (width != null && height != null) {
return Size(width, height);
}
}
}
return const Size(1280, 720); // Default size
}
Future<void> _saveWindowSize() async {
final prefs = await SharedPreferences.getInstance();
final size = appWindow.size;
await prefs.setString(kAppWindowSize, '${size.width}x${size.height}');
}
void main() async { 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 +121,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 +164,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 +188,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)),
@ -233,6 +260,7 @@ class _AppSplashScreen extends StatefulWidget {
class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener { class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
bool _isBusy = false; bool _isBusy = false;
double _initPercentage = 0;
String _phaseText = 'appInitStarting'; String _phaseText = 'appInitStarting';
void _tryRequestRating() async { void _tryRequestRating() async {
@ -264,11 +292,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 +309,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) {
@ -311,28 +335,36 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
// The Network initialization must be done after the HomeWidget initialization // The Network initialization must be done after the HomeWidget initialization
// The Network initialization will save the server url to the HomeWidget // The Network initialization will save the server url to the HomeWidget
// The Network initialization will also save initialize the Config, so it not need to be initialized again // The Network initialization will also save initialize the Config, so it not need to be initialized again
_initPercentage = 0.1;
_setPhaseText('network'); _setPhaseText('network');
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.initializeUserAgent(); await sn.initializeUserAgent();
await sn.setConfigWithNative(); await sn.setConfigWithNative();
if (!mounted) return; if (!mounted) return;
_initPercentage = 0.2;
_setPhaseText('userdata'); _setPhaseText('userdata');
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
await ua.initialize(); await ua.initialize();
if (!mounted) return; if (!mounted) return;
_initPercentage = 0.3;
_setPhaseText('websocket'); _setPhaseText('websocket');
final ws = context.read<WebSocketProvider>(); final ws = context.read<WebSocketProvider>();
await ws.tryConnect(); await ws.tryConnect();
try {
if (!mounted) return;
_initPercentage = 0.9;
_setPhaseText('keyPair');
final kp = context.read<KeyPairProvider>();
kp.reloadActive();
kp.listen();
} catch (_) {}
if (ua.isAuthorized) {
if (!mounted) return; if (!mounted) return;
_setPhaseText('notification'); _setPhaseText('notification');
final notify = context.read<NotificationProvider>(); final notify = context.read<NotificationProvider>();
notify.listen(); notify.listen();
await notify.registerPushNotifications(); try {
if (!mounted) return; notify.registerPushNotifications();
_setPhaseText('keyPair');
final kp = context.read<KeyPairProvider>();
await kp.reloadActive();
kp.listen();
if (!mounted) return; if (!mounted) return;
_setPhaseText('stickers'); _setPhaseText('stickers');
final sticker = context.read<SnStickerProvider>(); final sticker = context.read<SnStickerProvider>();
@ -349,7 +381,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
_setPhaseText('chat'); _setPhaseText('chat');
final ct = context.read<ChatChannelProvider>(); final ct = context.read<ChatChannelProvider>();
await ct.refreshAvailableChannels(); await ct.refreshAvailableChannels();
_initPercentage = 1;
_setPhaseText('done'); _setPhaseText('done');
} catch (_) {}
_playIntro();
}
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
await context.showErrorDialog(err); await context.showErrorDialog(err);
@ -365,28 +401,42 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
// The quit key has been removed, and the logic of the quit key is moved to system menu bar activator. // The quit key has been removed, and the logic of the quit key is moved to system menu bar activator.
} }
void _playIntro() async {
final cfg = context.read<ConfigProvider>();
if (!cfg.soundEffects) return;
final date = DateTime.now();
final player = AudioPlayer(playerId: 'launch-done-player');
await player.play(
(cfg.aprilFoolFeatures && date.month == 4 && date.day == 1)
? AssetSource('audio/sfx/launch-intro.mp3')
: AssetSource('audio/sfx/launch-done.mp3'),
volume: 0.8,
ctx: AudioContext(
android: AudioContextAndroid(
contentType: AndroidContentType.sonification,
usageType: AndroidUsageType.notificationEvent,
),
iOS: AudioContextIOS(category: AVAudioSessionCategory.ambient),
),
mode: PlayerMode.lowLatency,
);
player.onPlayerComplete.listen((_) {
player.dispose();
});
}
final Menu _appTrayMenu = Menu( 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 +464,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,11 +475,11 @@ 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);
);
} }
try {
_trayInitialization(); _trayInitialization();
_hotkeyInitialization(); _hotkeyInitialization();
_notifyInitialization(); _notifyInitialization();
@ -440,7 +488,13 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
_tryRequestRating(); _tryRequestRating();
_checkForUpdate(); _checkForUpdate();
setState(() => _isBusy = false); setState(() => _isBusy = false);
}).catchError((err) {
logging.error('[Bootstrap] Unable to initialize app', err);
setState(() => _isBusy = false);
}); });
} catch (err) {
logging.error('[Bootstrap] Unable to initialize (pre-stage) app', err);
}
} }
Future<AppExitResponse> _onExitRequested() async { Future<AppExitResponse> _onExitRequested() async {
@ -449,6 +503,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();
@ -530,41 +585,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
}); });
return SizeChangedLayoutNotifier( return SizeChangedLayoutNotifier(
child: _isBusy child: _isBusy
? Material( ? _AppLoadingScreen(
key: Key('app-splash-screen-$_isBusy'), isBusy: _isBusy,
child: Stack( initPercentage: _initPercentage,
children: [ phaseText: _phaseText,
CustomPaint(painter: GraphPainter()),
Center(
child: Container(
constraints: const BoxConstraints(
maxWidth: 240,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/icon/icon.png',
width: 64,
height: 64,
color:
Theme.of(context).colorScheme.onSurface,
),
Text('Solar Network').bold(),
AppVersionLabel(),
Gap(8),
Text(
_phaseText,
textAlign: TextAlign.center,
),
Gap(16),
const LinearProgressIndicator(),
],
),
),
),
],
),
) )
: widget.child, : widget.child,
); );
@ -575,43 +599,233 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
} }
} }
class GraphPainter extends CustomPainter { class _AppLoadingScreen extends StatelessWidget {
final Random random = Random(); const _AppLoadingScreen({
final int numNodes = 20; required this.isBusy,
final double maxDistance = 100; // Max distance to draw a line required this.initPercentage,
required this.phaseText,
});
final bool isBusy;
final double initPercentage;
final String phaseText;
@override @override
void paint(Canvas canvas, Size size) { Widget build(BuildContext context) {
final paintNode = Paint()..color = Colors.white; if (ResponsiveScaffold.getIsExpand(context)) {
final paintEdge = Paint() return Material(
..color = Colors.white.withOpacity(0.3) key: Key('app-splash-screen-$isBusy'),
..strokeWidth = 1; child: Stack(
children: [
// Generate random points Container(
List<Offset> nodes = List.generate( decoration: BoxDecoration(
numNodes, image: DecorationImage(
(_) => Offset( image: AssetImage('assets/icon/kanban-1st.jpg'),
random.nextDouble() * size.width, fit: BoxFit.cover,
random.nextDouble() * size.height, opacity: 0.1,
),
color: Theme.of(context).colorScheme.surface,
backgroundBlendMode: BlendMode.darken,
),
),
Center(
child: Row(
children: [
Expanded(
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: initPercentage),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${(value * 100).toStringAsFixed(0)}%')
.padding(left: 32, bottom: 4),
LinearProgressIndicator(
value: value,
borderRadius: const BorderRadius.all(
Radius.circular(0),
),
stopIndicatorColor: Colors.transparent,
backgroundColor: Colors.transparent,
),
const Gap(24),
],
),
),
),
Expanded(
child: TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: initPercentage),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('${(value * 100).toStringAsFixed(0)}%')
.padding(right: 32, bottom: 4),
Transform.flip(
flipX: true,
child: LinearProgressIndicator(
value: value,
borderRadius: const BorderRadius.all(
Radius.circular(0),
),
stopIndicatorColor: Colors.transparent,
backgroundColor: Colors.transparent,
),
),
const Gap(24),
],
),
),
),
],
),
),
Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 240, minWidth: 160),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 24),
decoration: BoxDecoration(
color:
Theme.of(context).colorScheme.surface.withOpacity(0.85),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 3,
),
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'splashScreenServer',
style: GoogleFonts.notoSerifHk(height: 1, fontSize: 11),
textAlign: TextAlign.center,
).tr().opacity(0.85),
Text(
'splashScreenServerName',
style: GoogleFonts.notoSerifHk(
fontSize: 24,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
).tr().opacity(0.85),
Text.rich(
TextSpan(
text: '#',
style: GoogleFonts.notoSerifHk(),
children: [
TextSpan(
text: '0',
style: GoogleFonts.notoSerifHk(
fontSize: 80,
fontWeight: FontWeight.bold,
),
),
],
),
textAlign: TextAlign.center,
).padding(vertical: 16),
],
),
),
),
Positioned(
left: 0,
right: 0,
bottom: MediaQuery.of(context).size.height * 0.2,
child: Column(
children: [
Text(
phaseText,
textAlign: TextAlign.center,
),
AnimateWidgetExtensions(Text(
'splashScreenCaption',
textAlign: TextAlign.center,
).tr())
.animate(onPlay: (e) => e.repeat())
.fadeIn(duration: 500.ms, curve: Curves.easeOut)
.then()
.fadeOut(
duration: 500.ms,
delay: 1000.ms,
curve: Curves.easeIn,
),
],
),
),
Positioned(
bottom: 8,
left: 16,
right: 16,
child: Row(
children: [
Image.asset(
'assets/icon/icon.png',
width: 40,
height: 40,
color: Theme.of(context).colorScheme.onSurface,
).padding(all: 4),
const Gap(4),
Text('Solar Network').bold(),
Expanded(child: const SizedBox()),
AppVersionLabel(),
const Gap(12),
],
),
),
],
), ),
); );
// 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 return Material(
for (var node in nodes) { key: Key('app-splash-screen-$isBusy'),
canvas.drawCircle(node, 4, paintNode); child: Stack(
children: [
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/icon/kanban-1st.jpg'),
fit: BoxFit.cover,
opacity: 0.1,
),
color: Theme.of(context).colorScheme.surface,
backgroundBlendMode: BlendMode.darken,
),
),
Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 240),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/icon/icon.png',
width: 64,
height: 64,
color: Theme.of(context).colorScheme.onSurface,
),
Text('Solar Network').bold(),
AppVersionLabel(),
Gap(8),
Text(phaseText, textAlign: TextAlign.center),
Gap(16),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: initPercentage),
duration: Duration(milliseconds: 300),
builder: (context, value, _) =>
LinearProgressIndicator(value: value),
),
],
),
),
),
],
),
);
} }
} }
@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

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
import 'package:livekit_noise_filter/livekit_noise_filter.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
@ -131,10 +132,14 @@ class ChatCallProvider extends ChangeNotifier {
void initRoom() { void initRoom() {
initHardware(); initHardware();
final timeout = const Duration(seconds: 60);
_room = Room( _room = Room(
roomOptions: const RoomOptions( roomOptions: RoomOptions(
dynacast: true, dynacast: true,
adaptiveStream: true, adaptiveStream: true,
defaultAudioCaptureOptions: AudioCaptureOptions(
processor: LiveKitNoiseFilter(),
),
defaultAudioPublishOptions: AudioPublishOptions( defaultAudioPublishOptions: AudioPublishOptions(
name: 'call_voice', name: 'call_voice',
stream: 'call_stream', stream: 'call_stream',
@ -154,6 +159,16 @@ class ChatCallProvider extends ChangeNotifier {
params: VideoParametersPresets.h1080_169, params: VideoParametersPresets.h1080_169,
), ),
), ),
connectOptions: ConnectOptions(
autoSubscribe: true,
timeouts: Timeouts(
connection: timeout,
debounce: timeout,
publish: timeout,
peerConnection: timeout,
iceRestart: timeout,
),
),
); );
_listener = _room.createListener(); _listener = _room.createListener();
WakelockPlus.enable(); WakelockPlus.enable();

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,25 @@ 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,
ctx: AudioContext(
android: AudioContextAndroid(
contentType: AndroidContentType.sonification,
usageType: AndroidUsageType.notificationEvent,
),
iOS: AudioContextIOS(category: AVAudioSessionCategory.ambient),
),
mode: PlayerMode.lowLatency,
);
}
}
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

@ -1,144 +1,31 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_realm.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/poll.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/types/realm.dart';
class SnPostContentProvider { class SnPostContentProvider {
late final SnNetworkProvider _sn; late final SnNetworkProvider _sn;
late final UserDirectoryProvider _ud;
late final SnAttachmentProvider _attach;
late final SnRealmProvider _realm;
SnPostContentProvider(BuildContext context) { SnPostContentProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>(); _sn = context.read<SnNetworkProvider>();
_ud = context.read<UserDirectoryProvider>();
_attach = context.read<SnAttachmentProvider>();
_realm = context.read<SnRealmProvider>();
}
Future<SnPoll> _fetchPoll(int id) async {
final resp = await _sn.client.get('/cgi/co/polls/$id');
return SnPoll.fromJson(resp.data);
} }
Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async { Future<List<SnPost>> _preloadRelatedDataInBatch(List<SnPost> out) async {
Set<String> rids = {};
Set<int> uids = {};
for (var i = 0; i < out.length; i++) {
rids.addAll(out[i].body['attachments']?.cast<String>() ?? []);
if (out[i].body['thumbnail'] != null) {
rids.add(out[i].body['thumbnail']);
}
if (out[i].body['video'] != null) {
rids.add(out[i].body['video']);
}
if (out[i].repostTo != null) {
out[i] = out[i].copyWith(
repostTo: await _preloadRelatedDataSingle(out[i].repostTo!),
);
}
if (out[i].publisher.type == 0) {
uids.add(out[i].publisher.accountId);
}
}
final attachments = await _attach.getMultiple(rids.toList());
for (var i = 0; i < out.length; i++) {
SnPoll? poll;
SnRealm? realm;
if (out[i].pollId != null) {
poll = await _fetchPoll(out[i].pollId!);
}
if (out[i].realmId != null) {
realm = await _realm.getRealm(out[i].realmId!);
}
out[i] = out[i].copyWith(
preload: SnPostPreload(
thumbnail: attachments
.where((ele) => ele?.rid == out[i].body['thumbnail'])
.firstOrNull,
attachments: attachments
.where((ele) =>
out[i].body['attachments']?.contains(ele?.rid) ?? false)
.toList(),
video: attachments
.where((ele) => ele?.rid == out[i].body['video'])
.firstOrNull,
poll: poll,
realm: realm,
),
);
}
uids.addAll(
attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
await _ud.listAccount(uids);
return out; return out;
} }
Future<SnPost> _preloadRelatedDataSingle(SnPost out) async { Future<SnPost> _preloadRelatedDataSingle(SnPost out) async {
Set<String> rids = {};
Set<int> uids = {};
rids.addAll(out.body['attachments']?.cast<String>() ?? []);
if (out.body['thumbnail'] != null) {
rids.add(out.body['thumbnail']);
}
if (out.body['video'] != null) {
rids.add(out.body['video']);
}
if (out.repostTo != null) {
out = out.copyWith(
repostTo: await _preloadRelatedDataSingle(out.repostTo!),
);
}
if (out.publisher.type == 0) {
uids.add(out.publisher.accountId);
}
final attachments = await _attach.getMultiple(rids.toList());
SnPoll? poll;
SnRealm? realm;
if (out.pollId != null) {
poll = await _fetchPoll(out.pollId!);
}
if (out.realmId != null) {
realm = await _realm.getRealm(out.realmId!);
}
out = out.copyWith(
preload: SnPostPreload(
thumbnail: attachments
.where((ele) => ele?.rid == out.body['thumbnail'])
.firstOrNull,
attachments: attachments
.where(
(ele) => out.body['attachments']?.contains(ele?.rid) ?? false)
.toList(),
video: attachments
.where((ele) => ele?.rid == out.body['video'])
.firstOrNull,
poll: poll,
realm: realm,
),
);
uids.addAll(
attachments.where((ele) => ele != null).map((ele) => ele!.accountId));
await _ud.listAccount(uids);
return out; return out;
} }
Future<List<SnPost>> listRecommendations() async { Future<List<SnPost>> listRecommendations() async {
final resp = await _sn.client.get('/cgi/co/recommendations'); final resp = await _sn.client.get(
'/cgi/co/recommendations',
options: Options(headers: {
'X-API-Version': '2',
}),
);
final out = _preloadRelatedDataInBatch( final out = _preloadRelatedDataInBatch(
List.from(resp.data.map((ele) => SnPost.fromJson(ele))), List.from(resp.data.map((ele) => SnPost.fromJson(ele))),
); );
@ -202,6 +89,9 @@ class SnPostContentProvider {
if (realm != null) 'realm': realm, if (realm != null) 'realm': realm,
if (channel != null) 'channel': channel, if (channel != null) 'channel': channel,
}, },
options: Options(headers: {
'X-API-Version': '2',
}),
); );
final List<SnPost> out = await _preloadRelatedDataInBatch( final List<SnPost> out = await _preloadRelatedDataInBatch(
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
@ -215,11 +105,16 @@ class SnPostContentProvider {
int take = 10, int take = 10,
int offset = 0, int offset = 0,
}) async { }) async {
final resp = await _sn.client final resp = await _sn.client.get(
.get('/cgi/co/posts/$parentId/replies', queryParameters: { '/cgi/co/posts/$parentId/replies',
queryParameters: {
'take': take, 'take': take,
'offset': offset, 'offset': offset,
}); },
options: Options(headers: {
'X-API-Version': '2',
}),
);
final List<SnPost> out = await _preloadRelatedDataInBatch( final List<SnPost> out = await _preloadRelatedDataInBatch(
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
); );
@ -234,13 +129,20 @@ class SnPostContentProvider {
Iterable<String>? tags, Iterable<String>? tags,
Iterable<String>? categories, Iterable<String>? categories,
}) async { }) async {
final resp = await _sn.client.get('/cgi/co/posts/search', queryParameters: { final resp = await _sn.client.get(
'/cgi/co/posts/search',
queryParameters: {
'take': take, 'take': take,
'offset': offset, 'offset': offset,
'probe': searchTerm, 'probe': searchTerm,
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','), if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','), if (categories?.isNotEmpty ?? false)
}); 'categories': categories!.join(','),
},
options: Options(headers: {
'X-API-Version': '2',
}),
);
final List<SnPost> out = await _preloadRelatedDataInBatch( final List<SnPost> out = await _preloadRelatedDataInBatch(
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []), List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
); );
@ -249,7 +151,12 @@ class SnPostContentProvider {
} }
Future<SnPost> getPost(dynamic id) async { Future<SnPost> getPost(dynamic id) async {
final resp = await _sn.client.get('/cgi/co/posts/$id'); final resp = await _sn.client.get(
'/cgi/co/posts/$id',
options: Options(headers: {
'X-API-Version': '2',
}),
);
final out = _preloadRelatedDataSingle( final out = _preloadRelatedDataSingle(
SnPost.fromJson(resp.data), SnPost.fromJson(resp.data),
); );

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

@ -1,9 +1,9 @@
import 'package:animations/animations.dart';
import 'package:flutter/material.dart'; 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 +13,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';
@ -52,16 +53,6 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/about.dart'; import 'package:surface/widgets/about.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
Widget _fadeThroughTransition(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: child,
);
}
final _appRoutes = [ final _appRoutes = [
GoRoute( GoRoute(
path: '/', path: '/',
@ -70,8 +61,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 +100,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 +230,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,13 +289,12 @@ final _appRoutes = [
), ),
], ],
), ),
],
),
GoRoute( GoRoute(
path: '/realm', path: '/realm',
name: 'realm', name: 'realm',
pageBuilder: (context, state) => CustomTransitionPage( builder: (context, state) => const RealmScreen(),
transitionsBuilder: _fadeThroughTransition,
child: const RealmScreen(),
),
routes: [ routes: [
GoRoute( GoRoute(
path: '/:alias/community', path: '/:alias/community',

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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAccountProfileEdit').tr()),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -251,12 +263,15 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 7,
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';
@ -60,6 +61,21 @@ final Map<String, (String, IconData, Color)> kBadgesMeta = {
Symbols.thumb_up, Symbols.thumb_up,
Colors.lightGreen, Colors.lightGreen,
), ),
'programs.developers': (
'badgeProgramDeveloper',
Symbols.code,
Colors.blue,
),
'programs.stellar': (
'badgeProgramStellar',
Symbols.family_star,
Colors.orange,
),
'programs.moderator': (
'badgeProgramModerator',
Symbols.sword_rose,
Colors.blue,
),
}; };
class UserScreen extends StatefulWidget { class UserScreen extends StatefulWidget {
@ -227,7 +243,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 +505,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: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar(
title: Text('accountProgram').tr(),
),
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
itemCount: _programs.length,
itemBuilder: (context, idx) {
final ele = _programs[idx];
return Card(
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(8)),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => _ProgramJoinPopup(
program: ele,
isJoined:
_programMembers.any((e) => e.programId == ele.id),
),
).then((value) {
if (value == true) {
_fetchProgramMembers();
}
});
},
child: Column(
children: [
if (ele.appearance['banner'] != null)
AspectRatio(
aspectRatio: 16 / 5,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context)
.colorScheme
.surfaceVariant,
child: Image.network(
ele.appearance['banner'],
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
ele.name,
style: Theme.of(context)
.textTheme
.titleMedium,
).bold(),
Text(
ele.description,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
if (_programMembers
.any((e) => e.programId == ele.id))
Text('accountProgramAlreadyJoined'.tr())
.opacity(0.75),
],
),
),
],
),
),
],
),
),
).padding(horizontal: 8);
},
),
),
],
),
);
}
}
class _ProgramJoinPopup extends StatefulWidget {
final SnProgram program;
final bool isJoined;
const _ProgramJoinPopup({required this.program, required this.isJoined});
@override
State<_ProgramJoinPopup> createState() => _ProgramJoinPopupState();
}
class _ProgramJoinPopupState extends State<_ProgramJoinPopup> {
bool _isBusy = false;
Future<void> _joinProgram() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.post('/cgi/id/programs/${widget.program.id}');
if (!mounted) return;
Navigator.pop(context, true);
context.showSnackbar('accountProgramJoined'.tr());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _leaveProgram() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
await sn.client.delete('/cgi/id/programs/${widget.program.id}');
if (!mounted) return;
Navigator.pop(context, true);
context.showSnackbar('accountProgramLeft'.tr());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.75,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.add, size: 24),
const Gap(16),
Text(
'accountProgramJoin',
style: Theme.of(context).textTheme.titleLarge,
).tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.program.appearance['banner'] != null)
AspectRatio(
aspectRatio: 16 / 5,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Container(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Image.network(
widget.program.appearance['banner'],
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
).padding(bottom: 12),
Text(
widget.program.name,
style: Theme.of(context).textTheme.titleMedium,
).bold(),
MarkdownTextContent(content: widget.program.description),
const Gap(8),
Text(
'accountProgramJoinRequirements',
style: Theme.of(context).textTheme.titleMedium,
).tr().bold(),
Text('≥EXP ${widget.program.expRequirement}'),
Text('≥Lv${getLevelFromExp(widget.program.expRequirement)}'),
const Gap(8),
Text(
'accountProgramJoinPricing',
style: Theme.of(context).textTheme.titleMedium,
).tr().bold(),
Text('walletCurrency${widget.program.price['currency'].toString().capitalize().replaceFirst('Normal', '')}')
.plural(widget.program.price['amount'].toDouble()),
Text('accountProgramJoinPricingHint').tr().opacity(0.75),
const Gap(8),
if (widget.isJoined)
Text('accountProgramLeaveHint')
.tr()
.opacity(0.75)
.padding(bottom: 8),
if (!widget.isJoined)
ElevatedButton(
onPressed: _isBusy ? null : _joinProgram,
child: Text('join').tr(),
)
else
ElevatedButton(
onPressed: _isBusy ? null : _leaveProgram,
child: Text('leave').tr(),
),
],
).padding(horizontal: 24),
Gap(MediaQuery.of(context).padding.bottom),
],
),
),
);
}
}

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: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar(
leading: PageBackButton(),
title: Text('screenAccountPublisherEdit').tr()),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
@ -206,12 +214,15 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 7,
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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar(
title: Text('accountPunishments').tr(),
leading: PageBackButton(),
),
body: Column(
children: [
LoadingIndicator(isActive: _isBusy),
Card(
margin: EdgeInsets.only(bottom: 8, left: 8, right: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Symbols.visibility, size: 20),
const Gap(6),
Expanded(
child: Text('punishmentOverall').tr().fontSize(16).bold(),
),
],
),
Builder(
builder: (context) {
if (_punishments == null) return Text('loading').tr();
if (_punishments!.any((ele) => ele.type == 2)) {
return Text('punishmentStatusBanned').tr();
}
if (_punishments!.any(
(ele) => ele.type == 1 && ele.permNodes.isEmpty,
)) {
return Text('punishmentStatusLimitedFully').tr();
} else if (_punishments!.any((ele) => ele.type == 1)) {
return Text('punishmentStatusLimited').tr();
}
if (_punishments!.any((ele) => ele.type == 0)) {
return Text('punishmentStatusWarned').tr();
}
return Text('punishmentStatusNormal').tr();
},
),
],
).padding(horizontal: 24, vertical: 16),
),
Expanded(
child: RefreshIndicator(
onRefresh: _fetchPunishments,
child: ListView.separated(
padding: EdgeInsets.zero,
itemCount: _punishments?.length ?? 0,
itemBuilder: (context, index) {
final ele = _punishments![index];
return Card(
margin: EdgeInsets.symmetric(horizontal: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(kPunishmentIcons[ele.type], size: 20),
const Gap(6),
Expanded(
child: Text('punishmentType${ele.type}')
.tr()
.fontSize(16)
.bold(),
),
],
),
Text(ele.reason),
const Gap(4),
Text(
'punishmentCreatedAt'.tr(args: [
DateFormat().format(
ele.createdAt.toLocal(),
)
]),
).opacity(0.8),
Text(
ele.expiredAt == null
? 'punishmentExpiredNever'.tr()
: 'punishmentExpiredAt'.tr(args: [
DateFormat().format(
ele.expiredAt!.toLocal(),
)
]),
).opacity(0.8),
const Gap(8),
if (ele.moderator != null)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('punishmentModerator').tr().opacity(0.75),
InkWell(
child: Row(
children: [
AccountImage(
content: ele.moderator!.avatar,
radius: 8,
),
const Gap(4),
Text(ele.moderator?.nick ?? 'unknown'),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'accountProfilePage',
pathParameters: {
'name': ele.moderator!.name,
},
);
},
),
],
)
else
Text('punishmentMadeBySystem').tr().opacity(0.75),
],
).padding(horizontal: 24, vertical: 16),
);
},
separatorBuilder: (_, __) => const Gap(8),
),
),
),
],
),
);
}
}

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: ResponsiveScaffold.getIsExpand(context),
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

@ -8,9 +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/sn_network.dart'; import 'package:surface/providers/sn_network.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';
@ -54,7 +52,6 @@ class _AlbumScreenState extends State<AlbumScreen> {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ud = context.read<UserDirectoryProvider>();
final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: { final resp = await sn.client.get('/cgi/uc/attachments', queryParameters: {
'take': 10, 'take': 10,
'offset': _attachments.length, 'offset': _attachments.length,
@ -65,8 +62,6 @@ class _AlbumScreenState extends State<AlbumScreen> {
_attachments.addAll(attachments); _attachments.addAll(attachments);
_heroTags.addAll(_attachments.map((_) => uuid.v4())); _heroTags.addAll(_attachments.map((_) => uuid.v4()));
await ud.listAccount(attachments.map((e) => e.accountId).toSet());
_totalCount = resp.data['count'] as int?; _totalCount = resp.data['count'] as int?;
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
@ -106,7 +101,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 +114,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 +125,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: ResponsiveScaffold.getIsExpand(context),
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 / 7,
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(
_focusedRealm!.banner!,
),
fit: BoxFit.cover,
),
),
ListTile(
minTileHeight: 48,
tileColor: Theme.of(context)
.colorScheme
.surfaceContainer,
leading: AccountImage(
content: _focusedRealm!.avatar,
radius: 16,
),
contentPadding: EdgeInsets.only(
left: 20,
right: 16,
),
trailing: IconButton(
icon: const Icon(Symbols.close),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
visualDensity: VisualDensity.compact,
onPressed: () {
setState(() => _focusedRealm = null);
},
),
title: Text(_focusedRealm!.name),
),
...(_channels!
.where(
(ele) => ele.realmId == _focusedRealm?.id)
.map(
(ele) {
return _ChatChannelEntry(
channel: ele,
unreadCount: _unreadCounts?[ele.id],
lastMessage: _lastMessages?[ele.id],
onTap: () => _onTapChannel(ele),
isCompact: true,
);
},
))
],
);
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (Widget child,
Animation<double> primaryAnimation,
Animation<double> secondaryAnimation) {
return SharedAxisTransition(
animation: primaryAnimation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
transitionType: SharedAxisTransitionType.horizontal,
child: child,
);
},
child: (_focusedRealm == null && !_isDirect)
? scopeList
: _isDirect
? directChatList
: realmScopedChatList,
);
}),
),
)
else if (_channels != null)
Expanded( 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

@ -32,17 +32,16 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
} }
} }
Widget _buildListLayout() { Widget _buildMeetLayout() {
final call = context.read<ChatCallProvider>(); final call = context.read<ChatCallProvider>();
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,
participant: call.focusTrack!, participant: call.focusTrack!,
onTap: () {},
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
@ -61,23 +60,19 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
return Container(); return Container();
} }
return Padding( return SizedBox(
padding: const EdgeInsets.only(top: 8, left: 8), height: 128,
child: ClipRRect( width: 128,
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget( child: InteractiveParticipantWidget(
isFixedAvatar: true,
width: 120,
height: 120,
color: Theme.of(context).cardColor,
participant: track, participant: track,
avatarSize: 32,
onTap: () { onTap: () {
if (track.participant.sid != call.focusTrack?.participant.sid) { if (track.participant.sid !=
call.focusTrack?.participant.sid) {
call.setFocusTrack(track); call.setFocusTrack(track);
} }
}, },
), ),
),
); );
}, },
), ),
@ -87,46 +82,26 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
); );
} }
Widget _buildGridLayout() { Widget _buildListLayout() {
final call = context.read<ChatCallProvider>(); final call = context.read<ChatCallProvider>();
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(
double screenWidth = constraints.maxWidth; builder: (context, constraints) {
double screenHeight = constraints.maxHeight; return ListView.builder(
padding: EdgeInsets.zero,
int columns = (math.sqrt(call.participantTracks.length)).ceil();
int rows = (call.participantTracks.length / columns).ceil();
double tileWidth = screenWidth / columns;
double tileHeight = screenHeight / rows;
return StyledWidget(GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
childAspectRatio: tileWidth / tileHeight,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: math.max(0, call.participantTracks.length), itemCount: math.max(0, call.participantTracks.length),
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final track = call.participantTracks[index]; final track = call.participantTracks[index];
return Card( return InteractiveParticipantWidget(
child: ClipRRect( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
borderRadius: const BorderRadius.all(Radius.circular(8)), isList: true,
child: InteractiveParticipantWidget( avatarSize: 24,
color: Theme.of(context).colorScheme.surfaceContainerHigh.withOpacity(0.75),
participant: track, participant: track,
onTap: () {
if (track.participant.sid != call.focusTrack?.participant.sid) {
call.setFocusTrack(track);
}
},
),
),
); );
}, },
)).padding(all: 8); );
}); },
);
} }
@override @override
@ -149,6 +124,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
listenable: call, listenable: call,
builder: (context, _) { builder: (context, _) {
return AppScaffold( return AppScaffold(
noBackground: ResponsiveScaffold.getIsExpand(context),
appBar: AppBar( appBar: AppBar(
title: RichText( title: RichText(
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -169,9 +145,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
]), ]),
), ),
), ),
body: GestureDetector( body: Column(
behavior: HitTestBehavior.translucent,
child: Column(
children: [ children: [
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
@ -183,7 +157,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 +180,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,
) )
@ -233,6 +219,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: Colors.white, color: Colors.white,
strokeWidth: 2, strokeWidth: 2,
padding: EdgeInsets.zero,
), ),
).padding(all: 3), ).padding(all: 3),
], ],
@ -244,7 +231,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();
}, },
@ -261,9 +250,9 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
builder: (context) { builder: (context) {
switch (_layoutMode) { switch (_layoutMode) {
case 1: case 1:
return _buildGridLayout();
default:
return _buildListLayout(); return _buildListLayout();
default:
return _buildMeetLayout();
} }
}, },
), ),
@ -277,10 +266,9 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
call.room.localParticipant!, call.room.localParticipant!,
), ),
), ),
Gap(MediaQuery.of(context).padding.bottom),
], ],
), ),
onTap: () {},
),
); );
}); });
} }

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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
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

@ -11,13 +11,11 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/notification.dart'; import 'package:surface/providers/notification.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/notification.dart'; import 'package:surface/types/notification.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/app_bar_leading.dart'; import 'package:surface/widgets/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/markdown_content.dart'; import 'package:surface/widgets/markdown_content.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../providers/userinfo.dart'; import '../providers/userinfo.dart';
@ -149,8 +147,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()),
); );
} }
@ -218,34 +217,24 @@ class _NotificationScreenState extends State<NotificationScreen> {
'interactive.subscription', 'interactive.subscription',
].contains(nty.topic) && ].contains(nty.topic) &&
nty.metadata['related_post'] != null) nty.metadata['related_post'] != null)
GestureDetector( TextButton(
child: Container( style: ButtonStyle(
decoration: BoxDecoration( padding: WidgetStatePropertyAll(
borderRadius: const BorderRadius.all( EdgeInsets.zero,
Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1),
), ),
child: PostItem( visualDensity: VisualDensity.compact,
data: SnPost.fromJson(
nty.metadata['related_post']!),
showComments: false,
showReactions: false,
showMenu: false,
).padding(vertical: 4),
), ),
onTap: () { child: Text('postReadMore').tr(),
onPressed: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'postDetail', 'postDetail',
pathParameters: { pathParameters: {
'slug': nty 'slug': nty.metadata['related_post']['id']
.metadata['related_post']!['id'] .toString(),
.toString()
}, },
); );
}, },
).padding(top: 8), ),
const Gap(8), const Gap(8),
Row( Row(
children: [ children: [

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: ResponsiveScaffold.getIsExpand(context),
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,10 +73,13 @@ 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,
maxWidth: 640, maxWidth: 640,
useReplace: true,
onChanged: (ele) { onChanged: (ele) {
_posts[idx] = ele; _posts[idx] = ele;
setState(() {}); setState(() {});
@ -92,6 +87,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: ResponsiveScaffold.getIsExpand(context),
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: ResponsiveScaffold.getIsExpand(context),
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

@ -1,4 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:surface/types/account.dart';
part 'attachment.freezed.dart'; part 'attachment.freezed.dart';
@ -39,6 +40,7 @@ abstract class SnAttachment with _$SnAttachment {
required int? refId, required int? refId,
required SnAttachmentPool? pool, required SnAttachmentPool? pool,
required int? poolId, required int? poolId,
required SnAccount? account,
required int accountId, required int accountId,
int? thumbnailId, int? thumbnailId,
SnAttachment? thumbnail, SnAttachment? thumbnail,
@ -49,7 +51,8 @@ abstract class SnAttachment with _$SnAttachment {
@Default({}) Map<String, dynamic> metadata, @Default({}) Map<String, dynamic> metadata,
}) = _SnAttachment; }) = _SnAttachment;
factory SnAttachment.fromJson(Map<String, Object?> json) => _$SnAttachmentFromJson(json); factory SnAttachment.fromJson(Map<String, Object?> json) =>
_$SnAttachmentFromJson(json);
Map<String, dynamic> get data => { Map<String, dynamic> get data => {
...metadata, ...metadata,
@ -85,7 +88,8 @@ abstract class SnAttachmentFragment with _$SnAttachmentFragment {
@Default([]) List<String> fileChunksMissing, @Default([]) List<String> fileChunksMissing,
}) = _SnAttachmentFragment; }) = _SnAttachmentFragment;
factory SnAttachmentFragment.fromJson(Map<String, Object?> json) => _$SnAttachmentFragmentFromJson(json); factory SnAttachmentFragment.fromJson(Map<String, Object?> json) =>
_$SnAttachmentFragmentFromJson(json);
SnMediaType get mediaType => switch (mimetype.split('/').firstOrNull) { SnMediaType get mediaType => switch (mimetype.split('/').firstOrNull) {
'image' => SnMediaType.image, 'image' => SnMediaType.image,
@ -109,7 +113,8 @@ abstract class SnAttachmentPool with _$SnAttachmentPool {
required int? accountId, required int? accountId,
}) = _SnAttachmentPool; }) = _SnAttachmentPool;
factory SnAttachmentPool.fromJson(Map<String, Object?> json) => _$SnAttachmentPoolFromJson(json); factory SnAttachmentPool.fromJson(Map<String, Object?> json) =>
_$SnAttachmentPoolFromJson(json);
} }
@freezed @freezed
@ -122,7 +127,8 @@ abstract class SnAttachmentDestination with _$SnAttachmentDestination {
required bool isBoost, required bool isBoost,
}) = _SnAttachmentDestination; }) = _SnAttachmentDestination;
factory SnAttachmentDestination.fromJson(Map<String, Object?> json) => _$SnAttachmentDestinationFromJson(json); factory SnAttachmentDestination.fromJson(Map<String, Object?> json) =>
_$SnAttachmentDestinationFromJson(json);
} }
@freezed @freezed
@ -139,7 +145,8 @@ abstract class SnAttachmentBoost with _$SnAttachmentBoost {
required int account, required int account,
}) = _SnAttachmentBoost; }) = _SnAttachmentBoost;
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) => _$SnAttachmentBoostFromJson(json); factory SnAttachmentBoost.fromJson(Map<String, Object?> json) =>
_$SnAttachmentBoostFromJson(json);
} }
@freezed @freezed
@ -158,7 +165,8 @@ abstract class SnSticker with _$SnSticker {
required int accountId, required int accountId,
}) = _SnSticker; }) = _SnSticker;
factory SnSticker.fromJson(Map<String, Object?> json) => _$SnStickerFromJson(json); factory SnSticker.fromJson(Map<String, Object?> json) =>
_$SnStickerFromJson(json);
} }
@freezed @freezed
@ -175,7 +183,8 @@ abstract class SnStickerPack with _$SnStickerPack {
required int accountId, required int accountId,
}) = _SnStickerPack; }) = _SnStickerPack;
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json); factory SnStickerPack.fromJson(Map<String, Object?> json) =>
_$SnStickerPackFromJson(json);
} }
@freezed @freezed
@ -186,5 +195,6 @@ abstract class SnAttachmentBilling with _$SnAttachmentBilling {
required double includedRatio, required double includedRatio,
}) = _SnAttachmentBilling; }) = _SnAttachmentBilling;
factory SnAttachmentBilling.fromJson(Map<String, Object?> json) => _$SnAttachmentBillingFromJson(json); factory SnAttachmentBilling.fromJson(Map<String, Object?> json) =>
_$SnAttachmentBillingFromJson(json);
} }

View File

@ -38,6 +38,7 @@ mixin _$SnAttachment {
int? get refId; int? get refId;
SnAttachmentPool? get pool; SnAttachmentPool? get pool;
int? get poolId; int? get poolId;
SnAccount? get account;
int get accountId; int get accountId;
int? get thumbnailId; int? get thumbnailId;
SnAttachment? get thumbnail; SnAttachment? get thumbnail;
@ -98,6 +99,7 @@ mixin _$SnAttachment {
(identical(other.refId, refId) || other.refId == refId) && (identical(other.refId, refId) || other.refId == refId) &&
(identical(other.pool, pool) || other.pool == pool) && (identical(other.pool, pool) || other.pool == pool) &&
(identical(other.poolId, poolId) || other.poolId == poolId) && (identical(other.poolId, poolId) || other.poolId == poolId) &&
(identical(other.account, account) || other.account == account) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
other.accountId == accountId) && other.accountId == accountId) &&
(identical(other.thumbnailId, thumbnailId) || (identical(other.thumbnailId, thumbnailId) ||
@ -140,6 +142,7 @@ mixin _$SnAttachment {
refId, refId,
pool, pool,
poolId, poolId,
account,
accountId, accountId,
thumbnailId, thumbnailId,
thumbnail, thumbnail,
@ -152,7 +155,7 @@ mixin _$SnAttachment {
@override @override
String toString() { String toString() {
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)'; return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
} }
} }
@ -186,6 +189,7 @@ abstract mixin class $SnAttachmentCopyWith<$Res> {
int? refId, int? refId,
SnAttachmentPool? pool, SnAttachmentPool? pool,
int? poolId, int? poolId,
SnAccount? account,
int accountId, int accountId,
int? thumbnailId, int? thumbnailId,
SnAttachment? thumbnail, SnAttachment? thumbnail,
@ -197,6 +201,7 @@ abstract mixin class $SnAttachmentCopyWith<$Res> {
$SnAttachmentCopyWith<$Res>? get ref; $SnAttachmentCopyWith<$Res>? get ref;
$SnAttachmentPoolCopyWith<$Res>? get pool; $SnAttachmentPoolCopyWith<$Res>? get pool;
$SnAccountCopyWith<$Res>? get account;
$SnAttachmentCopyWith<$Res>? get thumbnail; $SnAttachmentCopyWith<$Res>? get thumbnail;
$SnAttachmentCopyWith<$Res>? get compressed; $SnAttachmentCopyWith<$Res>? get compressed;
} }
@ -236,6 +241,7 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
Object? refId = freezed, Object? refId = freezed,
Object? pool = freezed, Object? pool = freezed,
Object? poolId = freezed, Object? poolId = freezed,
Object? account = freezed,
Object? accountId = null, Object? accountId = null,
Object? thumbnailId = freezed, Object? thumbnailId = freezed,
Object? thumbnail = freezed, Object? thumbnail = freezed,
@ -338,6 +344,10 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
? _self.poolId ? _self.poolId
: poolId // ignore: cast_nullable_to_non_nullable : poolId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
account: freezed == account
? _self.account
: account // ignore: cast_nullable_to_non_nullable
as SnAccount?,
accountId: null == accountId accountId: null == accountId
? _self.accountId ? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
@ -401,6 +411,20 @@ class _$SnAttachmentCopyWithImpl<$Res> implements $SnAttachmentCopyWith<$Res> {
}); });
} }
/// Create a copy of SnAttachment
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
/// Create a copy of SnAttachment /// Create a copy of SnAttachment
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@ -457,6 +481,7 @@ class _SnAttachment extends SnAttachment {
required this.refId, required this.refId,
required this.pool, required this.pool,
required this.poolId, required this.poolId,
required this.account,
required this.accountId, required this.accountId,
this.thumbnailId, this.thumbnailId,
this.thumbnail, this.thumbnail,
@ -521,6 +546,8 @@ class _SnAttachment extends SnAttachment {
@override @override
final int? poolId; final int? poolId;
@override @override
final SnAccount? account;
@override
final int accountId; final int accountId;
@override @override
final int? thumbnailId; final int? thumbnailId;
@ -612,6 +639,7 @@ class _SnAttachment extends SnAttachment {
(identical(other.refId, refId) || other.refId == refId) && (identical(other.refId, refId) || other.refId == refId) &&
(identical(other.pool, pool) || other.pool == pool) && (identical(other.pool, pool) || other.pool == pool) &&
(identical(other.poolId, poolId) || other.poolId == poolId) && (identical(other.poolId, poolId) || other.poolId == poolId) &&
(identical(other.account, account) || other.account == account) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
other.accountId == accountId) && other.accountId == accountId) &&
(identical(other.thumbnailId, thumbnailId) || (identical(other.thumbnailId, thumbnailId) ||
@ -654,6 +682,7 @@ class _SnAttachment extends SnAttachment {
refId, refId,
pool, pool,
poolId, poolId,
account,
accountId, accountId,
thumbnailId, thumbnailId,
thumbnail, thumbnail,
@ -666,7 +695,7 @@ class _SnAttachment extends SnAttachment {
@override @override
String toString() { String toString() {
return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)'; return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isSelfRef: $isSelfRef, isIndexable: $isIndexable, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, account: $account, accountId: $accountId, thumbnailId: $thumbnailId, thumbnail: $thumbnail, compressedId: $compressedId, compressed: $compressed, boosts: $boosts, usermeta: $usermeta, metadata: $metadata)';
} }
} }
@ -702,6 +731,7 @@ abstract mixin class _$SnAttachmentCopyWith<$Res>
int? refId, int? refId,
SnAttachmentPool? pool, SnAttachmentPool? pool,
int? poolId, int? poolId,
SnAccount? account,
int accountId, int accountId,
int? thumbnailId, int? thumbnailId,
SnAttachment? thumbnail, SnAttachment? thumbnail,
@ -716,6 +746,8 @@ abstract mixin class _$SnAttachmentCopyWith<$Res>
@override @override
$SnAttachmentPoolCopyWith<$Res>? get pool; $SnAttachmentPoolCopyWith<$Res>? get pool;
@override @override
$SnAccountCopyWith<$Res>? get account;
@override
$SnAttachmentCopyWith<$Res>? get thumbnail; $SnAttachmentCopyWith<$Res>? get thumbnail;
@override @override
$SnAttachmentCopyWith<$Res>? get compressed; $SnAttachmentCopyWith<$Res>? get compressed;
@ -757,6 +789,7 @@ class __$SnAttachmentCopyWithImpl<$Res>
Object? refId = freezed, Object? refId = freezed,
Object? pool = freezed, Object? pool = freezed,
Object? poolId = freezed, Object? poolId = freezed,
Object? account = freezed,
Object? accountId = null, Object? accountId = null,
Object? thumbnailId = freezed, Object? thumbnailId = freezed,
Object? thumbnail = freezed, Object? thumbnail = freezed,
@ -859,6 +892,10 @@ class __$SnAttachmentCopyWithImpl<$Res>
? _self.poolId ? _self.poolId
: poolId // ignore: cast_nullable_to_non_nullable : poolId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
account: freezed == account
? _self.account
: account // ignore: cast_nullable_to_non_nullable
as SnAccount?,
accountId: null == accountId accountId: null == accountId
? _self.accountId ? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
@ -922,6 +959,20 @@ class __$SnAttachmentCopyWithImpl<$Res>
}); });
} }
/// Create a copy of SnAttachment
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
/// Create a copy of SnAttachment /// Create a copy of SnAttachment
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override

View File

@ -39,6 +39,9 @@ _SnAttachment _$SnAttachmentFromJson(Map<String, dynamic> json) =>
? null ? null
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>), : SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
poolId: (json['pool_id'] as num?)?.toInt(), poolId: (json['pool_id'] as num?)?.toInt(),
account: json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(), thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
thumbnail: json['thumbnail'] == null thumbnail: json['thumbnail'] == null
@ -82,6 +85,7 @@ Map<String, dynamic> _$SnAttachmentToJson(_SnAttachment instance) =>
'ref_id': instance.refId, 'ref_id': instance.refId,
'pool': instance.pool?.toJson(), 'pool': instance.pool?.toJson(),
'pool_id': instance.poolId, 'pool_id': instance.poolId,
'account': instance.account?.toJson(),
'account_id': instance.accountId, 'account_id': instance.accountId,
'thumbnail_id': instance.thumbnailId, 'thumbnail_id': instance.thumbnailId,
'thumbnail': instance.thumbnail?.toJson(), 'thumbnail': instance.thumbnail?.toJson(),

View File

@ -1,4 +1,5 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:surface/types/account.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/types/poll.dart'; import 'package:surface/types/poll.dart';
import 'package:surface/types/realm.dart'; import 'package:surface/types/realm.dart';
@ -26,6 +27,7 @@ abstract class SnPost with _$SnPost {
required int? replyId, required int? replyId,
required int? repostId, required int? repostId,
required int? realmId, required int? realmId,
required SnRealm? realm,
required SnPost? replyTo, required SnPost? replyTo,
required SnPost? repostTo, required SnPost? repostTo,
required List<int>? visibleUsersList, required List<int>? visibleUsersList,
@ -43,9 +45,9 @@ abstract class SnPost with _$SnPost {
@Default(0) int totalAggregatedViews, @Default(0) int totalAggregatedViews,
required int publisherId, required int publisherId,
required int? pollId, required int? pollId,
required SnPoll? poll,
required SnPublisher publisher, required SnPublisher publisher,
required SnMetric metric, required SnMetric metric,
SnPostPreload? preload,
}) = _SnPost; }) = _SnPost;
factory SnPost.fromJson(Map<String, Object?> json) => _$SnPostFromJson(json); factory SnPost.fromJson(Map<String, Object?> json) => _$SnPostFromJson(json);
@ -146,6 +148,7 @@ abstract class SnPublisher with _$SnPublisher {
required int totalDownvote, required int totalDownvote,
required int? realmId, required int? realmId,
required int accountId, required int accountId,
required SnAccount? account,
}) = _SnPublisher; }) = _SnPublisher;
factory SnPublisher.fromJson(Map<String, Object?> json) => factory SnPublisher.fromJson(Map<String, Object?> json) =>

View File

@ -30,6 +30,7 @@ mixin _$SnPost {
int? get replyId; int? get replyId;
int? get repostId; int? get repostId;
int? get realmId; int? get realmId;
SnRealm? get realm;
SnPost? get replyTo; SnPost? get replyTo;
SnPost? get repostTo; SnPost? get repostTo;
List<int>? get visibleUsersList; List<int>? get visibleUsersList;
@ -47,9 +48,9 @@ mixin _$SnPost {
int get totalAggregatedViews; int get totalAggregatedViews;
int get publisherId; int get publisherId;
int? get pollId; int? get pollId;
SnPoll? get poll;
SnPublisher get publisher; SnPublisher get publisher;
SnMetric get metric; SnMetric get metric;
SnPostPreload? get preload;
/// Create a copy of SnPost /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -88,6 +89,7 @@ mixin _$SnPost {
(identical(other.repostId, repostId) || (identical(other.repostId, repostId) ||
other.repostId == repostId) && other.repostId == repostId) &&
(identical(other.realmId, realmId) || other.realmId == realmId) && (identical(other.realmId, realmId) || other.realmId == realmId) &&
(identical(other.realm, realm) || other.realm == realm) &&
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) && (identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
(identical(other.repostTo, repostTo) || (identical(other.repostTo, repostTo) ||
other.repostTo == repostTo) && other.repostTo == repostTo) &&
@ -119,10 +121,10 @@ mixin _$SnPost {
(identical(other.publisherId, publisherId) || (identical(other.publisherId, publisherId) ||
other.publisherId == publisherId) && other.publisherId == publisherId) &&
(identical(other.pollId, pollId) || other.pollId == pollId) && (identical(other.pollId, pollId) || other.pollId == pollId) &&
(identical(other.poll, poll) || other.poll == poll) &&
(identical(other.publisher, publisher) || (identical(other.publisher, publisher) ||
other.publisher == publisher) && other.publisher == publisher) &&
(identical(other.metric, metric) || other.metric == metric) && (identical(other.metric, metric) || other.metric == metric));
(identical(other.preload, preload) || other.preload == preload));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -144,6 +146,7 @@ mixin _$SnPost {
replyId, replyId,
repostId, repostId,
realmId, realmId,
realm,
replyTo, replyTo,
repostTo, repostTo,
const DeepCollectionEquality().hash(visibleUsersList), const DeepCollectionEquality().hash(visibleUsersList),
@ -161,14 +164,14 @@ mixin _$SnPost {
totalAggregatedViews, totalAggregatedViews,
publisherId, publisherId,
pollId, pollId,
poll,
publisher, publisher,
metric, metric
preload
]); ]);
@override @override
String toString() { String toString() {
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)'; return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, realm: $realm, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, poll: $poll, publisher: $publisher, metric: $metric)';
} }
} }
@ -193,6 +196,7 @@ abstract mixin class $SnPostCopyWith<$Res> {
int? replyId, int? replyId,
int? repostId, int? repostId,
int? realmId, int? realmId,
SnRealm? realm,
SnPost? replyTo, SnPost? replyTo,
SnPost? repostTo, SnPost? repostTo,
List<int>? visibleUsersList, List<int>? visibleUsersList,
@ -210,15 +214,16 @@ abstract mixin class $SnPostCopyWith<$Res> {
int totalAggregatedViews, int totalAggregatedViews,
int publisherId, int publisherId,
int? pollId, int? pollId,
SnPoll? poll,
SnPublisher publisher, SnPublisher publisher,
SnMetric metric, SnMetric metric});
SnPostPreload? preload});
$SnRealmCopyWith<$Res>? get realm;
$SnPostCopyWith<$Res>? get replyTo; $SnPostCopyWith<$Res>? get replyTo;
$SnPostCopyWith<$Res>? get repostTo; $SnPostCopyWith<$Res>? get repostTo;
$SnPollCopyWith<$Res>? get poll;
$SnPublisherCopyWith<$Res> get publisher; $SnPublisherCopyWith<$Res> get publisher;
$SnMetricCopyWith<$Res> get metric; $SnMetricCopyWith<$Res> get metric;
$SnPostPreloadCopyWith<$Res>? get preload;
} }
/// @nodoc /// @nodoc
@ -248,6 +253,7 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
Object? replyId = freezed, Object? replyId = freezed,
Object? repostId = freezed, Object? repostId = freezed,
Object? realmId = freezed, Object? realmId = freezed,
Object? realm = freezed,
Object? replyTo = freezed, Object? replyTo = freezed,
Object? repostTo = freezed, Object? repostTo = freezed,
Object? visibleUsersList = freezed, Object? visibleUsersList = freezed,
@ -265,9 +271,9 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
Object? totalAggregatedViews = null, Object? totalAggregatedViews = null,
Object? publisherId = null, Object? publisherId = null,
Object? pollId = freezed, Object? pollId = freezed,
Object? poll = freezed,
Object? publisher = null, Object? publisher = null,
Object? metric = null, Object? metric = null,
Object? preload = freezed,
}) { }) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id id: null == id
@ -330,6 +336,10 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
? _self.realmId ? _self.realmId
: realmId // ignore: cast_nullable_to_non_nullable : realmId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
realm: freezed == realm
? _self.realm
: realm // ignore: cast_nullable_to_non_nullable
as SnRealm?,
replyTo: freezed == replyTo replyTo: freezed == replyTo
? _self.replyTo ? _self.replyTo
: replyTo // ignore: cast_nullable_to_non_nullable : replyTo // ignore: cast_nullable_to_non_nullable
@ -398,6 +408,10 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
? _self.pollId ? _self.pollId
: pollId // ignore: cast_nullable_to_non_nullable : pollId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
poll: freezed == poll
? _self.poll
: poll // ignore: cast_nullable_to_non_nullable
as SnPoll?,
publisher: null == publisher publisher: null == publisher
? _self.publisher ? _self.publisher
: publisher // ignore: cast_nullable_to_non_nullable : publisher // ignore: cast_nullable_to_non_nullable
@ -406,13 +420,23 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
? _self.metric ? _self.metric
: metric // ignore: cast_nullable_to_non_nullable : metric // ignore: cast_nullable_to_non_nullable
as SnMetric, as SnMetric,
preload: freezed == preload
? _self.preload
: preload // ignore: cast_nullable_to_non_nullable
as SnPostPreload?,
)); ));
} }
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnRealmCopyWith<$Res>? get realm {
if (_self.realm == null) {
return null;
}
return $SnRealmCopyWith<$Res>(_self.realm!, (value) {
return _then(_self.copyWith(realm: value));
});
}
/// Create a copy of SnPost /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@ -441,6 +465,20 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
}); });
} }
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPollCopyWith<$Res>? get poll {
if (_self.poll == null) {
return null;
}
return $SnPollCopyWith<$Res>(_self.poll!, (value) {
return _then(_self.copyWith(poll: value));
});
}
/// Create a copy of SnPost /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@ -460,20 +498,6 @@ class _$SnPostCopyWithImpl<$Res> implements $SnPostCopyWith<$Res> {
return _then(_self.copyWith(metric: value)); return _then(_self.copyWith(metric: value));
}); });
} }
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostPreloadCopyWith<$Res>? get preload {
if (_self.preload == null) {
return null;
}
return $SnPostPreloadCopyWith<$Res>(_self.preload!, (value) {
return _then(_self.copyWith(preload: value));
});
}
} }
/// @nodoc /// @nodoc
@ -495,6 +519,7 @@ class _SnPost extends SnPost {
required this.replyId, required this.replyId,
required this.repostId, required this.repostId,
required this.realmId, required this.realmId,
required this.realm,
required this.replyTo, required this.replyTo,
required this.repostTo, required this.repostTo,
required final List<int>? visibleUsersList, required final List<int>? visibleUsersList,
@ -512,9 +537,9 @@ class _SnPost extends SnPost {
this.totalAggregatedViews = 0, this.totalAggregatedViews = 0,
required this.publisherId, required this.publisherId,
required this.pollId, required this.pollId,
required this.poll,
required this.publisher, required this.publisher,
required this.metric, required this.metric})
this.preload})
: _body = body, : _body = body,
_tags = tags, _tags = tags,
_categories = categories, _categories = categories,
@ -583,6 +608,8 @@ class _SnPost extends SnPost {
@override @override
final int? realmId; final int? realmId;
@override @override
final SnRealm? realm;
@override
final SnPost? replyTo; final SnPost? replyTo;
@override @override
final SnPost? repostTo; final SnPost? repostTo;
@ -637,11 +664,11 @@ class _SnPost extends SnPost {
@override @override
final int? pollId; final int? pollId;
@override @override
final SnPoll? poll;
@override
final SnPublisher publisher; final SnPublisher publisher;
@override @override
final SnMetric metric; final SnMetric metric;
@override
final SnPostPreload? preload;
/// Create a copy of SnPost /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -685,6 +712,7 @@ class _SnPost extends SnPost {
(identical(other.repostId, repostId) || (identical(other.repostId, repostId) ||
other.repostId == repostId) && other.repostId == repostId) &&
(identical(other.realmId, realmId) || other.realmId == realmId) && (identical(other.realmId, realmId) || other.realmId == realmId) &&
(identical(other.realm, realm) || other.realm == realm) &&
(identical(other.replyTo, replyTo) || other.replyTo == replyTo) && (identical(other.replyTo, replyTo) || other.replyTo == replyTo) &&
(identical(other.repostTo, repostTo) || (identical(other.repostTo, repostTo) ||
other.repostTo == repostTo) && other.repostTo == repostTo) &&
@ -716,10 +744,10 @@ class _SnPost extends SnPost {
(identical(other.publisherId, publisherId) || (identical(other.publisherId, publisherId) ||
other.publisherId == publisherId) && other.publisherId == publisherId) &&
(identical(other.pollId, pollId) || other.pollId == pollId) && (identical(other.pollId, pollId) || other.pollId == pollId) &&
(identical(other.poll, poll) || other.poll == poll) &&
(identical(other.publisher, publisher) || (identical(other.publisher, publisher) ||
other.publisher == publisher) && other.publisher == publisher) &&
(identical(other.metric, metric) || other.metric == metric) && (identical(other.metric, metric) || other.metric == metric));
(identical(other.preload, preload) || other.preload == preload));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -741,6 +769,7 @@ class _SnPost extends SnPost {
replyId, replyId,
repostId, repostId,
realmId, realmId,
realm,
replyTo, replyTo,
repostTo, repostTo,
const DeepCollectionEquality().hash(_visibleUsersList), const DeepCollectionEquality().hash(_visibleUsersList),
@ -758,14 +787,14 @@ class _SnPost extends SnPost {
totalAggregatedViews, totalAggregatedViews,
publisherId, publisherId,
pollId, pollId,
poll,
publisher, publisher,
metric, metric
preload
]); ]);
@override @override
String toString() { String toString() {
return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, publisher: $publisher, metric: $metric, preload: $preload)'; return 'SnPost(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, body: $body, language: $language, alias: $alias, aliasPrefix: $aliasPrefix, tags: $tags, categories: $categories, replies: $replies, replyId: $replyId, repostId: $repostId, realmId: $realmId, realm: $realm, replyTo: $replyTo, repostTo: $repostTo, visibleUsersList: $visibleUsersList, invisibleUsersList: $invisibleUsersList, visibility: $visibility, editedAt: $editedAt, pinnedAt: $pinnedAt, lockedAt: $lockedAt, isDraft: $isDraft, publishedAt: $publishedAt, publishedUntil: $publishedUntil, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, totalViews: $totalViews, totalAggregatedViews: $totalAggregatedViews, publisherId: $publisherId, pollId: $pollId, poll: $poll, publisher: $publisher, metric: $metric)';
} }
} }
@ -791,6 +820,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
int? replyId, int? replyId,
int? repostId, int? repostId,
int? realmId, int? realmId,
SnRealm? realm,
SnPost? replyTo, SnPost? replyTo,
SnPost? repostTo, SnPost? repostTo,
List<int>? visibleUsersList, List<int>? visibleUsersList,
@ -808,20 +838,22 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
int totalAggregatedViews, int totalAggregatedViews,
int publisherId, int publisherId,
int? pollId, int? pollId,
SnPoll? poll,
SnPublisher publisher, SnPublisher publisher,
SnMetric metric, SnMetric metric});
SnPostPreload? preload});
@override
$SnRealmCopyWith<$Res>? get realm;
@override @override
$SnPostCopyWith<$Res>? get replyTo; $SnPostCopyWith<$Res>? get replyTo;
@override @override
$SnPostCopyWith<$Res>? get repostTo; $SnPostCopyWith<$Res>? get repostTo;
@override @override
$SnPollCopyWith<$Res>? get poll;
@override
$SnPublisherCopyWith<$Res> get publisher; $SnPublisherCopyWith<$Res> get publisher;
@override @override
$SnMetricCopyWith<$Res> get metric; $SnMetricCopyWith<$Res> get metric;
@override
$SnPostPreloadCopyWith<$Res>? get preload;
} }
/// @nodoc /// @nodoc
@ -851,6 +883,7 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
Object? replyId = freezed, Object? replyId = freezed,
Object? repostId = freezed, Object? repostId = freezed,
Object? realmId = freezed, Object? realmId = freezed,
Object? realm = freezed,
Object? replyTo = freezed, Object? replyTo = freezed,
Object? repostTo = freezed, Object? repostTo = freezed,
Object? visibleUsersList = freezed, Object? visibleUsersList = freezed,
@ -868,9 +901,9 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
Object? totalAggregatedViews = null, Object? totalAggregatedViews = null,
Object? publisherId = null, Object? publisherId = null,
Object? pollId = freezed, Object? pollId = freezed,
Object? poll = freezed,
Object? publisher = null, Object? publisher = null,
Object? metric = null, Object? metric = null,
Object? preload = freezed,
}) { }) {
return _then(_SnPost( return _then(_SnPost(
id: null == id id: null == id
@ -933,6 +966,10 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
? _self.realmId ? _self.realmId
: realmId // ignore: cast_nullable_to_non_nullable : realmId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
realm: freezed == realm
? _self.realm
: realm // ignore: cast_nullable_to_non_nullable
as SnRealm?,
replyTo: freezed == replyTo replyTo: freezed == replyTo
? _self.replyTo ? _self.replyTo
: replyTo // ignore: cast_nullable_to_non_nullable : replyTo // ignore: cast_nullable_to_non_nullable
@ -1001,6 +1038,10 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
? _self.pollId ? _self.pollId
: pollId // ignore: cast_nullable_to_non_nullable : pollId // ignore: cast_nullable_to_non_nullable
as int?, as int?,
poll: freezed == poll
? _self.poll
: poll // ignore: cast_nullable_to_non_nullable
as SnPoll?,
publisher: null == publisher publisher: null == publisher
? _self.publisher ? _self.publisher
: publisher // ignore: cast_nullable_to_non_nullable : publisher // ignore: cast_nullable_to_non_nullable
@ -1009,13 +1050,23 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
? _self.metric ? _self.metric
: metric // ignore: cast_nullable_to_non_nullable : metric // ignore: cast_nullable_to_non_nullable
as SnMetric, as SnMetric,
preload: freezed == preload
? _self.preload
: preload // ignore: cast_nullable_to_non_nullable
as SnPostPreload?,
)); ));
} }
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnRealmCopyWith<$Res>? get realm {
if (_self.realm == null) {
return null;
}
return $SnRealmCopyWith<$Res>(_self.realm!, (value) {
return _then(_self.copyWith(realm: value));
});
}
/// Create a copy of SnPost /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@ -1044,6 +1095,20 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
}); });
} }
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPollCopyWith<$Res>? get poll {
if (_self.poll == null) {
return null;
}
return $SnPollCopyWith<$Res>(_self.poll!, (value) {
return _then(_self.copyWith(poll: value));
});
}
/// Create a copy of SnPost /// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@ -1063,20 +1128,6 @@ class __$SnPostCopyWithImpl<$Res> implements _$SnPostCopyWith<$Res> {
return _then(_self.copyWith(metric: value)); return _then(_self.copyWith(metric: value));
}); });
} }
/// Create a copy of SnPost
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPostPreloadCopyWith<$Res>? get preload {
if (_self.preload == null) {
return null;
}
return $SnPostPreloadCopyWith<$Res>(_self.preload!, (value) {
return _then(_self.copyWith(preload: value));
});
}
} }
/// @nodoc /// @nodoc
@ -2465,6 +2516,7 @@ mixin _$SnPublisher {
int get totalDownvote; int get totalDownvote;
int? get realmId; int? get realmId;
int get accountId; int get accountId;
SnAccount? get account;
/// Create a copy of SnPublisher /// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -2501,7 +2553,8 @@ mixin _$SnPublisher {
other.totalDownvote == totalDownvote) && other.totalDownvote == totalDownvote) &&
(identical(other.realmId, realmId) || other.realmId == realmId) && (identical(other.realmId, realmId) || other.realmId == realmId) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
other.accountId == accountId)); other.accountId == accountId) &&
(identical(other.account, account) || other.account == account));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -2521,11 +2574,12 @@ mixin _$SnPublisher {
totalUpvote, totalUpvote,
totalDownvote, totalDownvote,
realmId, realmId,
accountId); accountId,
account);
@override @override
String toString() { String toString() {
return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId)'; return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId, account: $account)';
} }
} }
@ -2549,7 +2603,10 @@ abstract mixin class $SnPublisherCopyWith<$Res> {
int totalUpvote, int totalUpvote,
int totalDownvote, int totalDownvote,
int? realmId, int? realmId,
int accountId}); int accountId,
SnAccount? account});
$SnAccountCopyWith<$Res>? get account;
} }
/// @nodoc /// @nodoc
@ -2578,6 +2635,7 @@ class _$SnPublisherCopyWithImpl<$Res> implements $SnPublisherCopyWith<$Res> {
Object? totalDownvote = null, Object? totalDownvote = null,
Object? realmId = freezed, Object? realmId = freezed,
Object? accountId = null, Object? accountId = null,
Object? account = freezed,
}) { }) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: null == id id: null == id
@ -2636,8 +2694,26 @@ class _$SnPublisherCopyWithImpl<$Res> implements $SnPublisherCopyWith<$Res> {
? _self.accountId ? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
as int, as int,
account: freezed == account
? _self.account
: account // ignore: cast_nullable_to_non_nullable
as SnAccount?,
)); ));
} }
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
} }
/// @nodoc /// @nodoc
@ -2657,7 +2733,8 @@ class _SnPublisher implements SnPublisher {
required this.totalUpvote, required this.totalUpvote,
required this.totalDownvote, required this.totalDownvote,
required this.realmId, required this.realmId,
required this.accountId}); required this.accountId,
required this.account});
factory _SnPublisher.fromJson(Map<String, dynamic> json) => factory _SnPublisher.fromJson(Map<String, dynamic> json) =>
_$SnPublisherFromJson(json); _$SnPublisherFromJson(json);
@ -2689,6 +2766,8 @@ class _SnPublisher implements SnPublisher {
final int? realmId; final int? realmId;
@override @override
final int accountId; final int accountId;
@override
final SnAccount? account;
/// Create a copy of SnPublisher /// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -2730,7 +2809,8 @@ class _SnPublisher implements SnPublisher {
other.totalDownvote == totalDownvote) && other.totalDownvote == totalDownvote) &&
(identical(other.realmId, realmId) || other.realmId == realmId) && (identical(other.realmId, realmId) || other.realmId == realmId) &&
(identical(other.accountId, accountId) || (identical(other.accountId, accountId) ||
other.accountId == accountId)); other.accountId == accountId) &&
(identical(other.account, account) || other.account == account));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -2750,11 +2830,12 @@ class _SnPublisher implements SnPublisher {
totalUpvote, totalUpvote,
totalDownvote, totalDownvote,
realmId, realmId,
accountId); accountId,
account);
@override @override
String toString() { String toString() {
return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId)'; return 'SnPublisher(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, type: $type, name: $name, nick: $nick, description: $description, avatar: $avatar, banner: $banner, totalUpvote: $totalUpvote, totalDownvote: $totalDownvote, realmId: $realmId, accountId: $accountId, account: $account)';
} }
} }
@ -2780,7 +2861,11 @@ abstract mixin class _$SnPublisherCopyWith<$Res>
int totalUpvote, int totalUpvote,
int totalDownvote, int totalDownvote,
int? realmId, int? realmId,
int accountId}); int accountId,
SnAccount? account});
@override
$SnAccountCopyWith<$Res>? get account;
} }
/// @nodoc /// @nodoc
@ -2809,6 +2894,7 @@ class __$SnPublisherCopyWithImpl<$Res> implements _$SnPublisherCopyWith<$Res> {
Object? totalDownvote = null, Object? totalDownvote = null,
Object? realmId = freezed, Object? realmId = freezed,
Object? accountId = null, Object? accountId = null,
Object? account = freezed,
}) { }) {
return _then(_SnPublisher( return _then(_SnPublisher(
id: null == id id: null == id
@ -2867,8 +2953,26 @@ class __$SnPublisherCopyWithImpl<$Res> implements _$SnPublisherCopyWith<$Res> {
? _self.accountId ? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable : accountId // ignore: cast_nullable_to_non_nullable
as int, as int,
account: freezed == account
? _self.account
: account // ignore: cast_nullable_to_non_nullable
as SnAccount?,
)); ));
} }
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
} }
/// @nodoc /// @nodoc

View File

@ -32,6 +32,9 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
replyId: (json['reply_id'] as num?)?.toInt(), replyId: (json['reply_id'] as num?)?.toInt(),
repostId: (json['repost_id'] as num?)?.toInt(), repostId: (json['repost_id'] as num?)?.toInt(),
realmId: (json['realm_id'] as num?)?.toInt(), realmId: (json['realm_id'] as num?)?.toInt(),
realm: json['realm'] == null
? null
: SnRealm.fromJson(json['realm'] as Map<String, dynamic>),
replyTo: json['reply_to'] == null replyTo: json['reply_to'] == null
? null ? null
: SnPost.fromJson(json['reply_to'] as Map<String, dynamic>), : SnPost.fromJson(json['reply_to'] as Map<String, dynamic>),
@ -68,12 +71,12 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
(json['total_aggregated_views'] as num?)?.toInt() ?? 0, (json['total_aggregated_views'] as num?)?.toInt() ?? 0,
publisherId: (json['publisher_id'] as num).toInt(), publisherId: (json['publisher_id'] as num).toInt(),
pollId: (json['poll_id'] as num?)?.toInt(), pollId: (json['poll_id'] as num?)?.toInt(),
poll: json['poll'] == null
? null
: SnPoll.fromJson(json['poll'] as Map<String, dynamic>),
publisher: publisher:
SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>), SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
metric: SnMetric.fromJson(json['metric'] as Map<String, dynamic>), metric: SnMetric.fromJson(json['metric'] as Map<String, dynamic>),
preload: json['preload'] == null
? null
: SnPostPreload.fromJson(json['preload'] as Map<String, dynamic>),
); );
Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
@ -92,6 +95,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'reply_id': instance.replyId, 'reply_id': instance.replyId,
'repost_id': instance.repostId, 'repost_id': instance.repostId,
'realm_id': instance.realmId, 'realm_id': instance.realmId,
'realm': instance.realm?.toJson(),
'reply_to': instance.replyTo?.toJson(), 'reply_to': instance.replyTo?.toJson(),
'repost_to': instance.repostTo?.toJson(), 'repost_to': instance.repostTo?.toJson(),
'visible_users_list': instance.visibleUsersList, 'visible_users_list': instance.visibleUsersList,
@ -109,9 +113,9 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'total_aggregated_views': instance.totalAggregatedViews, 'total_aggregated_views': instance.totalAggregatedViews,
'publisher_id': instance.publisherId, 'publisher_id': instance.publisherId,
'poll_id': instance.pollId, 'poll_id': instance.pollId,
'poll': instance.poll?.toJson(),
'publisher': instance.publisher.toJson(), 'publisher': instance.publisher.toJson(),
'metric': instance.metric.toJson(), 'metric': instance.metric.toJson(),
'preload': instance.preload?.toJson(),
}; };
_SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) => _SnPostTag( _SnPostTag _$SnPostTagFromJson(Map<String, dynamic> json) => _SnPostTag(
@ -241,6 +245,9 @@ _SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
totalDownvote: (json['total_downvote'] as num).toInt(), totalDownvote: (json['total_downvote'] as num).toInt(),
realmId: (json['realm_id'] as num?)?.toInt(), realmId: (json['realm_id'] as num?)?.toInt(),
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
account: json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
); );
Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) => Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
@ -259,6 +266,7 @@ Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
'total_downvote': instance.totalDownvote, 'total_downvote': instance.totalDownvote,
'realm_id': instance.realmId, 'realm_id': instance.realmId,
'account_id': instance.accountId, 'account_id': instance.accountId,
'account': instance.account?.toJson(),
}; };
_SnSubscription _$SnSubscriptionFromJson(Map<String, dynamic> json) => _SnSubscription _$SnSubscriptionFromJson(Map<String, dynamic> json) =>

View File

@ -17,4 +17,5 @@ const Map<String, ReactInfo> kTemplateReactions = {
'party': ReactInfo(icon: '🎉', attitude: 1), 'party': ReactInfo(icon: '🎉', attitude: 1),
'joy': ReactInfo(icon: '🤣', attitude: 1), 'joy': ReactInfo(icon: '🤣', attitude: 1),
'pray': ReactInfo(icon: '🙏', attitude: 1), 'pray': ReactInfo(icon: '🙏', attitude: 1),
'heart': ReactInfo(icon: '❤️', attitude: 1),
}; };

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

@ -54,11 +54,15 @@ class AccountImage extends StatelessWidget {
)) ))
.center(), .center(),
) )
: AutoResizeUniversalImage( : UniversalImage(
sn.getAttachmentUrl(url), sn.getAttachmentUrl(url),
filterQuality: filterQuality, filterQuality: filterQuality,
key: Key('attachment-${content.hashCode}'), key: Key('attachment-${content.hashCode}'),
fit: BoxFit.cover, fit: BoxFit.cover,
width: (radius != null ? radius! : 20) * 2,
height: (radius != null ? radius! : 20) * 2,
cacheWidth: (radius != null ? radius! : 20) * 2,
cacheHeight: (radius != null ? radius! : 20) * 2,
), ),
), ),
), ),

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,6 @@ import 'package:photo_view/photo_view_gallery.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/providers/user_directory.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
@ -418,8 +417,7 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ud = context.read<UserDirectoryProvider>(); final account = data.account!;
final account = ud.getFromCache(data.accountId);
const tableGap = TableRow( const tableGap = TableRow(
children: [ children: [
@ -461,12 +459,12 @@ class _AttachmentZoomDetailPopup extends StatelessWidget {
children: [ children: [
if (data.accountId > 0) if (data.accountId > 0)
AccountImage( AccountImage(
content: account?.avatar, content: account.avatar,
radius: 8, radius: 8,
), ),
const Gap(8), const Gap(8),
Text(data.accountId > 0 Text(data.accountId > 0
? account?.nick ?? 'unknown'.tr() ? account.nick
: 'unknown'.tr()), : 'unknown'.tr()),
const Gap(8), const Gap(8),
Text('#${data.accountId}', Text('#${data.accountId}',

View File

@ -8,12 +8,12 @@ import 'package:surface/widgets/account/account_image.dart';
class NoContentWidget extends StatefulWidget { class NoContentWidget extends StatefulWidget {
final SnAccount? userinfo; final SnAccount? userinfo;
final bool isSpeaking; final bool isSpeaking;
final bool isFixed; final double? avatarSize;
const NoContentWidget({ const NoContentWidget({
super.key, super.key,
this.userinfo, this.userinfo,
this.isFixed = false, this.avatarSize,
required this.isSpeaking, required this.isSpeaking,
}); });
@ -45,17 +45,13 @@ class _NoContentWidgetState extends State<NoContentWidget>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double radius = widget.isFixed final double radius = widget.avatarSize ??
? 32 math.min(
: math.min(
MediaQuery.of(context).size.width * 0.1, MediaQuery.of(context).size.width * 0.1,
MediaQuery.of(context).size.height * 0.1, MediaQuery.of(context).size.height * 0.1,
); );
return Container( return Animate(
alignment: Alignment.center,
child: Center(
child: Animate(
autoPlay: false, autoPlay: false,
controller: _animationController, controller: _animationController,
effects: [ effects: [
@ -79,8 +75,6 @@ class _NoContentWidgetState extends State<NoContentWidget>
content: widget.userinfo?.avatar, content: widget.userinfo?.avatar,
radius: radius, radius: radius,
), ),
),
),
); );
} }

View File

@ -2,7 +2,9 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:gap/gap.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/types/account.dart'; import 'package:surface/types/account.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/widgets/chat/call/call_no_content.dart'; import 'package:surface/widgets/chat/call/call_no_content.dart';
@ -11,23 +13,32 @@ import 'package:surface/widgets/chat/call/call_participant_menu.dart';
import 'package:surface/widgets/chat/call/call_participant_stats.dart'; import 'package:surface/widgets/chat/call/call_participant_stats.dart';
abstract class ParticipantWidget extends StatefulWidget { abstract class ParticipantWidget extends StatefulWidget {
static ParticipantWidget widgetFor(ParticipantTrack participantTrack, static ParticipantWidget widgetFor(
{bool isFixed = false, bool showStatsLayer = false}) { ParticipantTrack participantTrack, {
double? avatarSize,
EdgeInsets? padding,
bool showStatsLayer = false,
bool isList = false,
}) {
if (participantTrack.participant is LocalParticipant) { if (participantTrack.participant is LocalParticipant) {
return LocalParticipantWidget( return LocalParticipantWidget(
participantTrack.participant as LocalParticipant, participantTrack.participant as LocalParticipant,
participantTrack.videoTrack, participantTrack.videoTrack,
isFixed, avatarSize,
participantTrack.isScreenShare, participantTrack.isScreenShare,
showStatsLayer, showStatsLayer,
isList,
padding,
); );
} else if (participantTrack.participant is RemoteParticipant) { } else if (participantTrack.participant is RemoteParticipant) {
return RemoteParticipantWidget( return RemoteParticipantWidget(
participantTrack.participant as RemoteParticipant, participantTrack.participant as RemoteParticipant,
participantTrack.videoTrack, participantTrack.videoTrack,
isFixed, avatarSize,
participantTrack.isScreenShare, participantTrack.isScreenShare,
showStatsLayer, showStatsLayer,
isList,
padding,
); );
} }
throw UnimplementedError('Unknown participant type'); throw UnimplementedError('Unknown participant type');
@ -36,8 +47,10 @@ abstract class ParticipantWidget extends StatefulWidget {
abstract final Participant participant; abstract final Participant participant;
abstract final VideoTrack? videoTrack; abstract final VideoTrack? videoTrack;
abstract final bool isScreenShare; abstract final bool isScreenShare;
abstract final bool isFixed; abstract final double? avatarSize;
abstract final bool showStatsLayer; abstract final bool showStatsLayer;
abstract final bool isList;
abstract final EdgeInsets? padding;
final VideoQuality quality; final VideoQuality quality;
const ParticipantWidget({ const ParticipantWidget({
@ -52,18 +65,24 @@ class LocalParticipantWidget extends ParticipantWidget {
@override @override
final VideoTrack? videoTrack; final VideoTrack? videoTrack;
@override @override
final bool isFixed; final double? avatarSize;
@override @override
final bool isScreenShare; final bool isScreenShare;
@override @override
final bool showStatsLayer; final bool showStatsLayer;
@override
final bool isList;
@override
final EdgeInsets? padding;
const LocalParticipantWidget( const LocalParticipantWidget(
this.participant, this.participant,
this.videoTrack, this.videoTrack,
this.isFixed, this.avatarSize,
this.isScreenShare, this.isScreenShare,
this.showStatsLayer, { this.showStatsLayer,
this.isList,
this.padding, {
super.key, super.key,
}); });
@ -77,18 +96,24 @@ class RemoteParticipantWidget extends ParticipantWidget {
@override @override
final VideoTrack? videoTrack; final VideoTrack? videoTrack;
@override @override
final bool isFixed; final double? avatarSize;
@override @override
final bool isScreenShare; final bool isScreenShare;
@override @override
final bool showStatsLayer; final bool showStatsLayer;
@override
final bool isList;
@override
final EdgeInsets? padding;
const RemoteParticipantWidget( const RemoteParticipantWidget(
this.participant, this.participant,
this.videoTrack, this.videoTrack,
this.isFixed, this.avatarSize,
this.isScreenShare, this.isScreenShare,
this.showStatsLayer, { this.showStatsLayer,
this.isList,
this.padding, {
super.key, super.key,
}); });
@ -136,19 +161,82 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
} }
@override @override
Widget build(BuildContext ctx) { Widget build(BuildContext context) {
if (widget.isList) {
return Padding(
padding: widget.padding ?? EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
SizedBox(
width: (widget.avatarSize ?? 32) * 2,
height: (widget.avatarSize ?? 32) * 2,
child: Center(
child: NoContentWidget(
userinfo: _userinfoMetadata,
avatarSize: widget.avatarSize,
isSpeaking: widget.participant.isSpeaking,
),
),
),
const Gap(8),
Expanded(
child: SizedBox(
height: (widget.avatarSize ?? 32) * 2,
child: ParticipantInfoWidget(
isList: true,
title: widget.participant.name.isNotEmpty
? widget.participant.name
: widget.participant.identity,
audioAvailable: _firstAudioPublication?.muted == false &&
_firstAudioPublication?.subscribed == true,
connectionQuality: widget.participant.connectionQuality,
isScreenShare: widget.isScreenShare,
),
),
),
],
),
if (_activeVideoTrack != null && !_activeVideoTrack!.muted)
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Material(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: Theme.of(context)
.colorScheme
.surfaceContainer
.withOpacity(0.75),
child: VideoTrackRenderer(
_activeVideoTrack!,
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
),
),
).padding(top: 8),
),
],
),
);
}
return Stack( return Stack(
children: [ children: [
_activeVideoTrack != null && !_activeVideoTrack!.muted if (_activeVideoTrack != null && !_activeVideoTrack!.muted)
? VideoTrackRenderer( VideoTrackRenderer(
_activeVideoTrack!, _activeVideoTrack!,
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
) )
: NoContentWidget( else
Center(
child: NoContentWidget(
userinfo: _userinfoMetadata, userinfo: _userinfoMetadata,
isFixed: widget.isFixed, avatarSize: widget.avatarSize,
isSpeaking: widget.participant.isSpeaking, isSpeaking: widget.participant.isSpeaking,
), ),
),
if (widget.showStatsLayer) if (widget.showStatsLayer)
Positioned( Positioned(
top: 30, top: 30,
@ -199,33 +287,31 @@ class _RemoteParticipantWidgetState
} }
class InteractiveParticipantWidget extends StatelessWidget { class InteractiveParticipantWidget extends StatelessWidget {
final double? width; final double? avatarSize;
final double? height; final bool isList;
final Color? color;
final bool isFixedAvatar;
final ParticipantTrack participant; final ParticipantTrack participant;
final Function() onTap; final Function? onTap;
final EdgeInsets? padding;
const InteractiveParticipantWidget({ const InteractiveParticipantWidget({
super.key, super.key,
this.width, this.avatarSize,
this.height, this.isList = false,
this.color, this.padding,
this.isFixedAvatar = false,
required this.participant, required this.participant,
required this.onTap, this.onTap,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return Material(
child: Container( color: Colors.transparent,
width: width, child: InkWell(
height: height, onTap: onTap != null
color: color, ? () {
child: ParticipantWidget.widgetFor(participant, isFixed: isFixedAvatar), onTap?.call();
), }
onTap: () => onTap(), : null,
onLongPress: () { onLongPress: () {
if (participant.participant is LocalParticipant) return; if (participant.participant is LocalParticipant) return;
showModalBottomSheet( showModalBottomSheet(
@ -237,6 +323,15 @@ class InteractiveParticipantWidget extends StatelessWidget {
), ),
); );
}, },
child: Container(
child: ParticipantWidget.widgetFor(
participant,
avatarSize: avatarSize,
isList: isList,
padding: padding,
),
),
),
); );
} }
} }

View File

@ -9,6 +9,7 @@ class ParticipantInfoWidget extends StatelessWidget {
final bool audioAvailable; final bool audioAvailable;
final ConnectionQuality connectionQuality; final ConnectionQuality connectionQuality;
final bool isScreenShare; final bool isScreenShare;
final bool isList;
const ParticipantInfoWidget({ const ParticipantInfoWidget({
super.key, super.key,
@ -16,10 +17,69 @@ class ParticipantInfoWidget extends StatelessWidget {
this.audioAvailable = true, this.audioAvailable = true,
this.connectionQuality = ConnectionQuality.unknown, this.connectionQuality = ConnectionQuality.unknown,
this.isScreenShare = false, this.isScreenShare = false,
this.isList = false,
}); });
@override @override
Widget build(BuildContext context) => Container( Widget build(BuildContext context) {
if (isList) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title != null)
Text(
title!,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
).padding(left: 2),
Row(
children: [
isScreenShare
? const Icon(
Symbols.monitor,
color: Colors.white,
size: 16,
)
: Icon(
audioAvailable ? Symbols.mic : Symbols.mic_off,
color: audioAvailable ? Colors.white : Colors.red,
size: 16,
),
const Gap(3),
if (connectionQuality != ConnectionQuality.unknown)
Icon(
{
ConnectionQuality.excellent: Symbols.signal_cellular_alt,
ConnectionQuality.good: Symbols.signal_cellular_alt_2_bar,
ConnectionQuality.poor: Symbols.signal_cellular_alt_1_bar,
}[connectionQuality],
color: {
ConnectionQuality.excellent: Colors.green,
ConnectionQuality.good: Colors.orange,
ConnectionQuality.poor: Colors.red,
}[connectionQuality],
size: 16,
)
else
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
).padding(all: 3),
],
)
],
);
}
return Container(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75), color: Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 7, vertical: 7,
@ -77,3 +137,4 @@ class ParticipantInfoWidget extends StatelessWidget {
), ),
); );
} }
}

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

@ -26,11 +26,13 @@ class _LinkPreviewWidgetState extends State<LinkPreviewWidget> {
Future<void> _getLinkMeta() async { Future<void> _getLinkMeta() async {
final linkRegex = RegExp(r'https?:\/\/[^\s/$.?#].[^\s]*'); final linkRegex = RegExp(r'https?:\/\/[^\s/$.?#].[^\s]*');
final links = linkRegex.allMatches(widget.text).map((e) => e.group(0)).toSet(); final links =
linkRegex.allMatches(widget.text).map((e) => e.group(0)).toSet();
final lp = context.read<SnLinkPreviewProvider>(); final lp = context.read<SnLinkPreviewProvider>();
final List<Future<SnLinkMeta?>> futures = links.where((e) => e != null).map((e) => lp.getLinkMeta(e!)).toList(); final List<Future<SnLinkMeta?>> futures =
links.where((e) => e != null).map((e) => lp.getLinkMeta(e!)).toList();
final results = await Future.wait(futures); final results = await Future.wait(futures);
_links.addAll(results.where((e) => e != null).map((e) => e!).toList()); _links.addAll(results.where((e) => e != null).map((e) => e!).toList());
@ -66,7 +68,9 @@ class _LinkPreviewEntry extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE) ? double.infinity : 480, maxWidth: ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)
? double.infinity
: 480,
), ),
child: GestureDetector( child: GestureDetector(
child: Card( child: Card(
@ -74,20 +78,29 @@ class _LinkPreviewEntry extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (meta.image != null) if (meta.image != null)
Container( ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: Container(
margin: const EdgeInsets.only(bottom: 4), margin: const EdgeInsets.only(bottom: 4),
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: AutoResizeUniversalImage( child: AutoResizeUniversalImage(
meta.image!.startsWith('//') ? 'https:${meta.image}' : meta.image!, meta.image!.startsWith('//')
? 'https:${meta.image}'
: meta.image!,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
), ),
), ),
), ),
),
SizedBox( SizedBox(
height: 48, height: 48,
child: Row( child: Row(
@ -98,7 +111,8 @@ class _LinkPreviewEntry extends StatelessWidget {
width: 36, width: 36,
height: 36, height: 36,
child: meta.icon!.endsWith('.svg') child: meta.icon!.endsWith('.svg')
? SvgPicture.network(meta.icon!, width: 36, height: 36) ? SvgPicture.network(meta.icon!,
width: 36, height: 36)
: UniversalImage( : UniversalImage(
meta.icon!, meta.icon!,
noErrorWidget: true, noErrorWidget: true,

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,56 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
child: WindowTitleBarBox(), child: WindowTitleBarBox(),
), ),
Gap(MediaQuery.of(context).padding.top), Gap(MediaQuery.of(context).padding.top),
Expanded( Column(
child: _DrawerContentList(), mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Solar Network').bold(),
AppVersionLabel(),
],
).padding(
horizontal: 32,
vertical: 12,
), ),
if (cfg.hideBottomNav) Expanded(
Row( child: ListView(
spacing: 8, padding: EdgeInsets.zero,
children: nav.destinations.where((ele) => ele.isPinned).map( children: [
(ele) { ...nav.destinations.mapIndexed((idx, ele) {
return Expanded( return ListTile(
child: Tooltip( leading: ele.icon,
message: ele.label.tr(), title: Text(ele.label).tr(),
child: IconButton.filledTonal( contentPadding: EdgeInsets.symmetric(horizontal: 24),
icon: ele.icon, selected: nav.currentIndex == idx,
color: Theme.of(context) onTap: () {
.colorScheme GoRouter.of(context).pushNamed(ele.screen);
.onPrimaryContainer, nav.setIndex(idx);
onPressed: () {
GoRouter.of(context).goNamed(ele.screen);
Scaffold.of(context).closeDrawer(); 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 +139,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
}, },
), ),
), ),
Gap(MediaQuery.of(context).padding.bottom), Gap(MediaQuery.of(context).padding.bottom + 8),
], ],
), ),
); );
@ -146,163 +147,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: ResponsiveScaffold.getIsExpand(context),
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;
@ -190,9 +179,7 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return SizedBox( return Column(
height: MediaQuery.of(context).size.height * 0.85,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
@ -240,7 +227,6 @@ class _PostCommentListPopupState extends State<PostCommentListPopup> {
), ),
), ),
], ],
),
); );
} }
} }

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,9 +271,13 @@ 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.body['attachments']
?.where((ele) => ?.map((e) => SnAttachment.fromJson(e))
.cast<SnAttachment>()
.where((ele) =>
ele?.mediaType != SnMediaType.image || ele?.mediaType != SnMediaType.image ||
widget.data.type != 'article') widget.data.type != 'article')
.toList(); .toList();
@ -290,7 +286,7 @@ class _PostItemState extends State<PostItem> {
var attachmentSize = math.min( var attachmentSize = math.min(
MediaQuery.of(context).size.width, widget.maxWidth ?? double.infinity); MediaQuery.of(context).size.width, widget.maxWidth ?? double.infinity);
if ((widget.data.preload?.attachments?.length ?? 0) > 1) { if ((widget.data.body['attachments']?.length ?? 0) > 1) {
attachmentSize -= 80; attachmentSize -= 80;
} }
@ -333,6 +329,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,
@ -346,7 +343,7 @@ class _PostItemState extends State<PostItem> {
], ],
), ),
const Gap(8), const Gap(8),
if (widget.data.preload?.thumbnail != null) if (widget.data.body['thumbnail'] != null)
Container( Container(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -366,14 +363,14 @@ class _PostItemState extends State<PostItem> {
), ),
child: AutoResizeUniversalImage( child: AutoResizeUniversalImage(
sn.getAttachmentUrl( sn.getAttachmentUrl(
widget.data.preload!.thumbnail!.rid, widget.data.body['thumbnail']['rid'],
), ),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
), ),
), ),
if (widget.data.preload?.video != null) if (widget.data.body['video'] != null)
_PostVideoPlayer(data: widget.data).padding(bottom: 8), _PostVideoPlayer(data: widget.data).padding(bottom: 8),
if (widget.data.type == 'question') if (widget.data.type == 'question')
_PostQuestionHint(data: widget.data).padding(bottom: 8), _PostQuestionHint(data: widget.data).padding(bottom: 8),
@ -460,8 +457,8 @@ class _PostItemState extends State<PostItem> {
if (widget.data.repostTo != null) if (widget.data.repostTo != null)
_PostQuoteContent(child: widget.data.repostTo!).padding( _PostQuoteContent(child: widget.data.repostTo!).padding(
top: 4, top: 4,
bottom: widget.data.preload?.attachments?.isNotEmpty ?? bottom:
false widget.data.body['attachments'].isNotEmpty ?? false
? 12 ? 12
: 0, : 0,
), ),
@ -484,11 +481,11 @@ class _PostItemState extends State<PostItem> {
fit: widget.showFullPost ? BoxFit.cover : BoxFit.contain, fit: widget.showFullPost ? BoxFit.cover : BoxFit.contain,
padding: EdgeInsets.only(left: 12, right: 12), padding: EdgeInsets.only(left: 12, right: 12),
), ),
if (widget.data.preload?.poll != null) if (widget.data.poll != null)
StyledWidget(Container( StyledWidget(Container(
constraints: constraints:
BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity), BoxConstraints(maxWidth: widget.maxWidth ?? double.infinity),
child: PostPoll(poll: widget.data.preload!.poll!), child: PostPoll(poll: widget.data.poll!),
)) ))
.padding( .padding(
left: 12, left: 12,
@ -577,6 +574,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,
@ -589,7 +587,7 @@ class _PostItemState extends State<PostItem> {
), ),
], ],
).padding(bottom: widget.showCompactAvatar ? 4 : 0), ).padding(bottom: widget.showCompactAvatar ? 4 : 0),
if (widget.data.preload?.thumbnail != null) if (widget.data.body['thumbnail'] != null)
Container( Container(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -609,14 +607,14 @@ class _PostItemState extends State<PostItem> {
), ),
child: AutoResizeUniversalImage( child: AutoResizeUniversalImage(
sn.getAttachmentUrl( sn.getAttachmentUrl(
widget.data.preload!.thumbnail!.rid, widget.data.body['thumbnail']['rid'],
), ),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
), ),
), ),
if (widget.data.preload?.video != null) if (widget.data.body['video'] != null)
_PostVideoPlayer(data: widget.data) _PostVideoPlayer(data: widget.data)
.padding(bottom: 8), .padding(bottom: 8),
if (widget.data.type == 'question') if (widget.data.type == 'question')
@ -716,7 +714,7 @@ class _PostItemState extends State<PostItem> {
_isTranslated || _isTranslated ||
_isTranslating) && _isTranslating) &&
(widget.data.repostTo != null || (widget.data.repostTo != null ||
(widget.data.preload?.attachments (widget.data.body['attachments']
?.isNotEmpty ?? ?.isNotEmpty ??
false)) false))
? 8 ? 8
@ -726,7 +724,7 @@ class _PostItemState extends State<PostItem> {
_PostQuoteContent(child: widget.data.repostTo!) _PostQuoteContent(child: widget.data.repostTo!)
.padding( .padding(
bottom: bottom:
(widget.data.preload?.attachments?.isNotEmpty ?? (widget.data.body['attachments']?.isNotEmpty ??
false) false)
? 8 ? 8
: 0, : 0,
@ -750,8 +748,8 @@ class _PostItemState extends State<PostItem> {
padding: padding:
EdgeInsets.only(left: widget.showAvatar ? 60 : 12, right: 12), EdgeInsets.only(left: widget.showAvatar ? 60 : 12, right: 12),
), ),
if (widget.data.preload?.poll != null) if (widget.data.poll != null)
PostPoll(poll: widget.data.preload!.poll!).padding( PostPoll(poll: widget.data.poll!).padding(
left: widget.showAvatar ? 60 : 12, left: widget.showAvatar ? 60 : 12,
right: 12, right: 12,
top: 12, top: 12,
@ -812,7 +810,7 @@ class PostShareImageWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (data.preload?.thumbnail != null) if (data.body['thumbnail'] != null)
AspectRatio( AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: ClipRRect( child: ClipRRect(
@ -821,7 +819,7 @@ class PostShareImageWidget extends StatelessWidget {
topRight: Radius.circular(8), topRight: Radius.circular(8),
), ),
child: AutoResizeUniversalImage( child: AutoResizeUniversalImage(
sn.getAttachmentUrl(data.preload!.thumbnail!.rid), sn.getAttachmentUrl(data.body['thumbnail']['rid']),
fit: BoxFit.cover, fit: BoxFit.cover,
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
), ),
@ -859,9 +857,13 @@ class PostShareImageWidget extends StatelessWidget {
isRelativeDate: false, isRelativeDate: false,
).padding(horizontal: 16, bottom: 8), ).padding(horizontal: 16, bottom: 8),
if (data.type != 'article' && if (data.type != 'article' &&
(data.preload?.attachments?.isNotEmpty ?? false)) (data.body['attachments']?.isNotEmpty ?? false))
StyledWidget(AttachmentList( StyledWidget(AttachmentList(
data: data.preload!.attachments!, data: data.body['attachments']
?.map((e) => SnAttachment.fromJson(e))
.cast<SnAttachment>()
.toList() ??
[],
columned: true, columned: true,
fit: BoxFit.contain, fit: BoxFit.contain,
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
@ -1150,31 +1152,9 @@ class _PostHeadline extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isEnlarge) { if (isEnlarge) {
final sn = context.read<SnNetworkProvider>();
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (data.preload?.thumbnail != null)
Container(
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
child: AspectRatio(
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(data.preload!.thumbnail!.rid),
fit: BoxFit.cover,
),
),
),
),
if (data.body['title'] != null || (title?.isNotEmpty ?? false)) if (data.body['title'] != null || (title?.isNotEmpty ?? false))
Text( Text(
title ?? data.body['title'], title ?? data.body['title'],
@ -1259,7 +1239,7 @@ class _PostAvatar extends StatelessWidget {
: null; : null;
return GestureDetector( return GestureDetector(
child: data.preload?.realm == null child: data.realm == null
? AccountImage( ? AccountImage(
filterQuality: filterQuality, filterQuality: filterQuality,
content: data.publisher.avatar, content: data.publisher.avatar,
@ -1275,7 +1255,7 @@ class _PostAvatar extends StatelessWidget {
) )
: AccountImage( : AccountImage(
filterQuality: filterQuality, filterQuality: filterQuality,
content: data.preload!.realm!.avatar, content: data.realm!.avatar,
radius: isCompact ? 12 : 20, radius: isCompact ? 12 : 20,
borderRadius: isCompact ? 4 : 8, borderRadius: isCompact ? 4 : 8,
badgeOffset: Offset(-6, -4), badgeOffset: Offset(-6, -4),
@ -1317,6 +1297,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 +1305,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 +1379,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 +1392,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(
@ -1570,6 +1552,7 @@ class _PostContentHeader extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (isCompact) { if (isCompact) {
return Row( return Row(
spacing: 4,
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
@ -1577,7 +1560,6 @@ class _PostContentHeader extends StatelessWidget {
maxLines: 1, maxLines: 1,
).bold(), ).bold(),
), ),
const Gap(4),
Flexible( Flexible(
child: Text( child: Text(
isRelativeDate isRelativeDate
@ -1589,6 +1571,10 @@ class _PostContentHeader extends StatelessWidget {
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
).fontSize(13).opacity(0.8), ).fontSize(13).opacity(0.8),
), ),
if (data.editedAt != null)
Flexible(
child: Text('postEditedHint').tr().fontSize(13).opacity(0.8),
)
], ],
); );
} else { } else {
@ -1598,20 +1584,20 @@ class _PostContentHeader extends StatelessWidget {
Row( Row(
children: [ children: [
Text(data.publisher.nick).bold(), Text(data.publisher.nick).bold(),
if (data.preload?.realm != null) if (data.realm != null)
const Icon(Symbols.arrow_right, size: 16) const Icon(Symbols.arrow_right, size: 16)
.padding(horizontal: 2) .padding(horizontal: 2)
.opacity(0.5), .opacity(0.5),
if (data.preload?.realm != null) Text(data.preload!.realm!.name), if (data.realm != null) Text(data.realm!.name),
], ],
), ),
Row( Row(
spacing: 4,
children: [ children: [
Text( Text(
'@${data.publisher.name}', '@${data.publisher.name}',
maxLines: 1, maxLines: 1,
).fontSize(13), ).fontSize(13),
const Gap(4),
Text( Text(
isRelativeDate isRelativeDate
? RelativeTime(context) ? RelativeTime(context)
@ -1621,6 +1607,8 @@ class _PostContentHeader extends StatelessWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
).fontSize(13), ).fontSize(13),
if (data.editedAt != null)
Text('postEditedHint').tr().fontSize(13),
], ],
).opacity(0.8), ).opacity(0.8),
], ],
@ -1650,7 +1638,11 @@ class _PostContentBody extends StatelessWidget {
RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''), RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''),
textScaler: isEnlarge ? TextScaler.linear(1.1) : null, textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: text, content: text,
attachments: data.preload?.attachments, attachments: data.body['attachments']
?.map((e) => SnAttachment.fromJson(e))
.cast<SnAttachment>()
.toList() ??
[],
); );
if (isSelectable) { if (isSelectable) {
@ -1708,14 +1700,14 @@ class _PostQuoteContent extends StatelessWidget {
], ],
).padding(horizontal: 16), ).padding(horizontal: 16),
if (child.type != 'article' && if (child.type != 'article' &&
(child.preload?.attachments?.isNotEmpty ?? false)) (child.body['attachments']?.isNotEmpty ?? false))
ClipRRect( ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8), bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8), bottomRight: Radius.circular(8),
), ),
child: AttachmentList( child: AttachmentList(
data: child.preload!.attachments!, data: child.body['attachments']!,
maxHeight: 360, maxHeight: 360,
minWidth: 640, minWidth: 640,
fit: BoxFit.contain, fit: BoxFit.contain,
@ -2064,8 +2056,6 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) => PostCommentListPopup( builder: (context) => PostCommentListPopup(
post: widget.data, post: widget.data,
commentCount: widget.data.metric.replyCount, commentCount: widget.data.metric.replyCount,
@ -2354,7 +2344,7 @@ class _PostVideoPlayer extends StatelessWidget {
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AttachmentItem( child: AttachmentItem(
data: data.preload!.video!, data: data.body['video'],
heroTag: 'post-video-${data.id}', heroTag: 'post-video-${data.id}',
), ),
), ),

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
@ -23,13 +24,13 @@ import gal
import hotkey_manager_macos import hotkey_manager_macos
import in_app_review import in_app_review
import livekit_client import livekit_client
import livekit_noise_filter
import local_notifier import local_notifier
import media_kit_libs_macos_video import media_kit_libs_macos_video
import media_kit_video 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 +38,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"))
@ -58,13 +61,13 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin")) HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
LiveKitKrispNoiseFilterPlugin.register(with: registry.registrar(forPlugin: "LiveKitKrispNoiseFilterPlugin"))
LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin")) LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
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 +75,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):
@ -150,12 +152,15 @@ PODS:
- flutter_webrtc - flutter_webrtc
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
- livekit_noise_filter (0.0.1):
- flutter_webrtc
- FlutterMacOS
- LiveKitKrispNoiseFilter (= 0.0.7)
- LiveKitKrispNoiseFilter (0.0.7)
- local_notifier (0.1.0): - local_notifier (0.1.0):
- 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 +178,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 +214,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`)
@ -236,14 +242,13 @@ DEPENDENCIES:
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
- livekit_noise_filter (from `Flutter/ephemeral/.symlinks/plugins/livekit_noise_filter/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 +256,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:
@ -265,6 +271,7 @@ SPEC REPOS:
- GoogleDataTransport - GoogleDataTransport
- GoogleUtilities - GoogleUtilities
- HotKey - HotKey
- LiveKitKrispNoiseFilter
- nanopb - nanopb
- OrderedSet - OrderedSet
- PromisesObjC - PromisesObjC
@ -273,6 +280,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:
@ -313,12 +322,12 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos :path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
livekit_client: livekit_client:
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos :path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
livekit_noise_filter:
:path: Flutter/ephemeral/.symlinks/plugins/livekit_noise_filter/macos
local_notifier: local_notifier:
: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 +336,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 +350,65 @@ 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 livekit_noise_filter: c5710c0871ef3621b48c0b44d3c3ff938ba414b2
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 LiveKitKrispNoiseFilter: efe418ceca28163ace0ff222bd2cc02384645d84
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758
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:
@ -1325,6 +1381,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" version: "2.4.1"
livekit_noise_filter:
dependency: "direct main"
description:
name: livekit_noise_filter
sha256: "398bfd1cc63ada9dee9fd7ea415e2fc1e51e091a6d217aad3649b882c35c7fcb"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
local_notifier: local_notifier:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1393,18 +1457,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 +1481,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 +1497,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 +1889,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 +2262,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 +2318,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 +2462,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 +2518,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+86
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,8 @@ 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
livekit_noise_filter: ^0.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -179,6 +181,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"));
} }

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