Compare commits

...

44 Commits

Author SHA1 Message Date
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
2ffde9a3dd 🚀 Launch 2.2.2+53 2025-01-15 16:00:59 +08:00
5967a91ae1 Chat user popover 2025-01-15 15:52:52 +08:00
32c1effcb5 💄 Post editor content max width 2025-01-15 15:19:46 +08:00
9d0e19c56f 🐛 Fix chat messages 2025-01-15 15:14:13 +08:00
acf4e634fe 🐛 Fix websocket will put message in wrong channel 2025-01-14 23:32:02 +08:00
25942c2338 💄 Optimize chat max width 2025-01-14 23:30:35 +08:00
a4f81f6ba1 🐛 Post auto warp 2025-01-14 23:24:11 +08:00
c1b9090e51 💄 Optimized attachment list 2025-01-14 23:17:34 +08:00
f494f70003 🚀 Launch 2.2.2+52 2025-01-08 18:07:51 +08:00
fb2a55a909 🐛 Fix editing message did not load the attachment 2025-01-08 17:48:46 +08:00
4edfa7fd50 🐛 Optimizing styling of chat 2025-01-08 17:37:16 +08:00
d699cac9b1 🚀 Launch 2.2.2+51 2025-01-07 18:10:20 +08:00
c0428e12c1 🐛 Fixed the drawer styling issue 2025-01-07 13:11:45 +08:00
55f434ff05 🚀 Launch 2.2.2+40 2025-01-06 23:39:49 +08:00
f2b3bdda2d 🐛 Add ability check to text selection chat message action 2025-01-06 23:17:24 +08:00
1f6bf33b0e Chat message action on system text selection area 2025-01-06 23:15:18 +08:00
e2027b1a32 Able to prefer sidebar to collapse 2025-01-06 22:57:44 +08:00
2b3a58b55e Optimize temporary save post scenario 2025-01-06 22:12:10 +08:00
6ac536412a 🚀 Launch 2.2.2+49 2025-01-06 22:05:20 +08:00
52f8ffe4e4 💄 Update the app bar color when in transparent mode 2025-01-06 21:57:50 +08:00
aca81431aa 🐛 Fix desktop share post as image do not include file extension name 2025-01-06 21:53:35 +08:00
1fadd850b7 💄 Optimize some styling 2025-01-06 21:46:21 +08:00
ed2a9a21b6 🐛 Fix chat username height difference 2025-01-06 19:18:23 +08:00
57279eb3e4 🚀 Launch 2.2.1+48 2025-01-05 13:41:38 +08:00
c403a2914a 💄 Optimized article attachments displaying strategy 2025-01-05 13:34:37 +08:00
bcb176344c 💄 Optimize some styling 2025-01-05 13:29:39 +08:00
ecf362cffc 🚀 Launch 2.2.1+47 2025-01-05 12:13:43 +08:00
f4ab7671d8 🐛 Bug fixes on resetting post write controller 2025-01-05 12:06:49 +08:00
a2a3018917 🚀 Launch 2.2.1+46 2025-01-04 22:05:39 +08:00
0bdb664000 ⚗️ Remove initialization screen 2025-01-04 21:51:22 +08:00
9c3b61ce57 Lunar calendar festivals 2025-01-04 21:49:48 +08:00
d06df3d278 Stickers 2025-01-04 21:26:28 +08:00
547ba19e61 🐛 Fix attachment zoom meta throw error 2025-01-04 19:14:40 +08:00
53 changed files with 2381 additions and 784 deletions

View File

@ -1,12 +1,12 @@
{
"sync": {
"region": "solian-next",
"region": "solian",
"configPath": "roadsign.toml"
},
"deployments": [
{
"region": "solian-next",
"site": "solian-next-web",
"region": "solian",
"site": "solian-web",
"path": "build/web"
}
]

View File

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

View File

@ -7,11 +7,7 @@ meta {
post {
url: {{endpoint}}/cgi/uc/boosts/1/activate
body: none
auth: bearer
}
auth:bearer {
token: {{atk}}
auth: inherit
}
body:json {

View File

@ -0,0 +1,19 @@
meta {
name: Create Sticker Pack
type: http
seq: 1
}
post {
url: {{endpoint}}/cgi/uc/stickers/packs
body: json
auth: inherit
}
body:json {
{
"prefix": "cat",
"name": "Solar Network full of Cats!",
"description": "The sticker packs is full of stickers which related with cats!"
}
}

View File

@ -0,0 +1,20 @@
meta {
name: Create Sticker
type: http
seq: 2
}
post {
url: {{endpoint}}/cgi/uc/stickers
body: json
auth: inherit
}
body:json {
{
"alias": "AteChip",
"name": "Cat ate chips",
"attachment_id": "d0b692cc64054463",
"pack_id": 2
}
}

View File

@ -7,11 +7,7 @@ meta {
post {
url: {{endpoint}}/cgi/id/dev/notify/all
body: json
auth: bearer
}
auth:bearer {
token: {{atk}}
auth: inherit
}
body:json {

7
api/collection.bru Normal file
View File

@ -0,0 +1,7 @@
auth {
mode: bearer
}
auth:bearer {
token: {{atk}}
}

View File

@ -181,6 +181,8 @@
"settingsAppearance": "Appearance",
"settingsAppBarTransparent": "Transparent App Bar",
"settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
"settingsDrawerPreferCollapse": "Prefer Drawer Collapse",
"settingsDrawerPreferCollapseDescription": "Make the drawer to collapse even when the screen is wide enough.",
"settingsBackgroundImage": "Background Image",
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
"settingsBackgroundImageClear": "Clear Existing Background Image",
@ -213,8 +215,9 @@
"sensitiveContentCollapsed": "Sensitive content has been collapsed.",
"sensitiveContentDescription": "This content has been marked as sensitive, and may not be suitable for all viewers.",
"sensitiveContentReveal": "Reveal",
"serverConnecting": "Connecting to server...",
"serverDisconnected": "Lost connection from server",
"serverConnecting": "Connecting...",
"serverDisconnected": "Connection Lost",
"serverConnected": "Connected",
"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.",
"fieldChatName": "Name",
@ -292,6 +295,7 @@
"addAttachmentFromCameraPhoto": "Take photo",
"addAttachmentFromCameraVideo": "Take video",
"addAttachmentFromRandomId": "Link via RID",
"attachmentDetailInfo": "Attachment details",
"attachmentPastedImage": "Pasted Image",
"attachmentInsertLink": "Insert Link",
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
@ -414,6 +418,9 @@
"celebrateBirthday": "Happy birthday, {}!",
"celebrateMerryXmas": "Merry christmas, {}",
"celebrateNewYear": "Happy new year, {}",
"celebrateLunarNewYear": "Happy lunar new year, {}",
"celebrateMidAutumn": "Happy mid-autumn festival, {}",
"celebrateDragonBoat": "Happy dragon boat festival, {}",
"celebrateValentineDay": "Today is valentine's day, {}!",
"celebrateLaborDay": "Today is labor day, {}.",
"celebrateMotherDay": "Today is mother's day, {}.",
@ -423,6 +430,9 @@
"celebrateThanksgiving": "Today is thanksgiving day, {}!",
"pendingBirthday": "Birthday in {}",
"pendingMerryXmas": "Christmas in {}",
"pendingLunarNewYear": "Lunar new year in {}",
"pendingMidAutumn": "Mid-autumn festival in {}",
"pendingDragonBoat": "Dragon boat festival in {}",
"pendingNewYear": "New year in {}",
"pendingValentineDay": "Valentine's day in {}",
"pendingLaborDay": "Labor day in {}",

View File

@ -185,6 +185,8 @@
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
"settingsAppBarTransparent": "透明顶栏",
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
"settingsDrawerPreferCollapse": "侧边栏偏好折叠",
"settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。",
"settingsColorScheme": "主题色",
"settingsColorSchemeDescription": "设置应用主题色。",
"settingsColorSeed": "预设色彩主题",
@ -211,8 +213,9 @@
"sensitiveContentCollapsed": "敏感内容已折叠。",
"sensitiveContentDescription": "此内容已被标记,可能不适合所有人查看。",
"sensitiveContentReveal": "显示内容",
"serverConnecting": "正在连接服务器…",
"serverDisconnected": "已与服务器断开连接",
"serverConnecting": "正在连接…",
"serverDisconnected": "已断开连接",
"serverConnected": "已连接",
"fieldChatAlias": "频道别名",
"fieldChatAliasHint": "全站范围内唯一的频道别名,用于在 URL 中表示该频道,留空则自动生成。应遵循 URL-Safe 的原则。",
"fieldChatName": "名称",
@ -290,6 +293,7 @@
"addAttachmentFromCameraPhoto": "拍摄照片",
"addAttachmentFromCameraVideo": "拍摄视频",
"addAttachmentFromRandomId": "通过访问 ID 链接",
"attachmentDetailInfo": "附件详细信息",
"attachmentPastedImage": "粘贴的图片",
"attachmentInsertLink": "插入连接",
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
@ -410,6 +414,9 @@
"dailyCheckNegativeHint6": "出门",
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
"celebrateBirthday": "生日快乐,{}",
"celebrateLunarNewYear": "春节快乐,{}",
"celebrateMidAutumn": "中秋节快乐,{}",
"celebrateDragonBoat": "端午节快乐,{}",
"celebrateMerryXmas": "圣诞快乐,{}",
"celebrateNewYear": "新年快乐,{}",
"celebrateValentineDay": "今天是情人节,{}",
@ -419,6 +426,9 @@
"celebrateFatherDay": "今天是父亲节,{}。",
"celebrateHalloween": "快乐在圣诞节,{}",
"celebrateThanksgiving": "今天是感恩节,{}",
"pendingLunarNewYear": "{} 过春节",
"pendingMidAutumn": "{} 过中秋节",
"pendingDragonBoat": "{} 过端午节",
"pendingBirthday": "{} 过生日",
"pendingMerryXmas": "{} 过圣诞节",
"pendingNewYear": "{} 跨年",

View File

@ -185,6 +185,8 @@
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
"settingsAppBarTransparent": "透明頂欄",
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
"settingsColorScheme": "主題色",
"settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題",
@ -279,6 +281,10 @@
"one": "{} 個附件",
"other": "{} 個附件"
},
"messageTyping": {
"one": "{} 正在輸入",
"other": "{} 正在輸入"
},
"fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件",
@ -406,6 +412,9 @@
"dailyCheckNegativeHint6": "出門",
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
"celebrateBirthday": "生日快樂,{}",
"celebrateLunarNewYear": "春節快樂,{}",
"celebrateMidAutumn": "中秋節快樂,{}",
"celebrateDragonBoat": "端午節快樂,{}",
"celebrateMerryXmas": "聖誕快樂,{}",
"celebrateNewYear": "新年快樂,{}",
"celebrateValentineDay": "今天是情人節,{}",
@ -415,6 +424,9 @@
"celebrateFatherDay": "今天是父親節,{}。",
"celebrateHalloween": "快樂在聖誕節,{}",
"celebrateThanksgiving": "今天是感恩節,{}",
"pendingLunarNewYear": "{} 過春節",
"pendingMidAutumn": "{} 過中秋節",
"pendingDragonBoat": "{} 過端午節",
"pendingBirthday": "{} 過生日",
"pendingMerryXmas": "{} 過聖誕節",
"pendingNewYear": "{} 跨年",

View File

@ -185,6 +185,8 @@
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
"settingsAppBarTransparent": "透明頂欄",
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
"settingsColorScheme": "主題色",
"settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題",
@ -279,6 +281,10 @@
"one": "{} 個附件",
"other": "{} 個附件"
},
"messageTyping": {
"one": "{} 正在輸入",
"other": "{} 正在輸入"
},
"fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件",
@ -406,6 +412,9 @@
"dailyCheckNegativeHint6": "出門",
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
"celebrateBirthday": "生日快樂,{}",
"celebrateLunarNewYear": "春節快樂,{}",
"celebrateMidAutumn": "中秋節快樂,{}",
"celebrateDragonBoat": "端午節快樂,{}",
"celebrateMerryXmas": "聖誕快樂,{}",
"celebrateNewYear": "新年快樂,{}",
"celebrateValentineDay": "今天是情人節,{}",
@ -415,6 +424,9 @@
"celebrateFatherDay": "今天是父親節,{}。",
"celebrateHalloween": "快樂在聖誕節,{}",
"celebrateThanksgiving": "今天是感恩節,{}",
"pendingLunarNewYear": "{} 過春節",
"pendingMidAutumn": "{} 過中秋節",
"pendingDragonBoat": "{} 過端午節",
"pendingBirthday": "{} 過生日",
"pendingMerryXmas": "{} 過聖誕節",
"pendingNewYear": "{} 跨年",

View File

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

View File

@ -74,6 +74,7 @@ class ChatMessageController extends ChangeNotifier {
_wsSubscription = _ws.stream.stream.listen((event) {
switch (event.method) {
case 'events.new':
if (event.payload?['channel_id'] != channel?.id) break;
final payload = SnChatMessage.fromJson(event.payload!);
_addMessage(payload);
break;
@ -83,7 +84,6 @@ class ChatMessageController extends ChangeNotifier {
if (member.id == profile?.id) break;
if (!typingMembers.any((x) => x.id == member.id)) {
typingMembers.add(member);
print('Typing member: ${typingMembers.map((ele) => member.id)}');
notifyListeners();
}
typingInactiveTimer[member.id]?.cancel();

View File

@ -154,7 +154,10 @@ class PostWriteController extends ChangeNotifier {
final TextEditingController descriptionController = TextEditingController();
final TextEditingController aliasController = TextEditingController();
PostWriteController() {
bool _temporarySaveActive = false;
PostWriteController({bool doLoadFromTemporary = true}) {
_temporarySaveActive = doLoadFromTemporary;
titleController.addListener(() {
_temporaryPlanSave();
notifyListeners();
@ -166,7 +169,7 @@ class PostWriteController extends ChangeNotifier {
contentController.addListener(() {
_temporaryPlanSave();
});
_temporaryLoad();
if (doLoadFromTemporary) _temporaryLoad();
}
String mode = kTitleMap.keys.first;
@ -317,6 +320,7 @@ class PostWriteController extends ChangeNotifier {
Timer? _temporarySaveTimer;
void _temporaryPlanSave() {
if (!_temporarySaveActive) return;
_temporarySaveTimer?.cancel();
_temporarySaveTimer = Timer(const Duration(seconds: 1), () {
_temporarySave();
@ -623,13 +627,15 @@ class PostWriteController extends ChangeNotifier {
void reset() {
publishedAt = null;
publishedUntil = null;
thumbnail = null;
visibility = 0;
titleController.clear();
descriptionController.clear();
contentController.clear();
aliasController.clear();
tags.clear();
categories.clear();
attachments.clear();
tags = List.empty(growable: true);
categories = List.empty(growable: true);
attachments = List.empty(growable: true);
editingPost = null;
replyingPost = null;
repostingPost = null;

View File

@ -10,7 +10,6 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:package_info_plus/package_info_plus.dart';
@ -18,7 +17,6 @@ import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/firebase_options.dart';
import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.dart';
@ -30,6 +28,7 @@ import 'package:surface/providers/post.dart';
import 'package:surface/providers/relationship.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/providers/special_day.dart';
import 'package:surface/providers/theme.dart';
import 'package:surface/providers/user_directory.dart';
@ -41,7 +40,6 @@ import 'package:surface/types/chat.dart';
import 'package:surface/types/realm.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/version_label.dart';
import 'package:version/version.dart';
import 'package:workmanager/workmanager.dart';
import 'package:in_app_review/in_app_review.dart';
@ -144,6 +142,7 @@ class SolianApp extends StatelessWidget {
Provider(create: (ctx) => SnPostContentProvider(ctx)),
Provider(create: (ctx) => SnRelationshipProvider(ctx)),
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
Provider(create: (ctx) => SnStickerProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
@ -208,8 +207,6 @@ class _AppSplashScreen extends StatefulWidget {
}
class _AppSplashScreenState extends State<_AppSplashScreen> {
bool _isReady = false;
void _tryRequestRating() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('first_boot_time')) {
@ -261,6 +258,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
Future<void> _initialize() async {
try {
final cfg = context.read<ConfigProvider>();
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
final home = context.read<HomeWidgetProvider>();
await home.initialize();
if (!mounted) return;
@ -278,12 +279,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
await ws.tryConnect();
if (!mounted) return;
final notify = context.read<NotificationProvider>();
notify.listen();
await notify.registerPushNotifications();
} catch (err) {
if (!mounted) return;
await context.showErrorDialog(err);
} finally {
setState(() => _isReady = true);
}
}
@ -303,32 +303,17 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
@override
Widget build(BuildContext context) {
if (!_isReady) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: Container(
constraints: const BoxConstraints(maxWidth: 180),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (MediaQuery.of(context).platformBrightness == Brightness.dark)
Image.asset("assets/icon/icon-dark.png", width: 64, height: 64)
else
Image.asset("assets/icon/icon.png", width: 64, height: 64),
const Gap(6),
LinearProgressIndicator(
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
),
const Gap(20),
Text('appInitializing'.tr(), textAlign: TextAlign.center),
AppVersionLabel(),
],
),
).center(),
);
}
return widget.child;
final cfg = context.read<ConfigProvider>();
return NotificationListener<SizeChangedLayoutNotification>(
onNotification: (notification) {
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
return false;
},
child: SizeChangedLayoutNotifier(
child: widget.child,
),
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/providers/widget.dart';
@ -12,6 +13,7 @@ const kNetworkServerStoreKey = 'app_server_url';
const kAppbarTransparentStoreKey = 'app_bar_transparent';
const kAppBackgroundStoreKey = 'app_has_background';
const kAppColorSchemeStoreKey = 'app_color_scheme';
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none,
@ -33,6 +35,24 @@ class ConfigProvider extends ChangeNotifier {
prefs = await SharedPreferences.getInstance();
}
bool drawerIsCollapsed = false;
bool drawerIsExpanded = false;
void calcDrawerSize(BuildContext context) {
final rpb = ResponsiveBreakpoints.of(context);
final newDrawerIsCollapsed = rpb.smallerOrEqualTo(MOBILE);
final newDrawerIsExpanded = rpb.largerThan(TABLET)
? (prefs.getBool(kAppDrawerPreferCollapse) ?? false)
? false
: true
: false;
if (newDrawerIsExpanded != drawerIsExpanded || newDrawerIsCollapsed != drawerIsCollapsed) {
drawerIsExpanded = newDrawerIsExpanded;
drawerIsCollapsed = newDrawerIsCollapsed;
notifyListeners();
}
}
FilterQuality get imageQuality {
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
}

View File

@ -8,14 +8,18 @@ import 'package:flutter_udid/flutter_udid.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/providers/websocket.dart';
import 'package:surface/types/notification.dart';
class NotificationProvider extends ChangeNotifier {
late final SnNetworkProvider _sn;
late final UserProvider _ua;
late final WebSocketProvider _ws;
NotificationProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
_ua = context.read<UserProvider>();
_ws = context.read<WebSocketProvider>();
}
Future<void> registerPushNotifications() async {
@ -62,4 +66,21 @@ 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();
}
});
}
void clear() {
notifications.clear();
notifyListeners();
}
}

View File

@ -0,0 +1,38 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart';
class SnStickerProvider {
late final SnNetworkProvider _sn;
final Map<String, SnSticker?> _cache = {};
SnStickerProvider(BuildContext context) {
_sn = context.read<SnNetworkProvider>();
}
bool hasNotSticker(String alias) {
return _cache.containsKey(alias) && _cache[alias] == null;
}
Future<SnSticker?> lookupSticker(String alias) async {
if (_cache.containsKey(alias)) {
return _cache[alias];
}
try {
final resp = await _sn.client.get('/cgi/uc/stickers/lookup/$alias');
final sticker = SnSticker.fromJson(resp.data);
_cache[alias] = sticker;
return sticker;
} catch (err) {
_cache[alias] = null;
log('[Sticker] Failed to lookup sticker $alias: $err');
}
return null;
}
}

View File

@ -3,9 +3,12 @@ import 'package:provider/provider.dart';
import 'package:surface/providers/userinfo.dart';
// Stored as key: month, day
const Map<String, (int, int)> kSpecialDays = {
final Map<String, (int, int)> kSpecialDays = {
// Birthday is dynamically generated according to the user's profile
'NewYear': (1, 1),
'LunarNewYear': (lunarToGregorian(null, 1, 1).month, lunarToGregorian(null, 1, 1).day),
'MidAutumn': (lunarToGregorian(null, 8, 15).month, lunarToGregorian(null, 8, 15).day),
'DragonBoat': (lunarToGregorian(null, 5, 5).month, lunarToGregorian(null, 5, 5).day),
'ValentineDay': (2, 14),
'LaborDay': (5, 1),
'MotherDay': (5, 11),
@ -19,6 +22,9 @@ const Map<String, (int, int)> kSpecialDays = {
const Map<String, String> kSpecialDaysSymbol = {
'Birthday': '🎂',
'NewYear': '🎉',
'LunarNewYear': '🎉',
'MidAutumn': '🥮',
'DragonBoat': '🐲',
'MerryXmas': '🎄',
'ValentineDay': '💑',
'LaborDay': '🏋️',
@ -134,3 +140,45 @@ class SpecialDayProvider {
return (elapsedDuration / totalDuration).clamp(0.0, 1.0);
}
}
final Map<int, LunarYear> lunarYearData = {
2025: LunarYear(
startDate: DateTime(2025, 1, 29),
months: [29, 30, 30, 29, 30, 29, 29, 30, 30, 29, 30, 29],
leapMonth: 0,
),
};
class LunarYear {
final DateTime startDate;
final List<int> months;
final int leapMonth;
LunarYear({required this.startDate, required this.months, required this.leapMonth});
}
DateTime lunarToGregorian(int? year, int month, int day, {bool isLeapMonth = false}) {
year = year ?? DateTime.now().year;
final lunarYear = lunarYearData[year];
if (lunarYear == null) {
throw Exception('Lunar data for year $year not found');
}
int leapMonth = lunarYear.leapMonth;
if (isLeapMonth && (leapMonth == 0 || leapMonth != month)) {
throw Exception('Invalid leap month for year $year');
}
int daysFromStart = 0;
for (int i = 0; i < month - 1; i++) {
daysFromStart += lunarYear.months[i];
}
if (isLeapMonth) {
daysFromStart += lunarYear.months[month - 1];
}
daysFromStart += day - 1;
return lunarYear.startDate.add(Duration(days: daysFromStart));
}

View File

@ -1,3 +0,0 @@
class StickerProvider {
}

View File

@ -35,7 +35,7 @@ class WebSocketProvider extends ChangeNotifier {
Future<void> connect({noRetry = false}) async {
if (!_ua.isAuthorized) return;
if (isConnected) {
if (isConnected || conn != null) {
disconnect();
}
@ -97,7 +97,7 @@ class WebSocketProvider extends ChangeNotifier {
onError: (err) {
isConnected = false;
notifyListeners();
Future.delayed(const Duration(seconds: 11), () => connect());
Future.delayed(const Duration(seconds: 1), () => connect());
},
);
}

View File

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:surface/screens/abuse_report.dart';
import 'package:surface/screens/account.dart';
import 'package:surface/screens/account/pfp.dart';
import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/screens/account/profile_edit.dart';
import 'package:surface/screens/account/publishers/publisher_edit.dart';
import 'package:surface/screens/account/publishers/publisher_new.dart';

View File

@ -236,7 +236,7 @@ class _ChatScreenState extends State<ChatScreen> {
'alias': channel.alias,
},
).then((value) {
if (value == true) _refreshChannels();
if (mounted) _refreshChannels();
});
},
);

View File

@ -1,3 +1,4 @@
import 'package:animations/animations.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
@ -8,6 +9,7 @@ import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/screens/post/post_detail.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/app_bar_leading.dart';
import 'package:surface/widgets/dialog.dart';
@ -210,6 +212,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
),
),
),
const SliverGap(8),
SliverInfiniteList(
itemCount: _posts.length,
isLoading: _isBusy,
@ -217,27 +220,37 @@ class _ExploreScreenState extends State<ExploreScreen> {
hasReachedMax: _postCount != null && _posts.length >= _postCount!,
onFetchData: _fetchPosts,
itemBuilder: (context, idx) {
return GestureDetector(
child: PostItem(
data: _posts[idx],
maxWidth: 640,
onChanged: (data) {
setState(() => _posts[idx] = data);
},
onDeleted: () {
_refreshPosts();
},
return Center(
child: OpenContainer(
closedBuilder: (_, __) => Container(
constraints: const BoxConstraints(maxWidth: 640),
child: PostItem(
data: _posts[idx],
maxWidth: 640,
onChanged: (data) {
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.surface,
transitionType: ContainerTransitionType.fade,
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
),
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

@ -197,7 +197,7 @@ class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
return Card(
child: ListTile(
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
title: Text('pending$name').tr(args: [RelativeTime(context).format(date)]),
title: Text('pending$name').tr(args: [RelativeTime(context).format(date).replaceFirst('in', '').trim()]),
subtitle: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [

View File

@ -206,10 +206,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
style: Theme.of(context).textTheme.titleSmall,
),
if (nty.subtitle != null) const Gap(4),
MarkdownTextContent(
content: nty.body,
isAutoWarp: true,
isSelectable: true,
SelectionArea(
child: MarkdownTextContent(
content: nty.body,
isAutoWarp: true,
),
),
if ([
'interactive.feedback',

View File

@ -13,6 +13,7 @@ import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/post.dart';
import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/post/post_comment_list.dart';
import 'package:surface/widgets/post/post_item.dart';
import 'package:surface/widgets/post/post_mini_editor.dart';
@ -20,12 +21,9 @@ import 'package:surface/widgets/post/post_mini_editor.dart';
class PostDetailScreen extends StatefulWidget {
final String slug;
final SnPost? preload;
final Function? onBack;
const PostDetailScreen({
super.key,
required this.slug,
this.preload,
});
const PostDetailScreen({super.key, required this.slug, this.preload, this.onBack});
@override
State<PostDetailScreen> createState() => _PostDetailScreenState();
@ -67,121 +65,129 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
final ua = context.watch<UserProvider>();
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return Scaffold(
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (GoRouter.of(context).canPop()) {
GoRouter.of(context).pop(context);
return;
}
GoRouter.of(context).replaceNamed('explore');
},
),
title: _data?.body['title'] != null
? RichText(
textAlign: TextAlign.center,
text: TextSpan(children: [
TextSpan(
text: _data?.body['title'] ?? 'postNoun'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
const TextSpan(text: '\n'),
TextSpan(
text: 'postDetail'.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
]),
)
: Text('postDetail').tr(),
),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: LoadingIndicator(isActive: _isBusy),
return AppBackground(
isRoot: widget.onBack != null,
child: Scaffold(
appBar: AppBar(
leading: BackButton(
onPressed: () {
if (widget.onBack != null) {
widget.onBack!.call();
}
if (GoRouter.of(context).canPop()) {
GoRouter.of(context).pop(context);
return;
}
GoRouter.of(context).replaceNamed('explore');
},
),
if (_data != null)
SliverToBoxAdapter(
child: PostItem(
data: _data!,
maxWidth: 640,
showComments: false,
showFullPost: true,
onChanged: (data) {
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,
title: _data?.body['title'] != null
? RichText(
textAlign: TextAlign.center,
text: TextSpan(children: [
TextSpan(
text: _data?.body['title'] ?? 'postNoun'.tr(),
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
child: PostMiniEditor(
postReplyId: _data!.id,
onPost: () {
setState(() {
_data = _data!.copyWith(
metric: _data!.metric.copyWith(
replyCount: _data!.metric.replyCount + 1,
),
);
});
_childListKey.currentState!.refresh();
),
const TextSpan(text: '\n'),
TextSpan(
text: 'postDetail'.tr(),
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
]),
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
: Text('postDetail').tr(),
),
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: LoadingIndicator(isActive: _isBusy),
),
if (_data != null)
SliverToBoxAdapter(
child: PostItem(
data: _data!,
maxWidth: 640,
showComments: false,
showFullPost: true,
onChanged: (data) {
setState(() => _data = data);
},
onDeleted: () {
Navigator.pop(context);
},
),
).center(),
),
if (_data != null)
PostCommentSliverList(
key: _childListKey,
parentPostId: _data!.id,
maxWidth: 640,
),
SliverGap(math.max(MediaQuery.of(context).padding.bottom, 16)),
],
),
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,
),
),
),
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

@ -54,7 +54,9 @@ class PostEditorScreen extends StatefulWidget {
}
class _PostEditorScreenState extends State<PostEditorScreen> {
final PostWriteController _writeController = PostWriteController();
late final PostWriteController _writeController = PostWriteController(
doLoadFromTemporary: widget.postEditId == null,
);
bool _isFetching = false;
@ -301,19 +303,22 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
],
),
// Content Input Area
TextField(
controller: _writeController.contentController,
maxLines: null,
decoration: InputDecoration(
hintText: 'fieldPostContent'.tr(),
hintStyle: TextStyle(fontSize: 14),
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
Container(
constraints: const BoxConstraints(maxWidth: 640),
child: TextField(
controller: _writeController.contentController,
maxLines: null,
decoration: InputDecoration(
hintText: 'fieldPostContent'.tr(),
hintStyle: TextStyle(fontSize: 14),
isCollapsed: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
),
border: InputBorder.none,
),
border: InputBorder.none,
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
]
.expandIndexed(
@ -364,6 +369,15 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LoadingIndicator(isActive: _isLoading),
if (_writeController.isBusy && _writeController.progress != null)
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _writeController.progress),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
)
else if (_writeController.isBusy)
const LinearProgressIndicator(value: null, minHeight: 2),
Container(
child: _writeController.temporaryRestored
? Container(
@ -394,15 +408,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
)
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
LoadingIndicator(isActive: _isLoading),
if (_writeController.isBusy && _writeController.progress != null)
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: _writeController.progress),
duration: Duration(milliseconds: 300),
builder: (context, value, _) => LinearProgressIndicator(value: value, minHeight: 2),
)
else if (_writeController.isBusy)
const LinearProgressIndicator(value: null, minHeight: 2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [

View File

@ -120,7 +120,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
subtitle: Text('settingsThemeMaterial3Description').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
secondary: const Icon(Symbols.new_releases),
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? false,
value: _prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
onChanged: (value) {
setState(() {
_prefs.setBool(
@ -240,6 +240,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {});
},
),
CheckboxListTile(
secondary: const Icon(Symbols.left_panel_close),
title: Text('settingsDrawerPreferCollapse').tr(),
subtitle: Text('settingsDrawerPreferCollapseDescription').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
value: _prefs.getBool(kAppDrawerPreferCollapse) ?? false,
onChanged: (value) {
_prefs.setBool(kAppDrawerPreferCollapse, value ?? false);
final cfg = context.read<ConfigProvider>();
cfg.calcDrawerSize(context);
setState(() {});
},
),
],
),
Column(

View File

@ -36,7 +36,7 @@ Future<ThemeData> createAppTheme(
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
return ThemeData(
useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false),
useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true),
colorScheme: colorScheme,
brightness: brightness,
iconTheme: IconThemeData(
@ -48,9 +48,19 @@ Future<ThemeData> createAppTheme(
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: hasAppBarBlurry ? 0 : null,
backgroundColor: hasAppBarBlurry ? colorScheme.surfaceContainer.withAlpha(200) : colorScheme.primary,
backgroundColor: hasAppBarBlurry ? colorScheme.primary.withOpacity(0.3) : colorScheme.primary,
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
),
scaffoldBackgroundColor: Colors.transparent,
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
TargetPlatform.iOS: ZoomPageTransitionsBuilder(),
TargetPlatform.macOS: ZoomPageTransitionsBuilder(),
TargetPlatform.fuchsia: ZoomPageTransitionsBuilder(),
TargetPlatform.linux: ZoomPageTransitionsBuilder(),
TargetPlatform.windows: ZoomPageTransitionsBuilder(),
},
),
);
}

View File

@ -141,3 +141,39 @@ class SnAttachmentBoost with _$SnAttachmentBoost {
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) => _$SnAttachmentBoostFromJson(json);
}
@freezed
class SnSticker with _$SnSticker {
const factory SnSticker({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String alias,
required String name,
required int attachmentId,
required SnAttachment attachment,
required int packId,
required SnStickerPack pack,
required int accountId,
}) = _SnSticker;
factory SnSticker.fromJson(Map<String, Object?> json) => _$SnStickerFromJson(json);
}
@freezed
class SnStickerPack with _$SnStickerPack {
const factory SnStickerPack({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required String prefix,
required String name,
required String description,
required List<SnSticker>? stickers,
required int accountId,
}) = _SnStickerPack;
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json);
}

View File

@ -2272,3 +2272,738 @@ abstract class _SnAttachmentBoost implements SnAttachmentBoost {
_$$SnAttachmentBoostImplCopyWith<_$SnAttachmentBoostImpl> get copyWith =>
throw _privateConstructorUsedError;
}
SnSticker _$SnStickerFromJson(Map<String, dynamic> json) {
return _SnSticker.fromJson(json);
}
/// @nodoc
mixin _$SnSticker {
int get id => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
DateTime get updatedAt => throw _privateConstructorUsedError;
DateTime? get deletedAt => throw _privateConstructorUsedError;
String get alias => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
int get attachmentId => throw _privateConstructorUsedError;
SnAttachment get attachment => throw _privateConstructorUsedError;
int get packId => throw _privateConstructorUsedError;
SnStickerPack get pack => throw _privateConstructorUsedError;
int get accountId => throw _privateConstructorUsedError;
/// Serializes this SnSticker to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnStickerCopyWith<SnSticker> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnStickerCopyWith<$Res> {
factory $SnStickerCopyWith(SnSticker value, $Res Function(SnSticker) then) =
_$SnStickerCopyWithImpl<$Res, SnSticker>;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String alias,
String name,
int attachmentId,
SnAttachment attachment,
int packId,
SnStickerPack pack,
int accountId});
$SnAttachmentCopyWith<$Res> get attachment;
$SnStickerPackCopyWith<$Res> get pack;
}
/// @nodoc
class _$SnStickerCopyWithImpl<$Res, $Val extends SnSticker>
implements $SnStickerCopyWith<$Res> {
_$SnStickerCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? alias = null,
Object? name = null,
Object? attachmentId = null,
Object? attachment = null,
Object? packId = null,
Object? pack = null,
Object? accountId = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
alias: null == alias
? _value.alias
: alias // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
attachmentId: null == attachmentId
? _value.attachmentId
: attachmentId // ignore: cast_nullable_to_non_nullable
as int,
attachment: null == attachment
? _value.attachment
: attachment // ignore: cast_nullable_to_non_nullable
as SnAttachment,
packId: null == packId
? _value.packId
: packId // ignore: cast_nullable_to_non_nullable
as int,
pack: null == pack
? _value.pack
: pack // ignore: cast_nullable_to_non_nullable
as SnStickerPack,
accountId: null == accountId
? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAttachmentCopyWith<$Res> get attachment {
return $SnAttachmentCopyWith<$Res>(_value.attachment, (value) {
return _then(_value.copyWith(attachment: value) as $Val);
});
}
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnStickerPackCopyWith<$Res> get pack {
return $SnStickerPackCopyWith<$Res>(_value.pack, (value) {
return _then(_value.copyWith(pack: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$SnStickerImplCopyWith<$Res>
implements $SnStickerCopyWith<$Res> {
factory _$$SnStickerImplCopyWith(
_$SnStickerImpl value, $Res Function(_$SnStickerImpl) then) =
__$$SnStickerImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String alias,
String name,
int attachmentId,
SnAttachment attachment,
int packId,
SnStickerPack pack,
int accountId});
@override
$SnAttachmentCopyWith<$Res> get attachment;
@override
$SnStickerPackCopyWith<$Res> get pack;
}
/// @nodoc
class __$$SnStickerImplCopyWithImpl<$Res>
extends _$SnStickerCopyWithImpl<$Res, _$SnStickerImpl>
implements _$$SnStickerImplCopyWith<$Res> {
__$$SnStickerImplCopyWithImpl(
_$SnStickerImpl _value, $Res Function(_$SnStickerImpl) _then)
: super(_value, _then);
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? alias = null,
Object? name = null,
Object? attachmentId = null,
Object? attachment = null,
Object? packId = null,
Object? pack = null,
Object? accountId = null,
}) {
return _then(_$SnStickerImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
alias: null == alias
? _value.alias
: alias // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
attachmentId: null == attachmentId
? _value.attachmentId
: attachmentId // ignore: cast_nullable_to_non_nullable
as int,
attachment: null == attachment
? _value.attachment
: attachment // ignore: cast_nullable_to_non_nullable
as SnAttachment,
packId: null == packId
? _value.packId
: packId // ignore: cast_nullable_to_non_nullable
as int,
pack: null == pack
? _value.pack
: pack // ignore: cast_nullable_to_non_nullable
as SnStickerPack,
accountId: null == accountId
? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnStickerImpl implements _SnSticker {
const _$SnStickerImpl(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.alias,
required this.name,
required this.attachmentId,
required this.attachment,
required this.packId,
required this.pack,
required this.accountId});
factory _$SnStickerImpl.fromJson(Map<String, dynamic> json) =>
_$$SnStickerImplFromJson(json);
@override
final int id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
final DateTime? deletedAt;
@override
final String alias;
@override
final String name;
@override
final int attachmentId;
@override
final SnAttachment attachment;
@override
final int packId;
@override
final SnStickerPack pack;
@override
final int accountId;
@override
String toString() {
return 'SnSticker(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, alias: $alias, name: $name, attachmentId: $attachmentId, attachment: $attachment, packId: $packId, pack: $pack, accountId: $accountId)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnStickerImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.alias, alias) || other.alias == alias) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.attachmentId, attachmentId) ||
other.attachmentId == attachmentId) &&
(identical(other.attachment, attachment) ||
other.attachment == attachment) &&
(identical(other.packId, packId) || other.packId == packId) &&
(identical(other.pack, pack) || other.pack == pack) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
createdAt,
updatedAt,
deletedAt,
alias,
name,
attachmentId,
attachment,
packId,
pack,
accountId);
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnStickerImplCopyWith<_$SnStickerImpl> get copyWith =>
__$$SnStickerImplCopyWithImpl<_$SnStickerImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnStickerImplToJson(
this,
);
}
}
abstract class _SnSticker implements SnSticker {
const factory _SnSticker(
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final DateTime? deletedAt,
required final String alias,
required final String name,
required final int attachmentId,
required final SnAttachment attachment,
required final int packId,
required final SnStickerPack pack,
required final int accountId}) = _$SnStickerImpl;
factory _SnSticker.fromJson(Map<String, dynamic> json) =
_$SnStickerImpl.fromJson;
@override
int get id;
@override
DateTime get createdAt;
@override
DateTime get updatedAt;
@override
DateTime? get deletedAt;
@override
String get alias;
@override
String get name;
@override
int get attachmentId;
@override
SnAttachment get attachment;
@override
int get packId;
@override
SnStickerPack get pack;
@override
int get accountId;
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnStickerImplCopyWith<_$SnStickerImpl> get copyWith =>
throw _privateConstructorUsedError;
}
SnStickerPack _$SnStickerPackFromJson(Map<String, dynamic> json) {
return _SnStickerPack.fromJson(json);
}
/// @nodoc
mixin _$SnStickerPack {
int get id => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
DateTime get updatedAt => throw _privateConstructorUsedError;
DateTime? get deletedAt => throw _privateConstructorUsedError;
String get prefix => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
String get description => throw _privateConstructorUsedError;
List<SnSticker>? get stickers => throw _privateConstructorUsedError;
int get accountId => throw _privateConstructorUsedError;
/// Serializes this SnStickerPack to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnStickerPackCopyWith<SnStickerPack> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnStickerPackCopyWith<$Res> {
factory $SnStickerPackCopyWith(
SnStickerPack value, $Res Function(SnStickerPack) then) =
_$SnStickerPackCopyWithImpl<$Res, SnStickerPack>;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String prefix,
String name,
String description,
List<SnSticker>? stickers,
int accountId});
}
/// @nodoc
class _$SnStickerPackCopyWithImpl<$Res, $Val extends SnStickerPack>
implements $SnStickerPackCopyWith<$Res> {
_$SnStickerPackCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? prefix = null,
Object? name = null,
Object? description = null,
Object? stickers = freezed,
Object? accountId = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
prefix: null == prefix
? _value.prefix
: prefix // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
stickers: freezed == stickers
? _value.stickers
: stickers // ignore: cast_nullable_to_non_nullable
as List<SnSticker>?,
accountId: null == accountId
? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$SnStickerPackImplCopyWith<$Res>
implements $SnStickerPackCopyWith<$Res> {
factory _$$SnStickerPackImplCopyWith(
_$SnStickerPackImpl value, $Res Function(_$SnStickerPackImpl) then) =
__$$SnStickerPackImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
String prefix,
String name,
String description,
List<SnSticker>? stickers,
int accountId});
}
/// @nodoc
class __$$SnStickerPackImplCopyWithImpl<$Res>
extends _$SnStickerPackCopyWithImpl<$Res, _$SnStickerPackImpl>
implements _$$SnStickerPackImplCopyWith<$Res> {
__$$SnStickerPackImplCopyWithImpl(
_$SnStickerPackImpl _value, $Res Function(_$SnStickerPackImpl) _then)
: super(_value, _then);
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? prefix = null,
Object? name = null,
Object? description = null,
Object? stickers = freezed,
Object? accountId = null,
}) {
return _then(_$SnStickerPackImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
prefix: null == prefix
? _value.prefix
: prefix // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
stickers: freezed == stickers
? _value._stickers
: stickers // ignore: cast_nullable_to_non_nullable
as List<SnSticker>?,
accountId: null == accountId
? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnStickerPackImpl implements _SnStickerPack {
const _$SnStickerPackImpl(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.prefix,
required this.name,
required this.description,
required final List<SnSticker>? stickers,
required this.accountId})
: _stickers = stickers;
factory _$SnStickerPackImpl.fromJson(Map<String, dynamic> json) =>
_$$SnStickerPackImplFromJson(json);
@override
final int id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
final DateTime? deletedAt;
@override
final String prefix;
@override
final String name;
@override
final String description;
final List<SnSticker>? _stickers;
@override
List<SnSticker>? get stickers {
final value = _stickers;
if (value == null) return null;
if (_stickers is EqualUnmodifiableListView) return _stickers;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
final int accountId;
@override
String toString() {
return 'SnStickerPack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, prefix: $prefix, name: $name, description: $description, stickers: $stickers, accountId: $accountId)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnStickerPackImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.prefix, prefix) || other.prefix == prefix) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.description, description) ||
other.description == description) &&
const DeepCollectionEquality().equals(other._stickers, _stickers) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
createdAt,
updatedAt,
deletedAt,
prefix,
name,
description,
const DeepCollectionEquality().hash(_stickers),
accountId);
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnStickerPackImplCopyWith<_$SnStickerPackImpl> get copyWith =>
__$$SnStickerPackImplCopyWithImpl<_$SnStickerPackImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnStickerPackImplToJson(
this,
);
}
}
abstract class _SnStickerPack implements SnStickerPack {
const factory _SnStickerPack(
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final DateTime? deletedAt,
required final String prefix,
required final String name,
required final String description,
required final List<SnSticker>? stickers,
required final int accountId}) = _$SnStickerPackImpl;
factory _SnStickerPack.fromJson(Map<String, dynamic> json) =
_$SnStickerPackImpl.fromJson;
@override
int get id;
@override
DateTime get createdAt;
@override
DateTime get updatedAt;
@override
DateTime? get deletedAt;
@override
String get prefix;
@override
String get name;
@override
String get description;
@override
List<SnSticker>? get stickers;
@override
int get accountId;
/// Create a copy of SnStickerPack
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnStickerPackImplCopyWith<_$SnStickerPackImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -218,3 +218,66 @@ Map<String, dynamic> _$$SnAttachmentBoostImplToJson(
'attachment': instance.attachment.toJson(),
'account': instance.account,
};
_$SnStickerImpl _$$SnStickerImplFromJson(Map<String, dynamic> json) =>
_$SnStickerImpl(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
alias: json['alias'] as String,
name: json['name'] as String,
attachmentId: (json['attachment_id'] as num).toInt(),
attachment:
SnAttachment.fromJson(json['attachment'] as Map<String, dynamic>),
packId: (json['pack_id'] as num).toInt(),
pack: SnStickerPack.fromJson(json['pack'] as Map<String, dynamic>),
accountId: (json['account_id'] as num).toInt(),
);
Map<String, dynamic> _$$SnStickerImplToJson(_$SnStickerImpl instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'alias': instance.alias,
'name': instance.name,
'attachment_id': instance.attachmentId,
'attachment': instance.attachment.toJson(),
'pack_id': instance.packId,
'pack': instance.pack.toJson(),
'account_id': instance.accountId,
};
_$SnStickerPackImpl _$$SnStickerPackImplFromJson(Map<String, dynamic> json) =>
_$SnStickerPackImpl(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
prefix: json['prefix'] as String,
name: json['name'] as String,
description: json['description'] as String,
stickers: (json['stickers'] as List<dynamic>?)
?.map((e) => SnSticker.fromJson(e as Map<String, dynamic>))
.toList(),
accountId: (json['account_id'] as num).toInt(),
);
Map<String, dynamic> _$$SnStickerPackImplToJson(_$SnStickerPackImpl instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'prefix': instance.prefix,
'name': instance.name,
'description': instance.description,
'stickers': instance.stickers?.map((e) => e.toJson()).toList(),
'account_id': instance.accountId,
};

View File

@ -0,0 +1,164 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/experience.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/types/account.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/universal_image.dart';
class AccountPopoverCard extends StatelessWidget {
final SnAccount data;
const AccountPopoverCard({super.key, required this.data});
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (data.banner.isNotEmpty)
Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: AspectRatio(
aspectRatio: 16 / 7,
child: AutoResizeUniversalImage(
sn.getAttachmentUrl(data.banner),
fit: BoxFit.cover,
),
),
),
// Top padding
Gap(16),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AccountImage(
content: data.avatar,
radius: 20,
),
Gap(16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(data.nick).bold(),
Text('@${data.name}').fontSize(13).opacity(0.75),
],
),
),
IconButton(
onPressed: () {
Navigator.pop(context);
GoRouter.of(context).pushNamed(
'accountProfilePage',
pathParameters: {'name': data.name},
);
},
icon: const Icon(Symbols.chevron_right),
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
),
const Gap(8)
],
).padding(horizontal: 16),
const Gap(16),
Wrap(
children: data.badges
.map(
(ele) => Tooltip(
richMessage: TextSpan(
children: [
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
if (ele.metadata['title'] != null)
TextSpan(
text: '\n${ele.metadata['title']}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(text: '\n'),
TextSpan(
text: DateFormat.yMEd().format(ele.createdAt),
),
],
),
child: Icon(
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
color: kBadgesMeta[ele.type]?.$3,
fill: 1,
),
),
)
.toList(),
).padding(horizontal: 24),
const Gap(8),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.star),
const Gap(8),
Text('Lv${getLevelFromExp(data.profile?.experience ?? 0)}'),
const Gap(8),
Text(calcLevelUpProgressLevel(data.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
const Gap(8),
Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 160),
child: LinearProgressIndicator(
value: calcLevelUpProgress(data.profile?.experience ?? 0),
borderRadius: BorderRadius.circular(8),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
).alignment(Alignment.centerLeft),
),
],
).padding(horizontal: 24),
FutureBuilder(
future: sn.client.get('/cgi/id/users/${data.name}/status'),
builder: (context, snapshot) {
final SnAccountStatusInfo? status =
snapshot.hasData ? SnAccountStatusInfo.fromJson(snapshot.data!.data) : null;
return Row(
children: [
Icon(
Symbols.circle,
fill: 1,
size: 16,
color: (status?.isOnline ?? false) ? Colors.green : Colors.grey,
).padding(all: 4),
const Gap(8),
Text(
status != null
? status.isOnline
? 'accountStatusOnline'.tr()
: 'accountStatusOffline'.tr()
: 'loading'.tr(),
),
if (status != null && !status.isOnline && status.lastSeenAt != null)
Text(
'accountStatusLastSeen'.tr(args: [
status.lastSeenAt != null
? RelativeTime(context).format(
status.lastSeenAt!.toLocal(),
)
: 'unknown',
]),
).padding(left: 6).opacity(0.75),
],
).padding(horizontal: 24);
},
),
// Bottom padding
const Gap(16),
],
);
}
}

View File

@ -106,6 +106,44 @@ class _AttachmentListState extends State<AttachmentList> {
}
if (widget.gridded) {
final fullOfImage =
widget.data.where((ele) => ele?.mediaType == SnMediaType.image).length == widget.data.length;
if(!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(
margin: widget.padding ?? EdgeInsets.zero,
decoration: BoxDecoration(

View File

@ -129,6 +129,8 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
bool _showDetail = false;
@override
Widget build(BuildContext context) {
final sn = context.read<SnNetworkProvider>();
@ -144,223 +146,348 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
onDismissed: () {
Navigator.of(context).pop();
},
direction: DismissiblePageDismissDirection.down,
direction: DismissiblePageDismissDirection.none,
backgroundColor: Colors.transparent,
isFullScreen: true,
child: Scaffold(
body: Stack(
children: [
Builder(builder: (context) {
if (widget.data.length == 1) {
final heroTag = widget.heroTags?.first ?? uuid.v4();
return Hero(
tag: 'attachment-${widget.data.first.rid}-$heroTag',
child: PhotoView(
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
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',
child: GestureDetector(
behavior: HitTestBehavior.translucent,
child: Scaffold(
body: Stack(
children: [
Builder(builder: (context) {
if (widget.data.length == 1) {
final heroTag = widget.heroTags?.first ?? uuid.v4();
return Hero(
tag: 'attachment-${widget.data.first.rid}-$heroTag',
child: PhotoView(
key: Key('attachment-detail-${widget.data.first.rid}-$heroTag'),
backgroundDecoration: BoxDecoration(color: Colors.transparent),
imageProvider: UniversalImage.provider(
sn.getAttachmentUrl(widget.data.first.rid),
),
),
);
},
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),
}
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(
child: SizedBox(
width: 20.0,
height: 20.0,
child: CircularProgressIndicator(
value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1),
),
),
),
),
backgroundDecoration: BoxDecoration(color: Colors.transparent),
);
}),
Align(
alignment: Alignment.bottomCenter,
child: IgnorePointer(
child: Container(
height: 300,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surface,
Colors.transparent,
],
backgroundDecoration: BoxDecoration(color: Colors.transparent),
);
}),
Align(
alignment: Alignment.bottomCenter,
child: IgnorePointer(
child: Container(
height: 300,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surface,
Colors.transparent,
],
),
),
),
),
),
),
Positioned(
left: 16,
right: 16,
bottom: 16 + MediaQuery.of(context).padding.bottom,
child: Material(
color: Colors.transparent,
child: Builder(builder: (context) {
final ud = context.read<UserDirectoryProvider>();
final item = widget.data.elementAt(
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
);
final account = ud.getAccountFromCache(item.accountId);
Positioned(
left: 16,
right: 16,
bottom: 16 + MediaQuery.of(context).padding.bottom,
child: Material(
color: Colors.transparent,
child: Builder(builder: (context) {
final ud = context.read<UserDirectoryProvider>();
final item = widget.data.elementAt(
widget.data.length > 1 ? _pageController.page?.round() ?? 0 : 0,
);
final account = ud.getAccountFromCache(item.accountId);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.accountId > 0)
Row(
children: [
IgnorePointer(
child: AccountImage(
content: account?.avatar,
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,
),
],
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.accountId > 0)
Row(
children: [
IgnorePointer(
child: AccountImage(
content: account?.avatar,
radius: 19,
),
),
),
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(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,
),
],
),
),
),
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),
IgnorePointer(
child: Wrap(
spacing: 6,
children: [
if (item.metadata['exif'] == null)
const Gap(2),
IgnorePointer(
child: Wrap(
spacing: 6,
children: [
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(
'#${item.rid}',
item.mimetype,
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']?['ShutterSpeed'] != null)
Text(
item.metadata['exif']?['ShutterSpeed'],
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} Bytes',
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 < 0) {
_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: 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,14 +1,18 @@
import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:popover/popover.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_popover.dart';
import 'package:surface/widgets/attachment/attachment_list.dart';
import 'package:surface/widgets/context_menu.dart';
import 'package:surface/widgets/link_preview.dart';
@ -95,8 +99,28 @@ class ChatMessage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMerged && !isCompact)
AccountImage(
content: user?.avatar,
GestureDetector(
child: AccountImage(
content: user?.avatar,
),
onTap: () {
if (user == null) return;
showPopover(
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
transition: PopoverTransition.other,
bodyBuilder: (context) => SizedBox(
width: math.min(400, MediaQuery.of(context).size.width - 10),
child: AccountPopoverCard(
data: user,
),
),
direction: PopoverDirection.bottom,
arrowHeight: 5,
arrowWidth: 15,
arrowDxOffset: -190,
);
},
)
else if (isMerged)
const Gap(40),
@ -124,10 +148,13 @@ class ChatMessage extends StatelessWidget {
dateFormatter.format(data.createdAt.toLocal()),
).fontSize(13),
],
),
).height(21),
if (isCompact) const Gap(8),
if (data.preload?.quoteEvent != null)
StyledWidget(Container(
constraints: BoxConstraints(
maxWidth: 480,
),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
@ -150,7 +177,12 @@ class ChatMessage extends StatelessWidget {
),
)).padding(bottom: 4, top: 4),
switch (data.type) {
'messages.new' => _ChatMessageText(data: data),
'messages.new' => _ChatMessageText(
data: data,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
_ => _ChatMessageSystemNotify(data: data),
},
],
@ -181,19 +213,75 @@ class ChatMessage extends StatelessWidget {
class _ChatMessageText extends StatelessWidget {
final SnChatMessage data;
final Function(SnChatMessage)? onReply;
final Function(SnChatMessage)? onEdit;
final Function(SnChatMessage)? onDelete;
const _ChatMessageText({required this.data});
const _ChatMessageText({required this.data, this.onReply, this.onEdit, this.onDelete});
@override
Widget build(BuildContext context) {
final ua = context.read<UserProvider>();
final isOwner = ua.isAuthorized && data.sender.accountId == ua.user?.id;
if (data.body['text'] != null && data.body['text'].isNotEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownTextContent(
content: data.body['text'],
isSelectable: true,
isAutoWarp: true,
SelectionArea(
contextMenuBuilder: (context, editableTextState) {
final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
if (onReply != null) {
items.insert(
0,
ContextMenuButtonItem(
label: 'reply'.tr(),
onPressed: () {
ContextMenuController.removeAny();
onReply?.call(data);
},
),
);
}
if (isOwner && onEdit != null) {
items.insert(
1,
ContextMenuButtonItem(
label: 'edit'.tr(),
onPressed: () {
ContextMenuController.removeAny();
onEdit?.call(data);
},
),
);
}
if (isOwner && onDelete != null) {
items.insert(
2,
ContextMenuButtonItem(
label: 'delete'.tr(),
onPressed: () {
ContextMenuController.removeAny();
onDelete?.call(data);
},
),
);
}
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: editableTextState.contextMenuAnchors,
buttonItems: items,
);
},
child: Container(
constraints: const BoxConstraints(maxWidth: 480),
child: MarkdownTextContent(
content: data.body['text'],
isAutoWarp: true,
),
),
),
if (data.updatedAt != data.createdAt)
Text(

View File

@ -48,6 +48,8 @@ class ChatMessageInputState extends State<ChatMessageInput> {
void setEdit(SnChatMessage? value) {
_contentController.text = value?.body['text'] ?? '';
_attachments.clear();
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
setState(() => _editingMessage = value);
}
@ -101,7 +103,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
},
);
_attachments[i] = PostWriteMedia(item);
setState(() {
_attachments[i] = PostWriteMedia(item);
});
}
} catch (err) {
if (!mounted) return;
@ -113,7 +117,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
// Send the message
// NOTICE This future should not be awaited, so that the message can be sent in the background and the user can continue to type
widget.controller.sendMessage(
'messages.new',
_editingMessage != null ? 'messages.edit' : 'messages.new',
_contentController.text,
attachments: _attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
relatedId: _editingMessage?.id,
@ -197,6 +201,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
InkWell(
child: Text('cancel'.tr()),
onTap: () {
_attachments.clear();
setState(() => _replyingMessage = null);
},
),
@ -236,6 +241,7 @@ class ChatMessageInputState extends State<ChatMessageInput> {
InkWell(
child: Text('cancel'.tr()),
onTap: () {
_attachments.clear();
_contentController.clear();
setState(() => _editingMessage = null);
},

View File

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

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/config.dart';
class ContextMenuArea extends StatelessWidget {
final ContextMenu contextMenu;
@ -22,11 +23,10 @@ class ContextMenuArea extends StatelessWidget {
return Listener(
onPointerDown: (event) {
mousePosition = event.position;
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
if (!isCollapseDrawer) {
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
final cfg = context.read<ConfigProvider>();
if (!cfg.drawerIsCollapsed) {
// Leave padding for side navigation
mousePosition = isExpandDrawer
mousePosition = cfg.drawerIsExpanded
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
}

View File

@ -1,39 +1,38 @@
import 'dart:ui';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:markdown/markdown.dart' as markdown;
import 'package:styled_widget/styled_widget.dart';
import 'package:provider/provider.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/universal_image.dart';
import 'package:syntax_highlight/syntax_highlight.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:path/path.dart';
import 'package:uuid/uuid.dart';
import 'attachment/attachment_zoom.dart';
class MarkdownTextContent extends StatelessWidget {
final String content;
final bool isSelectable;
final bool isAutoWarp;
final bool isEnlargeSticker;
final TextScaler? textScaler;
final List<SnAttachment?>? attachments;
const MarkdownTextContent({
super.key,
required this.content,
this.isSelectable = false,
this.isAutoWarp = false,
this.isEnlargeSticker = false,
this.textScaler,
this.attachments,
});
Widget _buildContent(BuildContext context) {
@override
Widget build(BuildContext context) {
return Markdown(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@ -42,33 +41,33 @@ class MarkdownTextContent extends StatelessWidget {
styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context),
).copyWith(
textScaler: textScaler,
blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
blockquoteDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
horizontalRuleDecoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0,
color: Theme.of(context).dividerColor,
),
),
),
codeblockDecoration: BoxDecoration(
border: Border.all(
textScaler: textScaler,
blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
blockquoteDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
horizontalRuleDecoration: BoxDecoration(
border: Border(
top: BorderSide(
width: 1.0,
color: Theme.of(context).dividerColor,
width: 0.3,
),
borderRadius: const BorderRadius.all(Radius.circular(4)),
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
)),
builders: {
'code': _MarkdownTextCodeElement(),
},
),
),
codeblockDecoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor,
width: 0.3,
),
borderRadius: const BorderRadius.all(Radius.circular(4)),
color: Theme.of(context).colorScheme.surface.withOpacity(0.5),
),
code: GoogleFonts.robotoMono(height: 1),
),
builders: {},
softLineBreak: true,
extensionSet: markdown.ExtensionSet(
<markdown.BlockSyntax>[
@ -78,6 +77,7 @@ class MarkdownTextContent extends StatelessWidget {
<markdown.InlineSyntax>[
if (isAutoWarp) markdown.LineBreakSyntax(),
_UserNameCardInlineSyntax(),
_CustomEmoteInlineSyntax(context),
markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(),
markdown.CodeSyntax(),
@ -108,9 +108,41 @@ class MarkdownTextContent extends StatelessWidget {
if (url.startsWith('solink://')) {
final segments = url.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'stickers':
final alias = segments[1];
final st = context.read<SnStickerProvider>();
final sn = context.read<SnNetworkProvider>();
final double size = isEnlargeSticker ? 128 : 32;
return Container(
width: size,
height: size,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: FutureBuilder<SnSticker?>(
future: st.lookupSticker(alias),
builder: (context, snapshot) {
if (snapshot.hasData) {
return UniversalImage(
sn.getAttachmentUrl(snapshot.data!.attachment.rid),
fit: BoxFit.cover,
width: size,
height: size,
cacheHeight: size,
cacheWidth: size,
);
}
return const SizedBox.shrink();
},
),
),
);
case 'attachments':
final attachment = attachments?.firstWhere(
(ele) => ele?.rid == segments[1],
(ele) => ele?.rid == segments[1],
orElse: () => null,
);
if (attachment != null) {
@ -168,14 +200,6 @@ class MarkdownTextContent extends StatelessWidget {
},
);
}
@override
Widget build(BuildContext context) {
if (isSelectable) {
return SelectionArea(child: _buildContent(context));
}
return _buildContent(context);
}
}
class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
@ -194,45 +218,24 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
}
}
class _MarkdownTextCodeElement extends MarkdownElementBuilder {
@override
Widget? visitElementAfter(
markdown.Element element,
TextStyle? preferredStyle,
) {
var language = '';
class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
final BuildContext context;
if (element.attributes['class'] != null) {
String lg = element.attributes['class'] as String;
language = lg.substring(9).trim();
_CustomEmoteInlineSyntax(this.context) : super(r':([-\w]+):');
@override
bool onMatch(markdown.InlineParser parser, Match match) {
final SnStickerProvider st = context.read<SnStickerProvider>();
final alias = match[1]!.toUpperCase();
if (st.hasNotSticker(alias)) {
parser.advanceBy(1);
return false;
}
return SizedBox(
child: FutureBuilder(
future: (() async {
final docPath = '../../../';
final highlightingPath = join(docPath, 'assets/highlighting', language);
await Highlighter.initialize([highlightingPath]);
return Highlighter(
language: highlightingPath,
theme: PlatformDispatcher.instance.platformBrightness == Brightness.light
? await HighlighterTheme.loadLightTheme()
: await HighlighterTheme.loadDarkTheme(),
);
})(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final highlighter = snapshot.data!;
return Text.rich(
highlighter.highlight(element.textContent.trim()),
style: GoogleFonts.robotoMono(),
);
}
return Text(
element.textContent.trim(),
style: GoogleFonts.robotoMono(),
);
},
),
).padding(all: 8);
final element = markdown.Element.empty('img');
element.attributes['src'] = 'solink://stickers/$alias';
parser.addNode(element);
return true;
}
}

View File

@ -1,9 +1,13 @@
import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/navigation.dart';
import 'package:surface/widgets/version_label.dart';
@ -28,8 +32,9 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
@override
Widget build(BuildContext context) {
final nav = context.watch<NavigationProvider>();
final cfg = context.watch<ConfigProvider>();
final backgroundColor = ResponsiveBreakpoints.of(context).largerThan(TABLET) ? Colors.transparent : null;
final backgroundColor = cfg.drawerIsExpanded ? Colors.transparent : null;
return ListenableBuilder(
listenable: nav,
@ -44,6 +49,18 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
backgroundColor: backgroundColor,
selectedIndex: nav.currentIndex,
children: [
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS) && !cfg.drawerIsExpanded)
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: WindowTitleBarBox(),
),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,

View File

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

View File

@ -6,8 +6,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/navigation.dart';
import 'package:surface/widgets/connection_indicator.dart';
import 'package:surface/widgets/dialog.dart';
@ -15,6 +17,7 @@ import 'package:surface/widgets/navigation/app_background.dart';
import 'package:surface/widgets/navigation/app_bottom_navigation.dart';
import 'package:surface/widgets/navigation/app_drawer_navigation.dart';
import 'package:surface/widgets/navigation/app_rail_navigation.dart';
import 'package:surface/widgets/notify_indicator.dart';
final globalRootScaffoldKey = GlobalKey<ScaffoldState>();
@ -57,10 +60,11 @@ class AppRootScaffold extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cfg = context.watch<ConfigProvider>();
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
final isCollapseDrawer = cfg.drawerIsCollapsed;
final isExpandedDrawer = cfg.drawerIsExpanded;
final routeName = GoRouter.of(context).routerDelegate.currentConfiguration.last.route.name;
final isShowBottomNavigation = NavigationProvider.kShowBottomNavScreen.contains(routeName)
@ -81,7 +85,7 @@ class AppRootScaffold extends StatelessWidget {
),
),
),
child: isExpandDrawer ? AppNavigationDrawer(elevation: 0) : AppRailNavigation(),
child: isExpandedDrawer ? AppNavigationDrawer(elevation: 0) : AppRailNavigation(),
),
Expanded(child: body),
],
@ -95,59 +99,66 @@ class AppRootScaffold extends StatelessWidget {
iconMouseDown: Theme.of(context).colorScheme.primary,
);
final safeTop = MediaQuery.of(context).padding.top;
return AppBackground(
isRoot: true,
child: Scaffold(
key: globalRootScaffoldKey,
body: Column(
body: Stack(
children: [
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
WindowTitleBarBox(
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),
],
),
],
),
Column(
children: [
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS))
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / devicePixelRatio,
),
),
],
),
),
ConnectionIndicator(),
Expanded(child: innerWidget),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: Platform.isMacOS ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
WindowTitleBarBox(
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),
],
),
],
),
),
),
],
),
),
Expanded(child: innerWidget),
],
),
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()),
],
),
drawer: !isExpandDrawer ? AppNavigationDrawer() : null,
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null,
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
),

View File

@ -0,0 +1,58 @@
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>();
return ListenableBuilder(
listenable: nty,
builder: (context, _) {
return 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(nty.notifications.isNotEmpty && ua.isAuthorized ? 1 : 0, animate: true).animate(
const Duration(milliseconds: 300),
Curves.easeInOut,
),
onTap: () {
nty.clear();
},
);
});
}
}

View File

@ -20,6 +20,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart';
import 'package:surface/types/reaction.dart';
import 'package:surface/widgets/account/account_image.dart';
@ -112,7 +113,7 @@ class PostItem extends StatelessWidget {
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
} else {
await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}', file: imageFile);
await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}.png', file: imageFile);
}
await imageFile.delete();
@ -198,6 +199,10 @@ class PostItem extends StatelessWidget {
).center();
}
final displayableAttachments = data.preload?.attachments
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
@ -247,9 +252,9 @@ class PostItem extends StatelessWidget {
],
),
),
if ((data.preload?.attachments?.isNotEmpty ?? false) && data.type != 'article')
if (displayableAttachments?.isNotEmpty ?? false)
AttachmentList(
data: data.preload!.attachments!,
data: displayableAttachments!,
bordered: true,
gridded: true,
maxHeight: showFullPost ? null : 480,
@ -874,12 +879,18 @@ class _PostContentBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (data.body['content'] == null) return const SizedBox.shrink();
return MarkdownTextContent(
isSelectable: isSelectable,
final content = MarkdownTextContent(
isAutoWarp: data.type == 'story',
isEnlargeSticker: true,
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: data.body['content'],
attachments: data.preload?.attachments,
);
if (isSelectable) {
return SelectionArea(child: content);
}
return content;
}
}

View File

@ -25,7 +25,7 @@ class PostMiniEditor extends StatefulWidget {
}
class _PostMiniEditorState extends State<PostMiniEditor> {
final PostWriteController _writeController = PostWriteController();
final PostWriteController _writeController = PostWriteController(doLoadFromTemporary: false);
bool _isFetching = false;

View File

@ -55,17 +55,20 @@ class UniversalImage extends StatelessWidget {
? null
: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: TweenAnimationBuilder(
tween: Tween(
begin: 0,
end: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: 0,
),
duration: const Duration(milliseconds: 300),
builder: (context, value, _) => CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
return Container(
constraints: BoxConstraints(maxHeight: 80),
child: Center(
child: TweenAnimationBuilder(
tween: Tween(
begin: 0,
end: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: 0,
),
duration: const Duration(milliseconds: 300),
builder: (context, value, _) => CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null ? value.toDouble() : null,
),
),
),
);

View File

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

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe
sha256: "27899c95f9e7ec06c8310e6e0eac967707714b9f1450c4a58fa00ca011a4a8ae"
url: "https://pub.dev"
source: hosted
version: "1.3.48"
version: "1.3.49"
_macros:
dependency: transitive
description: dart
@ -266,10 +266,10 @@ packages:
dependency: transitive
description:
name: connectivity_plus
sha256: e0817759ec6d2d8e57eb234e6e57d2173931367a865850c7acea40d4b4f9c27d
sha256: "8a68739d3ee113e51ad35583fdf9ab82c55d09d693d3c39da1aebab87c938412"
url: "https://pub.dev"
source: hosted
version: "6.1.1"
version: "6.1.2"
connectivity_plus_platform_interface:
dependency: transitive
description:
@ -290,10 +290,10 @@ packages:
dependency: "direct main"
description:
name: croppy
sha256: "14bb40fd6c1771b093a907ddbf24df9aa49a4e6e379dd630602eb446e30ec629"
sha256: bf99b00023df0d7d047e04d27d496d87cbefd968f578d0bd30f342ff75570a12
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.3"
cross_file:
dependency: "direct main"
description:
@ -354,18 +354,18 @@ packages:
dependency: transitive
description:
name: dbus
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
version: "0.7.11"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
sha256: "4fa68e53e26ab17b70ca39f072c285562cfc1589df5bb1e9295db90f6645f431"
sha256: b37d37c2f912ad4e8ec694187de87d05de2a3cb82b465ff1f65f65a2d05de544
url: "https://pub.dev"
source: hosted
version: "11.2.0"
version: "11.2.1"
device_info_plus_platform_interface:
dependency: transitive
description:
@ -538,34 +538,34 @@ packages:
dependency: "direct main"
description:
name: firebase_analytics
sha256: "366140abb55418ea23060b779893fa997c2d8e1974a4d1cc4d9590933b65c5fd"
sha256: "498c6cb8468e348a556709c745d92a52173ab3a9b906aa0593393f0787f201ea"
url: "https://pub.dev"
source: hosted
version: "11.3.6"
version: "11.4.0"
firebase_analytics_platform_interface:
dependency: transitive
description:
name: firebase_analytics_platform_interface
sha256: "8e987cf977c0c8f4ad02d9950a9b25b1a9606899f37b66a322a43af05be0246b"
sha256: ccbb350554e98afdb4b59852689292d194d31232a2647b5012a66622b3711df9
url: "https://pub.dev"
source: hosted
version: "4.2.8"
version: "4.3.0"
firebase_analytics_web:
dependency: transitive
description:
name: firebase_analytics_web
sha256: "0b64ef9060d394bba3d3b4777f49ee098efeeea7b0afb04663c956de6a3da170"
sha256: "68e1f18fc16482c211c658e739c25f015b202a260d9ad8249c6d3d7963b8105f"
url: "https://pub.dev"
source: hosted
version: "0.5.10+5"
version: "0.5.10+6"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde"
sha256: "0307c1fde82e2b8b97e0be2dab93612aff9a72f31ebe9bfac66ed8b37ef7c568"
url: "https://pub.dev"
source: hosted
version: "3.9.0"
version: "3.10.0"
firebase_core_platform_interface:
dependency: transitive
description:
@ -586,26 +586,26 @@ packages:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "151a3ee68736abf293aab66d1317ade53c88abe1db09c75a0460aebf7767bbdf"
sha256: "48a8a59197c1c5174060ba9aa1e0036e9b5a0d28a0cc22d19c1fcabc67fafe3c"
url: "https://pub.dev"
source: hosted
version: "15.1.6"
version: "15.2.0"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: f331ee51e40c243f90cc7bc059222dfec4e5df53125b08d31fb28961b00d2a9d
sha256: "9770a8e91f54296829dcaa61ce9b7c2f9ae9abbf99976dd3103a60470d5264dd"
url: "https://pub.dev"
source: hosted
version: "4.5.49"
version: "4.6.0"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: efaf3fdc54cd77e0eedb8e75f7f01c808828c64d052ddbf94d3009974e47d30f
sha256: "329ca4ef45ec616abe6f1d5e58feed0934a50840a65aa327052354ad3c64ed77"
url: "https://pub.dev"
source: hosted
version: "3.9.5"
version: "3.10.0"
fixnum:
dependency: transitive
description:
@ -618,10 +618,10 @@ packages:
dependency: "direct main"
description:
name: fl_chart
sha256: c724234b05e378383e958f3e82ca84a3e1e3c06a0898462044dd8a24b1ee9864
sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237"
url: "https://pub.dev"
source: hosted
version: "0.70.0"
version: "0.70.2"
flutter:
dependency: "direct main"
description: flutter
@ -679,10 +679,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5"
sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c
url: "https://pub.dev"
source: hosted
version: "0.14.2"
version: "0.14.3"
flutter_lints:
dependency: "direct dev"
description:
@ -700,18 +700,18 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
sha256: "255b00afa1a7bad19727da6a7780cf3db6c3c12e68d302d85e0ff1fdf173db9e"
sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487
url: "https://pub.dev"
source: hosted
version: "0.7.4+3"
version: "0.7.5"
flutter_native_splash:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "1152ab0067ca5a2ebeb862fe0a762057202cceb22b7e62692dcbabf6483891bb"
sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
version: "2.4.4"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -740,10 +740,10 @@ packages:
dependency: "direct main"
description:
name: flutter_svg
sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123"
sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
url: "https://pub.dev"
source: hosted
version: "2.0.16"
version: "2.0.17"
flutter_test:
dependency: "direct dev"
description: flutter
@ -766,10 +766,10 @@ packages:
dependency: "direct main"
description:
name: flutter_webrtc
sha256: "3efe9828f19a07d29a51a726759ad0c70a840d231548a1c7d0332075a94db1df"
sha256: "188401cc3275bc4f1f965babdff6cac612a4b46572f1e49f49db8af5361d5712"
url: "https://pub.dev"
source: hosted
version: "0.12.5+hotfix.1"
version: "0.12.6"
freezed:
dependency: "direct dev"
description:
@ -822,10 +822,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539"
sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d"
url: "https://pub.dev"
source: hosted
version: "14.6.2"
version: "14.6.3"
google_fonts:
dependency: "direct main"
description:
@ -902,10 +902,10 @@ packages:
dependency: transitive
description:
name: http_parser
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
version: "4.1.2"
icons_launcher:
dependency: "direct dev"
description:
@ -934,10 +934,10 @@ packages:
dependency: transitive
description:
name: image_picker_android
sha256: aa6f1280b670861ac45220cc95adc59bb6ae130259d36f980ccb62220dc5e59f
sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c
url: "https://pub.dev"
source: hosted
version: "0.8.12+19"
version: "0.8.12+20"
image_picker_for_web:
dependency: transitive
description:
@ -950,10 +950,10 @@ packages:
dependency: transitive
description:
name: image_picker_ios
sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
url: "https://pub.dev"
source: hosted
version: "0.8.12+1"
version: "0.8.12+2"
image_picker_linux:
dependency: transitive
description:
@ -974,10 +974,10 @@ packages:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80"
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
version: "2.10.1"
image_picker_windows:
dependency: transitive
description:
@ -1086,10 +1086,10 @@ packages:
dependency: "direct main"
description:
name: livekit_client
sha256: "7cdeb3eaeec7fb70a4cf88d9caabccbef9e3bd5f0b23c086320bc5c9acb2770b"
sha256: "02b4653d903852d0ae86b15fbe4324747606dae6410fe860d0c07a11c79988de"
url: "https://pub.dev"
source: hosted
version: "2.3.4"
version: "2.3.5"
logging:
dependency: transitive
description:
@ -1110,10 +1110,10 @@ packages:
dependency: "direct main"
description:
name: markdown
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1"
url: "https://pub.dev"
source: hosted
version: "7.2.2"
version: "7.3.0"
marquee:
dependency: "direct main"
description:
@ -1142,10 +1142,10 @@ packages:
dependency: "direct main"
description:
name: material_symbols_icons
sha256: "64404f47f8e0a9d20478468e5decef867a688660bad7173adcd20418d7f892c9"
sha256: "89aac72d25dd49303f71b3b1e70f8374791846729365b25bebc2a2531e5b86cd"
url: "https://pub.dev"
source: hosted
version: "4.2801.0"
version: "4.2801.1"
media_kit:
dependency: "direct main"
description:
@ -1270,10 +1270,10 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d"
sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790"
url: "https://pub.dev"
source: hosted
version: "8.1.2"
version: "8.1.3"
package_info_plus_platform_interface:
dependency: transitive
description:
@ -1502,10 +1502,10 @@ packages:
dependency: transitive
description:
name: pubspec_parse
sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0"
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.5.0"
qr:
dependency: transitive
description:
@ -1630,10 +1630,10 @@ packages:
dependency: "direct main"
description:
name: share_plus
sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400"
sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da
url: "https://pub.dev"
source: hosted
version: "10.1.3"
version: "10.1.4"
share_plus_platform_interface:
dependency: transitive
description:
@ -1646,18 +1646,18 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: "3c7e73920c694a436afaf65ab60ce3453d91f84208d761fbd83fc21182134d93"
sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a
url: "https://pub.dev"
source: hosted
version: "2.3.4"
version: "2.3.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d"
sha256: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.2"
shared_preferences_foundation:
dependency: transitive
description:
@ -1795,10 +1795,10 @@ packages:
dependency: transitive
description:
name: sqflite_darwin
sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474"
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
version: "2.4.1+1"
sqflite_platform_interface:
dependency: transitive
description:
@ -1863,14 +1863,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.3.0+3"
syntax_highlight:
dependency: "direct main"
description:
name: syntax_highlight
sha256: ee33b6aa82cc722bb9b40152a792181dee222353b486c0255fde666a3e3a4997
url: "https://pub.dev"
source: hosted
version: "0.4.0"
term_glyph:
dependency: transitive
description:
@ -1979,18 +1971,18 @@ packages:
dependency: transitive
description:
name: url_launcher_web
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
version: "2.4.0"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
version: "3.1.4"
uuid:
dependency: "direct main"
description:
@ -2011,10 +2003,10 @@ packages:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb"
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.dev"
source: hosted
version: "1.1.12"
version: "1.1.13"
vector_graphics_compiler:
dependency: transitive
description:
@ -2131,10 +2123,10 @@ packages:
dependency: transitive
description:
name: win32
sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69"
sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29"
url: "https://pub.dev"
source: hosted
version: "5.9.0"
version: "5.10.0"
win32_registry:
dependency: transitive
description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# 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.
version: 2.2.1+44
version: 2.2.2+54
environment:
sdk: ^3.5.4
@ -54,7 +54,6 @@ dependencies:
flutter_markdown: ^0.7.4+1
url_launcher: ^6.3.1
flutter_animate: ^4.5.0
syntax_highlight: ^0.4.0
google_fonts: ^6.2.1
path: ^1.9.0
relative_time: ^5.0.0

View File

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