Compare commits

...

41 Commits

Author SHA1 Message Date
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
cb05ff2e9e 🚀 Launch 2.2.1+44 2025-01-01 19:38:57 +08:00
f614da7918 💄 Optimize attachment list 2025-01-01 17:57:41 +08:00
a3c8dafff9 User typing status
🐛 Bug fixes
2025-01-01 16:45:37 +08:00
fa978a7cd1 🐛 Fix notification mark all as read issue 2025-01-01 11:50:06 +08:00
aaa0a562b4 💄 Fix transparent icon color issue 2025-01-01 01:48:35 +08:00
590a4ce2a6 🐛 Bug fixes on chat message rendering 2025-01-01 01:11:35 +08:00
f26edce071 🚀 Launch 2.2.1+43 2024-12-29 23:58:31 +08:00
603799ea32 🐛 Fix high quality icon issue 2024-12-29 23:52:48 +08:00
a32baf7798 Able to set attachment alt text 2024-12-29 23:50:56 +08:00
498c9af663 💄 Optimize chatting input
 Rollback universal image
2024-12-29 23:30:29 +08:00
202dbff6d3 🐛 Bug fixes 2024-12-29 23:11:50 +08:00
96fd64d85d 🚀 Launch 2.2.1+42 2024-12-29 22:43:58 +08:00
e236b7f98b 💄 Optimize attachment list width in post 2024-12-29 22:34:17 +08:00
5c7929e618 Post editor on device draft 2024-12-29 22:27:07 +08:00
7ba5260246 Improve image loading 2024-12-29 15:30:31 +08:00
a6d4947a23 🐛 Fix attachment list NaN height 2024-12-29 14:03:19 +08:00
47 changed files with 2053 additions and 588 deletions

View File

@ -7,11 +7,7 @@ meta {
post { post {
url: {{endpoint}}/cgi/uc/boosts/1/activate url: {{endpoint}}/cgi/uc/boosts/1/activate
body: none body: none
auth: bearer auth: inherit
}
auth:bearer {
token: {{atk}}
} }
body:json { 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 { post {
url: {{endpoint}}/cgi/id/dev/notify/all url: {{endpoint}}/cgi/id/dev/notify/all
body: json body: json
auth: bearer auth: inherit
}
auth:bearer {
token: {{atk}}
} }
body:json { 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", "settingsAppearance": "Appearance",
"settingsAppBarTransparent": "Transparent App Bar", "settingsAppBarTransparent": "Transparent App Bar",
"settingsAppBarTransparentDescription": "Enable transparent effect for the 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", "settingsBackgroundImage": "Background Image",
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.", "settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
"settingsBackgroundImageClear": "Clear Existing Background Image", "settingsBackgroundImageClear": "Clear Existing Background Image",
@ -281,7 +283,12 @@
"one": "{} attachment", "one": "{} attachment",
"other": "{} attachments" "other": "{} attachments"
}, },
"messageTyping": {
"one": "{} is typing...",
"other": "{} are typing..."
},
"fieldAttachmentRandomId": "Random ID", "fieldAttachmentRandomId": "Random ID",
"fieldAttachmentAlt": "Alternative text",
"addAttachmentFromAlbum": "Add from album", "addAttachmentFromAlbum": "Add from album",
"addAttachmentFromClipboard": "Paste file", "addAttachmentFromClipboard": "Paste file",
"addAttachmentFromCameraPhoto": "Take photo", "addAttachmentFromCameraPhoto": "Take photo",
@ -293,6 +300,7 @@
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail", "attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
"attachmentCompressVideo": "Re-encode video", "attachmentCompressVideo": "Re-encode video",
"attachmentSetThumbnail": "Set thumbnail", "attachmentSetThumbnail": "Set thumbnail",
"attachmentSetAlt": "Set alternative text",
"attachmentCopyRandomId": "Copy RID", "attachmentCopyRandomId": "Copy RID",
"attachmentUpload": "Upload", "attachmentUpload": "Upload",
"attachmentInputDialog": "Upload attachments", "attachmentInputDialog": "Upload attachments",
@ -408,6 +416,9 @@
"celebrateBirthday": "Happy birthday, {}!", "celebrateBirthday": "Happy birthday, {}!",
"celebrateMerryXmas": "Merry christmas, {}", "celebrateMerryXmas": "Merry christmas, {}",
"celebrateNewYear": "Happy new year, {}", "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, {}!", "celebrateValentineDay": "Today is valentine's day, {}!",
"celebrateLaborDay": "Today is labor day, {}.", "celebrateLaborDay": "Today is labor day, {}.",
"celebrateMotherDay": "Today is mother's day, {}.", "celebrateMotherDay": "Today is mother's day, {}.",
@ -417,6 +428,9 @@
"celebrateThanksgiving": "Today is thanksgiving day, {}!", "celebrateThanksgiving": "Today is thanksgiving day, {}!",
"pendingBirthday": "Birthday in {}", "pendingBirthday": "Birthday in {}",
"pendingMerryXmas": "Christmas in {}", "pendingMerryXmas": "Christmas in {}",
"pendingLunarNewYear": "Lunar new year in {}",
"pendingMidAutumn": "Mid-autumn festival in {}",
"pendingDragonBoat": "Dragon boat festival in {}",
"pendingNewYear": "New year in {}", "pendingNewYear": "New year in {}",
"pendingValentineDay": "Valentine's day in {}", "pendingValentineDay": "Valentine's day in {}",
"pendingLaborDay": "Labor day in {}", "pendingLaborDay": "Labor day in {}",
@ -467,6 +481,7 @@
"accountStatusLastSeen": "Last seen at {}", "accountStatusLastSeen": "Last seen at {}",
"postArticle": "Article on the Solar Network", "postArticle": "Article on the Solar Network",
"postStory": "Story on the Solar Network", "postStory": "Story on the Solar Network",
"postLocalDraftRestored": "Restored from device",
"articleWrittenAt": "Written at {}", "articleWrittenAt": "Written at {}",
"articleEditedAt": "Edited at {}", "articleEditedAt": "Edited at {}",
"attachmentSaved": "Saved to album", "attachmentSaved": "Saved to album",

View File

@ -185,6 +185,8 @@
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。", "settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
"settingsAppBarTransparent": "透明顶栏", "settingsAppBarTransparent": "透明顶栏",
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。", "settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
"settingsDrawerPreferCollapse": "侧边栏偏好折叠",
"settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。",
"settingsColorScheme": "主题色", "settingsColorScheme": "主题色",
"settingsColorSchemeDescription": "设置应用主题色。", "settingsColorSchemeDescription": "设置应用主题色。",
"settingsColorSeed": "预设色彩主题", "settingsColorSeed": "预设色彩主题",
@ -279,7 +281,12 @@
"one": "{} 个附件", "one": "{} 个附件",
"other": "{} 个附件" "other": "{} 个附件"
}, },
"messageTyping": {
"one": "{} 正在输入",
"other": "{} 正在输入"
},
"fieldAttachmentRandomId": "访问 ID", "fieldAttachmentRandomId": "访问 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "从相册中添加附件", "addAttachmentFromAlbum": "从相册中添加附件",
"addAttachmentFromClipboard": "粘贴附件", "addAttachmentFromClipboard": "粘贴附件",
"addAttachmentFromCameraPhoto": "拍摄照片", "addAttachmentFromCameraPhoto": "拍摄照片",
@ -291,6 +298,7 @@
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图", "attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
"attachmentCompressVideo": "重新编码视频", "attachmentCompressVideo": "重新编码视频",
"attachmentSetThumbnail": "设置缩略图", "attachmentSetThumbnail": "设置缩略图",
"attachmentSetAlt": "设置概述文字",
"attachmentCopyRandomId": "复制访问 ID", "attachmentCopyRandomId": "复制访问 ID",
"attachmentUpload": "上传", "attachmentUpload": "上传",
"attachmentInputDialog": "上传附件", "attachmentInputDialog": "上传附件",
@ -404,6 +412,9 @@
"dailyCheckNegativeHint6": "出门", "dailyCheckNegativeHint6": "出门",
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨", "dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
"celebrateBirthday": "生日快乐,{}", "celebrateBirthday": "生日快乐,{}",
"celebrateLunarNewYear": "春节快乐,{}",
"celebrateMidAutumn": "中秋节快乐,{}",
"celebrateDragonBoat": "端午节快乐,{}",
"celebrateMerryXmas": "圣诞快乐,{}", "celebrateMerryXmas": "圣诞快乐,{}",
"celebrateNewYear": "新年快乐,{}", "celebrateNewYear": "新年快乐,{}",
"celebrateValentineDay": "今天是情人节,{}", "celebrateValentineDay": "今天是情人节,{}",
@ -413,6 +424,9 @@
"celebrateFatherDay": "今天是父亲节,{}。", "celebrateFatherDay": "今天是父亲节,{}。",
"celebrateHalloween": "快乐在圣诞节,{}", "celebrateHalloween": "快乐在圣诞节,{}",
"celebrateThanksgiving": "今天是感恩节,{}", "celebrateThanksgiving": "今天是感恩节,{}",
"pendingLunarNewYear": "{} 过春节",
"pendingMidAutumn": "{} 过中秋节",
"pendingDragonBoat": "{} 过端午节",
"pendingBirthday": "{} 过生日", "pendingBirthday": "{} 过生日",
"pendingMerryXmas": "{} 过圣诞节", "pendingMerryXmas": "{} 过圣诞节",
"pendingNewYear": "{} 跨年", "pendingNewYear": "{} 跨年",
@ -465,6 +479,7 @@
"accountStatusLastSeen": "最后一次上线于 {}", "accountStatusLastSeen": "最后一次上线于 {}",
"postArticle": "Solar Network 上的文章", "postArticle": "Solar Network 上的文章",
"postStory": "Solar Network 上的故事", "postStory": "Solar Network 上的故事",
"postLocalDraftRestored": "从本地恢复草稿",
"articleWrittenAt": "发表于 {}", "articleWrittenAt": "发表于 {}",
"articleEditedAt": "编辑于 {}", "articleEditedAt": "编辑于 {}",
"attachmentSaved": "已保存到相册", "attachmentSaved": "已保存到相册",

View File

@ -185,6 +185,8 @@
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。", "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
"settingsAppBarTransparent": "透明頂欄", "settingsAppBarTransparent": "透明頂欄",
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。", "settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
"settingsColorScheme": "主題色", "settingsColorScheme": "主題色",
"settingsColorSchemeDescription": "設置應用主題色。", "settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題", "settingsColorSeed": "預設色彩主題",
@ -279,7 +281,12 @@
"one": "{} 個附件", "one": "{} 個附件",
"other": "{} 個附件" "other": "{} 個附件"
}, },
"messageTyping": {
"one": "{} 正在輸入",
"other": "{} 正在輸入"
},
"fieldAttachmentRandomId": "訪問 ID", "fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件", "addAttachmentFromAlbum": "從相冊中添加附件",
"addAttachmentFromClipboard": "粘貼附件", "addAttachmentFromClipboard": "粘貼附件",
"addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraPhoto": "拍攝照片",
@ -291,6 +298,7 @@
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖", "attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
"attachmentCompressVideo": "重新編碼視頻", "attachmentCompressVideo": "重新編碼視頻",
"attachmentSetThumbnail": "設置縮略圖", "attachmentSetThumbnail": "設置縮略圖",
"attachmentSetAlt": "設置概述文字",
"attachmentCopyRandomId": "複製訪問 ID", "attachmentCopyRandomId": "複製訪問 ID",
"attachmentUpload": "上傳", "attachmentUpload": "上傳",
"attachmentInputDialog": "上傳附件", "attachmentInputDialog": "上傳附件",
@ -404,6 +412,9 @@
"dailyCheckNegativeHint6": "出門", "dailyCheckNegativeHint6": "出門",
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
"celebrateBirthday": "生日快樂,{}", "celebrateBirthday": "生日快樂,{}",
"celebrateLunarNewYear": "春節快樂,{}",
"celebrateMidAutumn": "中秋節快樂,{}",
"celebrateDragonBoat": "端午節快樂,{}",
"celebrateMerryXmas": "聖誕快樂,{}", "celebrateMerryXmas": "聖誕快樂,{}",
"celebrateNewYear": "新年快樂,{}", "celebrateNewYear": "新年快樂,{}",
"celebrateValentineDay": "今天是情人節,{}", "celebrateValentineDay": "今天是情人節,{}",
@ -413,6 +424,9 @@
"celebrateFatherDay": "今天是父親節,{}。", "celebrateFatherDay": "今天是父親節,{}。",
"celebrateHalloween": "快樂在聖誕節,{}", "celebrateHalloween": "快樂在聖誕節,{}",
"celebrateThanksgiving": "今天是感恩節,{}", "celebrateThanksgiving": "今天是感恩節,{}",
"pendingLunarNewYear": "{} 過春節",
"pendingMidAutumn": "{} 過中秋節",
"pendingDragonBoat": "{} 過端午節",
"pendingBirthday": "{} 過生日", "pendingBirthday": "{} 過生日",
"pendingMerryXmas": "{} 過聖誕節", "pendingMerryXmas": "{} 過聖誕節",
"pendingNewYear": "{} 跨年", "pendingNewYear": "{} 跨年",
@ -456,6 +470,7 @@
"accountJoinedAt": "加入於 {}", "accountJoinedAt": "加入於 {}",
"accountBirthday": "出生於 {}", "accountBirthday": "出生於 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暫無運勢記錄",
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工", "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "狀態", "accountStatus": "狀態",
@ -464,6 +479,7 @@
"accountStatusLastSeen": "最後一次上線於 {}", "accountStatusLastSeen": "最後一次上線於 {}",
"postArticle": "Solar Network 上的文章", "postArticle": "Solar Network 上的文章",
"postStory": "Solar Network 上的故事", "postStory": "Solar Network 上的故事",
"postLocalDraftRestored": "從本地恢復草稿",
"articleWrittenAt": "發表於 {}", "articleWrittenAt": "發表於 {}",
"articleEditedAt": "編輯於 {}", "articleEditedAt": "編輯於 {}",
"attachmentSaved": "已保存到相冊", "attachmentSaved": "已保存到相冊",

View File

@ -185,6 +185,8 @@
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。", "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
"settingsAppBarTransparent": "透明頂欄", "settingsAppBarTransparent": "透明頂欄",
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。", "settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
"settingsColorScheme": "主題色", "settingsColorScheme": "主題色",
"settingsColorSchemeDescription": "設置應用主題色。", "settingsColorSchemeDescription": "設置應用主題色。",
"settingsColorSeed": "預設色彩主題", "settingsColorSeed": "預設色彩主題",
@ -279,7 +281,12 @@
"one": "{} 個附件", "one": "{} 個附件",
"other": "{} 個附件" "other": "{} 個附件"
}, },
"messageTyping": {
"one": "{} 正在輸入",
"other": "{} 正在輸入"
},
"fieldAttachmentRandomId": "訪問 ID", "fieldAttachmentRandomId": "訪問 ID",
"fieldAttachmentAlt": "概述文字",
"addAttachmentFromAlbum": "從相冊中添加附件", "addAttachmentFromAlbum": "從相冊中添加附件",
"addAttachmentFromClipboard": "粘貼附件", "addAttachmentFromClipboard": "粘貼附件",
"addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraPhoto": "拍攝照片",
@ -291,6 +298,7 @@
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖", "attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
"attachmentCompressVideo": "重新編碼視頻", "attachmentCompressVideo": "重新編碼視頻",
"attachmentSetThumbnail": "設置縮略圖", "attachmentSetThumbnail": "設置縮略圖",
"attachmentSetAlt": "設置概述文字",
"attachmentCopyRandomId": "複製訪問 ID", "attachmentCopyRandomId": "複製訪問 ID",
"attachmentUpload": "上傳", "attachmentUpload": "上傳",
"attachmentInputDialog": "上傳附件", "attachmentInputDialog": "上傳附件",
@ -404,6 +412,9 @@
"dailyCheckNegativeHint6": "出門", "dailyCheckNegativeHint6": "出門",
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨", "dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
"celebrateBirthday": "生日快樂,{}", "celebrateBirthday": "生日快樂,{}",
"celebrateLunarNewYear": "春節快樂,{}",
"celebrateMidAutumn": "中秋節快樂,{}",
"celebrateDragonBoat": "端午節快樂,{}",
"celebrateMerryXmas": "聖誕快樂,{}", "celebrateMerryXmas": "聖誕快樂,{}",
"celebrateNewYear": "新年快樂,{}", "celebrateNewYear": "新年快樂,{}",
"celebrateValentineDay": "今天是情人節,{}", "celebrateValentineDay": "今天是情人節,{}",
@ -413,6 +424,9 @@
"celebrateFatherDay": "今天是父親節,{}。", "celebrateFatherDay": "今天是父親節,{}。",
"celebrateHalloween": "快樂在聖誕節,{}", "celebrateHalloween": "快樂在聖誕節,{}",
"celebrateThanksgiving": "今天是感恩節,{}", "celebrateThanksgiving": "今天是感恩節,{}",
"pendingLunarNewYear": "{} 過春節",
"pendingMidAutumn": "{} 過中秋節",
"pendingDragonBoat": "{} 過端午節",
"pendingBirthday": "{} 過生日", "pendingBirthday": "{} 過生日",
"pendingMerryXmas": "{} 過聖誕節", "pendingMerryXmas": "{} 過聖誕節",
"pendingNewYear": "{} 跨年", "pendingNewYear": "{} 跨年",
@ -456,6 +470,7 @@
"accountJoinedAt": "加入於 {}", "accountJoinedAt": "加入於 {}",
"accountBirthday": "出生於 {}", "accountBirthday": "出生於 {}",
"accountBadge": "徽章", "accountBadge": "徽章",
"accountCheckInNoRecords": "暫無運勢記錄",
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工", "badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
"badgeSiteMigration": "Solar Network 原住民", "badgeSiteMigration": "Solar Network 原住民",
"accountStatus": "狀態", "accountStatus": "狀態",
@ -464,6 +479,7 @@
"accountStatusLastSeen": "最後一次上線於 {}", "accountStatusLastSeen": "最後一次上線於 {}",
"postArticle": "Solar Network 上的文章", "postArticle": "Solar Network 上的文章",
"postStory": "Solar Network 上的故事", "postStory": "Solar Network 上的故事",
"postLocalDraftRestored": "從本地恢復草稿",
"articleWrittenAt": "發表於 {}", "articleWrittenAt": "發表於 {}",
"articleEditedAt": "編輯於 {}", "articleEditedAt": "編輯於 {}",
"attachmentSaved": "已保存到相冊", "attachmentSaved": "已保存到相冊",

View File

@ -380,7 +380,7 @@ SPEC CHECKSUMS:
FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2 FirebaseMessaging: f8a160d99c2c2e5babbbcc90c4a3e15db036aee2
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563 flutter_webrtc: 1a53bd24f97bcfeff512f13699e721897f261563
gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5 gal: 6a522c75909f1244732d4596d11d6a2f86ff37a5

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -11,6 +12,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/user_directory.dart'; import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/websocket.dart'; import 'package:surface/providers/websocket.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/types/websocket.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ChatMessageController extends ChangeNotifier { class ChatMessageController extends ChangeNotifier {
@ -36,8 +38,7 @@ class ChatMessageController extends ChangeNotifier {
int? messageTotal; int? messageTotal;
bool get isAllLoaded => bool get isAllLoaded => messageTotal != null && messages.length >= messageTotal!;
messageTotal != null && messages.length >= messageTotal!;
String? _boxKey; String? _boxKey;
SnChannel? channel; SnChannel? channel;
@ -50,8 +51,10 @@ class ChatMessageController extends ChangeNotifier {
/// Stored as a list of nonce to provide the loading state /// Stored as a list of nonce to provide the loading state
final List<String> unconfirmedMessages = List.empty(growable: true); final List<String> unconfirmedMessages = List.empty(growable: true);
Box<SnChatMessage>? get _box => Box<SnChatMessage>? get _box => (_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
(_boxKey == null || isPending) ? null : Hive.box<SnChatMessage>(_boxKey!);
final List<SnChannelMember> typingMembers = List.empty(growable: true);
final Map<int, Timer> typingInactiveTimer = {};
Future<void> initialize(SnChannel chan) async { Future<void> initialize(SnChannel chan) async {
channel = chan; channel = chan;
@ -78,22 +81,16 @@ class ChatMessageController extends ChangeNotifier {
if (event.payload?['channel_id'] != channel?.id) break; if (event.payload?['channel_id'] != channel?.id) break;
final member = SnChannelMember.fromJson(event.payload!['member']); final member = SnChannelMember.fromJson(event.payload!['member']);
if (member.id == profile?.id) break; if (member.id == profile?.id) break;
// TODO impl typing users if (!typingMembers.any((x) => x.id == member.id)) {
// if (!_typingUsers.any((x) => x.id == member.id)) { typingMembers.add(member);
// setState(() { notifyListeners();
// _typingUsers.add(member); }
// }); typingInactiveTimer[member.id]?.cancel();
// } typingInactiveTimer[member.id] = Timer(const Duration(seconds: 3), () {
// _typingInactiveTimer[member.id]?.cancel(); typingMembers.removeWhere((x) => x.id == member.id);
// _typingInactiveTimer[member.id] = Timer( typingInactiveTimer.remove(member.id);
// const Duration(seconds: 3), notifyListeners();
// () { });
// setState(() {
// _typingUsers.removeWhere((x) => x.id == member.id);
// _typingInactiveTimer.remove(member.id);
// });
// },
// );
} }
}); });
@ -101,6 +98,35 @@ class ChatMessageController extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Timer? _typingNotifyTimer;
bool _typingStatus = false;
Future<void> _sendTypingStatusPackage() async {
_ws.conn?.sink.add(jsonEncode(
WebSocketPackage(
method: 'status.typing',
endpoint: 'im',
payload: {
'channel_id': channel!.id,
},
).toJson(),
));
}
void pingTypingStatus() {
if (!_typingStatus) {
_sendTypingStatusPackage();
_typingStatus = true;
}
if (_typingNotifyTimer == null || !_typingNotifyTimer!.isActive) {
_typingNotifyTimer?.cancel();
_typingNotifyTimer = Timer(const Duration(milliseconds: 1850), () {
_typingStatus = false;
});
}
}
Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async { Future<void> _saveMessageToLocal(Iterable<SnChatMessage> messages) async {
if (_box == null) return; if (_box == null) return;
await _box!.putAll({ await _box!.putAll({
@ -167,8 +193,7 @@ class ChatMessageController extends ChangeNotifier {
switch (message.type) { switch (message.type) {
case 'messages.edit': case 'messages.edit':
if (message.relatedEventId != null) { if (message.relatedEventId != null) {
final idx = final idx = messages.indexWhere((x) => x.id == message.relatedEventId);
messages.indexWhere((x) => x.id == message.relatedEventId);
if (idx != -1) { if (idx != -1) {
final newBody = message.body; final newBody = message.body;
newBody.remove('related_event'); newBody.remove('related_event');
@ -207,8 +232,7 @@ class ChatMessageController extends ChangeNotifier {
'algorithm': 'plain', 'algorithm': 'plain',
if (quoteId != null) 'quote_event': quoteId, if (quoteId != null) 'quote_event': quoteId,
if (relatedId != null) 'related_event': relatedId, if (relatedId != null) 'related_event': relatedId,
if (attachments != null && attachments.isNotEmpty) if (attachments != null && attachments.isNotEmpty) 'attachments': attachments,
'attachments': attachments,
}; };
// Mock the message locally // Mock the message locally
@ -305,8 +329,7 @@ class ChatMessageController extends ChangeNotifier {
if (out == null) { if (out == null) {
try { try {
final resp = await _sn.client final resp = await _sn.client.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
.get('/cgi/im/channels/${channel!.keyPath}/events/$id');
out = SnChatMessage.fromJson(resp.data); out = SnChatMessage.fromJson(resp.data);
_saveMessageToLocal([out]); _saveMessageToLocal([out]);
} catch (_) { } catch (_) {
@ -341,9 +364,7 @@ class ChatMessageController extends ChangeNotifier {
bool forceRemote = false, bool forceRemote = false,
}) async { }) async {
late List<SnChatMessage> out; late List<SnChatMessage> out;
if (_box != null && if (_box != null && (_box!.length >= take + offset || forceLocal) && !forceRemote) {
(_box!.length >= take + offset || forceLocal) &&
!forceRemote) {
out = _box!.keys out = _box!.keys
.toList() .toList()
.cast<int>() .cast<int>()
@ -386,8 +407,7 @@ class ChatMessageController extends ChangeNotifier {
quoteEvent: quoteEvent, quoteEvent: quoteEvent,
attachments: attachments attachments: attachments
.where( .where(
(ele) => (ele) => out[i].body['attachments']?.contains(ele?.rid) ?? false,
out[i].body['attachments']?.contains(ele?.rid) ?? false,
) )
.toList(), .toList(),
), ),
@ -395,10 +415,7 @@ class ChatMessageController extends ChangeNotifier {
} }
// Preload sender accounts // Preload sender accounts
final accountId = out final accountId = out.where((ele) => ele.sender.accountId >= 0).map((ele) => ele.sender.accountId).toSet();
.where((ele) => ele.sender.accountId >= 0)
.map((ele) => ele.sender.accountId)
.toSet();
await _ud.listAccount(accountId); await _ud.listAccount(accountId);
return out; return out;

View File

@ -1,3 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
@ -8,6 +11,7 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
@ -100,7 +104,7 @@ class PostWriteMedia {
if (attachment != null) { if (attachment != null) {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid)); final ImageProvider provider = UniversalImage.provider(sn.getAttachmentUrl(attachment!.rid));
if (width != null && height != null) { if (width != null && height != null && !kIsWeb) {
return ResizeImage( return ResizeImage(
provider, provider,
width: width, width: width,
@ -150,9 +154,22 @@ class PostWriteController extends ChangeNotifier {
final TextEditingController descriptionController = TextEditingController(); final TextEditingController descriptionController = TextEditingController();
final TextEditingController aliasController = TextEditingController(); final TextEditingController aliasController = TextEditingController();
PostWriteController() { bool _temporarySaveActive = false;
titleController.addListener(() => notifyListeners());
descriptionController.addListener(() => notifyListeners()); PostWriteController({bool doLoadFromTemporary = true}) {
_temporarySaveActive = doLoadFromTemporary;
titleController.addListener(() {
_temporaryPlanSave();
notifyListeners();
});
descriptionController.addListener(() {
_temporaryPlanSave();
notifyListeners();
});
contentController.addListener(() {
_temporaryPlanSave();
});
if (doLoadFromTemporary) _temporaryLoad();
} }
String mode = kTitleMap.keys.first; String mode = kTitleMap.keys.first;
@ -199,11 +216,11 @@ class PostWriteController extends ChangeNotifier {
aliasController.text = post.alias ?? ''; aliasController.text = post.alias ?? '';
publishedAt = post.publishedAt; publishedAt = post.publishedAt;
publishedUntil = post.publishedUntil; publishedUntil = post.publishedUntil;
visibleUsers = List.from(post.visibleUsersList ?? []); visibleUsers = List.from(post.visibleUsersList ?? [], growable: true);
invisibleUsers = List.from(post.invisibleUsersList ?? []); invisibleUsers = List.from(post.invisibleUsersList ?? [], growable: true);
visibility = post.visibility; visibility = post.visibility;
tags = List.from(post.tags.map((ele) => ele.alias)); tags = List.from(post.tags.map((ele) => ele.alias), growable: true);
categories = List.from(post.categories.map((ele) => ele.alias)); categories = List.from(post.categories.map((ele) => ele.alias), growable: true);
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []); attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) { if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
@ -298,6 +315,83 @@ class PostWriteController extends ChangeNotifier {
return compressedAttachment; return compressedAttachment;
} }
static const kTemporaryStorageKey = 'int_draft_post';
Timer? _temporarySaveTimer;
void _temporaryPlanSave() {
if (!_temporarySaveActive) return;
_temporarySaveTimer?.cancel();
_temporarySaveTimer = Timer(const Duration(seconds: 1), () {
_temporarySave();
log("[PostWriter] Temporary save saved.");
});
}
void _temporarySave() {
SharedPreferences.getInstance().then((prefs) {
if (titleController.text.isEmpty &&
descriptionController.text.isEmpty &&
contentController.text.isEmpty &&
thumbnail == null &&
attachments.isEmpty) {
prefs.remove(kTemporaryStorageKey);
return;
}
prefs.setString(
kTemporaryStorageKey,
jsonEncode({
'publisher': publisher,
'content': contentController.text,
if (aliasController.text.isNotEmpty) 'alias': aliasController.text,
if (titleController.text.isNotEmpty) 'title': titleController.text,
if (descriptionController.text.isNotEmpty) 'description': descriptionController.text,
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.toJson(),
'attachments':
attachments.where((e) => e.attachment != null).map((e) => e.attachment!.toJson()).toList(growable: true),
'tags': tags.map((ele) => {'alias': ele}).toList(growable: true),
'categories': categories.map((ele) => {'alias': ele}).toList(growable: true),
'visibility': visibility,
'visible_users_list': visibleUsers,
'invisible_users_list': invisibleUsers,
if (publishedAt != null) 'published_at': publishedAt!.toUtc().toIso8601String(),
if (publishedUntil != null) 'published_until': publishedAt!.toUtc().toIso8601String(),
if (replyingPost != null) 'reply_to': replyingPost!.toJson(),
if (repostingPost != null) 'repost_to': repostingPost!.toJson(),
}),
);
});
}
bool temporaryRestored = false;
void _temporaryLoad() {
SharedPreferences.getInstance().then((prefs) {
final raw = prefs.getString(kTemporaryStorageKey);
if (raw == null) return;
final data = jsonDecode(raw);
contentController.text = data['content'];
aliasController.text = data['alias'] ?? '';
titleController.text = data['title'] ?? '';
descriptionController.text = data['description'] ?? '';
if (data['thumbnail'] != null) thumbnail = PostWriteMedia(SnAttachment.fromJson(data['thumbnail']));
attachments
.addAll(data['attachments'].map((ele) => PostWriteMedia(SnAttachment.fromJson(ele))).cast<PostWriteMedia>());
tags = List.from(data['tags'].map((ele) => ele['alias']));
categories = List.from(data['categories'].map((ele) => ele['alias']));
visibility = data['visibility'];
visibleUsers = List.from(data['visible_users_list'] ?? []);
invisibleUsers = List.from(data['invisible_users_list'] ?? []);
if (data['published_at'] != null) publishedAt = DateTime.tryParse(data['published_at'])?.toLocal();
if (data['published_until'] != null) publishedUntil = DateTime.tryParse(data['published_until'])?.toLocal();
replyingPost = data['reply_to'] != null ? SnPost.fromJson(data['reply_to']) : null;
repostingPost = data['repost_to'] != null ? SnPost.fromJson(data['repost_to']) : null;
temporaryRestored = true;
notifyListeners();
});
}
Future<void> uploadSingleAttachment(BuildContext context, int idx) async { Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
if (isBusy) return; if (isBusy) return;
@ -354,9 +448,11 @@ class PostWriteController extends ChangeNotifier {
); );
try { try {
final compressedAttachment = await _tryCompressVideoCopy(context, media); if (context.mounted) {
if (compressedAttachment != null) { final compressedAttachment = await _tryCompressVideoCopy(context, media);
item = await attach.updateOne(item, compressedId: compressedAttachment.id); if (compressedAttachment != null) {
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
}
} }
} catch (err) { } catch (err) {
if (context.mounted) context.showErrorDialog(err); if (context.mounted) context.showErrorDialog(err);
@ -415,6 +511,7 @@ class PostWriteController extends ChangeNotifier {
method: editingPost != null ? 'PUT' : 'POST', method: editingPost != null ? 'PUT' : 'POST',
), ),
); );
reset();
} catch (err) { } catch (err) {
if (!context.mounted) return; if (!context.mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
@ -463,73 +560,88 @@ class PostWriteController extends ChangeNotifier {
void setPublisher(SnPublisher? item) { void setPublisher(SnPublisher? item) {
publisher = item; publisher = item;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setPublishedAt(DateTime? value) { void setPublishedAt(DateTime? value) {
publishedAt = value; publishedAt = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setPublishedUntil(DateTime? value) { void setPublishedUntil(DateTime? value) {
publishedUntil = value; publishedUntil = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setTags(List<String> value) { void setTags(List<String> value) {
tags = value; tags = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setCategories(List<String> value) { void setCategories(List<String> value) {
categories = value; categories = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setVisibility(int value) { void setVisibility(int value) {
visibility = value; visibility = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setVisibleUsers(List<int> value) { void setVisibleUsers(List<int> value) {
visibleUsers = value; visibleUsers = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setInvisibleUsers(List<int> value) { void setInvisibleUsers(List<int> value) {
invisibleUsers = value; invisibleUsers = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setProgress(double? value) { void setProgress(double? value) {
progress = value; progress = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setIsBusy(bool value) { void setIsBusy(bool value) {
isBusy = value; isBusy = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void setMode(String value) { void setMode(String value) {
mode = value; mode = value;
_temporaryPlanSave();
notifyListeners(); notifyListeners();
} }
void reset() { void reset() {
publishedAt = null; publishedAt = null;
publishedUntil = null; publishedUntil = null;
thumbnail = null;
visibility = 0;
titleController.clear(); titleController.clear();
descriptionController.clear(); descriptionController.clear();
contentController.clear(); contentController.clear();
aliasController.clear(); aliasController.clear();
tags.clear(); tags = List.empty(growable: true);
categories.clear(); categories = List.empty(growable: true);
attachments.clear(); attachments = List.empty(growable: true);
editingPost = null; editingPost = null;
replyingPost = null; replyingPost = null;
repostingPost = null; repostingPost = null;
mode = kTitleMap.keys.first; mode = kTitleMap.keys.first;
temporaryRestored = false;
SharedPreferences.getInstance().then((prefs) => prefs.remove(kTemporaryStorageKey));
notifyListeners(); notifyListeners();
} }

View File

@ -10,7 +10,6 @@ import 'package:easy_localization_loader/easy_localization_loader.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:package_info_plus/package_info_plus.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:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:responsive_framework/responsive_framework.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/firebase_options.dart'; import 'package:surface/firebase_options.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
import 'package:surface/providers/chat_call.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/relationship.dart';
import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/sn_sticker.dart';
import 'package:surface/providers/special_day.dart'; import 'package:surface/providers/special_day.dart';
import 'package:surface/providers/theme.dart'; import 'package:surface/providers/theme.dart';
import 'package:surface/providers/user_directory.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:surface/types/realm.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/version_label.dart';
import 'package:version/version.dart'; import 'package:version/version.dart';
import 'package:workmanager/workmanager.dart'; import 'package:workmanager/workmanager.dart';
import 'package:in_app_review/in_app_review.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) => SnPostContentProvider(ctx)),
Provider(create: (ctx) => SnRelationshipProvider(ctx)), Provider(create: (ctx) => SnRelationshipProvider(ctx)),
Provider(create: (ctx) => SnLinkPreviewProvider(ctx)), Provider(create: (ctx) => SnLinkPreviewProvider(ctx)),
Provider(create: (ctx) => SnStickerProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)), ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)), ChangeNotifierProvider(create: (ctx) => WebSocketProvider(ctx)),
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)), ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
@ -208,8 +207,6 @@ class _AppSplashScreen extends StatefulWidget {
} }
class _AppSplashScreenState extends State<_AppSplashScreen> { class _AppSplashScreenState extends State<_AppSplashScreen> {
bool _isReady = false;
void _tryRequestRating() async { void _tryRequestRating() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('first_boot_time')) { if (prefs.containsKey('first_boot_time')) {
@ -261,6 +258,10 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
Future<void> _initialize() async { Future<void> _initialize() async {
try { try {
final cfg = context.read<ConfigProvider>();
WidgetsBinding.instance.addPostFrameCallback((_) {
cfg.calcDrawerSize(context);
});
final home = context.read<HomeWidgetProvider>(); final home = context.read<HomeWidgetProvider>();
await home.initialize(); await home.initialize();
if (!mounted) return; if (!mounted) return;
@ -282,8 +283,6 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
await context.showErrorDialog(err); await context.showErrorDialog(err);
} finally {
setState(() => _isReady = true);
} }
} }
@ -303,32 +302,17 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!_isReady) { final cfg = context.read<ConfigProvider>();
return Scaffold( return NotificationListener<SizeChangedLayoutNotification>(
backgroundColor: Theme.of(context).colorScheme.surface, onNotification: (notification) {
body: Container( WidgetsBinding.instance.addPostFrameCallback((_) {
constraints: const BoxConstraints(maxWidth: 180), cfg.calcDrawerSize(context);
child: Column( });
mainAxisAlignment: MainAxisAlignment.center, return false;
mainAxisSize: MainAxisSize.min, },
children: [ child: SizeChangedLayoutNotifier(
if (MediaQuery.of(context).platformBrightness == Brightness.dark) child: widget.child,
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;
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:surface/providers/widget.dart'; import 'package:surface/providers/widget.dart';
@ -12,6 +13,7 @@ const kNetworkServerStoreKey = 'app_server_url';
const kAppbarTransparentStoreKey = 'app_bar_transparent'; const kAppbarTransparentStoreKey = 'app_bar_transparent';
const kAppBackgroundStoreKey = 'app_has_background'; const kAppBackgroundStoreKey = 'app_has_background';
const kAppColorSchemeStoreKey = 'app_color_scheme'; const kAppColorSchemeStoreKey = 'app_color_scheme';
const kAppDrawerPreferCollapse = 'app_drawer_prefer_collapse';
const Map<String, FilterQuality> kImageQualityLevel = { const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none, 'settingsImageQualityLowest': FilterQuality.none,
@ -33,6 +35,24 @@ class ConfigProvider extends ChangeNotifier {
prefs = await SharedPreferences.getInstance(); 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 { FilterQuality get imageQuality {
return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high; return kImageQualityLevel.values.elementAtOrNull(prefs.getInt('app_image_quality') ?? 3) ?? FilterQuality.high;
} }

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'; import 'package:surface/providers/userinfo.dart';
// Stored as key: month, day // 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 // Birthday is dynamically generated according to the user's profile
'NewYear': (1, 1), '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), 'ValentineDay': (2, 14),
'LaborDay': (5, 1), 'LaborDay': (5, 1),
'MotherDay': (5, 11), 'MotherDay': (5, 11),
@ -19,6 +22,9 @@ const Map<String, (int, int)> kSpecialDays = {
const Map<String, String> kSpecialDaysSymbol = { const Map<String, String> kSpecialDaysSymbol = {
'Birthday': '🎂', 'Birthday': '🎂',
'NewYear': '🎉', 'NewYear': '🎉',
'LunarNewYear': '🎉',
'MidAutumn': '🥮',
'DragonBoat': '🐲',
'MerryXmas': '🎄', 'MerryXmas': '🎄',
'ValentineDay': '💑', 'ValentineDay': '💑',
'LaborDay': '🏋️', 'LaborDay': '🏋️',
@ -134,3 +140,45 @@ class SpecialDayProvider {
return (elapsedDuration / totalDuration).clamp(0.0, 1.0); 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

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

View File

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

View File

@ -87,7 +87,7 @@ class _ChatManageScreenState extends State<ChatManageScreen> {
try { try {
final resp = await sn.client.request( final resp = await sn.client.request(
widget.editingChannelAlias != null widget.editingChannelAlias != null
? '/cgi/im/channels/$scope/${widget.editingChannelAlias}' ? '/cgi/im/channels/$scope/${_editingChannel!.id}'
: '/cgi/im/channels/$scope', : '/cgi/im/channels/$scope',
data: payload, data: payload,
options: Options( options: Options(

View File

@ -17,6 +17,7 @@ import 'package:surface/types/chat.dart';
import 'package:surface/widgets/chat/call/call_prejoin.dart'; import 'package:surface/widgets/chat/call/call_prejoin.dart';
import 'package:surface/widgets/chat/chat_message.dart'; import 'package:surface/widgets/chat/chat_message.dart';
import 'package:surface/widgets/chat/chat_message_input.dart'; import 'package:surface/widgets/chat/chat_message_input.dart';
import 'package:surface/widgets/chat/chat_typing_indicator.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -280,11 +281,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
Expanded( Expanded(
child: InfiniteList( child: InfiniteList(
reverse: true, reverse: true,
padding: const EdgeInsets.only( padding: const EdgeInsets.only(top: 12),
left: 12,
right: 12,
top: 12,
),
hasReachedMax: _messageController.isAllLoaded, hasReachedMax: _messageController.isAllLoaded,
itemCount: _messageController.messages.length, itemCount: _messageController.messages.length,
isLoading: _messageController.isLoading, isLoading: _messageController.isLoading,
@ -310,23 +307,20 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
return Align( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Container( child: ChatMessage(
constraints: BoxConstraints(maxWidth: 480), data: message,
child: ChatMessage( isMerged: canMerge,
data: message, hasMerged: canMergePrevious,
isMerged: canMerge, isPending: _messageController.unconfirmedMessages.contains(message.uuid),
hasMerged: canMergePrevious, onReply: (value) {
isPending: _messageController.unconfirmedMessages.contains(message.uuid), _inputGlobalKey.currentState?.setReply(value);
onReply: (value) { },
_inputGlobalKey.currentState?.setReply(value); onEdit: (value) {
}, _inputGlobalKey.currentState?.setEdit(value);
onEdit: (value) { },
_inputGlobalKey.currentState?.setEdit(value); onDelete: (value) {
}, _inputGlobalKey.currentState?.deleteMessage(value);
onDelete: (value) { },
_inputGlobalKey.currentState?.deleteMessage(value);
},
),
), ),
); );
}, },
@ -335,11 +329,17 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
if (!_messageController.isPending) if (!_messageController.isPending)
Material( Material(
elevation: 2, elevation: 2,
child: ChatMessageInput( child: Column(
key: _inputGlobalKey, children: [
otherMember: _otherMember, ChatTypingIndicator(controller: _messageController),
controller: _messageController, ChatMessageInput(
).padding(bottom: MediaQuery.of(context).padding.bottom), key: _inputGlobalKey,
otherMember: _otherMember,
controller: _messageController,
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
), ),
], ],
); );

View File

@ -153,9 +153,14 @@ class _HomeDashUpdateWidget extends StatelessWidget {
} }
} }
class _HomeDashSpecialDayWidget extends StatelessWidget { class _HomeDashSpecialDayWidget extends StatefulWidget {
const _HomeDashSpecialDayWidget(); const _HomeDashSpecialDayWidget();
@override
State<_HomeDashSpecialDayWidget> createState() => _HomeDashSpecialDayWidgetState();
}
class _HomeDashSpecialDayWidgetState extends State<_HomeDashSpecialDayWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
@ -165,21 +170,20 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
if (days.isNotEmpty) { if (days.isNotEmpty) {
return Column( return Column(
spacing: 8,
children: days.map((ele) { children: days.map((ele) {
return Card( return Card(
child: ListTile( child: ListTile(
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24), leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']), title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
subtitle: Text( subtitle: Text(
DateFormat('y/M/d').format(DateTime.now().copyWith( DateFormat('y/M/d').format(DateTime.now().copyWith(
month: kSpecialDays[ele]!.$1, month: kSpecialDays[ele]?.$1,
day: kSpecialDays[ele]!.$2, day: kSpecialDays[ele]?.$2,
)), )),
), ),
), ),
).padding(bottom: 8); ).padding(bottom: 8);
}).toList()); }).toList());
} }
final nextOne = dayz.getNextSpecialDay(); final nextOne = dayz.getNextSpecialDay();
@ -193,7 +197,7 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
return Card( return Card(
child: ListTile( child: ListTile(
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24), 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( subtitle: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@ -204,6 +208,9 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
separatorType: SeparatorType.symbol, separatorType: SeparatorType.symbol,
decoration: BoxDecoration(), decoration: BoxDecoration(),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onDone: () {
setState(() {});
},
), ),
const Gap(12), const Gap(12),
Expanded( Expanded(

View File

@ -82,24 +82,15 @@ class _NotificationScreenState extends State<NotificationScreen> {
if (!mounted) return; if (!mounted) return;
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
List<int> markList = List.empty(growable: true);
for (final element in _notifications) {
if (element.id <= 0) continue;
if (element.readAt != null) continue;
markList.add(element.id);
}
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
await sn.client.put('/cgi/id/notifications/read', data: { final resp = await sn.client.put('/cgi/id/notifications/read/all');
'messages': markList,
});
_notifications.clear(); _notifications.clear();
_fetchNotifications(); _fetchNotifications();
if (!mounted) return; if (!mounted) return;
context.showSnackbar( context.showSnackbar(
'notificationMarkAllReadPrompt'.plural(markList.length), 'notificationMarkAllReadPrompt'.plural(resp.data['count']),
); );
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
@ -215,10 +206,11 @@ class _NotificationScreenState extends State<NotificationScreen> {
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
), ),
if (nty.subtitle != null) const Gap(4), if (nty.subtitle != null) const Gap(4),
MarkdownTextContent( SelectionArea(
content: nty.body, child: MarkdownTextContent(
isAutoWarp: true, content: nty.body,
isSelectable: true, isAutoWarp: true,
),
), ),
if ([ if ([
'interactive.feedback', 'interactive.feedback',

View File

@ -96,6 +96,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
), ),
), ),
]), ]),
maxLines: 2,
overflow: TextOverflow.ellipsis,
) )
: Text('postDetail').tr(), : Text('postDetail').tr(),
), ),

View File

@ -54,7 +54,9 @@ class PostEditorScreen extends StatefulWidget {
} }
class _PostEditorScreenState extends State<PostEditorScreen> { class _PostEditorScreenState extends State<PostEditorScreen> {
final PostWriteController _writeController = PostWriteController(); late final PostWriteController _writeController = PostWriteController(
doLoadFromTemporary: widget.postEditId == null,
);
bool _isFetching = false; bool _isFetching = false;
@ -364,6 +366,36 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container(
child: _writeController.temporaryRestored
? Container(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 28, right: 22),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.restore, size: 20),
const Gap(8),
Expanded(child: Text('postLocalDraftRestored').tr()),
InkWell(
child: Text('dialogDismiss').tr(),
onTap: () {
_writeController.reset();
},
),
],
))
: const SizedBox.shrink(),
)
.height(_writeController.temporaryRestored ? 32 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastLinearToSlowEaseIn),
LoadingIndicator(isActive: _isLoading), LoadingIndicator(isActive: _isLoading),
if (_writeController.isBusy && _writeController.progress != null) if (_writeController.isBusy && _writeController.progress != null)
TweenAnimationBuilder<double>( TweenAnimationBuilder<double>(

View File

@ -88,9 +88,9 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
title: Text(_realm?.name ?? 'loading'.tr()), title: Text(_realm?.name ?? 'loading'.tr()),
bottom: TabBar( bottom: TabBar(
tabs: [ tabs: [
Tab(icon: const Icon(Symbols.home)), Tab(icon: Icon(Symbols.home, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: const Icon(Symbols.group)), Tab(icon: Icon(Symbols.group, color: Theme.of(context).appBarTheme.foregroundColor)),
Tab(icon: const Icon(Symbols.settings)), Tab(icon: Icon(Symbols.settings, color: Theme.of(context).appBarTheme.foregroundColor)),
], ],
), ),
), ),

View File

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

View File

@ -48,7 +48,7 @@ Future<ThemeData> createAppTheme(
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
centerTitle: true, centerTitle: true,
elevation: hasAppBarBlurry ? 0 : null, 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, foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
), ),
scaffoldBackgroundColor: Colors.transparent, scaffoldBackgroundColor: Colors.transparent,

View File

@ -141,3 +141,39 @@ class SnAttachmentBoost with _$SnAttachmentBoost {
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) => _$SnAttachmentBoostFromJson(json); 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 => _$$SnAttachmentBoostImplCopyWith<_$SnAttachmentBoostImpl> get copyWith =>
throw _privateConstructorUsedError; 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(), 'attachment': instance.attachment.toJson(),
'account': instance.account, '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

@ -315,6 +315,7 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
} }
return MaterialDesktopVideoControlsTheme( return MaterialDesktopVideoControlsTheme(
key: Key('material-desktop-video-controls-theme-$_showOriginal'),
normal: MaterialDesktopVideoControlsThemeData( normal: MaterialDesktopVideoControlsThemeData(
buttonBarButtonSize: 24, buttonBarButtonSize: 24,
buttonBarButtonColor: Colors.white, buttonBarButtonColor: Colors.white,
@ -324,14 +325,16 @@ class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo
MaterialDesktopCustomButton( MaterialDesktopCustomButton(
iconSize: 24, iconSize: 24,
onPressed: _toggleOriginal, onPressed: _toggleOriginal,
icon: Builder(builder: (context) { icon: Icon(
return _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24); _showOriginal ? Symbols.high_quality : Symbols.sd,
}), size: 24,
),
), ),
], ],
), ),
fullscreen: const MaterialDesktopVideoControlsThemeData(), fullscreen: const MaterialDesktopVideoControlsThemeData(),
child: MaterialVideoControlsTheme( child: MaterialVideoControlsTheme(
key: Key('material-video-controls-theme-$_showOriginal'),
normal: MaterialVideoControlsThemeData( normal: MaterialVideoControlsThemeData(
buttonBarButtonSize: 24, buttonBarButtonSize: 24,
buttonBarButtonColor: Colors.white, buttonBarButtonColor: Colors.white,

View File

@ -15,20 +15,22 @@ class AttachmentList extends StatefulWidget {
final List<SnAttachment?> data; final List<SnAttachment?> data;
final bool bordered; final bool bordered;
final bool gridded; final bool gridded;
final bool noGrow;
final BoxFit fit; final BoxFit fit;
final double? maxHeight; final double? maxHeight;
final EdgeInsets? listPadding; final double? minWidth;
final double? maxWidth;
final EdgeInsets? padding;
const AttachmentList({ const AttachmentList({
super.key, super.key,
required this.data, required this.data,
this.bordered = false, this.bordered = false,
this.gridded = false, this.gridded = false,
this.noGrow = false,
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.maxHeight, this.maxHeight,
this.listPadding, this.minWidth,
this.maxWidth,
this.padding,
}); });
static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8)); static const BorderRadius kDefaultRadius = BorderRadius.all(Radius.circular(8));
@ -43,8 +45,6 @@ class _AttachmentListState extends State<AttachmentList> {
(_) => const Uuid().v4(), (_) => const Uuid().v4(),
); );
static const double kAttachmentMaxWidth = 640;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
@ -53,8 +53,8 @@ class _AttachmentListState extends State<AttachmentList> {
widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none; widget.bordered ? BorderSide(width: 1, color: Theme.of(context).dividerColor) : BorderSide.none;
final backgroundColor = Theme.of(context).colorScheme.surfaceContainer; final backgroundColor = Theme.of(context).colorScheme.surfaceContainer;
final constraints = BoxConstraints( final constraints = BoxConstraints(
minWidth: 80, minWidth: widget.minWidth ?? 80,
maxHeight: widget.maxHeight ?? double.infinity, maxHeight: widget.maxHeight ?? MediaQuery.of(context).size.height,
); );
if (widget.data.isEmpty) return const SizedBox.shrink(); if (widget.data.isEmpty) return const SizedBox.shrink();
@ -67,118 +67,77 @@ class _AttachmentListState extends State<AttachmentList> {
} }
.toDouble(); .toDouble();
return Padding( return Container(
padding: widget.listPadding ?? EdgeInsets.zero, padding: widget.padding ?? EdgeInsets.zero,
child: Container( constraints: constraints,
constraints: constraints, child: GestureDetector(
width: double.infinity, child: AspectRatio(
child: GestureDetector( aspectRatio: singleAspectRatio,
child: AspectRatio( child: Container(
aspectRatio: singleAspectRatio, decoration: BoxDecoration(
child: Container( color: backgroundColor,
decoration: BoxDecoration( border: Border.fromBorderSide(borderSide),
color: backgroundColor, borderRadius: AttachmentList.kDefaultRadius,
border: Border.fromBorderSide(borderSide), ),
borderRadius: AttachmentList.kDefaultRadius, child: ClipRRect(
), borderRadius: AttachmentList.kDefaultRadius,
child: ClipRRect( child: AttachmentItem(
borderRadius: AttachmentList.kDefaultRadius, data: widget.data[0],
child: AttachmentItem( heroTag: heroTags[0],
data: widget.data[0], fit: widget.fit,
heroTag: heroTags[0],
fit: widget.fit,
),
), ),
), ),
), ),
onTap: () {
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null).cast(),
initialIndex: 0,
heroTags: heroTags,
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
), ),
onTap: () {
if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null).cast(),
initialIndex: 0,
heroTags: heroTags,
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
), ),
); );
} }
if (widget.gridded) { if (widget.gridded) {
return Padding( return Container(
padding: widget.listPadding ?? EdgeInsets.zero, margin: widget.padding ?? EdgeInsets.zero,
child: Container( decoration: BoxDecoration(
decoration: BoxDecoration( color: backgroundColor,
color: backgroundColor, border: Border(
border: Border( top: borderSide,
top: borderSide, bottom: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
),
child: ClipRRect(
borderRadius: AttachmentList.kDefaultRadius,
child: StaggeredGrid.count(
crossAxisCount: math.min(widget.data.length, 2),
crossAxisSpacing: 4,
mainAxisSpacing: 4,
children: widget.data
.mapIndexed(
(idx, ele) => GestureDetector(
child: Container(
constraints: constraints,
child: AttachmentItem(
data: ele,
heroTag: heroTags[idx],
fit: widget.fit,
),
),
onTap: () {
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null).cast(),
initialIndex: idx,
heroTags: heroTags,
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
),
)
.toList(),
),
), ),
borderRadius: AttachmentList.kDefaultRadius,
), ),
); child: ClipRRect(
} borderRadius: AttachmentList.kDefaultRadius,
child: StaggeredGrid.count(
return AspectRatio( crossAxisCount: math.min(widget.data.length, 2),
aspectRatio: (widget.data.firstOrNull?.data['ratio'] ?? 1).toDouble(), crossAxisSpacing: 4,
child: Container( mainAxisSpacing: 4,
constraints: BoxConstraints(maxHeight: constraints.maxHeight), children: widget.data
child: ScrollConfiguration( .mapIndexed(
behavior: _AttachmentListScrollBehavior(), (idx, ele) => GestureDetector(
child: ListView.separated( child: Container(
shrinkWrap: true, constraints: constraints,
itemCount: widget.data.length, child: AttachmentItem(
itemBuilder: (context, idx) { data: ele,
return Container( heroTag: heroTags[idx],
constraints: constraints, fit: BoxFit.cover,
child: AspectRatio( ),
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(), ),
child: GestureDetector(
onTap: () { onTap: () {
if (widget.data[idx]?.mediaType != SnMediaType.image) return; if (widget.data[idx]!.mediaType != SnMediaType.image) return;
context.pushTransparentRoute( context.pushTransparentRoute(
AttachmentZoomView( AttachmentZoomView(
data: data: widget.data.where((ele) => ele != null).cast(),
widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
initialIndex: idx, initialIndex: idx,
heroTags: heroTags, heroTags: heroTags,
), ),
@ -186,44 +145,76 @@ class _AttachmentListState extends State<AttachmentList> {
rootNavigator: true, rootNavigator: true,
); );
}, },
child: Stack( ),
fit: StackFit.expand, )
children: [ .toList(),
Container( ),
decoration: BoxDecoration( ),
color: backgroundColor, );
border: Border( }
top: borderSide,
bottom: borderSide, return Container(
), constraints: BoxConstraints(maxHeight: constraints.maxHeight),
borderRadius: AttachmentList.kDefaultRadius, child: ScrollConfiguration(
behavior: _AttachmentListScrollBehavior(),
child: ListView.separated(
padding: widget.padding,
shrinkWrap: true,
itemCount: widget.data.length,
itemBuilder: (context, idx) {
return Container(
constraints: constraints.copyWith(maxWidth: widget.maxWidth),
child: AspectRatio(
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
child: GestureDetector(
onTap: () {
if (widget.data[idx]?.mediaType != SnMediaType.image) return;
context.pushTransparentRoute(
AttachmentZoomView(
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
initialIndex: idx,
heroTags: heroTags,
),
backgroundColor: Colors.black.withOpacity(0.7),
rootNavigator: true,
);
},
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: BoxDecoration(
color: backgroundColor,
border: Border(
top: borderSide,
bottom: borderSide,
), ),
child: ClipRRect( borderRadius: AttachmentList.kDefaultRadius,
borderRadius: AttachmentList.kDefaultRadius, ),
child: AttachmentItem( child: ClipRRect(
data: widget.data[idx], borderRadius: AttachmentList.kDefaultRadius,
heroTag: heroTags[idx], child: AttachmentItem(
), data: widget.data[idx],
heroTag: heroTags[idx],
), ),
), ),
Positioned( ),
right: 8, Positioned(
bottom: 8, right: 8,
child: Chip( bottom: 8,
label: Text('${idx + 1}/${widget.data.length}'), child: Chip(
), label: Text('${idx + 1}/${widget.data.length}'),
), ),
], ),
), ],
), ),
), ),
); ),
}, );
separatorBuilder: (context, index) => const Gap(8), },
padding: widget.listPadding, separatorBuilder: (context, index) => const Gap(8),
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
),
), ),
), ),
); );

View File

@ -231,7 +231,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
children: [ children: [
IgnorePointer( IgnorePointer(
child: AccountImage( child: AccountImage(
content: account!.avatar, content: account?.avatar,
radius: 19, radius: 19,
), ),
), ),
@ -246,7 +246,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
Text( Text(
account.nick, account?.nick ?? 'unknown'.tr(),
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
], ],
@ -312,11 +312,6 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
]), ]),
style: metaTextStyle, style: metaTextStyle,
).padding(right: 2), ).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) if (item.metadata['exif']?['ISO'] != null)
Text( Text(
'ISO${item.metadata['exif']?['ISO']}', 'ISO${item.metadata['exif']?['ISO']}',
@ -334,7 +329,7 @@ class _AttachmentZoomViewState extends State<AttachmentZoomView> {
) )
else else
Text( Text(
'${item.size} Bytes', item.size.formatBytes(),
style: metaTextStyle, style: metaTextStyle,
), ),
if (item.metadata['width'] != null && item.metadata['height'] != null) if (item.metadata['width'] != null && item.metadata['height'] != null)

View File

@ -0,0 +1,86 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:surface/controllers/post_write_controller.dart';
import 'package:surface/providers/sn_attachment.dart';
import 'package:surface/widgets/dialog.dart';
class PendingAttachmentAltDialog extends StatefulWidget {
final PostWriteMedia media;
const PendingAttachmentAltDialog({super.key, required this.media});
@override
State<PendingAttachmentAltDialog> createState() => _PendingAttachmentAltDialogState();
}
class _PendingAttachmentAltDialogState extends State<PendingAttachmentAltDialog> {
final _contentController = TextEditingController();
@override
void initState() {
super.initState();
_contentController.text = widget.media.attachment!.alt;
}
bool _isBusy = false;
Future<void> _performAction() async {
if (_isBusy) return;
setState(() => _isBusy = true);
try {
final attach = context.read<SnAttachmentProvider>();
final result = await attach.updateOne(
widget.media.attachment!,
alt: _contentController.text,
);
if (!mounted) return;
attach.putCache([result]);
Navigator.pop(context, result);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
setState(() => _isBusy = false);
}
}
@override
void dispose() {
_contentController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('attachmentSetAlt').tr(),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _contentController,
decoration: InputDecoration(
labelText: 'fieldAttachmentAlt'.tr(),
border: const UnderlineInputBorder(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: [
TextButton(
onPressed: _isBusy ? null : () {
Navigator.pop(context);
},
child: Text('dialogDismiss'.tr()),
),
TextButton(
onPressed: _isBusy ? null : () => _performAction(),
child: Text('dialogConfirm'.tr()),
),
],
);
}
}

View File

@ -24,6 +24,7 @@ class ChatMessage extends StatelessWidget {
final Function(SnChatMessage)? onReply; final Function(SnChatMessage)? onReply;
final Function(SnChatMessage)? onEdit; final Function(SnChatMessage)? onEdit;
final Function(SnChatMessage)? onDelete; final Function(SnChatMessage)? onDelete;
final EdgeInsets padding;
const ChatMessage({ const ChatMessage({
super.key, super.key,
@ -35,6 +36,7 @@ class ChatMessage extends StatelessWidget {
this.onReply, this.onReply,
this.onEdit, this.onEdit,
this.onDelete, this.onDelete,
this.padding = const EdgeInsets.only(left: 12, right: 12),
}); });
@override @override
@ -53,7 +55,7 @@ class ChatMessage extends StatelessWidget {
iconOnRightSwipe: Symbols.edit, iconOnRightSwipe: Symbols.edit,
swipeSensitivity: 20, swipeSensitivity: 20,
onLeftSwipe: onReply != null ? (_) => onReply!(data) : null, onLeftSwipe: onReply != null ? (_) => onReply!(data) : null,
onRightSwipe: onEdit != null ? (_) => onEdit!(data) : null, onRightSwipe: (onEdit != null && isOwner) ? (_) => onEdit!(data) : null,
child: ContextMenuArea( child: ContextMenuArea(
contextMenu: ContextMenu( contextMenu: ContextMenu(
entries: [ entries: [
@ -87,84 +89,97 @@ class ChatMessage extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: isCompact ? EdgeInsets.zero : padding,
children: [ child: Row(
if (!isMerged && !isCompact) crossAxisAlignment: CrossAxisAlignment.start,
AccountImage( children: [
content: user?.avatar, if (!isMerged && !isCompact)
AccountImage(
content: user?.avatar,
)
else if (isMerged)
const Gap(40),
const Gap(8),
Expanded(
child: Container(
constraints: BoxConstraints(maxWidth: 480),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMerged)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (isCompact)
AccountImage(
content: user?.avatar,
radius: 12,
).padding(right: 8),
Text(
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
).bold(),
const Gap(8),
Text(
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(
color: Theme.of(context).dividerColor,
width: 1,
),
),
padding: const EdgeInsets.only(
left: 4,
right: 4,
top: 8,
bottom: 6,
),
child: ChatMessage(
data: data.preload!.quoteEvent!,
isCompact: true,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
)).padding(bottom: 4, top: 4),
switch (data.type) {
'messages.new' => _ChatMessageText(
data: data,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
_ => _ChatMessageSystemNotify(data: data),
},
],
),
),
) )
else if (isMerged) ],
const Gap(40), ).opacity(isPending ? 0.5 : 1),
const Gap(8), ),
Expanded( if (data.body['text'] != null && data.type == 'messages.new' && (data.body['text']?.isNotEmpty ?? false))
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMerged)
Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
if (isCompact)
AccountImage(
content: user?.avatar,
radius: 12,
).padding(right: 6),
Text(
(data.sender.nick?.isNotEmpty ?? false) ? data.sender.nick! : user?.nick ?? 'unknown',
).bold(),
const Gap(6),
Text(
dateFormatter.format(data.createdAt.toLocal()),
).fontSize(13),
],
),
if (isCompact) const Gap(4),
if (data.preload?.quoteEvent != null)
StyledWidget(Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
),
),
padding: const EdgeInsets.only(
left: 4,
right: 4,
top: 8,
bottom: 6,
),
child: ChatMessage(
data: data.preload!.quoteEvent!,
isCompact: true,
onReply: onReply,
onEdit: onEdit,
onDelete: onDelete,
),
)).padding(bottom: 4, top: 4),
switch (data.type) {
'messages.new' => _ChatMessageText(data: data),
_ => _ChatMessageSystemNotify(data: data),
},
],
),
)
],
).opacity(isPending ? 0.5 : 1),
if (data.body['text'] != null && (data.body['text']?.isNotEmpty ?? false))
LinkPreviewWidget(text: data.body['text']!), LinkPreviewWidget(text: data.body['text']!),
if (data.preload?.attachments?.isNotEmpty ?? false) if (data.preload?.attachments?.isNotEmpty ?? false)
AttachmentList( AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
bordered: true, bordered: true,
gridded: true, maxHeight: 560,
noGrow: true, maxWidth: 480,
maxHeight: 520, minWidth: 480,
listPadding: const EdgeInsets.only(top: 8), padding: padding.copyWith(top: 8),
), ),
if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(6), if (!hasMerged && !isCompact) const Gap(12) else if (!isCompact) const Gap(8),
], ],
), ),
), ),
@ -174,19 +189,72 @@ class ChatMessage extends StatelessWidget {
class _ChatMessageText extends StatelessWidget { class _ChatMessageText extends StatelessWidget {
final SnChatMessage data; 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 @override
Widget build(BuildContext context) { 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) { if (data.body['text'] != null && data.body['text'].isNotEmpty) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MarkdownTextContent( SelectionArea(
content: data.body['text'], contextMenuBuilder: (context, editableTextState) {
isSelectable: true, final List<ContextMenuButtonItem> items = editableTextState.contextMenuButtonItems;
isAutoWarp: true,
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: MarkdownTextContent(
content: data.body['text'],
isAutoWarp: true,
),
), ),
if (data.updatedAt != data.createdAt) if (data.updatedAt != data.createdAt)
Text( Text(

View File

@ -11,7 +11,6 @@ import 'package:surface/providers/user_directory.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/types/chat.dart'; import 'package:surface/types/chat.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/markdown_content.dart';
import 'package:surface/widgets/post/post_media_pending_list.dart'; import 'package:surface/widgets/post/post_media_pending_list.dart';
class ChatMessageInput extends StatefulWidget { class ChatMessageInput extends StatefulWidget {
@ -33,12 +32,24 @@ class ChatMessageInputState extends State<ChatMessageInput> {
final TextEditingController _contentController = TextEditingController(); final TextEditingController _contentController = TextEditingController();
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
_contentController.addListener(() {
if (_contentController.text.isNotEmpty) {
widget.controller.pingTypingStatus();
}
});
}
void setReply(SnChatMessage? value) { void setReply(SnChatMessage? value) {
setState(() => _replyingMessage = value); setState(() => _replyingMessage = value);
} }
void setEdit(SnChatMessage? value) { void setEdit(SnChatMessage? value) {
_contentController.text = value?.body['text'] ?? ''; _contentController.text = value?.body['text'] ?? '';
_attachments.clear();
_attachments.addAll(value?.preload?.attachments?.map((e) => PostWriteMedia(e)) ?? []);
setState(() => _editingMessage = value); setState(() => _editingMessage = value);
} }
@ -92,7 +103,9 @@ class ChatMessageInputState extends State<ChatMessageInput> {
}, },
); );
_attachments[i] = PostWriteMedia(item); setState(() {
_attachments[i] = PostWriteMedia(item);
});
} }
} catch (err) { } catch (err) {
if (!mounted) return; if (!mounted) return;
@ -161,75 +174,84 @@ class ChatMessageInputState extends State<ChatMessageInput> {
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut), .animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView( SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
child: Padding( child: _replyingMessage != null
padding: _replyingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero, ? Container(
child: _replyingMessage != null padding: const EdgeInsets.only(left: 16, right: 16),
? MaterialBanner( decoration: BoxDecoration(
padding: const EdgeInsets.only(left: 16.0), border: Border(
leading: const Icon(Symbols.reply), bottom: BorderSide(
backgroundColor: Colors.transparent, color: Theme.of(context).dividerColor,
content: SingleChildScrollView( width: 1 / MediaQuery.of(context).devicePixelRatio,
physics: const NeverScrollableScrollPhysics(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_replyingMessage?.body['text'] != null)
MarkdownTextContent(
content: _replyingMessage?.body['text'],
),
],
), ),
), ),
actions: [ ),
TextButton( child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.reply, size: 20),
const Gap(8),
Expanded(
child: Text(
_replyingMessage?.body['text'] ?? '${_replyingMessage?.sender.nick}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Gap(16),
InkWell(
child: Text('cancel'.tr()), child: Text('cancel'.tr()),
onPressed: () { onTap: () {
_attachments.clear();
setState(() => _replyingMessage = null); setState(() => _replyingMessage = null);
}, },
), ),
], ],
) ).padding(vertical: 8),
: const SizedBox.shrink(), )
), : const SizedBox.shrink(),
) )
.height(_replyingMessage != null ? 54 + 8 : 0, animate: true) .height(_replyingMessage != null ? 38 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut), .animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SingleChildScrollView( SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
child: Padding( child: _editingMessage != null
padding: _editingMessage != null ? const EdgeInsets.only(top: 8) : EdgeInsets.zero, ? Container(
child: _editingMessage != null padding: const EdgeInsets.only(left: 16, right: 16),
? MaterialBanner( decoration: BoxDecoration(
padding: const EdgeInsets.only(left: 16.0), border: Border(
leading: const Icon(Symbols.edit), bottom: BorderSide(
backgroundColor: Colors.transparent, color: Theme.of(context).dividerColor,
content: SingleChildScrollView( width: 1 / MediaQuery.of(context).devicePixelRatio,
physics: const NeverScrollableScrollPhysics(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_editingMessage?.body['text'] != null)
MarkdownTextContent(
content: _editingMessage?.body['text'],
),
],
), ),
), ),
actions: [ ),
TextButton( child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.edit, size: 20),
const Gap(8),
Expanded(
child: Text(
_editingMessage?.body['text'] ?? '${_editingMessage?.sender.nick}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Gap(16),
InkWell(
child: Text('cancel'.tr()), child: Text('cancel'.tr()),
onPressed: () { onTap: () {
_attachments.clear();
_contentController.clear();
setState(() => _editingMessage = null); setState(() => _editingMessage = null);
}, },
), ),
], ],
) ).padding(vertical: 8),
: const SizedBox.shrink(), )
), : const SizedBox.shrink(),
) )
.height(_editingMessage != null ? 54 + 8 : 0, animate: true) .height(_editingMessage != null ? 38 : 0, animate: true)
.animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut), .animate(const Duration(milliseconds: 300), Curves.fastEaseInToSlowEaseOut),
SizedBox( SizedBox(
height: 56, height: 56,

View File

@ -0,0 +1,53 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:surface/controllers/chat_message_controller.dart';
import 'package:surface/providers/user_directory.dart';
class ChatTypingIndicator extends StatelessWidget {
final ChatMessageController controller;
const ChatTypingIndicator({super.key, required this.controller});
@override
Widget build(BuildContext context) {
final ud = context.read<UserDirectoryProvider>();
return StyledWidget(controller.typingMembers.isEmpty
? const SizedBox.shrink()
: Container(
padding: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
),
),
child: Row(
children: [
const Icon(Symbols.more_horiz, weight: 600, size: 20),
const Gap(8),
Text(
'messageTyping'.plural(controller.typingMembers.length, args: [
controller.typingMembers
.map((ele) => (ele.nick?.isNotEmpty ?? false)
? ele.nick!
: ud.getAccountFromCache(ele.accountId)?.name ?? 'unknown')
.join(', '),
]),
),
],
),
))
.height(controller.typingMembers.isNotEmpty ? 38 : 0, animate: true)
.animate(
const Duration(milliseconds: 300),
Curves.fastLinearToSlowEaseIn,
);
}
}

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_context_menu/flutter_context_menu.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 { class ContextMenuArea extends StatelessWidget {
final ContextMenu contextMenu; final ContextMenu contextMenu;
@ -22,11 +23,10 @@ class ContextMenuArea extends StatelessWidget {
return Listener( return Listener(
onPointerDown: (event) { onPointerDown: (event) {
mousePosition = event.position; mousePosition = event.position;
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE); final cfg = context.read<ConfigProvider>();
if (!isCollapseDrawer) { if (!cfg.drawerIsCollapsed) {
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
// Leave padding for side navigation // Leave padding for side navigation
mousePosition = isExpandDrawer mousePosition = cfg.drawerIsExpanded
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2) ? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 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:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:markdown/markdown.dart' as markdown; 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/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_item.dart'; import 'package:surface/widgets/attachment/attachment_item.dart';
import 'package:surface/widgets/universal_image.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:url_launcher/url_launcher_string.dart';
import 'package:path/path.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'attachment/attachment_zoom.dart'; import 'attachment/attachment_zoom.dart';
class MarkdownTextContent extends StatelessWidget { class MarkdownTextContent extends StatelessWidget {
final String content; final String content;
final bool isSelectable;
final bool isAutoWarp; final bool isAutoWarp;
final bool isEnlargeSticker;
final TextScaler? textScaler; final TextScaler? textScaler;
final List<SnAttachment?>? attachments; final List<SnAttachment?>? attachments;
const MarkdownTextContent({ const MarkdownTextContent({
super.key, super.key,
required this.content, required this.content,
this.isSelectable = false,
this.isAutoWarp = false, this.isAutoWarp = false,
this.isEnlargeSticker = false,
this.textScaler, this.textScaler,
this.attachments, this.attachments,
}); });
Widget _buildContent(BuildContext context) { @override
Widget build(BuildContext context) {
return Markdown( return Markdown(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@ -42,33 +41,33 @@ class MarkdownTextContent extends StatelessWidget {
styleSheet: MarkdownStyleSheet.fromTheme( styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context), Theme.of(context),
).copyWith( ).copyWith(
textScaler: textScaler, textScaler: textScaler,
blockquote: TextStyle( blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
blockquoteDecoration: BoxDecoration( blockquoteDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(Radius.circular(4)), borderRadius: const BorderRadius.all(Radius.circular(4)),
), ),
horizontalRuleDecoration: BoxDecoration( horizontalRuleDecoration: BoxDecoration(
border: Border( border: Border(
top: BorderSide( top: BorderSide(
width: 1.0, width: 1.0,
color: Theme.of(context).dividerColor,
),
),
),
codeblockDecoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 0.3,
), ),
borderRadius: const BorderRadius.all(Radius.circular(4)), ),
color: Theme.of(context).colorScheme.surface.withOpacity(0.5), ),
)), codeblockDecoration: BoxDecoration(
builders: { border: Border.all(
'code': _MarkdownTextCodeElement(), 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, softLineBreak: true,
extensionSet: markdown.ExtensionSet( extensionSet: markdown.ExtensionSet(
<markdown.BlockSyntax>[ <markdown.BlockSyntax>[
@ -78,6 +77,7 @@ class MarkdownTextContent extends StatelessWidget {
<markdown.InlineSyntax>[ <markdown.InlineSyntax>[
if (isAutoWarp) markdown.LineBreakSyntax(), if (isAutoWarp) markdown.LineBreakSyntax(),
_UserNameCardInlineSyntax(), _UserNameCardInlineSyntax(),
_CustomEmoteInlineSyntax(context),
markdown.AutolinkSyntax(), markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(), markdown.AutolinkExtensionSyntax(),
markdown.CodeSyntax(), markdown.CodeSyntax(),
@ -108,9 +108,41 @@ class MarkdownTextContent extends StatelessWidget {
if (url.startsWith('solink://')) { if (url.startsWith('solink://')) {
final segments = url.replaceFirst('solink://', '').split('/'); final segments = url.replaceFirst('solink://', '').split('/');
switch (segments[0]) { 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': case 'attachments':
final attachment = attachments?.firstWhere( final attachment = attachments?.firstWhere(
(ele) => ele?.rid == segments[1], (ele) => ele?.rid == segments[1],
orElse: () => null, orElse: () => null,
); );
if (attachment != 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 { class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
@ -194,45 +218,24 @@ class _UserNameCardInlineSyntax extends markdown.InlineSyntax {
} }
} }
class _MarkdownTextCodeElement extends MarkdownElementBuilder { class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
@override final BuildContext context;
Widget? visitElementAfter(
markdown.Element element,
TextStyle? preferredStyle,
) {
var language = '';
if (element.attributes['class'] != null) { _CustomEmoteInlineSyntax(this.context) : super(r':([-\w]+):');
String lg = element.attributes['class'] as String;
language = lg.substring(9).trim(); @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( final element = markdown.Element.empty('img');
future: (() async { element.attributes['src'] = 'solink://stickers/$alias';
final docPath = '../../../'; parser.addNode(element);
final highlightingPath = join(docPath, 'assets/highlighting', language);
await Highlighter.initialize([highlightingPath]); return true;
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);
} }
} }

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:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/navigation.dart';
import 'package:surface/widgets/version_label.dart'; import 'package:surface/widgets/version_label.dart';
@ -28,8 +32,9 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final nav = context.watch<NavigationProvider>(); 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( return ListenableBuilder(
listenable: nav, listenable: nav,
@ -44,6 +49,18 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
selectedIndex: nav.currentIndex, selectedIndex: nav.currentIndex,
children: [ 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( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

View File

@ -6,8 +6,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:responsive_framework/responsive_framework.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/navigation.dart';
import 'package:surface/widgets/connection_indicator.dart'; import 'package:surface/widgets/connection_indicator.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
@ -57,10 +59,11 @@ class AppRootScaffold extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.watch<ConfigProvider>();
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
final isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE); final isCollapseDrawer = cfg.drawerIsCollapsed;
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET); final isExpandedDrawer = cfg.drawerIsExpanded;
final routeName = GoRouter.of(context).routerDelegate.currentConfiguration.last.route.name; final routeName = GoRouter.of(context).routerDelegate.currentConfiguration.last.route.name;
final isShowBottomNavigation = NavigationProvider.kShowBottomNavScreen.contains(routeName) final isShowBottomNavigation = NavigationProvider.kShowBottomNavScreen.contains(routeName)
@ -81,7 +84,7 @@ class AppRootScaffold extends StatelessWidget {
), ),
), ),
), ),
child: isExpandDrawer ? AppNavigationDrawer(elevation: 0) : AppRailNavigation(), child: isExpandedDrawer ? AppNavigationDrawer(elevation: 0) : AppRailNavigation(),
), ),
Expanded(child: body), Expanded(child: body),
], ],
@ -147,7 +150,7 @@ class AppRootScaffold extends StatelessWidget {
Expanded(child: innerWidget), Expanded(child: innerWidget),
], ],
), ),
drawer: !isExpandDrawer ? AppNavigationDrawer() : null, drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
drawerEdgeDragWidth: isPopable ? 0 : null, drawerEdgeDragWidth: isPopable ? 0 : null,
bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null, bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null,
), ),

View File

@ -20,6 +20,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart'; import 'package:surface/providers/config.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/types/attachment.dart';
import 'package:surface/types/post.dart'; import 'package:surface/types/post.dart';
import 'package:surface/types/reaction.dart'; import 'package:surface/types/reaction.dart';
import 'package:surface/widgets/account/account_image.dart'; import 'package:surface/widgets/account/account_image.dart';
@ -112,7 +113,7 @@ class PostItem extends StatelessWidget {
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
); );
} else { } 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(); await imageFile.delete();
@ -198,6 +199,10 @@ class PostItem extends StatelessWidget {
).center(); ).center();
} }
final displayableAttachments = data.preload?.attachments
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
.toList();
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
@ -247,14 +252,15 @@ class PostItem extends StatelessWidget {
], ],
), ),
), ),
if ((data.preload?.attachments?.isNotEmpty ?? false) && data.type != 'article') if (displayableAttachments?.isNotEmpty ?? false)
AttachmentList( AttachmentList(
data: data.preload!.attachments!, data: displayableAttachments!,
bordered: true, bordered: true,
gridded: true, gridded: true,
maxHeight: showFullPost ? null : 480, maxHeight: showFullPost ? null : 480,
minWidth: 640,
fit: showFullPost ? BoxFit.cover : BoxFit.contain, fit: showFullPost ? BoxFit.cover : BoxFit.contain,
listPadding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
), ),
if (data.body['content'] != null) if (data.body['content'] != null)
LinkPreviewWidget( LinkPreviewWidget(
@ -336,10 +342,10 @@ class PostShareImageWidget extends StatelessWidget {
isRelativeDate: false, isRelativeDate: false,
).padding(horizontal: 16, bottom: 8), ).padding(horizontal: 16, bottom: 8),
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false)) if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
AttachmentList( StyledWidget(AttachmentList(
data: data.preload!.attachments!, data: data.preload!.attachments!,
gridded: true, gridded: true,
).padding(horizontal: 16, bottom: 8), )).padding(horizontal: 16, bottom: 8),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -873,12 +879,17 @@ class _PostContentBody extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (data.body['content'] == null) return const SizedBox.shrink(); if (data.body['content'] == null) return const SizedBox.shrink();
return MarkdownTextContent( final content = MarkdownTextContent(
isSelectable: isSelectable, isEnlargeSticker: true,
textScaler: isEnlarge ? TextScaler.linear(1.1) : null, textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: data.body['content'], content: data.body['content'],
attachments: data.preload?.attachments, attachments: data.preload?.attachments,
); );
if (isSelectable) {
return SelectionArea(child: content);
}
return content;
} }
} }
@ -930,9 +941,10 @@ class _PostQuoteContent extends StatelessWidget {
child: AttachmentList( child: AttachmentList(
data: child.preload!.attachments!, data: child.preload!.attachments!,
maxHeight: 360, maxHeight: 360,
minWidth: 640,
fit: BoxFit.contain, fit: BoxFit.contain,
gridded: true, gridded: true,
listPadding: const EdgeInsets.symmetric(horizontal: 12), padding: const EdgeInsets.symmetric(horizontal: 12),
), ),
).padding( ).padding(
top: 8, top: 8,

View File

@ -21,6 +21,7 @@ import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/attachment.dart'; import 'package:surface/types/attachment.dart';
import 'package:surface/widgets/attachment/attachment_input.dart'; import 'package:surface/widgets/attachment/attachment_input.dart';
import 'package:surface/widgets/attachment/attachment_zoom.dart'; import 'package:surface/widgets/attachment/attachment_zoom.dart';
import 'package:surface/widgets/attachment/pending_attachment_alt.dart';
import 'package:surface/widgets/attachment/pending_attachment_boost.dart'; import 'package:surface/widgets/attachment/pending_attachment_boost.dart';
import 'package:surface/widgets/context_menu.dart'; import 'package:surface/widgets/context_menu.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
@ -157,6 +158,16 @@ class PostMediaPendingList extends StatelessWidget {
onUpdate!(idx, result); onUpdate!(idx, result);
} }
Future<void> _setAlt(BuildContext context, int idx) async {
final result = await showDialog<SnAttachment?>(
context: context,
builder: (context) => PendingAttachmentAltDialog(media: attachments[idx]),
);
if (result == null) return;
onUpdate!(idx, PostWriteMedia(result));
}
ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) { ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) {
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS); final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
return ContextMenu( return ContextMenu(
@ -169,6 +180,14 @@ class PostMediaPendingList extends StatelessWidget {
_compressVideo(context, idx); _compressVideo(context, idx);
}, },
), ),
if (media.attachment != null)
MenuItem(
label: 'attachmentSetAlt'.tr(),
icon: Symbols.description,
onSelected: () {
_setAlt(context, idx);
},
),
if (media.attachment != null) if (media.attachment != null)
MenuItem( MenuItem(
label: 'attachmentBoost'.tr(), label: 'attachmentBoost'.tr(),

View File

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

View File

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

View File

@ -618,10 +618,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fl_chart name: fl_chart
sha256: c724234b05e378383e958f3e82ca84a3e1e3c06a0898462044dd8a24b1ee9864 sha256: "10ddaf334fe84d59333a12d153043e366f243e0bdfff2df0313e1e249f5bf926"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.70.0" version: "0.70.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -700,18 +700,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_markdown name: flutter_markdown
sha256: "255b00afa1a7bad19727da6a7780cf3db6c3c12e68d302d85e0ff1fdf173db9e" sha256: e37f4c69a07b07bb92622ef6b131a53c9aae48f64b176340af9e8e5238718487
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.4+3" version: "0.7.5"
flutter_native_splash: flutter_native_splash:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_native_splash name: flutter_native_splash
sha256: "1152ab0067ca5a2ebeb862fe0a762057202cceb22b7e62692dcbabf6483891bb" sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.3" version: "2.4.4"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -766,10 +766,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_webrtc name: flutter_webrtc
sha256: "3efe9828f19a07d29a51a726759ad0c70a840d231548a1c7d0332075a94db1df" sha256: e82ffd0d0b79621c5554eed73509d7f5bd286d57fef29a573846785c65237fb1
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.5+hotfix.1" version: "0.12.5+hotfix.2"
freezed: freezed:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -822,10 +822,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539" sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.6.2" version: "14.6.3"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:
@ -902,10 +902,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: http_parser name: http_parser
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.1" version: "4.1.2"
icons_launcher: icons_launcher:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -950,10 +950,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_ios name: image_picker_ios
sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b" sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.12+1" version: "0.8.12+2"
image_picker_linux: image_picker_linux:
dependency: transitive dependency: transitive
description: description:
@ -1086,10 +1086,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: livekit_client name: livekit_client
sha256: "7cdeb3eaeec7fb70a4cf88d9caabccbef9e3bd5f0b23c086320bc5c9acb2770b" sha256: a19bcf8640b45e0730b1e3e3e78be7882dad680c6ebe8ae75294fd8d4612450d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.4" version: "2.3.4+hotfix.2"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@ -1142,10 +1142,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: material_symbols_icons name: material_symbols_icons
sha256: "64404f47f8e0a9d20478468e5decef867a688660bad7173adcd20418d7f892c9" sha256: "89aac72d25dd49303f71b3b1e70f8374791846729365b25bebc2a2531e5b86cd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2801.0" version: "4.2801.1"
media_kit: media_kit:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1646,10 +1646,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: "3c7e73920c694a436afaf65ab60ce3453d91f84208d761fbd83fc21182134d93" sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.4" version: "2.3.5"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
@ -1795,10 +1795,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqflite_darwin name: sqflite_darwin
sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474" sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" version: "2.4.1+1"
sqflite_platform_interface: sqflite_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1863,14 +1863,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.0+3" 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: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -2131,10 +2123,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.9.0" version: "5.10.0"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:
@ -2177,4 +2169,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.6.0 <4.0.0" dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.0" flutter: ">=3.24.0"

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 2.2.1+41 version: 2.2.2+52
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@ -53,9 +53,7 @@ dependencies:
markdown: ^7.2.2 markdown: ^7.2.2
flutter_markdown: ^0.7.4+1 flutter_markdown: ^0.7.4+1
url_launcher: ^6.3.1 url_launcher: ^6.3.1
cached_network_image: ^3.4.1
flutter_animate: ^4.5.0 flutter_animate: ^4.5.0
syntax_highlight: ^0.4.0
google_fonts: ^6.2.1 google_fonts: ^6.2.1
path: ^1.9.0 path: ^1.9.0
relative_time: ^5.0.0 relative_time: ^5.0.0
@ -116,6 +114,7 @@ dependencies:
flutter_webrtc: ^0.12.5+hotfix.1 flutter_webrtc: ^0.12.5+hotfix.1
slide_countdown: ^2.0.2 slide_countdown: ^2.0.2
video_compress: ^3.1.3 video_compress: ^3.1.3
cached_network_image: ^3.4.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: