Compare commits

..

20 Commits

Author SHA1 Message Date
87029e3538 🚀 Launch 2.2.2+55 2025-01-21 21:04:33 +08:00
127d9adc09 💄 Finishing up refactor background image related changes 2025-01-21 20:50:07 +08:00
c82dc7ad85 ♻️ Refactored background image (skip ci) 2025-01-21 20:35:04 +08:00
36bcff7a7c Add haptic feedback on post reaction 2025-01-21 15:07:44 +08:00
38201b547a 💄 Snackbar use floating mode while using m3 2025-01-21 15:06:27 +08:00
ed0334fcda 🐛 Bug fixes 2025-01-21 15:04:55 +08:00
fbb486b90b 💄 Optimized attachment list 2025-01-21 14:57:04 +08:00
9b34f385d5 🐛 Fix app bar buttons clicking event got absorb by indicators 2025-01-21 14:47:13 +08:00
bb7b731602 Rollback to old style attachment list 2025-01-21 12:12:21 +08:00
19076f8136 🚀 Launch 2.2.2+54 2025-01-20 17:35:13 +08:00
dc77a936ce ⬆️ Upgrade deps 2025-01-20 16:53:38 +08:00
7f58710c6f Notification indicator 2025-01-20 16:52:53 +08:00
068ddcdcdc 💄 Optimized connection indicator 2025-01-20 14:40:26 +08:00
f4e9252ca0 💄 Optimized post list 2025-01-20 14:21:41 +08:00
3b1e918117 🐛 Fix side nav cause render error 2025-01-20 01:43:11 +08:00
ed7981fdaf 🧪 Post max width 2025-01-19 17:20:24 +08:00
9698ca53e4 Swipe up to view attachment details 2025-01-19 11:44:14 +08:00
ddc1dc7daf 💄 Optimize attachment zoom page 2025-01-19 01:00:00 +08:00
1625a957f8 👔 Use material design 3 by default 2025-01-19 00:39:47 +08:00
2dc50d627e 🧱 Fix roadsign config 2025-01-16 21:51:28 +08:00
53 changed files with 1838 additions and 1433 deletions

View File

@ -17,6 +17,7 @@
android:label="Solian" android:label="Solian"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true"
android:requestLegacyExternalStorage="true"> android:requestLegacyExternalStorage="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"

View File

@ -193,6 +193,9 @@
"settingsColorSchemeDescription": "Set the application primary color.", "settingsColorSchemeDescription": "Set the application primary color.",
"settingsColorSeed": "Color Seed", "settingsColorSeed": "Color Seed",
"settingsColorSeedDescription": "Select one of the present color schemes.", "settingsColorSeedDescription": "Select one of the present color schemes.",
"settingsFeatures": "Features",
"settingsNotifyWithHaptic": "Haptic when Notified",
"settingsNotifyWithHapticDescription": "Vibrate lightly when a new notification appears in the foreground.",
"settingsNetwork": "Network", "settingsNetwork": "Network",
"settingsNetworkServer": "HyperNet Server", "settingsNetworkServer": "HyperNet Server",
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.", "settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
@ -215,8 +218,9 @@
"sensitiveContentCollapsed": "Sensitive content has been collapsed.", "sensitiveContentCollapsed": "Sensitive content has been collapsed.",
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.", "sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
"sensitiveContentReveal": "Reveal", "sensitiveContentReveal": "Reveal",
"serverConnecting": "Connecting to server...", "serverConnecting": "Connecting...",
"serverDisconnected": "Lost connection from server", "serverDisconnected": "Connection Lost",
"serverConnected": "Connected",
"fieldChatAlias": "Channel Alias", "fieldChatAlias": "Channel Alias",
"fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.", "fieldChatAliasHint": "The unique channel alias within the site, used to represent the channel in URL, leave blank to auto generate. Should be URL-Safe.",
"fieldChatName": "Name", "fieldChatName": "Name",
@ -294,6 +298,7 @@
"addAttachmentFromCameraPhoto": "Take photo", "addAttachmentFromCameraPhoto": "Take photo",
"addAttachmentFromCameraVideo": "Take video", "addAttachmentFromCameraVideo": "Take video",
"addAttachmentFromRandomId": "Link via RID", "addAttachmentFromRandomId": "Link via RID",
"attachmentDetailInfo": "Attachment details",
"attachmentPastedImage": "Pasted Image", "attachmentPastedImage": "Pasted Image",
"attachmentInsertLink": "Insert Link", "attachmentInsertLink": "Insert Link",
"attachmentSetAsPostThumbnail": "Set as post thumbnail", "attachmentSetAsPostThumbnail": "Set as post thumbnail",

View File

@ -191,6 +191,9 @@
"settingsColorSchemeDescription": "设置应用主题色。", "settingsColorSchemeDescription": "设置应用主题色。",
"settingsColorSeed": "预设色彩主题", "settingsColorSeed": "预设色彩主题",
"settingsColorSeedDescription": "选择一个预设色彩主题。", "settingsColorSeedDescription": "选择一个预设色彩主题。",
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知时振动",
"settingsNotifyWithHapticDescription": "在应用在前台时收到新通知出现时出发轻量的振动。",
"settingsNetwork": "网络", "settingsNetwork": "网络",
"settingsNetworkServer": "HyperNet 服务器", "settingsNetworkServer": "HyperNet 服务器",
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。", "settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
@ -213,8 +216,9 @@
"sensitiveContentCollapsed": "敏感内容已折叠。", "sensitiveContentCollapsed": "敏感内容已折叠。",
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。", "sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
"sensitiveContentReveal": "显示内容", "sensitiveContentReveal": "显示内容",
"serverConnecting": "正在连接服务器…", "serverConnecting": "正在连接…",
"serverDisconnected": "已与服务器断开连接", "serverDisconnected": "已断开连接",
"serverConnected": "已连接",
"fieldChatAlias": "频道别名", "fieldChatAlias": "频道别名",
"fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。", "fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
"fieldChatName": "名称", "fieldChatName": "名称",
@ -292,6 +296,7 @@
"addAttachmentFromCameraPhoto": "拍摄照片", "addAttachmentFromCameraPhoto": "拍摄照片",
"addAttachmentFromCameraVideo": "拍摄视频", "addAttachmentFromCameraVideo": "拍摄视频",
"addAttachmentFromRandomId": "通过访问 ID 链接", "addAttachmentFromRandomId": "通过访问 ID 链接",
"attachmentDetailInfo": "附件详细信息",
"attachmentPastedImage": "粘贴的图片", "attachmentPastedImage": "粘贴的图片",
"attachmentInsertLink": "插入连接", "attachmentInsertLink": "插入连接",
"attachmentSetAsPostThumbnail": "设置为帖子缩略图", "attachmentSetAsPostThumbnail": "设置为帖子缩略图",

View File

@ -191,6 +191,9 @@
"settingsColorSchemeDescription": "設置應用主題色。", "settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題", "settingsColorSeed": "預設色彩主題",
"settingsColorSeedDescription": "選擇一個預設色彩主題。", "settingsColorSeedDescription": "選擇一個預設色彩主題。",
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知時振動",
"settingsNotifyWithHapticDescription": "在應用在前台時收到新通知出現時出發輕量的振動。",
"settingsNetwork": "網絡", "settingsNetwork": "網絡",
"settingsNetworkServer": "HyperNet 服務器", "settingsNetworkServer": "HyperNet 服務器",
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
@ -213,8 +216,9 @@
"sensitiveContentCollapsed": "敏感內容已摺疊。", "sensitiveContentCollapsed": "敏感內容已摺疊。",
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
"sensitiveContentReveal": "顯示內容", "sensitiveContentReveal": "顯示內容",
"serverConnecting": "正在連接服務器…", "serverConnecting": "正在連接…",
"serverDisconnected": "已與服務器斷開連接", "serverDisconnected": "已斷開連接",
"serverConnected": "已連接",
"fieldChatAlias": "頻道別名", "fieldChatAlias": "頻道別名",
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
"fieldChatName": "名稱", "fieldChatName": "名稱",
@ -292,6 +296,7 @@
"addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraPhoto": "拍攝照片",
"addAttachmentFromCameraVideo": "拍攝視頻", "addAttachmentFromCameraVideo": "拍攝視頻",
"addAttachmentFromRandomId": "通過訪問 ID 鏈接", "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
"attachmentDetailInfo": "附件詳細信息",
"attachmentPastedImage": "粘貼的圖片", "attachmentPastedImage": "粘貼的圖片",
"attachmentInsertLink": "插入連接", "attachmentInsertLink": "插入連接",
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖", "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",

View File

@ -191,6 +191,9 @@
"settingsColorSchemeDescription": "設置應用主題色。", "settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題", "settingsColorSeed": "預設色彩主題",
"settingsColorSeedDescription": "選擇一個預設色彩主題。", "settingsColorSeedDescription": "選擇一個預設色彩主題。",
"settingsFeatures": "功能",
"settingsNotifyWithHaptic": "新通知時振動",
"settingsNotifyWithHapticDescription": "在應用在前臺時收到新通知出現時出發輕量的振動。",
"settingsNetwork": "網絡", "settingsNetwork": "網絡",
"settingsNetworkServer": "HyperNet 服務器", "settingsNetworkServer": "HyperNet 服務器",
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
@ -213,8 +216,9 @@
"sensitiveContentCollapsed": "敏感內容已摺疊。", "sensitiveContentCollapsed": "敏感內容已摺疊。",
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
"sensitiveContentReveal": "顯示內容", "sensitiveContentReveal": "顯示內容",
"serverConnecting": "正在連接服務器…", "serverConnecting": "正在連接…",
"serverDisconnected": "已與服務器斷開連接", "serverDisconnected": "已斷開連接",
"serverConnected": "已連接",
"fieldChatAlias": "頻道別名", "fieldChatAlias": "頻道別名",
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
"fieldChatName": "名稱", "fieldChatName": "名稱",
@ -292,6 +296,7 @@
"addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraPhoto": "拍攝照片",
"addAttachmentFromCameraVideo": "拍攝視頻", "addAttachmentFromCameraVideo": "拍攝視頻",
"addAttachmentFromRandomId": "通過訪問 ID 鏈接", "addAttachmentFromRandomId": "通過訪問 ID 鏈接",
"attachmentDetailInfo": "附件詳細信息",
"attachmentPastedImage": "粘貼的圖片", "attachmentPastedImage": "粘貼的圖片",
"attachmentInsertLink": "插入連接", "attachmentInsertLink": "插入連接",
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖", "attachmentSetAsPostThumbnail": "設置為帖子縮略圖",

View File

@ -43,58 +43,58 @@ PODS:
- Flutter - Flutter
- file_saver (0.0.1): - file_saver (0.0.1):
- Flutter - Flutter
- Firebase/Analytics (11.4.0): - Firebase/Analytics (11.6.0):
- Firebase/Core - Firebase/Core
- Firebase/Core (11.4.0): - Firebase/Core (11.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseAnalytics (~> 11.4.0) - FirebaseAnalytics (~> 11.6.0)
- Firebase/CoreOnly (11.4.0): - Firebase/CoreOnly (11.6.0):
- FirebaseCore (= 11.4.0) - FirebaseCore (~> 11.6.0)
- Firebase/Messaging (11.4.0): - Firebase/Messaging (11.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 11.4.0) - FirebaseMessaging (~> 11.6.0)
- firebase_analytics (11.3.6): - firebase_analytics (11.4.0):
- Firebase/Analytics (= 11.4.0) - Firebase/Analytics (= 11.6.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_core (3.9.0): - firebase_core (3.10.0):
- Firebase/CoreOnly (= 11.4.0) - Firebase/CoreOnly (= 11.6.0)
- Flutter - Flutter
- firebase_messaging (15.1.6): - firebase_messaging (15.2.0):
- Firebase/Messaging (= 11.4.0) - Firebase/Messaging (= 11.6.0)
- firebase_core - firebase_core
- Flutter - Flutter
- FirebaseAnalytics (11.4.0): - FirebaseAnalytics (11.6.0):
- FirebaseAnalytics/AdIdSupport (= 11.4.0) - FirebaseAnalytics/AdIdSupport (= 11.6.0)
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseAnalytics/AdIdSupport (11.4.0): - FirebaseAnalytics/AdIdSupport (11.6.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement (= 11.4.0) - GoogleAppMeasurement (= 11.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseCore (11.4.0): - FirebaseCore (11.6.0):
- FirebaseCoreInternal (~> 11.0) - FirebaseCoreInternal (~> 11.6.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0) - GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreInternal (11.6.0): - FirebaseCoreInternal (11.6.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseInstallations (11.4.0): - FirebaseInstallations (11.6.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- FirebaseMessaging (11.4.0): - FirebaseMessaging (11.6.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0) - GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -110,27 +110,27 @@ PODS:
- flutter_udid (0.0.1): - flutter_udid (0.0.1):
- Flutter - Flutter
- SAMKeychain - SAMKeychain
- flutter_webrtc (0.12.2): - flutter_webrtc (0.12.6):
- Flutter - Flutter
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
- gal (1.0.0): - gal (1.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- GoogleAppMeasurement (11.4.0): - GoogleAppMeasurement (11.6.0):
- GoogleAppMeasurement/AdIdSupport (= 11.4.0) - GoogleAppMeasurement/AdIdSupport (= 11.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/AdIdSupport (11.4.0): - GoogleAppMeasurement/AdIdSupport (11.6.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0) - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/WithoutAdIdSupport (11.4.0): - GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
@ -173,7 +173,7 @@ PODS:
- in_app_review (2.0.0): - in_app_review (2.0.0):
- Flutter - Flutter
- Kingfisher (8.1.3) - Kingfisher (8.1.3)
- livekit_client (2.3.4): - livekit_client (2.3.5):
- Flutter - Flutter
- flutter_webrtc - flutter_webrtc
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
@ -369,29 +369,29 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
firebase_analytics: 2815af29d49c1a994652abd37a5b001a88bc7b75 firebase_analytics: 07bd7cfbac54bfcdccf2bb2530f9a65486f7ef3f
firebase_core: b62a5080210edad3f2934314a8b2c6f5124e8e10 firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10
firebase_messaging: 98619a0572d82cfb3668e78859ba9f1110e268c9 firebase_messaging: e2f0ba891b1509668c07f5099761518a5af8fe3c
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49 FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563 flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011 in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
livekit_client: 4eaa7a2968fc7e7c57888f43d90394547cc8d9e9 livekit_client: dcc5fd47ba69c98fc6baeb12e862c9d43807d976
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e

View File

@ -279,6 +279,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
await ws.tryConnect(); await ws.tryConnect();
if (!mounted) return; if (!mounted) return;
final notify = context.read<NotificationProvider>(); final notify = context.read<NotificationProvider>();
notify.listen();
await notify.registerPushNotifications(); await notify.registerPushNotifications();
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;

View File

@ -14,6 +14,7 @@ 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 kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
const Map<String, FilterQuality> kImageQualityLevel = { const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none, 'settingsImageQualityLowest': FilterQuality.none,

View File

@ -4,18 +4,26 @@ import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_udid/flutter_udid.dart'; import 'package:flutter_udid/flutter_udid.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.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/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart';
import 'package:surface/types/notification.dart';
class NotificationProvider extends ChangeNotifier { class NotificationProvider extends ChangeNotifier {
late final SnNetworkProvider _sn; late final SnNetworkProvider _sn;
late final UserProvider _ua; late final UserProvider _ua;
late final WebSocketProvider _ws;
late final ConfigProvider _cfg;
NotificationProvider(BuildContext context) { NotificationProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>(); _sn = context.read<SnNetworkProvider>();
_ua = context.read<UserProvider>(); _ua = context.read<UserProvider>();
_ws = context.read<WebSocketProvider>();
_cfg = context.read<ConfigProvider>();
} }
Future<void> registerPushNotifications() async { Future<void> registerPushNotifications() async {
@ -62,4 +70,23 @@ class NotificationProvider extends ChangeNotifier {
}, },
); );
} }
List<SnNotification> notifications = List.empty(growable: true);
void listen() {
_ws.stream.stream.listen((event) {
if (event.method == 'notifications.new') {
final notification = SnNotification.fromJson(event.payload!);
notifications.add(notification);
notifyListeners();
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
if (doHaptic) HapticFeedback.lightImpact();
}
});
}
void clear() {
notifications.clear();
notifyListeners();
}
} }

View File

@ -36,10 +36,7 @@ import 'package:surface/widgets/navigation/app_scaffold.dart';
final _appRoutes = [ final _appRoutes = [
ShellRoute( ShellRoute(
builder: (context, state, child) => AppPageScaffold( builder: (context, state, child) => child,
body: child,
showAppBar: false,
),
routes: [ routes: [
GoRoute( GoRoute(
path: '/', path: '/',
@ -58,47 +55,39 @@ final _appRoutes = [
GoRoute( GoRoute(
path: '/write/:mode', path: '/write/:mode',
name: 'postEditor', name: 'postEditor',
builder: (context, state) => AppBackground( builder: (context, state) => PostEditorScreen(
child: PostEditorScreen( mode: state.pathParameters['mode']!,
mode: state.pathParameters['mode']!, postEditId: int.tryParse(
postEditId: int.tryParse( state.uri.queryParameters['editing'] ?? '',
state.uri.queryParameters['editing'] ?? '',
),
postReplyId: int.tryParse(
state.uri.queryParameters['replying'] ?? '',
),
postRepostId: int.tryParse(
state.uri.queryParameters['reposting'] ?? '',
),
extraProps: state.extra as PostEditorExtraProps?,
), ),
postReplyId: int.tryParse(
state.uri.queryParameters['replying'] ?? '',
),
postRepostId: int.tryParse(
state.uri.queryParameters['reposting'] ?? '',
),
extraProps: state.extra as PostEditorExtraProps?,
), ),
), ),
GoRoute( GoRoute(
path: '/search', path: '/search',
name: 'postSearch', name: 'postSearch',
builder: (context, state) => AppBackground( builder: (context, state) => PostSearchScreen(
child: PostSearchScreen( initialTags: state.uri.queryParameters['tags']?.split(','),
initialTags: state.uri.queryParameters['tags']?.split(','), initialCategories: state.uri.queryParameters['categories']?.split(','),
initialCategories: state.uri.queryParameters['categories']?.split(','),
),
), ),
), ),
GoRoute( GoRoute(
path: '/publishers/:name', path: '/publishers/:name',
name: 'postPublisher', name: 'postPublisher',
builder: (context, state) => AppBackground( builder: (context, state) => PostPublisherScreen(name: state.pathParameters['name']!),
child: PostPublisherScreen(name: state.pathParameters['name']!),
),
), ),
GoRoute( GoRoute(
path: '/:slug', path: '/:slug',
name: 'postDetail', name: 'postDetail',
builder: (context, state) => AppBackground( builder: (context, state) => PostDetailScreen(
child: PostDetailScreen( slug: state.pathParameters['slug']!,
slug: state.pathParameters['slug']!, preload: state.extra as SnPost?,
preload: state.extra as SnPost?,
),
), ),
), ),
], ],
@ -106,7 +95,15 @@ final _appRoutes = [
GoRoute( GoRoute(
path: '/account', path: '/account',
name: 'account', name: 'account',
pageBuilder: (context, state) => NoTransitionPage( pageBuilder: (context, state) => CustomTransitionPage(
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: child,
);
},
child: const AccountScreen(), child: const AccountScreen(),
), ),
routes: [], routes: [],
@ -114,7 +111,15 @@ final _appRoutes = [
GoRoute( GoRoute(
path: '/chat', path: '/chat',
name: 'chat', name: 'chat',
pageBuilder: (context, state) => NoTransitionPage( pageBuilder: (context, state) => CustomTransitionPage(
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent,
child: child,
);
},
child: const ChatScreen(), child: const ChatScreen(),
), ),
routes: [ routes: [
@ -228,57 +233,43 @@ final _appRoutes = [
], ],
), ),
ShellRoute( ShellRoute(
builder: (context, state, child) => AppPageScaffold(body: child), builder: (context, state, child) => child,
routes: [ routes: [
GoRoute( GoRoute(
path: '/auth/login', path: '/auth/login',
name: 'authLogin', name: 'authLogin',
builder: (context, state) => const AppBackground( builder: (context, state) => LoginScreen(),
child: LoginScreen(),
),
), ),
GoRoute( GoRoute(
path: '/auth/register', path: '/auth/register',
name: 'authRegister', name: 'authRegister',
builder: (context, state) => const AppBackground( builder: (context, state) => RegisterScreen(),
child: RegisterScreen(),
),
), ),
GoRoute( GoRoute(
path: '/reports', path: '/reports',
name: 'abuseReport', name: 'abuseReport',
builder: (context, state) => const AppBackground( builder: (context, state) => AbuseReportScreen(),
child: AbuseReportScreen(),
),
), ),
GoRoute( GoRoute(
path: '/account/profile/edit', path: '/account/profile/edit',
name: 'accountProfileEdit', name: 'accountProfileEdit',
builder: (context, state) => const AppBackground( builder: (context, state) => ProfileEditScreen(),
child: ProfileEditScreen(),
),
), ),
GoRoute( GoRoute(
path: '/account/publishers', path: '/account/publishers',
name: 'accountPublishers', name: 'accountPublishers',
builder: (context, state) => const AppBackground( builder: (context, state) => PublisherScreen(),
child: PublisherScreen(),
),
), ),
GoRoute( GoRoute(
path: '/account/publishers/new', path: '/account/publishers/new',
name: 'accountPublisherNew', name: 'accountPublisherNew',
builder: (context, state) => const AppBackground( builder: (context, state) => AccountPublisherNewScreen(),
child: AccountPublisherNewScreen(),
),
), ),
GoRoute( GoRoute(
path: '/account/publishers/edit/:name', path: '/account/publishers/edit/:name',
name: 'accountPublisherEdit', name: 'accountPublisherEdit',
builder: (context, state) => AppBackground( builder: (context, state) => AccountPublisherEditScreen(
child: AccountPublisherEditScreen( name: state.pathParameters['name']!,
name: state.pathParameters['name']!,
),
), ),
), ),
], ],
@ -291,26 +282,22 @@ final _appRoutes = [
), ),
), ),
ShellRoute( ShellRoute(
builder: (context, state, child) => AppPageScaffold(body: child), builder: (context, state, child) => child,
routes: [ routes: [
GoRoute( GoRoute(
path: '/settings', path: '/settings',
name: 'settings', name: 'settings',
builder: (context, state) => const AppBackground( builder: (context, state) => SettingsScreen(),
child: SettingsScreen(),
),
), ),
], ],
), ),
ShellRoute( ShellRoute(
builder: (context, state, child) => AppPageScaffold(body: child), builder: (context, state, child) => child,
routes: [ routes: [
GoRoute( GoRoute(
path: '/about', path: '/about',
name: 'about', name: 'about',
builder: (context, state) => const AppBackground( builder: (context, state) => AboutScreen(),
child: AboutScreen(),
),
), ),
], ],
), ),

View File

@ -6,6 +6,7 @@ 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/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import '../types/account.dart'; import '../types/account.dart';
@ -56,7 +57,11 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAbuseReport').tr(),
),
body: Column( body: Column(
children: [ children: [
ListTile( ListTile(
@ -73,6 +78,7 @@ class _AbuseReportScreenState extends State<AbuseReportScreen> {
else else
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
padding: EdgeInsets.only(top: 8),
itemCount: _reports.length, itemCount: _reports.length,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
return ListTile( return ListTile(

View File

@ -12,6 +12,7 @@ import 'package:surface/providers/websocket.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.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/navigation/app_scaffold.dart';
class AccountScreen extends StatelessWidget { class AccountScreen extends StatelessWidget {
const AccountScreen({super.key}); const AccountScreen({super.key});
@ -20,7 +21,7 @@ class AccountScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text("screenAccount").tr(), title: Text("screenAccount").tr(),

View File

@ -18,6 +18,7 @@ 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/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/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
class ProfileEditScreen extends StatefulWidget { class ProfileEditScreen extends StatefulWidget {
@ -81,8 +82,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
onDateTimeChanged: (DateTime newDate) { onDateTimeChanged: (DateTime newDate) {
setState(() { setState(() {
_birthday = newDate; _birthday = newDate;
_birthdayController.text = _birthdayController.text = DateFormat(_kDateFormat).format(_birthday!);
DateFormat(_kDateFormat).format(_birthday!);
}); });
}, },
), ),
@ -96,11 +96,9 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
if (image == null) return; if (image == null) return;
if (!mounted) return; if (!mounted) return;
final ImageProvider imageProvider = final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path)); final aspectRatios =
final aspectRatios = place == 'banner' place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
? [CropAspectRatio(width: 16, height: 7)]
: [CropAspectRatio(width: 1, height: 1)];
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS)) final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
? await showCupertinoImageCropper( ? await showCupertinoImageCropper(
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
@ -122,10 +120,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final rawBytes = final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
.buffer
.asUint8List();
try { try {
final attachment = await attach.directUploadOne( final attachment = await attach.directUploadOne(
@ -212,136 +207,141 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return SingleChildScrollView( return AppScaffold(
child: Column( appBar: AppBar(
crossAxisAlignment: CrossAxisAlignment.start, leading: const PageBackButton(),
children: [ title: Text('screenAccountProfileEdit').tr(),
LoadingIndicator(isActive: _isBusy), ),
const Gap(24), body: SingleChildScrollView(
Stack( child: Column(
clipBehavior: Clip.none, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Material( LoadingIndicator(isActive: _isBusy),
elevation: 0, const Gap(24),
child: InkWell( Stack(
child: ClipRRect( clipBehavior: Clip.none,
borderRadius: const BorderRadius.all(Radius.circular(8)), children: [
child: AspectRatio( Material(
aspectRatio: 16 / 9, elevation: 0,
child: Container( child: InkWell(
color: child: ClipRRect(
Theme.of(context).colorScheme.surfaceContainerHigh, borderRadius: const BorderRadius.all(Radius.circular(8)),
child: _banner != null child: AspectRatio(
? AutoResizeUniversalImage( aspectRatio: 16 / 9,
sn.getAttachmentUrl(_banner!), child: Container(
fit: BoxFit.cover, color: Theme.of(context).colorScheme.surfaceContainerHigh,
) child: _banner != null
: const SizedBox.shrink(), ? AutoResizeUniversalImage(
sn.getAttachmentUrl(_banner!),
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
), ),
), ),
),
onTap: () {
_updateImage('banner');
},
),
),
Positioned(
bottom: -28,
left: 16,
child: Material(
elevation: 2,
borderRadius: const BorderRadius.all(Radius.circular(40)),
child: InkWell(
child: AccountImage(content: _avatar, radius: 40),
onTap: () { onTap: () {
_updateImage('avatar'); _updateImage('banner');
}, },
), ),
), ),
), Positioned(
], bottom: -28,
).padding(horizontal: padding), left: 16,
const Gap(8 + 28), child: Material(
Column( elevation: 2,
children: [ borderRadius: const BorderRadius.all(Radius.circular(40)),
TextField( child: InkWell(
readOnly: true, child: AccountImage(content: _avatar, radius: 40),
controller: _usernameController, onTap: () {
decoration: InputDecoration( _updateImage('avatar');
border: const UnderlineInputBorder(), },
labelText: 'fieldUsername'.tr(),
helperText: 'fieldUsernameCannotEditHint'.tr(),
),
),
const Gap(4),
TextField(
controller: _nicknameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr(),
),
),
const Gap(4),
Row(
children: [
Flexible(
flex: 1,
child: TextField(
controller: _firstNameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldFirstName'.tr(),
),
), ),
), ),
const Gap(8), ),
Flexible( ],
flex: 1, ).padding(horizontal: padding),
child: TextField( const Gap(8 + 28),
controller: _lastNameController, Column(
decoration: InputDecoration( children: [
border: const UnderlineInputBorder(), TextField(
labelText: 'fieldLastName'.tr(), readOnly: true,
controller: _usernameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldUsername'.tr(),
helperText: 'fieldUsernameCannotEditHint'.tr(),
),
),
const Gap(4),
TextField(
controller: _nicknameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr(),
),
),
const Gap(4),
Row(
children: [
Flexible(
flex: 1,
child: TextField(
controller: _firstNameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldFirstName'.tr(),
),
), ),
), ),
const Gap(8),
Flexible(
flex: 1,
child: TextField(
controller: _lastNameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldLastName'.tr(),
),
),
),
],
),
const Gap(4),
TextField(
controller: _descriptionController,
keyboardType: TextInputType.multiline,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldDescription'.tr(),
), ),
],
),
const Gap(4),
TextField(
controller: _descriptionController,
keyboardType: TextInputType.multiline,
maxLines: null,
minLines: 3,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldDescription'.tr(),
), ),
), const Gap(4),
const Gap(4), TextField(
TextField( controller: _birthdayController,
controller: _birthdayController, readOnly: true,
readOnly: true, decoration: InputDecoration(
decoration: InputDecoration( border: const UnderlineInputBorder(),
border: const UnderlineInputBorder(), labelText: 'fieldBirthday'.tr(),
labelText: 'fieldBirthday'.tr(), ),
onTap: () => _selectBirthday(),
), ),
onTap: () => _selectBirthday(), ],
), ).padding(horizontal: padding + 8),
], const Gap(12),
).padding(horizontal: padding + 8), Row(
const Gap(12), mainAxisAlignment: MainAxisAlignment.end,
Row( children: [
mainAxisAlignment: MainAxisAlignment.end, ElevatedButton.icon(
children: [ onPressed: _isBusy ? null : _updateUserInfo,
ElevatedButton.icon( icon: const Icon(Symbols.save),
onPressed: _isBusy ? null : _updateUserInfo, label: Text('apply').tr(),
icon: const Icon(Symbols.save), ),
label: Text('apply').tr(), ],
), ).padding(horizontal: padding),
], ],
).padding(horizontal: padding), ),
],
), ),
); );
} }

View File

@ -19,6 +19,7 @@ import 'package:surface/types/check_in.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
const Map<String, (String, IconData, Color)> kBadgesMeta = { const Map<String, (String, IconData, Color)> kBadgesMeta = {
@ -241,6 +242,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return Scaffold( return Scaffold(
backgroundColor: Colors.transparent,
body: CustomScrollView( body: CustomScrollView(
controller: _scrollController, controller: _scrollController,
slivers: [ slivers: [

View File

@ -18,6 +18,7 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
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/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
class AccountPublisherEditScreen extends StatefulWidget { class AccountPublisherEditScreen extends StatefulWidget {
@ -176,7 +177,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return Scaffold( return AppScaffold(
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [

View File

@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/realm.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/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class AccountPublisherNewScreen extends StatefulWidget { class AccountPublisherNewScreen extends StatefulWidget {
const AccountPublisherNewScreen({super.key}); const AccountPublisherNewScreen({super.key});
@ -24,7 +25,11 @@ class _AccountPublisherNewScreenState extends State<AccountPublisherNewScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAccountPublisherNew').tr(),
),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [

View File

@ -10,6 +10,7 @@ import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class PublisherScreen extends StatefulWidget { class PublisherScreen extends StatefulWidget {
const PublisherScreen({super.key}); const PublisherScreen({super.key});
@ -32,8 +33,7 @@ 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( final List<SnPublisher> out = List<SnPublisher>.from(resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? []);
if (!mounted) return; if (!mounted) return;
@ -53,7 +53,11 @@ class _PublisherScreenState extends State<PublisherScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenAccountPublishers').tr(),
),
body: Column( body: Column(
children: [ children: [
ListTile( ListTile(
@ -62,9 +66,7 @@ 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) GoRouter.of(context).pushNamed('accountPublisherNew').then((value) {
.pushNamed('accountPublisherNew')
.then((value) {
if (value == true) { if (value == true) {
_publishers.clear(); _publishers.clear();
_fetchPublishers(); _fetchPublishers();
@ -75,48 +77,52 @@ class _PublisherScreenState extends State<PublisherScreen> {
const Divider(height: 1), const Divider(height: 1),
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
Expanded( Expanded(
child: RefreshIndicator( child: MediaQuery.removePadding(
onRefresh: () { context: context,
_publishers.clear(); removeTop: true,
return _fetchPublishers(); child: RefreshIndicator(
}, onRefresh: () {
child: ListView.builder( _publishers.clear();
itemCount: _publishers.length, return _fetchPublishers();
itemBuilder: (context, idx) {
final publisher = _publishers[idx];
return ListTile(
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(content: publisher.avatar),
trailing: PopupMenuButton(
itemBuilder: (BuildContext context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.edit),
const Gap(16),
Text('edit').tr(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'accountPublisherEdit',
pathParameters: {
'name': publisher.name,
},
).then((value) {
if (value == true) {
_publishers.clear();
_fetchPublishers();
}
});
},
),
],
),
);
}, },
child: ListView.builder(
itemCount: _publishers.length,
itemBuilder: (context, idx) {
final publisher = _publishers[idx];
return ListTile(
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(content: publisher.avatar),
trailing: PopupMenuButton(
itemBuilder: (BuildContext context) => [
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.edit),
const Gap(16),
Text('edit').tr(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'accountPublisherEdit',
pathParameters: {
'name': publisher.name,
},
).then((value) {
if (value == true) {
_publishers.clear();
_fetchPublishers();
}
});
},
),
],
),
);
},
),
), ),
), ),
), ),

View File

@ -11,6 +11,7 @@ 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';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class AlbumScreen extends StatefulWidget { class AlbumScreen extends StatefulWidget {
@ -82,7 +83,7 @@ class _AlbumScreenState extends State<AlbumScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
body: CustomScrollView( body: CustomScrollView(
controller: _scrollController, controller: _scrollController,
slivers: [ slivers: [

View File

@ -9,6 +9,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/auth.dart'; import 'package:surface/types/auth.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.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';
import '../../providers/websocket.dart'; import '../../providers/websocket.dart';
@ -35,67 +36,73 @@ class _LoginScreenState extends State<LoginScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Theme( return AppScaffold(
data: Theme.of(context).copyWith(canvasColor: Colors.transparent), appBar: AppBar(
child: SingleChildScrollView( leading: const PageBackButton(),
child: PageTransitionSwitcher( title: Text('screenAuthLogin').tr(),
transitionBuilder: ( ),
Widget child, body: Theme(
Animation<double> primaryAnimation, data: Theme.of(context).copyWith(canvasColor: Colors.transparent),
Animation<double> secondaryAnimation, child: SingleChildScrollView(
) { child: PageTransitionSwitcher(
return SharedAxisTransition( transitionBuilder: (
animation: primaryAnimation, Widget child,
secondaryAnimation: secondaryAnimation, Animation<double> primaryAnimation,
transitionType: SharedAxisTransitionType.horizontal, Animation<double> secondaryAnimation,
child: Container( ) {
constraints: BoxConstraints(maxWidth: 380), return SharedAxisTransition(
child: child, animation: primaryAnimation,
), secondaryAnimation: secondaryAnimation,
); transitionType: SharedAxisTransitionType.horizontal,
}, child: Container(
child: switch (_period % 3) { constraints: BoxConstraints(maxWidth: 380),
1 => _LoginPickerScreen( child: child,
key: const ValueKey(1), ),
ticket: _currentTicket, );
factors: _factors, },
onTicket: (p0) => setState(() { child: switch (_period % 3) {
_currentTicket = p0; 1 => _LoginPickerScreen(
}), key: const ValueKey(1),
onPickFactor: (p0) => setState(() { ticket: _currentTicket,
_factorPicked = p0; factors: _factors,
}), onTicket: (p0) => setState(() {
onNext: () => setState(() { _currentTicket = p0;
_period++; }),
}), onPickFactor: (p0) => setState(() {
), _factorPicked = p0;
2 => _LoginCheckScreen( }),
key: const ValueKey(2), onNext: () => setState(() {
ticket: _currentTicket, _period++;
factor: _factorPicked, }),
onTicket: (p0) => setState(() { ),
_currentTicket = p0; 2 => _LoginCheckScreen(
}), key: const ValueKey(2),
onNext: () => setState(() { ticket: _currentTicket,
_period = 1; factor: _factorPicked,
}), onTicket: (p0) => setState(() {
), _currentTicket = p0;
_ => _LoginLookupScreen( }),
key: const ValueKey(0), onNext: () => setState(() {
ticket: _currentTicket, _period = 1;
onTicket: (p0) => setState(() { }),
_currentTicket = p0; ),
}), _ => _LoginLookupScreen(
onFactor: (p0) => setState(() { key: const ValueKey(0),
_factors = p0; ticket: _currentTicket,
}), onTicket: (p0) => setState(() {
onNext: () => setState(() { _currentTicket = p0;
_period++; }),
}), onFactor: (p0) => setState(() {
), _factors = p0;
}, }),
).padding(all: 24), onNext: () => setState(() {
).center(), _period++;
}),
),
},
).padding(all: 24),
).center(),
),
); );
} }
} }
@ -441,7 +448,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
widget.onNext(); widget.onNext();
} catch (err) { } catch (err) {
if(mounted) context.showErrorDialog(err); if (mounted) context.showErrorDialog(err);
return; return;
} finally { } finally {
setState(() => _isBusy = false); setState(() => _isBusy = false);

View File

@ -8,6 +8,7 @@ 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/widgets/dialog.dart'; import 'package:surface/widgets/dialog.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';
class RegisterScreen extends StatefulWidget { class RegisterScreen extends StatefulWidget {
@ -54,175 +55,178 @@ class _RegisterScreenState extends State<RegisterScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StyledWidget(Container( return AppScaffold(
constraints: const BoxConstraints(maxWidth: 380), appBar: AppBar(
child: SingleChildScrollView( leading: const PageBackButton(),
child: Column( title: Text('screenAuthRegister').tr(),
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ body: StyledWidget(Container(
Align( constraints: const BoxConstraints(maxWidth: 380),
alignment: Alignment.centerLeft, child: SingleChildScrollView(
child: CircleAvatar( child: Column(
radius: 26, crossAxisAlignment: CrossAxisAlignment.start,
child: const Icon( children: [
Symbols.person_add, Align(
size: 28, alignment: Alignment.centerLeft,
), child: CircleAvatar(
).padding(bottom: 8), radius: 26,
), child: const Icon(
Text( Symbols.person_add,
'screenAuthRegister', size: 28,
style: const TextStyle( ),
fontSize: 28, ).padding(bottom: 8),
fontWeight: FontWeight.w900,
), ),
).tr().padding(left: 4, bottom: 16), Text(
Form( 'screenAuthRegister',
key: _formKey, style: const TextStyle(
autovalidateMode: AutovalidateMode.onUserInteraction, fontSize: 28,
child: Column( fontWeight: FontWeight.w900,
children: [ ),
TextFormField( ).tr().padding(left: 4, bottom: 16),
validator: (value) { Form(
if (value == null || value.length < 4 || value.length > 32) { key: _formKey,
return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]); autovalidateMode: AutovalidateMode.onUserInteraction,
} child: Column(
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) { children: [
return 'fieldUsernameAlphanumOnly'.tr(); TextFormField(
} validator: (value) {
return null; if (value == null || value.length < 4 || value.length > 32) {
}, return 'fieldUsernameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
autocorrect: false, }
enableSuggestions: false, if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
controller: _usernameController, return 'fieldUsernameAlphanumOnly'.tr();
autofillHints: const [AutofillHints.username], }
decoration: InputDecoration( return null;
isDense: true, },
border: const UnderlineInputBorder(), autocorrect: false,
labelText: 'fieldUsername'.tr(), enableSuggestions: false,
), controller: _usernameController,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), autofillHints: const [AutofillHints.username],
), decoration: InputDecoration(
const Gap(12), isDense: true,
TextFormField( border: const UnderlineInputBorder(),
validator: (value) { labelText: 'fieldUsername'.tr(),
if (value == null || value.length < 4 || value.length > 32) {
return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
}
return null;
},
autocorrect: false,
enableSuggestions: false,
controller: _nicknameController,
autofillHints: const [AutofillHints.nickname],
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
if (!EmailValidator.validate(value)) {
return 'fieldEmailAddressMustBeValid'.tr();
}
return null;
},
autocorrect: false,
enableSuggestions: false,
controller: _emailController,
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldEmail'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
return null;
},
obscureText: true,
autocorrect: false,
enableSuggestions: false,
autofillHints: const [AutofillHints.password],
controller: _passwordController,
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldPassword'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
).padding(horizontal: 7),
),
const Gap(16),
Align(
alignment: Alignment.centerRight,
child: StyledWidget(
Container(
constraints: const BoxConstraints(maxWidth: 290),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'termAcceptNextWithAgree'.tr(),
textAlign: TextAlign.end,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withAlpha((255 * 0.75).round()),
),
), ),
Material( onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
color: Colors.transparent, ),
child: InkWell( const Gap(12),
child: Row( TextFormField(
mainAxisSize: MainAxisSize.min, validator: (value) {
children: [ if (value == null || value.length < 4 || value.length > 32) {
Text('termAcceptLink'.tr()), return 'fieldNicknameLengthLimit'.tr(args: [4.toString(), 32.toString()]);
const Gap(4), }
const Icon(Symbols.launch, size: 14), return null;
], },
autocorrect: false,
enableSuggestions: false,
controller: _nicknameController,
autofillHints: const [AutofillHints.nickname],
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
if (!EmailValidator.validate(value)) {
return 'fieldEmailAddressMustBeValid'.tr();
}
return null;
},
autocorrect: false,
enableSuggestions: false,
controller: _emailController,
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldEmail'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(12),
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'fieldCannotBeEmpty'.tr();
}
return null;
},
obscureText: true,
autocorrect: false,
enableSuggestions: false,
autofillHints: const [AutofillHints.password],
controller: _passwordController,
decoration: InputDecoration(
isDense: true,
border: const UnderlineInputBorder(),
labelText: 'fieldPassword'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
).padding(horizontal: 7),
),
const Gap(16),
Align(
alignment: Alignment.centerRight,
child: StyledWidget(
Container(
constraints: const BoxConstraints(maxWidth: 290),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'termAcceptNextWithAgree'.tr(),
textAlign: TextAlign.end,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
),
),
Material(
color: Colors.transparent,
child: InkWell(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('termAcceptLink'.tr()),
const Gap(4),
const Icon(Symbols.launch, size: 14),
],
),
onTap: () {
launchUrlString('https://solsynth.dev/terms');
},
), ),
onTap: () {
launchUrlString('https://solsynth.dev/terms');
},
), ),
), ],
),
),
).padding(horizontal: 16),
),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => _performAction(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('next').tr(),
const Icon(Symbols.chevron_right),
], ],
), ),
), ),
).padding(horizontal: 16),
),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => _performAction(context),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('next').tr(),
const Icon(Symbols.chevron_right),
],
),
), ),
), ],
], ),
), ),
), )).padding(all: 24).center(),
)).padding(all: 24).center(); );
} }
} }

View File

@ -13,6 +13,7 @@ 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_scaffold.dart';
import 'package:surface/widgets/unauthorized_hint.dart'; import 'package:surface/widgets/unauthorized_hint.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -120,7 +121,7 @@ class _ChatScreenState extends State<ChatScreen> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenChat').tr(), title: Text('screenChat').tr(),
@ -131,7 +132,7 @@ class _ChatScreenState extends State<ChatScreen> {
); );
} }
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenChat').tr(), title: Text('screenChat').tr(),
@ -195,22 +196,58 @@ class _ChatScreenState extends State<ChatScreen> {
children: [ children: [
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
Expanded( Expanded(
child: RefreshIndicator( child: MediaQuery.removePadding(
onRefresh: () => Future.sync(() => _refreshChannels()), context: context,
child: ListView.builder( removeTop: true,
itemCount: _channels?.length ?? 0, child: RefreshIndicator(
itemBuilder: (context, idx) { onRefresh: () => Future.sync(() => _refreshChannels()),
final channel = _channels![idx]; child: ListView.builder(
final lastMessage = _lastMessages?[channel.id]; itemCount: _channels?.length ?? 0,
itemBuilder: (context, idx) {
final channel = _channels![idx];
final lastMessage = _lastMessages?[channel.id];
if (channel.type == 1) { if (channel.type == 1) {
final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere( final otherMember = channel.members?.cast<SnChannelMember?>().firstWhere(
(ele) => ele?.accountId != ua.user?.id, (ele) => ele?.accountId != ua.user?.id,
orElse: () => null, orElse: () => null,
); );
return ListTile(
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name),
subtitle: lastMessage != null
? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
'channelDirectMessageDescription'.tr(args: [
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar,
),
onTap: () {
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
).then((value) {
if (mounted) _refreshChannels();
});
},
);
}
return ListTile( return ListTile(
title: Text(ud.getAccountFromCache(otherMember?.accountId)?.nick ?? channel.name), title: Text(channel.name),
subtitle: lastMessage != null subtitle: lastMessage != null
? Text( ? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}', '${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
@ -218,15 +255,14 @@ class _ChatScreenState extends State<ChatScreen> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
) )
: Text( : Text(
'channelDirectMessageDescription'.tr(args: [ channel.description,
'@${ud.getAccountFromCache(otherMember?.accountId)?.name}',
]),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
contentPadding: const EdgeInsets.symmetric(horizontal: 16), contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage( leading: AccountImage(
content: ud.getAccountFromCache(otherMember?.accountId)?.avatar, content: null,
fallbackWidget: const Icon(Symbols.chat, size: 20),
), ),
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
@ -236,43 +272,12 @@ class _ChatScreenState extends State<ChatScreen> {
'alias': channel.alias, 'alias': channel.alias,
}, },
).then((value) { ).then((value) {
if (mounted) _refreshChannels(); if (value == true) _refreshChannels();
}); });
}, },
); );
} },
),
return ListTile(
title: Text(channel.name),
subtitle: lastMessage != null
? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
: Text(
channel.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
leading: AccountImage(
content: null,
fallbackWidget: const Icon(Symbols.chat, size: 20),
),
onTap: () {
GoRouter.of(context).pushNamed(
'chatRoom',
pathParameters: {
'scope': channel.realm?.alias ?? 'global',
'alias': channel.alias,
},
).then((value) {
if (value == true) _refreshChannels();
});
},
);
},
), ),
), ),
), ),

View File

@ -9,6 +9,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/chat_call.dart'; import 'package:surface/providers/chat_call.dart';
import 'package:surface/widgets/chat/call/call_controls.dart'; import 'package:surface/widgets/chat/call/call_controls.dart';
import 'package:surface/widgets/chat/call/call_participant.dart'; import 'package:surface/widgets/chat/call/call_participant.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
class CallRoomScreen extends StatefulWidget { class CallRoomScreen extends StatefulWidget {
final String scope; final String scope;
@ -152,7 +153,7 @@ class _CallRoomScreenState extends State<CallRoomScreen> {
return ListenableBuilder( return ListenableBuilder(
listenable: call, listenable: call,
builder: (context, _) { builder: (context, _) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: RichText( title: RichText(
textAlign: TextAlign.center, textAlign: TextAlign.center,

View File

@ -14,6 +14,7 @@ import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/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:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
class ChannelDetailScreen extends StatefulWidget { class ChannelDetailScreen extends StatefulWidget {
@ -189,7 +190,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 Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: _channel != null ? Text(_channel!.name) : Text('loading').tr(), title: _channel != null ? Text(_channel!.name) : Text('loading').tr(),
), ),

View File

@ -12,6 +12,7 @@ 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/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:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ChatManageScreen extends StatefulWidget { class ChatManageScreen extends StatefulWidget {
@ -121,7 +122,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: widget.editingChannelAlias != null title: widget.editingChannelAlias != null
? Text('screenChatManage').tr() ? Text('screenChatManage').tr()

View File

@ -20,6 +20,7 @@ import 'package:surface/widgets/chat/chat_message_input.dart';
import 'package:surface/widgets/chat/chat_typing_indicator.dart'; import 'package:surface/widgets/chat/chat_typing_indicator.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:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../../providers/user_directory.dart'; import '../../providers/user_directory.dart';
@ -211,7 +212,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
final call = context.watch<ChatCallProvider>(); final call = context.watch<ChatCallProvider>();
final ud = context.read<UserDirectoryProvider>(); final ud = context.read<UserDirectoryProvider>();
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
_channel?.type == 1 _channel?.type == 1

View File

@ -1,3 +1,4 @@
import 'package:animations/animations.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';
@ -8,9 +9,11 @@ import 'package:provider/provider.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';
import 'package:surface/screens/post/post_detail.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';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package: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';
@ -93,7 +96,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
floatingActionButtonLocation: ExpandableFab.location, floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab( floatingActionButton: ExpandableFab(
key: _fabKey, key: _fabKey,
@ -210,6 +213,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
), ),
), ),
), ),
const SliverGap(12),
SliverInfiniteList( SliverInfiniteList(
itemCount: _posts.length, itemCount: _posts.length,
isLoading: _isBusy, isLoading: _isBusy,
@ -217,27 +221,37 @@ class _ExploreScreenState extends State<ExploreScreen> {
hasReachedMax: _postCount != null && _posts.length >= _postCount!, hasReachedMax: _postCount != null && _posts.length >= _postCount!,
onFetchData: _fetchPosts, onFetchData: _fetchPosts,
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
return GestureDetector( return Center(
child: PostItem( child: OpenContainer(
data: _posts[idx], closedBuilder: (_, __) => Container(
maxWidth: 640, constraints: const BoxConstraints(maxWidth: 640),
onChanged: (data) { child: PostItem(
setState(() => _posts[idx] = data); data: _posts[idx],
}, maxWidth: 640,
onDeleted: () { onChanged: (data) {
_refreshPosts(); setState(() => _posts[idx] = data);
}, },
onDeleted: () {
_refreshPosts();
},
),
),
openBuilder: (_, close) => PostDetailScreen(
slug: _posts[idx].id.toString(),
preload: _posts[idx],
onBack: close,
),
openColor: Colors.transparent,
openElevation: 0,
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75),
transitionType: ContainerTransitionType.fade,
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
), ),
onTap: () {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {'slug': _posts[idx].id.toString()},
extra: _posts[idx],
);
},
); );
}, },
separatorBuilder: (context, index) => const Divider(height: 1), separatorBuilder: (_, __) => const Gap(8),
), ),
], ],
), ),

View File

@ -11,6 +11,7 @@ import 'package:surface/widgets/account/account_image.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_scaffold.dart';
import '../providers/userinfo.dart'; import '../providers/userinfo.dart';
import '../widgets/unauthorized_hint.dart'; import '../widgets/unauthorized_hint.dart';
@ -180,7 +181,7 @@ class _FriendScreenState extends State<FriendScreen> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenFriend').tr(), title: Text('screenFriend').tr(),
@ -191,7 +192,7 @@ class _FriendScreenState extends State<FriendScreen> {
); );
} }
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenFriend').tr(), title: Text('screenFriend').tr(),
@ -233,52 +234,56 @@ class _FriendScreenState extends State<FriendScreen> {
if (_requests.isNotEmpty || _blocks.isNotEmpty) if (_requests.isNotEmpty || _blocks.isNotEmpty)
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: RefreshIndicator( child: MediaQuery.removePadding(
onRefresh: () => Future.wait([ context: context,
_fetchRelations(), removeTop: true,
_fetchRequests(), child: RefreshIndicator(
]), onRefresh: () => Future.wait([
child: ListView.builder( _fetchRelations(),
itemCount: _relations.length, _fetchRequests(),
itemBuilder: (context, index) { ]),
final relation = _relations[index]; child: ListView.builder(
final other = relation.related; itemCount: _relations.length,
return ListTile( itemBuilder: (context, index) {
contentPadding: const EdgeInsets.only(right: 24, left: 16), final relation = _relations[index];
leading: AccountImage(content: other?.avatar), final other = relation.related;
title: Text(other?.nick ?? 'unknown'), return ListTile(
subtitle: Text(other?.nick ?? 'unknown'), contentPadding: const EdgeInsets.only(right: 24, left: 16),
trailing: SizedBox( leading: AccountImage(content: other?.avatar),
height: 48, title: Text(other?.nick ?? 'unknown'),
width: 120, subtitle: Text(other?.nick ?? 'unknown'),
child: Column( trailing: SizedBox(
mainAxisSize: MainAxisSize.min, height: 48,
mainAxisAlignment: MainAxisAlignment.center, width: 120,
crossAxisAlignment: CrossAxisAlignment.end, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
Row( mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
InkWell( Row(
onTap: _isUpdating mainAxisAlignment: MainAxisAlignment.end,
? null children: [
: () => _changeRelation(relation, 2), InkWell(
child: Text('friendBlock').tr(), onTap: _isUpdating
), ? null
const Gap(8), : () => _changeRelation(relation, 2),
InkWell( child: Text('friendBlock').tr(),
onTap: _isUpdating ),
? null const Gap(8),
: () => _deleteRelation(relation), InkWell(
child: Text('friendDeleteAction').tr(), onTap: _isUpdating
), ? null
], : () => _deleteRelation(relation),
), child: Text('friendDeleteAction').tr(),
], ),
],
),
],
),
), ),
), );
); },
}, ),
), ),
), ),
), ),

View File

@ -25,6 +25,7 @@ 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';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
class HomeScreenDashEntry { class HomeScreenDashEntry {
@ -67,7 +68,7 @@ class _HomeScreenState extends State<HomeScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text("screenHome").tr(), title: Text("screenHome").tr(),
@ -387,6 +388,8 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
Text( Text(
'dailyCheckInNone', 'dailyCheckInNone',
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
maxLines: 2,
overflow: TextOverflow.ellipsis,
).tr(), ).tr(),
], ],
) )

View File

@ -14,6 +14,7 @@ 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/post/post_item.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';
@ -137,7 +138,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenNotification').tr(), title: Text('screenNotification').tr(),
@ -148,7 +149,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
); );
} }
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenNotification').tr(), title: Text('screenNotification').tr(),

View File

@ -13,6 +13,8 @@ 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/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';
import 'package:surface/widgets/post/post_mini_editor.dart'; import 'package:surface/widgets/post/post_mini_editor.dart';
@ -20,12 +22,9 @@ import 'package:surface/widgets/post/post_mini_editor.dart';
class PostDetailScreen extends StatefulWidget { class PostDetailScreen extends StatefulWidget {
final String slug; final String slug;
final SnPost? preload; final SnPost? preload;
final Function? onBack;
const PostDetailScreen({ const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack});
super.key,
required this.slug,
this.preload,
});
@override @override
State<PostDetailScreen> createState() => _PostDetailScreenState(); State<PostDetailScreen> createState() => _PostDetailScreenState();
@ -67,123 +66,129 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return Scaffold( return AppBackground(
appBar: AppBar( isRoot: widget.onBack != null,
leading: BackButton( child: AppScaffold(
onPressed: () { appBar: AppBar(
if (GoRouter.of(context).canPop()) { leading: BackButton(
GoRouter.of(context).pop(context); onPressed: () {
return; if (widget.onBack != null) {
} widget.onBack!.call();
GoRouter.of(context).replaceNamed('explore'); }
}, if (GoRouter.of(context).canPop()) {
), GoRouter.of(context).pop(context);
title: _data?.body['title'] != null return;
? RichText( }
textAlign: TextAlign.center, GoRouter.of(context).replaceNamed('explore');
text: TextSpan(children: [ },
TextSpan(
text: _data?.body['title'] ?? 'postNoun'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
const TextSpan(text: '\n'),
TextSpan(
text: 'postDetail'.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
]),
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
: Text('postDetail').tr(),
),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: LoadingIndicator(isActive: _isBusy),
), ),
if (_data != null) title: _data?.body['title'] != null
SliverToBoxAdapter( ? RichText(
child: PostItem( textAlign: TextAlign.center,
data: _data!, text: TextSpan(children: [
maxWidth: 640, TextSpan(
showComments: false, text: _data?.body['title'] ?? 'postNoun'.tr(),
showFullPost: true, style: Theme.of(context).textTheme.titleLarge!.copyWith(
onChanged: (data) { color: Theme.of(context).appBarTheme.foregroundColor!,
setState(() => _data = data);
},
onDeleted: () {
Navigator.pop(context);
},
),
),
const SliverToBoxAdapter(child: Divider(height: 1)),
if (_data != null)
SliverToBoxAdapter(
child: Container(
constraints: const BoxConstraints(maxWidth: 640),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.comment, size: 24),
const Gap(16),
Text('postCommentsDetailed')
.plural(_data!.metric.replyCount)
.textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, vertical: 12).center(),
),
),
if (_data != null && ua.isAuthorized)
SliverToBoxAdapter(
child: Container(
height: 240,
constraints: const BoxConstraints(maxWidth: 640),
margin:
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.all(8) : EdgeInsets.zero,
decoration: BoxDecoration(
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? const BorderRadius.all(Radius.circular(8))
: BorderRadius.zero,
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? Border.all(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
)
: Border.symmetric(
horizontal: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
), ),
), ),
), const TextSpan(text: '\n'),
child: PostMiniEditor( TextSpan(
postReplyId: _data!.id, text: 'postDetail'.tr(),
onPost: () { style: Theme.of(context).textTheme.bodySmall!.copyWith(
setState(() { color: Theme.of(context).appBarTheme.foregroundColor!,
_data = _data!.copyWith( ),
metric: _data!.metric.copyWith( ),
replyCount: _data!.metric.replyCount + 1, ]),
), maxLines: 2,
); overflow: TextOverflow.ellipsis,
}); )
_childListKey.currentState!.refresh(); : Text('postDetail').tr(),
),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: LoadingIndicator(isActive: _isBusy),
),
if (_data != null)
SliverToBoxAdapter(
child: PostItem(
data: _data!,
maxWidth: 640,
showComments: false,
showFullPost: true,
onChanged: (data) {
setState(() => _data = data);
},
onDeleted: () {
Navigator.pop(context);
}, },
), ),
).center(), ),
), const SliverToBoxAdapter(child: Divider(height: 1)),
if (_data != null) if (_data != null)
PostCommentSliverList( SliverToBoxAdapter(
key: _childListKey, child: Container(
parentPostId: _data!.id, constraints: const BoxConstraints(maxWidth: 640),
maxWidth: 640, child: Row(
), crossAxisAlignment: CrossAxisAlignment.center,
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)), children: [
], const Icon(Symbols.comment, size: 24),
const Gap(16),
Text('postCommentsDetailed')
.plural(_data!.metric.replyCount)
.textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, vertical: 12).center(),
),
),
if (_data != null && ua.isAuthorized)
SliverToBoxAdapter(
child: Container(
height: 240,
constraints: const BoxConstraints(maxWidth: 640),
margin:
ResponsiveBreakpoints.of(context).largerThan(MOBILE) ? const EdgeInsets.all(8) : EdgeInsets.zero,
decoration: BoxDecoration(
borderRadius: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? const BorderRadius.all(Radius.circular(8))
: BorderRadius.zero,
border: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
? Border.all(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
)
: Border.symmetric(
horizontal: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
),
),
),
child: PostMiniEditor(
postReplyId: _data!.id,
onPost: () {
setState(() {
_data = _data!.copyWith(
metric: _data!.metric.copyWith(
replyCount: _data!.metric.replyCount + 1,
),
);
});
_childListKey.currentState!.refresh();
},
),
).center(),
),
if (_data != null)
PostCommentSliverList(
key: _childListKey,
parentPostId: _data!.id,
maxWidth: 640,
),
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
],
),
), ),
); );
} }

View File

@ -13,6 +13,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/post/post_media_pending_list.dart'; import 'package:surface/widgets/post/post_media_pending_list.dart';
import 'package:surface/widgets/post/post_meta_editor.dart'; import 'package:surface/widgets/post/post_meta_editor.dart';
@ -128,7 +129,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
return ListenableBuilder( return ListenableBuilder(
listenable: _writeController, listenable: _writeController,
builder: (context, _) { builder: (context, _) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: BackButton( leading: BackButton(
onPressed: () { onPressed: () {

View File

@ -8,6 +8,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/post/post_tags_field.dart'; import 'package:surface/widgets/post/post_tags_field.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -119,7 +120,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
), ),
]; ];
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: Text('screenPostSearch').tr(), title: Text('screenPostSearch').tr(),
actions: [ actions: [

View File

@ -17,6 +17,7 @@ import 'package:surface/types/post.dart';
import 'package:surface/types/realm.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/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -274,7 +275,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return Scaffold( return AppScaffold(
body: NestedScrollView( body: NestedScrollView(
controller: _scrollController, controller: _scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {

View File

@ -12,6 +12,7 @@ import 'package:surface/widgets/account/account_image.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_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:surface/widgets/universal_image.dart';
@ -83,7 +84,7 @@ class _RealmScreenState extends State<RealmScreen> {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
if (!ua.isAuthorized) { if (!ua.isAuthorized) {
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenRealm').tr(), title: Text('screenRealm').tr(),
@ -94,7 +95,7 @@ class _RealmScreenState extends State<RealmScreen> {
); );
} }
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenRealm').tr(), title: Text('screenRealm').tr(),
@ -118,113 +119,61 @@ class _RealmScreenState extends State<RealmScreen> {
children: [ children: [
LoadingIndicator(isActive: _isBusy), LoadingIndicator(isActive: _isBusy),
Expanded( Expanded(
child: RefreshIndicator( child: MediaQuery.removePadding(
onRefresh: _fetchRealms, context: context,
child: ListView.builder( removeTop: true,
itemCount: _realms?.length ?? 0, child: RefreshIndicator(
itemBuilder: (context, idx) { onRefresh: _fetchRealms,
final realm = _realms![idx]; child: ListView.builder(
if (_isCompactView) { itemCount: _realms?.length ?? 0,
return ListTile( itemBuilder: (context, idx) {
contentPadding: const EdgeInsets.symmetric(horizontal: 16), final realm = _realms![idx];
leading: AccountImage( if (_isCompactView) {
content: realm.avatar, return ListTile(
fallbackWidget: const Icon(Symbols.group, size: 20), contentPadding: const EdgeInsets.symmetric(horizontal: 16),
), leading: AccountImage(
title: Text(realm.name), content: realm.avatar,
subtitle: Text( fallbackWidget: const Icon(Symbols.group, size: 20),
realm.description, ),
maxLines: 1, title: Text(realm.name),
overflow: TextOverflow.ellipsis, subtitle: Text(
), realm.description,
trailing: PopupMenuButton( maxLines: 1,
itemBuilder: (BuildContext context) => [ overflow: TextOverflow.ellipsis,
PopupMenuItem( ),
child: Row( trailing: PopupMenuButton(
children: [ itemBuilder: (BuildContext context) => [
const Icon(Symbols.edit), PopupMenuItem(
const Gap(16), child: Row(
Text('edit').tr(),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmManage',
queryParameters: {'editing': realm.alias},
).then((value) {
if (value != null) {
_fetchRealms();
}
});
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
onTap: () {
_deleteRealm(realm);
},
),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': realm.alias},
);
},
);
}
return Container(
constraints: BoxConstraints(maxWidth: 640),
child: Card(
margin: const EdgeInsets.all(12),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [ children: [
Container( const Icon(Symbols.edit),
color: Theme.of(context).colorScheme.surfaceContainer, const Gap(16),
child: (realm.banner?.isEmpty ?? true) Text('edit').tr(),
? const SizedBox.shrink()
: AutoResizeUniversalImage(
sn.getAttachmentUrl(realm.banner!),
fit: BoxFit.cover,
),
),
Positioned(
bottom: -30,
left: 18,
child: AccountImage(
content: realm.avatar,
radius: 24,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
),
], ],
), ),
onTap: () {
GoRouter.of(context).pushNamed(
'realmManage',
queryParameters: {'editing': realm.alias},
).then((value) {
if (value != null) {
_fetchRealms();
}
});
},
),
PopupMenuItem(
child: Row(
children: [
const Icon(Symbols.delete),
const Gap(16),
Text('delete').tr(),
],
),
onTap: () {
_deleteRealm(realm);
},
), ),
const Gap(20 + 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).padding(horizontal: 24, bottom: 14),
], ],
), ),
onTap: () { onTap: () {
@ -233,10 +182,69 @@ class _RealmScreenState extends State<RealmScreen> {
pathParameters: {'alias': realm.alias}, pathParameters: {'alias': realm.alias},
); );
}, },
);
}
return Container(
constraints: BoxConstraints(maxWidth: 640),
child: Card(
margin: const EdgeInsets.all(12),
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: (realm.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoResizeUniversalImage(
sn.getAttachmentUrl(realm.banner!),
fit: BoxFit.cover,
),
),
),
Positioned(
bottom: -30,
left: 18,
child: AccountImage(
content: realm.avatar,
radius: 24,
fallbackWidget: const Icon(Symbols.group, size: 24),
),
),
],
),
),
const Gap(20 + 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(realm.name).textStyle(Theme.of(context).textTheme.titleMedium!),
Text(realm.description).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).padding(horizontal: 24, bottom: 14),
],
),
onTap: () {
GoRouter.of(context).pushNamed(
'realmDetail',
pathParameters: {'alias': realm.alias},
);
},
),
), ),
), ).center();
).center(); },
}, ),
), ),
), ),
), ),

View File

@ -18,6 +18,7 @@ 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/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/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -179,7 +180,7 @@ class _RealmManageScreenState extends State<RealmManageScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: widget.editingRealmAlias != null title: widget.editingRealmAlias != null
? Text('screenRealmManage').tr() ? Text('screenRealmManage').tr()

View File

@ -8,13 +8,13 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/post.dart';
import 'package:surface/types/realm.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/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
import '../../types/post.dart';
class RealmDetailScreen extends StatefulWidget { class RealmDetailScreen extends StatefulWidget {
final String alias; final String alias;
@ -70,19 +70,11 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DefaultTabController( return DefaultTabController(
length: 3, length: 3,
child: Scaffold( child: AppScaffold(
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
// These are the slivers that show up in the "outer" scroll view.
return <Widget>[ return <Widget>[
SliverOverlapAbsorber( SliverOverlapAbsorber(
// This widget takes the overlapping behavior of the SliverAppBar,
// and redirects it to the SliverOverlapInjector below. If it is
// missing, then it is possible for the nested "inner" scroll view
// below to end up under the SliverAppBar even when the inner
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar( sliver: SliverAppBar(
title: Text(_realm?.name ?? 'loading'.tr()), title: Text(_realm?.name ?? 'loading'.tr()),
@ -428,7 +420,7 @@ class _RealmSettingsWidgetState extends State<_RealmSettingsWidget> {
return Column( return Column(
children: [ children: [
const Gap(16), const Gap(8),
ListTile( ListTile(
leading: const Icon(Symbols.edit), leading: const Icon(Symbols.edit),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),

View File

@ -18,6 +18,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/theme.dart'; import 'package:surface/providers/theme.dart';
import 'package:surface/theme.dart'; import 'package:surface/theme.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart';
const Map<String, Color> kColorSchemes = { const Map<String, Color> kColorSchemes = {
'colorSchemeIndigo': Colors.indigo, 'colorSchemeIndigo': Colors.indigo,
@ -67,7 +68,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
return Scaffold( return AppScaffold(
appBar: AppBar(
leading: const PageBackButton(),
title: Text('screenSettings').tr(),
),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: Column(
spacing: 16, spacing: 16,
@ -120,7 +125,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
subtitle: Text('settingsThemeMaterial3Description').tr(), subtitle: Text('settingsThemeMaterial3Description').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17), contentPadding: const EdgeInsets.only(left: 24, right: 17),
secondary: const Icon(Symbols.new_releases), secondary: const Icon(Symbols.new_releases),
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false, value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_prefs.setBool( _prefs.setBool(
@ -255,6 +260,24 @@ class _SettingsScreenState extends State<SettingsScreen> {
), ),
], ],
), ),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('settingsFeatures').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
CheckboxListTile(
secondary: const Icon(Symbols.vibration),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
title: Text('settingsNotifyWithHaptic').tr(),
subtitle: Text('settingsNotifyWithHapticDescription').tr(),
value: _prefs.getBool(kAppNotifyWithHaptic) ?? true,
onChanged: (value) {
setState(() {
_prefs.setBool(kAppNotifyWithHaptic, value ?? false);
});
},
),
],
),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [

View File

@ -20,7 +20,7 @@ Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3
Future<ThemeData> createAppTheme( Future<ThemeData> createAppTheme(
Brightness brightness, { Brightness brightness, {
Color? seedColorOverride, Color? seedColorOverride,
bool? useMaterial3, bool? useMaterial3,
}) async { }) async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@ -34,9 +34,10 @@ Future<ThemeData> createAppTheme(
); );
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false; final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
return ThemeData( return ThemeData(
useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false), useMaterial3: useM3,
colorScheme: colorScheme, colorScheme: colorScheme,
brightness: brightness, brightness: brightness,
iconTheme: IconThemeData( iconTheme: IconThemeData(
@ -45,12 +46,24 @@ Future<ThemeData> createAppTheme(
opticalSize: 20, opticalSize: 20,
color: colorScheme.onSurface, color: colorScheme.onSurface,
), ),
snackBarTheme: SnackBarThemeData(
behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed,
),
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
centerTitle: true, centerTitle: true,
elevation: hasAppBarBlurry ? 0 : null, elevation: hasAppBarBlurry ? 0 : null,
backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary, backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary, foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
), ),
scaffoldBackgroundColor: Colors.transparent, pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
TargetPlatform.linux: ZoomPageTransitionsBuilder(),
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
},
),
); );
} }

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.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';
class AboutScreen extends StatelessWidget { class AboutScreen extends StatelessWidget {
@ -12,97 +13,103 @@ class AboutScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4)); const denseButtonStyle = ButtonStyle(visualDensity: VisualDensity(vertical: -4));
return SizedBox( return AppScaffold(
width: double.infinity, appBar: AppBar(
child: Column( leading: const PageBackButton(),
mainAxisAlignment: MainAxisAlignment.center, title: Text('screenAbout').tr(),
children: [ ),
ClipRRect( body: SizedBox(
borderRadius: const BorderRadius.all(Radius.circular(16)), width: double.infinity,
child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120), child: Column(
), mainAxisAlignment: MainAxisAlignment.center,
const Gap(8), children: [
Text( ClipRRect(
'Solian', borderRadius: const BorderRadius.all(Radius.circular(16)),
style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36), child: Image.asset('assets/icon/icon-light-radius.png', width: 120, height: 120),
), ),
const Text( const Gap(8),
'The Solar Network', Text(
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), 'Solian',
), style: Theme.of(context).textTheme.titleLarge!.copyWith(fontSize: 36),
const Gap(8), ),
FutureBuilder( const Text(
future: PackageInfo.fromPlatform(), 'The Solar Network',
builder: (context, snapshot) { style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
if (!snapshot.hasData) { ),
return const SizedBox.shrink(); const Gap(8),
} FutureBuilder(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
return Text( return Text(
'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}', 'v${snapshot.data!.version} · ${snapshot.data!.buildNumber}',
style: const TextStyle(fontFamily: 'monospace'), style: const TextStyle(fontFamily: 'monospace'),
); );
}, },
), ),
Text('Copyright © ${DateTime.now().year} Solsynth LLC'), Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
const Gap(16), const Gap(16),
Container( Container(
constraints: const BoxConstraints(maxWidth: 280), constraints: const BoxConstraints(maxWidth: 280),
child: Wrap( child: Wrap(
spacing: 4, spacing: 4,
runSpacing: 4, runSpacing: 4,
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
children: [ children: [
TextButton( TextButton(
style: denseButtonStyle, style: denseButtonStyle,
child: Text('appDetails').tr(), child: Text('appDetails').tr(),
onPressed: () async { onPressed: () async {
final info = await PackageInfo.fromPlatform(); final info = await PackageInfo.fromPlatform();
if (!context.mounted) return; if (!context.mounted) return;
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationName: 'Solian', applicationName: 'Solian',
applicationVersion: '${info.version}+${info.buildNumber}', applicationVersion: '${info.version}+${info.buildNumber}',
applicationLegalese: applicationLegalese:
'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.', 'The Solar Network App is an intuitive and open-source social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
applicationIcon: ClipRRect( applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)), borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Image.asset( child: Image.asset(
'assets/icon/icon-light-radius.png', 'assets/icon/icon-light-radius.png',
width: 60, width: 60,
height: 60, height: 60,
),
), ),
), );
); },
}, ),
), TextButton(
TextButton( style: denseButtonStyle,
style: denseButtonStyle, child: Text('termRelated').tr(),
child: Text('termRelated').tr(), onPressed: () {
onPressed: () { launchUrlString('https://solsynth.dev/terms');
launchUrlString('https://solsynth.dev/terms'); },
}, ),
), TextButton(
TextButton( style: denseButtonStyle,
style: denseButtonStyle, child: Text('serviceStatus').tr(),
child: Text('serviceStatus').tr(), onPressed: () {
onPressed: () { launchUrlString('https://status.solsynth.dev');
launchUrlString('https://status.solsynth.dev'); },
}, ),
), ],
], ),
).center(),
const Gap(16),
const Text(
'Open-sourced under AGPLv3',
style: TextStyle(
fontWeight: FontWeight.w300,
fontSize: 12,
),
), ),
).center(), ],
const Gap(16), ),
const Text(
'Open-sourced under AGPLv3',
style: TextStyle(
fontWeight: FontWeight.w300,
fontSize: 12,
),
),
],
), ),
); );
} }

View File

@ -10,7 +10,6 @@ import 'package:surface/providers/experience.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/screens/account/profile_page.dart'; import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/types/account.dart'; import 'package:surface/types/account.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/universal_image.dart'; import 'package:surface/widgets/universal_image.dart';

View File

@ -15,6 +15,7 @@ class AttachmentList extends StatefulWidget {
final List<SnAttachment?> data; final List<SnAttachment?> data;
final bool bordered; final bool bordered;
final bool gridded; final bool gridded;
final bool columned;
final BoxFit fit; final BoxFit fit;
final double? maxHeight; final double? maxHeight;
final double? minWidth; final double? minWidth;
@ -26,6 +27,7 @@ class AttachmentList extends StatefulWidget {
required this.data, required this.data,
this.bordered = false, this.bordered = false,
this.gridded = false, this.gridded = false,
this.columned = false,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.maxHeight, this.maxHeight,
this.minWidth, this.minWidth,
@ -105,45 +107,10 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
if (widget.gridded) { final fullOfImage =
final fullOfImage = widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
if(!fullOfImage) { if (widget.gridded && fullOfImage) {
return Container(
margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: Column(
spacing: 4,
children: widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: AspectRatio(
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
child: Container(
constraints: constraints,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: BoxFit.cover,
),
),
),
),
)
.toList(),
),
),
);
}
return Container( return Container(
margin: widget.padding ?? EdgeInsets.zero, margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -191,6 +158,44 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
if ((!fullOfImage && widget.gridded) || widget.columned) {
return Container(
margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: Column(
children: widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: AspectRatio(
aspectRatio: ele?.data['ratio']?.toDouble() ?? 1,
child: Container(
constraints: constraints,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: BoxFit.cover,
),
),
),
),
)
.expand((ele) => [ele, const Divider(height: 1)])
.toList()
..removeLast(),
),
),
);
}
return Container( return Container(
constraints: BoxConstraints(maxHeight: constraints.maxHeight), constraints: BoxConstraints(maxHeight: constraints.maxHeight),
child: ScrollConfiguration( child: ScrollConfiguration(

View File

@ -129,6 +129,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75); Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
bool _showDetail = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
@ -144,218 +146,350 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
onDismissed: () { onDismissed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
direction: DismissiblePageDismissDirection.down, direction: DismissiblePageDismissDirection.none,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
isFullScreen: true, isFullScreen: true,
child: Scaffold( child: GestureDetector(
body: Stack( behavior: HitTestBehavior.translucent,
children: [ child: Scaffold(
Builder(builder: (context) { body: Stack(
if (widget.data.length == 1) { children: [
final heroTag = widget.heroTags?.first ?? uuid.v4(); Builder(builder: (context) {
return Hero( if (widget.data.length == 1) {
tag: 'attachment-${widget.data.first.rid}-$heroTag', final heroTag = widget.heroTags?.first ?? uuid.v4();
child: PhotoView( return Hero(
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'), tag: 'attachment-${widget.data.first.rid}-$heroTag',
backgroundDecoration: BoxDecoration(color: Colors.transparent), child: PhotoView(
imageProvider: UniversalImage.provider( key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
sn.getAttachmentUrl(widget.data.first.rid), backgroundDecoration: BoxDecoration(color: Colors.transparent),
), imageProvider: UniversalImage.provider(
), sn.getAttachmentUrl(widget.data.first.rid),
); ),
}
return PhotoViewGallery.builder(
pageController: _pageController,
scrollPhysics: const BouncingScrollPhysics(),
builder: (context, idx) {
final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
return PhotoViewGalleryPageOptions(
imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
),
heroAttributes: PhotoViewHeroAttributes(
tag: 'attachment-${widget.data.first.rid}-$heroTag',
), ),
); );
}, }
itemCount: widget.data.length,
loadingBuilder: (context, event) => Center( return PhotoViewGallery.builder(
child: SizedBox( pageController: _pageController,
width: 20.0, scrollPhysics: const BouncingScrollPhysics(),
height: 20.0, builder: (context, idx) {
child: CircularProgressIndicator( final heroTag = widget.heroTags?.elementAt(idx) ?? uuid.v4();
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1), return PhotoViewGalleryPageOptions(
imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(widget.data.elementAt(idx).rid),
),
heroAttributes: PhotoViewHeroAttributes(
tag: 'attachment-${widget.data.first.rid}-$heroTag',
),
);
},
itemCount: widget.data.length,
loadingBuilder: (context, event) => Center(
child: SizedBox(
width: 20.0,
height: 20.0,
child: CircularProgressIndicator(
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
),
), ),
), ),
), backgroundDecoration: BoxDecoration(color: Colors.transparent),
backgroundDecoration: BoxDecoration(color: Colors.transparent), );
); }),
}), Align(
Align( alignment: Alignment.bottomCenter,
alignment: Alignment.bottomCenter, child: IgnorePointer(
child: IgnorePointer( child: Container(
child: Container( height: 300,
height: 300, decoration: BoxDecoration(
decoration: BoxDecoration( gradient: LinearGradient(
gradient: LinearGradient( begin: Alignment.bottomCenter,
begin: Alignment.bottomCenter, end: Alignment.topCenter,
end: Alignment.topCenter, colors: [
colors: [ Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surface, Colors.transparent,
Colors.transparent, ],
], ),
), ),
), ),
), ),
), ),
), Positioned(
Positioned( left: 16,
left: 16, right: 16,
right: 16, bottom: 16 + MediaQuery.of(context).padding.bottom,
bottom: 16 + MediaQuery.of(context).padding.bottom, child: Material(
child: Material( color: Colors.transparent,
color: Colors.transparent, child: Builder(builder: (context) {
child: Builder(builder: (context) { final ud = context.read<UserDirectoryProvider>();
final ud = context.read<UserDirectoryProvider>(); final item = widget.data.elementAt(
final item = widget.data.elementAt( widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0, );
); final account = ud.getAccountFromCache(item.accountId);
final account = ud.getAccountFromCache(item.accountId);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (item.accountId > 0) if (item.accountId > 0)
Row( Row(
children: [ children: [
IgnorePointer( IgnorePointer(
child: AccountImage( child: AccountImage(
content: account?.avatar, content: account?.avatar,
radius: 19, radius: 19,
),
),
const Gap(8),
Expanded(
child: IgnorePointer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'attachmentUploadBy'.tr(),
style: Theme.of(context).textTheme.bodySmall,
),
Text(
account?.nick ?? 'unknown'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
), ),
), ),
), const Gap(8),
if (widget.data.length > 1) Expanded(
IgnorePointer( child: IgnorePointer(
child: Text( child: Column(
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}', crossAxisAlignment: CrossAxisAlignment.start,
style: GoogleFonts.robotoMono(fontSize: 13), children: [
).padding(right: 8), Text(
), 'attachmentUploadBy'.tr(),
InkWell( style: Theme.of(context).textTheme.bodySmall,
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: _isDownloading
? null
: () => _saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
child: Container(
padding: const EdgeInsets.all(6),
child: !_isDownloading
? !_isCompletedDownload
? const Icon(Symbols.save_alt)
: const Icon(Symbols.download_done)
: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _progressOfDownload,
strokeWidth: 3,
),
), ),
Text(
account?.nick ?? 'unknown'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
), ),
if (widget.data.length > 1)
IgnorePointer(
child: Text(
'${(_pageController.page?.round() ?? 0) + 1}/${widget.data.length}',
style: GoogleFonts.robotoMono(fontSize: 13),
).padding(right: 8),
),
InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: _isDownloading
? null
: () =>
_saveToAlbum(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
child: Container(
padding: const EdgeInsets.all(6),
child: !_isDownloading
? !_isCompletedDownload
? const Icon(Symbols.save_alt)
: const Icon(Symbols.download_done)
: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _progressOfDownload,
strokeWidth: 3,
),
),
),
),
],
),
const Gap(4),
IgnorePointer(
child: Text(
item.alt,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
), ),
],
),
const Gap(4),
IgnorePointer(
child: Text(
item.alt,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
), ),
), ),
), const Gap(2),
const Gap(2), IgnorePointer(
IgnorePointer( child: Wrap(
child: Wrap( spacing: 6,
spacing: 6, children: [
children: [ if (item.metadata['exif'] == null)
if (item.metadata['exif'] == null) Text(
'#${item.rid}',
style: metaTextStyle,
),
if (item.metadata['exif']?['Model'] != null)
Text(
'attachmentShotOn'.tr(args: [
item.metadata['exif']?['Model'],
]),
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['ISO'] != null)
Text(
'ISO${item.metadata['exif']?['ISO']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Aperture'] != null)
Text(
'f/${item.metadata['exif']?['Aperture']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Megapixels'] != null &&
item.metadata['exif']?['Model'] != null)
Text(
'${item.metadata['exif']?['Megapixels']}MP',
style: metaTextStyle,
)
else
Text(
item.size.formatBytes(),
style: metaTextStyle,
),
if (item.metadata['width'] != null && item.metadata['height'] != null)
Text(
'${item.metadata['width']}x${item.metadata['height']}',
style: metaTextStyle,
),
if (item.metadata['ratio'] != null)
Text(
(item.metadata['ratio'] as num).toStringAsFixed(2),
style: metaTextStyle,
),
Text( Text(
'#${item.rid}', item.mimetype,
style: metaTextStyle, style: metaTextStyle,
), ),
if (item.metadata['exif']?['Model'] != null) ],
Text( ),
'attachmentShotOn'.tr(args: [
item.metadata['exif']?['Model'],
]),
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['ISO'] != null)
Text(
'ISO${item.metadata['exif']?['ISO']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Aperture'] != null)
Text(
'f/${item.metadata['exif']?['Aperture']}',
style: metaTextStyle,
).padding(right: 2),
if (item.metadata['exif']?['Megapixels'] != null && item.metadata['exif']?['Model'] != null)
Text(
'${item.metadata['exif']?['Megapixels']}MP',
style: metaTextStyle,
)
else
Text(
item.size.formatBytes(),
style: metaTextStyle,
),
if (item.metadata['width'] != null && item.metadata['height'] != null)
Text(
'${item.metadata['width']}x${item.metadata['height']}',
style: metaTextStyle,
),
if (item.metadata['ratio'] != null)
Text(
(item.metadata['ratio'] as num).toStringAsFixed(2),
style: metaTextStyle,
),
Text(
item.mimetype,
style: metaTextStyle,
),
],
), ),
), ],
], );
); }),
}), ),
), ),
), ],
], ),
), ),
onVerticalDragUpdate: (details) {
if (_showDetail) return;
if (details.delta.dy <= -40) {
_showDetail = true;
showModalBottomSheet(
context: context,
builder: (context) => _AttachmentZoomDetailPopup(
data: widget.data.elementAt(widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0),
),
).then((_) {
_showDetail = false;
});
}
},
onTap: () {
Navigator.of(context).pop();
},
),
);
}
}
class _AttachmentZoomDetailPopup extends StatelessWidget {
final SnAttachment data;
const _AttachmentZoomDetailPopup({required this.data});
@override
Widget build(BuildContext context) {
final ud = context.read<UserDirectoryProvider>();
final account = ud.getAccountFromCache(data.accountId);
const tableGap = TableRow(
children: [
TableCell(child: SizedBox(height: 16)),
TableCell(child: SizedBox(height: 16)),
],
);
return SizedBox(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.info, size: 24),
const Gap(16),
Text('attachmentDetailInfo').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
],
).padding(horizontal: 20, top: 16, bottom: 12),
Expanded(
child: SingleChildScrollView(
child: Table(
columnWidths: {
0: IntrinsicColumnWidth(),
1: FlexColumnWidth(),
},
children: [
TableRow(
children: [
TableCell(
child: Text('attachmentUploadBy').tr().padding(right: 16),
),
TableCell(
child: Row(
children: [
if (data.accountId > 0)
AccountImage(
content: account?.avatar,
radius: 8,
),
const Gap(8),
Text(data.accountId > 0 ? account?.nick ?? 'unknown'.tr() : 'unknown'.tr()),
const Gap(8),
Text('#${data.accountId}', style: GoogleFonts.robotoMono()).opacity(0.75),
],
),
),
],
),
tableGap,
TableRow(
children: [
TableCell(child: Text('Mimetype').padding(right: 16)),
TableCell(child: Text(data.mimetype)),
],
),
TableRow(
children: [
TableCell(child: Text('Size').padding(right: 16)),
TableCell(
child: Row(
children: [
Text(data.size.formatBytes()),
const Gap(12),
Text('${data.size} Bytes', style: GoogleFonts.robotoMono()).opacity(0.75),
],
)),
],
),
TableRow(
children: [
TableCell(child: Text('Name').padding(right: 16)),
TableCell(child: Text(data.name)),
],
),
if (data.hash.isNotEmpty)
TableRow(
children: [
TableCell(child: Text('Hash').padding(right: 16)),
TableCell(child: Text(data.hash, style: GoogleFonts.robotoMono(fontSize: 11)).opacity(0.9)),
],
),
tableGap,
...(data.metadata['exif']?.keys.map((k) => TableRow(
children: [
TableCell(child: Text(k).padding(right: 16)),
TableCell(child: Text(data.metadata['exif'][k].toString())),
],
)) ??
[]),
],
).padding(horizontal: 20, vertical: 8),
),
),
],
), ),
); );
} }

View File

@ -1,5 +1,7 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package: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/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
@ -16,45 +18,49 @@ class ConnectionIndicator extends StatelessWidget {
listenable: ws, listenable: ws,
builder: (context, _) { builder: (context, _) {
final ua = context.read<UserProvider>(); final ua = context.read<UserProvider>();
final show = (ws.isBusy || !ws.isConnected) && ua.isAuthorized;
return GestureDetector( return IgnorePointer(
child: Container( ignoring: !show,
padding: EdgeInsets.only( child: GestureDetector(
bottom: 8, child: Material(
top: MediaQuery.of(context).padding.top + 8, elevation: 2,
left: 24, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
right: 24, color: Theme.of(context).colorScheme.secondaryContainer,
), child: ua.isAuthorized
color: Theme.of(context).colorScheme.secondaryContainer, ? Row(
child: ua.isAuthorized mainAxisAlignment: MainAxisAlignment.center,
? Row( crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: [
crossAxisAlignment: CrossAxisAlignment.center, if (ws.isBusy)
children: [ Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
if (ws.isBusy) else if (!ws.isConnected)
Text('serverConnecting').tr().textColor( Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
Theme.of(context).colorScheme.onSecondaryContainer) else
else if (!ws.isConnected) Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
Text('serverDisconnected').tr().textColor( const Gap(8),
Theme.of(context).colorScheme.onSecondaryContainer), if (ws.isBusy)
], const CircularProgressIndicator(strokeWidth: 2.5)
) .width(12)
: const SizedBox.shrink(), .height(12)
) .padding(horizontal: 4, right: 4)
.height( else if (!ws.isConnected)
(ws.isBusy || !ws.isConnected) && ua.isAuthorized const Icon(Symbols.power_off, size: 18)
? MediaQuery.of(context).padding.top + 36 else
: 0, const Icon(Symbols.power, size: 18),
animate: true) ],
.animate( ).padding(horizontal: 8, vertical: 4)
const Duration(milliseconds: 300), : const SizedBox.shrink(),
Curves.easeInOut, ).opacity(show ? 1 : 0, animate: true).animate(
), const Duration(milliseconds: 300),
onTap: () { Curves.easeInOut,
if (!ws.isConnected && !ws.isBusy) { ),
ws.connect(); onTap: () {
} if (!ws.isConnected && !ws.isBusy) {
}, ws.connect();
}
},
),
); );
}, },
); );

View File

@ -94,11 +94,14 @@ class _LinkPreviewEntry extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if (meta.icon?.isNotEmpty ?? false) if (meta.icon?.isNotEmpty ?? false)
StyledWidget( SizedBox(
meta.icon!.endsWith('.svg') width: 36,
? SvgPicture.network(meta.icon!) height: 36,
child: meta.icon!.endsWith('.svg')
? SvgPicture.network(meta.icon!, width: 36, height: 36)
: UniversalImage( : UniversalImage(
meta.icon!, meta.icon!,
noErrorWidget: true,
width: 36, width: 36,
height: 36, height: 36,
cacheHeight: 36, cacheHeight: 36,

View File

@ -18,9 +18,7 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
context context.read<NavigationProvider>().autoDetectIndex(GoRouter.maybeOf(context));
.read<NavigationProvider>()
.autoDetectIndex(GoRouter.maybeOf(context));
}); });
} }
@ -31,11 +29,11 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
return ListenableBuilder( return ListenableBuilder(
listenable: nav, listenable: nav,
builder: (context, _) { builder: (context, _) {
final destinations = final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
nav.destinations.where((ele) => ele.isPinned).toList();
return NavigationRail( return NavigationRail(
selectedIndex: nav.currentIndex, selectedIndex:
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
destinations: [ destinations: [
...destinations.where((ele) => ele.isPinned).map((ele) { ...destinations.where((ele) => ele.isPinned).map((ele) {
return NavigationRailDestination( return NavigationRailDestination(

View File

@ -17,37 +17,80 @@ import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_bottom_navigation.dart'; import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
import 'package:surface/widgets/navigation/app_drawer_navigation.dart'; import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
import 'package:surface/widgets/navigation/app_rail_navigation.dart'; import 'package:surface/widgets/navigation/app_rail_navigation.dart';
import 'package:surface/widgets/notify_indicator.dart';
final globalRootScaffoldKey = GlobalKey<ScaffoldState>(); final globalRootScaffoldKey = GlobalKey<ScaffoldState>();
class AppPageScaffold extends StatelessWidget { class AppScaffold extends StatelessWidget {
final String? title;
final Widget? body; final Widget? body;
final bool showAppBar; final PreferredSizeWidget? bottomNavigationBar;
final bool showBottomNavigation; final PreferredSizeWidget? bottomSheet;
final Drawer? drawer;
final Widget? endDrawer;
final FloatingActionButtonAnimator? floatingActionButtonAnimator;
final FloatingActionButtonLocation? floatingActionButtonLocation;
final Widget? floatingActionButton;
final AppBar? appBar;
final DrawerCallback? onDrawerChanged;
final DrawerCallback? onEndDrawerChanged;
const AppPageScaffold({ const AppScaffold({
super.key, super.key,
this.title, this.appBar,
this.body, this.body,
this.showAppBar = true, this.floatingActionButton,
this.showBottomNavigation = false, this.floatingActionButtonLocation,
this.floatingActionButtonAnimator,
this.bottomNavigationBar,
this.bottomSheet,
this.drawer,
this.endDrawer,
this.onDrawerChanged,
this.onEndDrawerChanged,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final state = GoRouter.maybeOf(context); final appBarHeight = appBar?.preferredSize.height ?? 0;
final routeName = state?.routerDelegate.currentConfiguration.last.route.name; final safeTop = MediaQuery.of(context).padding.top;
final autoTitle = state != null ? 'screen${routeName?.capitalize()}' : 'screen';
return Scaffold( return Scaffold(
appBar: showAppBar extendBody: true,
? AppBar( extendBodyBehindAppBar: true,
title: Text(title ?? autoTitle.tr()), backgroundColor: Theme.of(context).scaffoldBackgroundColor,
) body: SizedBox.expand(
: null, child: AppBackground(
body: body, child: Column(
children: [
IgnorePointer(child: SizedBox(height: appBar != null ? appBarHeight + safeTop : 0)),
if (body != null) Expanded(child: body!),
],
),
),
),
appBar: appBar,
bottomNavigationBar: bottomNavigationBar,
bottomSheet: bottomSheet,
drawer: drawer,
endDrawer: endDrawer,
floatingActionButton: floatingActionButton,
floatingActionButtonAnimator: floatingActionButtonAnimator,
floatingActionButtonLocation: floatingActionButtonLocation,
onDrawerChanged: onDrawerChanged,
onEndDrawerChanged: onEndDrawerChanged,
);
}
}
class PageBackButton extends StatelessWidget {
const PageBackButton({super.key});
@override
Widget build(BuildContext context) {
return BackButton(
onPressed: () {
GoRouter.of(context).pop();
},
); );
} }
} }
@ -98,62 +141,64 @@ class AppRootScaffold extends StatelessWidget {
iconMouseDown: Theme.of(context).colorScheme.primary, iconMouseDown: Theme.of(context).colorScheme.primary,
); );
return AppBackground( final safeTop = MediaQuery.of(context).padding.top;
isRoot: true,
child: Scaffold( return Scaffold(
key: globalRootScaffoldKey, key: globalRootScaffoldKey,
body: Column( backgroundColor: Theme.of(context).colorScheme.surface,
children: [ body: Stack(
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) children: [
Container( Column(
decoration: BoxDecoration( children: [
border: Border( if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
bottom: BorderSide( WindowTitleBarBox(
color: Theme.of(context).dividerColor, child: Container(
width: 1 / devicePixelRatio, decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
),
),
),
child: MoveWindow(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
Text(
'Solar Network',
style: GoogleFonts.spaceGrotesk(),
).padding(horizontal: 12, vertical: 5),
if (!Platform.isMacOS)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(child: MoveWindow()),
Row(
children: [
MinimizeWindowButton(colors: windowButtonColor),
MaximizeWindowButton(colors: windowButtonColor),
CloseWindowButton(colors: windowButtonColor),
],
),
],
),
],
),
), ),
), ),
), ),
child: Row( Expanded(child: innerWidget),
crossAxisAlignment: CrossAxisAlignment.center, ],
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start, ),
children: [ Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
WindowTitleBarBox( Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()),
child: MoveWindow( ],
child: Text(
'Solar Network',
style: GoogleFonts.spaceGrotesk(),
).padding(horizontal: 12, vertical: 5),
),
),
if (!Platform.isMacOS)
Expanded(
child: WindowTitleBarBox(
child: Row(
children: [
Expanded(child: MoveWindow()),
Row(
children: [
MinimizeWindowButton(colors: windowButtonColor),
MaximizeWindowButton(colors: windowButtonColor),
CloseWindowButton(colors: windowButtonColor),
],
),
],
),
),
),
],
),
),
ConnectionIndicator(),
Expanded(child: innerWidget),
],
),
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null,
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
), ),
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null,
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
); );
} }
} }

View File

@ -0,0 +1,63 @@
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/notification.dart';
import 'package:surface/providers/userinfo.dart';
class NotifyIndicator extends StatelessWidget {
const NotifyIndicator({super.key});
@override
Widget build(BuildContext context) {
final ua = context.read<UserProvider>();
final nty = context.watch<NotificationProvider>();
final show = nty.notifications.isNotEmpty && ua.isAuthorized;
return ListenableBuilder(
listenable: nty,
builder: (context, _) {
return IgnorePointer(
ignoring: !show,
child: GestureDetector(
child: Material(
elevation: 2,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
color: Theme.of(context).colorScheme.secondaryContainer,
child: ua.isAuthorized
? Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
nty.notifications.lastOrNull?.title ??
'notificationUnreadCount'.plural(nty.notifications.length),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (nty.notifications.lastOrNull?.body != null)
Text(
nty.notifications.lastOrNull!.body,
maxLines: 1,
overflow: TextOverflow.ellipsis,
).padding(left: 4),
const Gap(8),
const Icon(Symbols.notifications_unread, size: 18),
],
).padding(horizontal: 8, vertical: 4)
: const SizedBox.shrink(),
).opacity(show ? 1 : 0, animate: true).animate(
const Duration(milliseconds: 300),
Curves.easeInOut,
),
onTap: () {
nty.clear();
},
),
);
});
}
}

View File

@ -256,9 +256,8 @@ class PostItem extends StatelessWidget {
AttachmentList( AttachmentList(
data: displayableAttachments!, data: displayableAttachments!,
bordered: true, bordered: true,
gridded: true,
maxHeight: showFullPost ? null : 480, maxHeight: showFullPost ? null : 480,
minWidth: 640, maxWidth: MediaQuery.of(context).size.width - 20,
fit: showFullPost ? BoxFit.cover : BoxFit.contain, fit: showFullPost ? BoxFit.cover : BoxFit.contain,
padding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
), ),
@ -344,7 +343,7 @@ class PostShareImageWidget extends StatelessWidget {
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false)) if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
StyledWidget(AttachmentList( StyledWidget(AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
gridded: true, columned: true,
)).padding(horizontal: 16, bottom: 8), )).padding(horizontal: 16, bottom: 8),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -1,5 +1,6 @@
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/services.dart';
import 'package:gap/gap.dart'; 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';
@ -12,6 +13,7 @@ import 'package:surface/widgets/dialog.dart';
class PostReactionPopup extends StatefulWidget { class PostReactionPopup extends StatefulWidget {
final SnPost data; final SnPost data;
final Function(Map<String, int> value, int attr, int delta)? onChanged; final Function(Map<String, int> value, int attr, int delta)? onChanged;
const PostReactionPopup({super.key, required this.data, this.onChanged}); const PostReactionPopup({super.key, required this.data, this.onChanged});
@override @override
@ -59,6 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
); );
} }
} }
HapticFeedback.mediumImpact();
} catch (err) { } catch (err) {
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
if (context.mounted) context.showErrorDialog(err); if (context.mounted) context.showErrorDialog(err);
@ -84,9 +87,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
children: [ children: [
const Icon(Symbols.mood, size: 24), const Icon(Symbols.mood, size: 24),
const Gap(16), const Gap(16),
Text('postReactions') Text('postReactions').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
.tr()
.textStyle(Theme.of(context).textTheme.titleLarge!),
], ],
).padding(horizontal: 20, top: 16, bottom: 12), ).padding(horizontal: 20, top: 16, bottom: 12),
Container( Container(
@ -102,9 +103,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
Text('postReactionDownvote').plural(widget.data.totalDownvote), Text('postReactionDownvote').plural(widget.data.totalDownvote),
const Gap(24), const Gap(24),
Icon( Icon(
widget.data.totalUpvote >= widget.data.totalDownvote widget.data.totalUpvote >= widget.data.totalDownvote ? Symbols.trending_up : Symbols.trending_down,
? Symbols.trending_up
: Symbols.trending_down,
size: 16, size: 16,
), ),
const Gap(8), const Gap(8),

View File

@ -12,59 +12,59 @@ PODS:
- FlutterMacOS - FlutterMacOS
- file_selector_macos (0.0.1): - file_selector_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- Firebase/Analytics (11.4.0): - Firebase/Analytics (11.6.0):
- Firebase/Core - Firebase/Core
- Firebase/Core (11.4.0): - Firebase/Core (11.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseAnalytics (~> 11.4.0) - FirebaseAnalytics (~> 11.6.0)
- Firebase/CoreOnly (11.4.0): - Firebase/CoreOnly (11.6.0):
- FirebaseCore (= 11.4.0) - FirebaseCore (~> 11.6.0)
- Firebase/Messaging (11.4.0): - Firebase/Messaging (11.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 11.4.0) - FirebaseMessaging (~> 11.6.0)
- firebase_analytics (11.3.6): - firebase_analytics (11.4.0):
- Firebase/Analytics (= 11.4.0) - Firebase/Analytics (= 11.6.0)
- firebase_core - firebase_core
- FlutterMacOS - FlutterMacOS
- firebase_core (3.9.0): - firebase_core (3.10.0):
- Firebase/CoreOnly (~> 11.4.0) - Firebase/CoreOnly (~> 11.6.0)
- FlutterMacOS - FlutterMacOS
- firebase_messaging (15.1.6): - firebase_messaging (15.2.0):
- Firebase/CoreOnly (~> 11.4.0) - Firebase/CoreOnly (~> 11.6.0)
- Firebase/Messaging (~> 11.4.0) - Firebase/Messaging (~> 11.6.0)
- firebase_core - firebase_core
- FlutterMacOS - FlutterMacOS
- FirebaseAnalytics (11.4.0): - FirebaseAnalytics (11.6.0):
- FirebaseAnalytics/AdIdSupport (= 11.4.0) - FirebaseAnalytics/AdIdSupport (= 11.6.0)
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseAnalytics/AdIdSupport (11.4.0): - FirebaseAnalytics/AdIdSupport (11.6.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement (= 11.4.0) - GoogleAppMeasurement (= 11.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseCore (11.4.0): - FirebaseCore (11.6.0):
- FirebaseCoreInternal (~> 11.0) - FirebaseCoreInternal (~> 11.6.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0) - GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreInternal (11.6.0): - FirebaseCoreInternal (11.6.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseInstallations (11.4.0): - FirebaseInstallations (11.6.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- FirebaseMessaging (11.4.0): - FirebaseMessaging (11.6.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.6.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0) - GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
@ -75,28 +75,28 @@ PODS:
- flutter_udid (0.0.1): - flutter_udid (0.0.1):
- FlutterMacOS - FlutterMacOS
- SAMKeychain - SAMKeychain
- flutter_webrtc (0.12.2): - flutter_webrtc (0.12.6):
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- gal (1.0.0): - gal (1.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- GoogleAppMeasurement (11.4.0): - GoogleAppMeasurement (11.6.0):
- GoogleAppMeasurement/AdIdSupport (= 11.4.0) - GoogleAppMeasurement/AdIdSupport (= 11.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/AdIdSupport (11.4.0): - GoogleAppMeasurement/AdIdSupport (11.6.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.4.0) - GoogleAppMeasurement/WithoutAdIdSupport (= 11.6.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- GoogleAppMeasurement/WithoutAdIdSupport (11.4.0): - GoogleAppMeasurement/WithoutAdIdSupport (11.6.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0) - GoogleUtilities/Network (~> 8.0)
@ -134,7 +134,7 @@ PODS:
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- in_app_review (2.0.0): - in_app_review (2.0.0):
- FlutterMacOS - FlutterMacOS
- livekit_client (2.3.4): - livekit_client (2.3.5):
- flutter_webrtc - flutter_webrtc
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.06) - WebRTC-SDK (= 125.6422.06)
@ -287,24 +287,24 @@ SPEC CHECKSUMS:
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
file_saver: 44e6fbf666677faf097302460e214e977fdd977b file_saver: 44e6fbf666677faf097302460e214e977fdd977b
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
Firebase: cf1b19f21410b029b6786a54e9764a0cacad3c99 Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
firebase_analytics: a80b3d6645f2f12d626fde928b61dae12e5ea2ef firebase_analytics: 5249f87da6fed852901581aab2602e0280ec2fdb
firebase_core: 1dfe1f4d02ad78be0277e320aa3d8384cf46231f firebase_core: 6d9bb8b0ea817e8fe0d928177d42275b45fdba6f
firebase_messaging: 61f678060b69a7ae1013e3a939ec8e1c56ef6fcf firebase_messaging: ae8e88b586e4d50abc7cac5bacf74d21967fd226
FirebaseAnalytics: 3feef9ae8733c567866342a1000691baaa7cad49 FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
FirebaseCore: e0510f1523bc0eb21653cac00792e1e2bd6f1771 FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2
FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 FirebaseInstallations: efc0946fc756e4d22d8113f7c761948120322e8c
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021
flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52 flutter_udid: 2e7b3da4b5fdfba86a396b97898f5fe8f4ec1a52
flutter_webrtc: 53c9e1285ab32dfb58afb1e1471416a877e23d7a flutter_webrtc: d55fd3f5c75b42940b6b4b2cf376a5797398d1f8
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5
GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e GoogleAppMeasurement: 6a9e6317b6a6d810ad03d4a66564ca6c4c5818a3
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93 in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
livekit_client: b7ab91e79e657d7d40da16cb2f90d517cb72d406 livekit_client: 91c68237edede55f8891a166a28c1daec8a1e4b1
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _flutterfire_internals name: _flutterfire_internals
sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe sha256: "27899c95f9e7ec06c8310e6e0eac967707714b9f1450c4a58fa00ca011a4a8ae"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.48" version: "1.3.49"
_macros: _macros:
dependency: transitive dependency: transitive
description: dart description: dart
@ -266,10 +266,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: connectivity_plus name: connectivity_plus
sha256: e0817759ec6d2d8e57eb234e6e57d2173931367a865850c7acea40d4b4f9c27d sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.1" version: "6.1.2"
connectivity_plus_platform_interface: connectivity_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -290,10 +290,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: croppy name: croppy
sha256: "14bb40fd6c1771b093a907ddbf24df9aa49a4e6e379dd630602eb446e30ec629" sha256: bf99b00023df0d7d047e04d27d496d87cbefd968f578d0bd30f342ff75570a12
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.3"
cross_file: cross_file:
dependency: "direct main" dependency: "direct main"
description: description:
@ -354,18 +354,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dbus name: dbus
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.10" version: "0.7.11"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
name: device_info_plus name: device_info_plus
sha256: "4fa68e53e26ab17b70ca39f072c285562cfc1589df5bb1e9295db90f6645f431" sha256: b37d37c2f912ad4e8ec694187de87d05de2a3cb82b465ff1f65f65a2d05de544
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.2.0" version: "11.2.1"
device_info_plus_platform_interface: device_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -538,34 +538,34 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_analytics name: firebase_analytics
sha256: "366140abb55418ea23060b779893fa997c2d8e1974a4d1cc4d9590933b65c5fd" sha256: "498c6cb8468e348a556709c745d92a52173ab3a9b906aa0593393f0787f201ea"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.3.6" version: "11.4.0"
firebase_analytics_platform_interface: firebase_analytics_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_analytics_platform_interface name: firebase_analytics_platform_interface
sha256: "8e987cf977c0c8f4ad02d9950a9b25b1a9606899f37b66a322a43af05be0246b" sha256: ccbb350554e98afdb4b59852689292d194d31232a2647b5012a66622b3711df9
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.8" version: "4.3.0"
firebase_analytics_web: firebase_analytics_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_analytics_web name: firebase_analytics_web
sha256: "0b64ef9060d394bba3d3b4777f49ee098efeeea7b0afb04663c956de6a3da170" sha256: "68e1f18fc16482c211c658e739c25f015b202a260d9ad8249c6d3d7963b8105f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.10+5" version: "0.5.10+6"
firebase_core: firebase_core:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_core name: firebase_core
sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde" sha256: "0307c1fde82e2b8b97e0be2dab93612aff9a72f31ebe9bfac66ed8b37ef7c568"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.9.0" version: "3.10.0"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -586,26 +586,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_messaging name: firebase_messaging
sha256: "151a3ee68736abf293aab66d1317ade53c88abe1db09c75a0460aebf7767bbdf" sha256: "48a8a59197c1c5174060ba9aa1e0036e9b5a0d28a0cc22d19c1fcabc67fafe3c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.1.6" version: "15.2.0"
firebase_messaging_platform_interface: firebase_messaging_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_platform_interface name: firebase_messaging_platform_interface
sha256: f331ee51e40c243f90cc7bc059222dfec4e5df53125b08d31fb28961b00d2a9d sha256: "9770a8e91f54296829dcaa61ce9b7c2f9ae9abbf99976dd3103a60470d5264dd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.49" version: "4.6.0"
firebase_messaging_web: firebase_messaging_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_web name: firebase_messaging_web
sha256: efaf3fdc54cd77e0eedb8e75f7f01c808828c64d052ddbf94d3009974e47d30f sha256: "329ca4ef45ec616abe6f1d5e58feed0934a50840a65aa327052354ad3c64ed77"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.9.5" version: "3.10.0"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -618,10 +618,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fl_chart name: fl_chart
sha256: "10ddaf334fe84d59333a12d153043e366f243e0bdfff2df0313e1e249f5bf926" sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.70.1" version: "0.70.2"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -679,10 +679,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_launcher_icons name: flutter_launcher_icons
sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5" sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.14.2" version: "0.14.3"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -740,10 +740,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_svg name: flutter_svg
sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123" sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.16" version: "2.0.17"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -766,10 +766,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_webrtc name: flutter_webrtc
sha256: e82ffd0d0b79621c5554eed73509d7f5bd286d57fef29a573846785c65237fb1 sha256: "188401cc3275bc4f1f965babdff6cac612a4b46572f1e49f49db8af5361d5712"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.5+hotfix.2" version: "0.12.6"
freezed: freezed:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -934,10 +934,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_android name: image_picker_android
sha256: aa6f1280b670861ac45220cc95adc59bb6ae130259d36f980ccb62220dc5e59f sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.12+19" version: "0.8.12+20"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
@ -974,10 +974,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_platform_interface name: image_picker_platform_interface
sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.10.0" version: "2.10.1"
image_picker_windows: image_picker_windows:
dependency: transitive dependency: transitive
description: description:
@ -1086,10 +1086,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: livekit_client name: livekit_client
sha256: a19bcf8640b45e0730b1e3e3e78be7882dad680c6ebe8ae75294fd8d4612450d sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.4+hotfix.2" version: "2.3.5"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@ -1110,10 +1110,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: markdown name: markdown
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.2.2" version: "7.3.0"
marquee: marquee:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1270,10 +1270,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d" sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.2" version: "8.1.3"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1502,10 +1502,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: pubspec_parse name: pubspec_parse
sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.5.0"
qr: qr:
dependency: transitive dependency: transitive
description: description:
@ -1630,10 +1630,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: share_plus name: share_plus
sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400" sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.1.3" version: "10.1.4"
share_plus_platform_interface: share_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1654,10 +1654,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d" sha256: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.2"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
@ -1971,18 +1971,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.3" version: "2.4.0"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.3" version: "3.1.4"
uuid: uuid:
dependency: "direct main" dependency: "direct main"
description: description:
@ -2003,10 +2003,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_graphics_codec name: vector_graphics_codec
sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.12" version: "1.1.13"
vector_graphics_compiler: vector_graphics_compiler:
dependency: transitive dependency: transitive
description: description:
@ -2169,4 +2169,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.6.0 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.24.0" flutter: ">=3.27.0"

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.2.2+53 version: 2.2.2+55
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4

View File

@ -2,8 +2,8 @@ id = "solian"
[[locations]] [[locations]]
id = "solian" id = "solian"
host = ["sn.solsynth.dev"] hosts = ["sn.solsynth.dev"]
path = ["/"] paths = ["/"]
[[locations.destinations]] [[locations.destinations]]
id = "solian-web" id = "solian-web"
uri = "files:///workdir/solian?fallback=index.html&index=index.html" uri = "files:///workdir/solian?fallback=index.html&index=index.html"