Compare commits
56 Commits
Author | SHA1 | Date | |
---|---|---|---|
55f434ff05 | |||
f2b3bdda2d | |||
1f6bf33b0e | |||
e2027b1a32 | |||
2b3a58b55e | |||
6ac536412a | |||
52f8ffe4e4 | |||
aca81431aa | |||
1fadd850b7 | |||
ed2a9a21b6 | |||
57279eb3e4 | |||
c403a2914a | |||
bcb176344c | |||
ecf362cffc | |||
f4ab7671d8 | |||
a2a3018917 | |||
0bdb664000 | |||
9c3b61ce57 | |||
d06df3d278 | |||
547ba19e61 | |||
cb05ff2e9e | |||
f614da7918 | |||
a3c8dafff9 | |||
fa978a7cd1 | |||
aaa0a562b4 | |||
590a4ce2a6 | |||
f26edce071 | |||
603799ea32 | |||
a32baf7798 | |||
498c9af663 | |||
202dbff6d3 | |||
96fd64d85d | |||
e236b7f98b | |||
5c7929e618 | |||
7ba5260246 | |||
a6d4947a23 | |||
7fbd4e9647 | |||
95d926b29f | |||
f6cf6d0440 | |||
e503c3f02f | |||
d4fbdd397e | |||
03943a7138 | |||
44f2c5fe0e | |||
bb66d5b684 | |||
1fca36293d | |||
2c7dc8c2ea | |||
cf0df91d8c | |||
91c85e8a58 | |||
2851780dda | |||
00fd58fb97 | |||
ee7d0ddd25 | |||
7656c08832 | |||
619c90cdd9 | |||
168d51c9fe | |||
d4b831f98e | |||
4d96a15c31 |
@ -15,6 +15,7 @@ analyzer:
|
|||||||
- "**/*.freezed.dart"
|
- "**/*.freezed.dart"
|
||||||
errors:
|
errors:
|
||||||
invalid_annotation_target: ignore # Due to freezed + json_serializable issue, ref https://github.com/rrousselGit/freezed/issues/488#issuecomment-894358980
|
invalid_annotation_target: ignore # Due to freezed + json_serializable issue, ref https://github.com/rrousselGit/freezed/issues/488#issuecomment-894358980
|
||||||
|
deprecated_member_use: ignore
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
# The lint rules applied to this project can be customized in the
|
||||||
|
26
api/Paperclip/Activate Boost.bru
Normal file
26
api/Paperclip/Activate Boost.bru
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
meta {
|
||||||
|
name: Activate Boost
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/uc/boosts/1/activate
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"client_id": "{{third_client_id}}",
|
||||||
|
"client_secret":"{{third_client_tk}}",
|
||||||
|
"type": "general",
|
||||||
|
"subject": "Merry Christmas!",
|
||||||
|
"subtitle": "一条来自 Solar Network 团队的信息",
|
||||||
|
"content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
|
||||||
|
"metadata": {
|
||||||
|
"image": "6EqsYQwmFRCkbmhR"
|
||||||
|
},
|
||||||
|
"priority": 10
|
||||||
|
}
|
||||||
|
}
|
19
api/Paperclip/Stickers/Create Sticker Pack.bru
Normal file
19
api/Paperclip/Stickers/Create Sticker Pack.bru
Normal 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!"
|
||||||
|
}
|
||||||
|
}
|
20
api/Paperclip/Stickers/Create Sticker.bru
Normal file
20
api/Paperclip/Stickers/Create Sticker.bru
Normal 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
|
||||||
|
}
|
||||||
|
}
|
26
api/Passport/Developer Notify All Users.bru
Normal file
26
api/Passport/Developer Notify All Users.bru
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
meta {
|
||||||
|
name: Developer Notify All Users
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/id/dev/notify/all
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"client_id": "{{third_client_id}}",
|
||||||
|
"client_secret":"{{third_client_tk}}",
|
||||||
|
"type": "general",
|
||||||
|
"subject": "Merry Christmas!",
|
||||||
|
"subtitle": "一条来自 Solar Network 团队的信息",
|
||||||
|
"content": "今天是 12 月 25 日 (UTC+8),小羊祝您圣诞快乐 🎄",
|
||||||
|
"metadata": {
|
||||||
|
"image": "6EqsYQwmFRCkbmhR"
|
||||||
|
},
|
||||||
|
"priority": 10
|
||||||
|
}
|
||||||
|
}
|
9
api/bruno.json
Normal file
9
api/bruno.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"name": "Solar Network",
|
||||||
|
"type": "collection",
|
||||||
|
"ignore": [
|
||||||
|
"node_modules",
|
||||||
|
".git"
|
||||||
|
]
|
||||||
|
}
|
7
api/collection.bru
Normal file
7
api/collection.bru
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
auth {
|
||||||
|
mode: bearer
|
||||||
|
}
|
||||||
|
|
||||||
|
auth:bearer {
|
||||||
|
token: {{atk}}
|
||||||
|
}
|
8
api/environments/Prod.bru
Normal file
8
api/environments/Prod.bru
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
vars {
|
||||||
|
endpoint: https://api.sn.solsynth.dev
|
||||||
|
third_client_id: alphabot
|
||||||
|
}
|
||||||
|
vars:secret [
|
||||||
|
atk,
|
||||||
|
third_client_tk
|
||||||
|
]
|
@ -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,16 +283,49 @@
|
|||||||
"one": "{} attachment",
|
"one": "{} attachment",
|
||||||
"other": "{} attachments"
|
"other": "{} attachments"
|
||||||
},
|
},
|
||||||
|
"messageTyping": {
|
||||||
|
"one": "{} is typing...",
|
||||||
|
"other": "{} are typing..."
|
||||||
|
},
|
||||||
|
"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",
|
||||||
"addAttachmentFromCameraVideo": "Take video",
|
"addAttachmentFromCameraVideo": "Take video",
|
||||||
|
"addAttachmentFromRandomId": "Link via RID",
|
||||||
"attachmentPastedImage": "Pasted Image",
|
"attachmentPastedImage": "Pasted Image",
|
||||||
"attachmentInsertLink": "Insert Link",
|
"attachmentInsertLink": "Insert Link",
|
||||||
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
|
"attachmentSetAsPostThumbnail": "Set as post thumbnail",
|
||||||
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
"attachmentUnsetAsPostThumbnail": "Unset as post thumbnail",
|
||||||
|
"attachmentCompressVideo": "Re-encode video",
|
||||||
"attachmentSetThumbnail": "Set thumbnail",
|
"attachmentSetThumbnail": "Set thumbnail",
|
||||||
|
"attachmentSetAlt": "Set alternative text",
|
||||||
|
"attachmentCopyRandomId": "Copy RID",
|
||||||
"attachmentUpload": "Upload",
|
"attachmentUpload": "Upload",
|
||||||
|
"attachmentInputDialog": "Upload attachments",
|
||||||
|
"attachmentInputUseRandomId": "Use Random ID",
|
||||||
|
"attachmentInputNew": "New Upload",
|
||||||
|
"waitingForUpload": "Waiting for upload",
|
||||||
|
"attachmentVideoCompressHint": "Compress a copy of this video",
|
||||||
|
"attachmentVideoCompressHintDescription": "Do you want to upload a compress copy of video {}? It will help your audience to preview this video faster and they still can watch the original video. It will take some while to process the video on your device, so please be patient.",
|
||||||
|
"attachmentCompressQuality": "Compress quality",
|
||||||
|
"attachmentCompressQualityHighest": "Highest",
|
||||||
|
"attachmentCompressQualityDefault": "Default",
|
||||||
|
"attachmentCompressQualityMedium": "Medium",
|
||||||
|
"attachmentCompressQualityLow": "Low",
|
||||||
|
"attachmentCompressQualityHint": "Solar Network doesn't prevent you from uploading large files, high resolution, high bitrate videos. But for your network conditions, we suggest you choose a suitable compression quality.",
|
||||||
|
"attachmentUploaded": "Uploaded",
|
||||||
|
"attachmentPending": "Pending",
|
||||||
|
"attachmentCopyCompressed": "Copy compressed",
|
||||||
|
"attachmentGotBoosted": "Boosted",
|
||||||
|
"attachmentBoost": "Boost",
|
||||||
|
"attachmentCreateBoost": "Create Boost",
|
||||||
|
"attachmentBoostHint": "Boost is a feature that allows you to upload attachments to a server closer to your audience or a faster content network. This feature is currently in beta and is subject to change. It's all free for now, you can feel free to try, you will get notified when the pricing plan changed.",
|
||||||
|
"attachmentDestinationRegion": "Destination Region",
|
||||||
|
"attachmentDestinationRegionAPAC": "Asia Pacific",
|
||||||
|
"attachmentDestinationRegionNGB": "Ning Bo, China, Zhejiang",
|
||||||
|
"attachmentDestinationRegionHKG": "Hong Kong",
|
||||||
"notification": "Notification",
|
"notification": "Notification",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "All notifications read",
|
"zero": "All notifications read",
|
||||||
@ -381,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, {}.",
|
||||||
@ -390,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 {}",
|
||||||
@ -431,6 +472,7 @@
|
|||||||
"accountJoinedAt": "Joined at {}",
|
"accountJoinedAt": "Joined at {}",
|
||||||
"accountBirthday": "Born on {}",
|
"accountBirthday": "Born on {}",
|
||||||
"accountBadge": "Badge",
|
"accountBadge": "Badge",
|
||||||
|
"accountCheckInNoRecords": "No check-in records",
|
||||||
"badgeCompanyStaff": "Solsynth Staff",
|
"badgeCompanyStaff": "Solsynth Staff",
|
||||||
"badgeSiteMigration": "Solar Network Native",
|
"badgeSiteMigration": "Solar Network Native",
|
||||||
"accountStatus": "Status",
|
"accountStatus": "Status",
|
||||||
@ -439,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",
|
||||||
@ -482,7 +525,7 @@
|
|||||||
"appInitializing": "Initializing",
|
"appInitializing": "Initializing",
|
||||||
"poweredBy": "Powered by {}",
|
"poweredBy": "Powered by {}",
|
||||||
"shareIntent": "Share",
|
"shareIntent": "Share",
|
||||||
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
||||||
"shareIntentPostStory": "Post a Story",
|
"shareIntentPostStory": "Post a Story",
|
||||||
"updateAvailable": "Update Available",
|
"updateAvailable": "Update Available",
|
||||||
"updateOngoing": "Updating, please wait...",
|
"updateOngoing": "Updating, please wait...",
|
||||||
|
@ -185,6 +185,8 @@
|
|||||||
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
|
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
|
||||||
"settingsAppBarTransparent": "透明顶栏",
|
"settingsAppBarTransparent": "透明顶栏",
|
||||||
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
|
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
|
||||||
|
"settingsDrawerPreferCollapse": "侧边栏偏好折叠",
|
||||||
|
"settingsDrawerPreferCollapseDescription": "将侧边栏优先折叠,即使屏幕宽度足够大去放下整个侧边栏。",
|
||||||
"settingsColorScheme": "主题色",
|
"settingsColorScheme": "主题色",
|
||||||
"settingsColorSchemeDescription": "设置应用主题色。",
|
"settingsColorSchemeDescription": "设置应用主题色。",
|
||||||
"settingsColorSeed": "预设色彩主题",
|
"settingsColorSeed": "预设色彩主题",
|
||||||
@ -279,16 +281,49 @@
|
|||||||
"one": "{} 个附件",
|
"one": "{} 个附件",
|
||||||
"other": "{} 个附件"
|
"other": "{} 个附件"
|
||||||
},
|
},
|
||||||
|
"messageTyping": {
|
||||||
|
"one": "{} 正在输入",
|
||||||
|
"other": "{} 正在输入"
|
||||||
|
},
|
||||||
|
"fieldAttachmentRandomId": "访问 ID",
|
||||||
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "从相册中添加附件",
|
"addAttachmentFromAlbum": "从相册中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘贴附件",
|
"addAttachmentFromClipboard": "粘贴附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍摄照片",
|
"addAttachmentFromCameraPhoto": "拍摄照片",
|
||||||
"addAttachmentFromCameraVideo": "拍摄视频",
|
"addAttachmentFromCameraVideo": "拍摄视频",
|
||||||
|
"addAttachmentFromRandomId": "通过访问 ID 链接",
|
||||||
"attachmentPastedImage": "粘贴的图片",
|
"attachmentPastedImage": "粘贴的图片",
|
||||||
"attachmentInsertLink": "插入连接",
|
"attachmentInsertLink": "插入连接",
|
||||||
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
||||||
|
"attachmentCompressVideo": "重新编码视频",
|
||||||
"attachmentSetThumbnail": "设置缩略图",
|
"attachmentSetThumbnail": "设置缩略图",
|
||||||
|
"attachmentSetAlt": "设置概述文字",
|
||||||
|
"attachmentCopyRandomId": "复制访问 ID",
|
||||||
"attachmentUpload": "上传",
|
"attachmentUpload": "上传",
|
||||||
|
"attachmentInputDialog": "上传附件",
|
||||||
|
"attachmentInputUseRandomId": "使用访问 ID",
|
||||||
|
"attachmentInputNew": "新上传附件",
|
||||||
|
"waitingForUpload": "等待上传",
|
||||||
|
"attachmentVideoCompressHint": "压缩一份视频的副本",
|
||||||
|
"attachmentVideoCompressHintDescription": "你想上传压缩视频 {} 的副本吗?它将帮助你的观众快速预览视频,并且他们仍然可以观看原始视频。这将会在在你的设备上处理视频,所以需要一些时间,所以请耐心等待。",
|
||||||
|
"attachmentCompressQuality": "压缩质量",
|
||||||
|
"attachmentCompressQualityHighest": "最高",
|
||||||
|
"attachmentCompressQualityDefault": "默认",
|
||||||
|
"attachmentCompressQualityMedium": "中等",
|
||||||
|
"attachmentCompressQualityLow": "低",
|
||||||
|
"attachmentCompressQualityHint": "Solar Network 并没有阻止你上传大文件、高分辨率、高码率的视频,但是为了你的网络情况观众考虑,我们建议你选择一个合适的压缩质量。",
|
||||||
|
"attachmentUploaded": "已上传",
|
||||||
|
"attachmentPending": "未上传",
|
||||||
|
"attachmentCopyCompressed": "有压缩副本",
|
||||||
|
"attachmentGotBoosted": "有加速传递",
|
||||||
|
"attachmentBoost": "加速包",
|
||||||
|
"attachmentCreateBoost": "加速传递",
|
||||||
|
"attachmentBoostHint": "加速传递允许您将附件上传到更近的受众或更快的内容网络。该功能目前处于 Beta 阶段。该功能限时免费,当有价格计划更改时,您将会被通知。",
|
||||||
|
"attachmentDestinationRegion": "目标节点",
|
||||||
|
"attachmentDestinationRegionAPAC": "亚太地区",
|
||||||
|
"attachmentDestinationRegionNGB": "中国 · 浙江 · 宁波",
|
||||||
|
"attachmentDestinationRegionHKG": "香港",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "无未读通知",
|
"zero": "无未读通知",
|
||||||
@ -377,6 +412,9 @@
|
|||||||
"dailyCheckNegativeHint6": "出门",
|
"dailyCheckNegativeHint6": "出门",
|
||||||
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
|
||||||
"celebrateBirthday": "生日快乐,{}!",
|
"celebrateBirthday": "生日快乐,{}!",
|
||||||
|
"celebrateLunarNewYear": "春节快乐,{}!",
|
||||||
|
"celebrateMidAutumn": "中秋节快乐,{}!",
|
||||||
|
"celebrateDragonBoat": "端午节快乐,{}!",
|
||||||
"celebrateMerryXmas": "圣诞快乐,{}!",
|
"celebrateMerryXmas": "圣诞快乐,{}!",
|
||||||
"celebrateNewYear": "新年快乐,{}!",
|
"celebrateNewYear": "新年快乐,{}!",
|
||||||
"celebrateValentineDay": "今天是情人节,{}!",
|
"celebrateValentineDay": "今天是情人节,{}!",
|
||||||
@ -386,6 +424,9 @@
|
|||||||
"celebrateFatherDay": "今天是父亲节,{}。",
|
"celebrateFatherDay": "今天是父亲节,{}。",
|
||||||
"celebrateHalloween": "快乐在圣诞节,{}!",
|
"celebrateHalloween": "快乐在圣诞节,{}!",
|
||||||
"celebrateThanksgiving": "今天是感恩节,{}!",
|
"celebrateThanksgiving": "今天是感恩节,{}!",
|
||||||
|
"pendingLunarNewYear": "{} 过春节",
|
||||||
|
"pendingMidAutumn": "{} 过中秋节",
|
||||||
|
"pendingDragonBoat": "{} 过端午节",
|
||||||
"pendingBirthday": "{} 过生日",
|
"pendingBirthday": "{} 过生日",
|
||||||
"pendingMerryXmas": "{} 过圣诞节",
|
"pendingMerryXmas": "{} 过圣诞节",
|
||||||
"pendingNewYear": "{} 跨年",
|
"pendingNewYear": "{} 跨年",
|
||||||
@ -429,6 +470,7 @@
|
|||||||
"accountJoinedAt": "加入于 {}",
|
"accountJoinedAt": "加入于 {}",
|
||||||
"accountBirthday": "出生于 {}",
|
"accountBirthday": "出生于 {}",
|
||||||
"accountBadge": "徽章",
|
"accountBadge": "徽章",
|
||||||
|
"accountCheckInNoRecords": "暂无运势记录",
|
||||||
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
|
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
|
||||||
"badgeSiteMigration": "Solar Network 原住民",
|
"badgeSiteMigration": "Solar Network 原住民",
|
||||||
"accountStatus": "状态",
|
"accountStatus": "状态",
|
||||||
@ -437,6 +479,7 @@
|
|||||||
"accountStatusLastSeen": "最后一次上线于 {}",
|
"accountStatusLastSeen": "最后一次上线于 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
|
"postLocalDraftRestored": "从本地恢复草稿",
|
||||||
"articleWrittenAt": "发表于 {}",
|
"articleWrittenAt": "发表于 {}",
|
||||||
"articleEditedAt": "编辑于 {}",
|
"articleEditedAt": "编辑于 {}",
|
||||||
"attachmentSaved": "已保存到相册",
|
"attachmentSaved": "已保存到相册",
|
||||||
|
@ -185,6 +185,8 @@
|
|||||||
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
||||||
"settingsAppBarTransparent": "透明頂欄",
|
"settingsAppBarTransparent": "透明頂欄",
|
||||||
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
|
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
|
||||||
|
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
|
||||||
|
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
|
||||||
"settingsColorScheme": "主題色",
|
"settingsColorScheme": "主題色",
|
||||||
"settingsColorSchemeDescription": "設置應用主題色。",
|
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||||
"settingsColorSeed": "預設色彩主題",
|
"settingsColorSeed": "預設色彩主題",
|
||||||
@ -279,16 +281,49 @@
|
|||||||
"one": "{} 個附件",
|
"one": "{} 個附件",
|
||||||
"other": "{} 個附件"
|
"other": "{} 個附件"
|
||||||
},
|
},
|
||||||
|
"messageTyping": {
|
||||||
|
"one": "{} 正在輸入",
|
||||||
|
"other": "{} 正在輸入"
|
||||||
|
},
|
||||||
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
|
"fieldAttachmentAlt": "概述文字",
|
||||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘貼附件",
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||||
|
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||||
"attachmentPastedImage": "粘貼的圖片",
|
"attachmentPastedImage": "粘貼的圖片",
|
||||||
"attachmentInsertLink": "插入連接",
|
"attachmentInsertLink": "插入連接",
|
||||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||||
|
"attachmentCompressVideo": "重新編碼視頻",
|
||||||
"attachmentSetThumbnail": "設置縮略圖",
|
"attachmentSetThumbnail": "設置縮略圖",
|
||||||
|
"attachmentSetAlt": "設置概述文字",
|
||||||
|
"attachmentCopyRandomId": "複製訪問 ID",
|
||||||
"attachmentUpload": "上傳",
|
"attachmentUpload": "上傳",
|
||||||
|
"attachmentInputDialog": "上傳附件",
|
||||||
|
"attachmentInputUseRandomId": "使用訪問 ID",
|
||||||
|
"attachmentInputNew": "新上傳附件",
|
||||||
|
"waitingForUpload": "等待上傳",
|
||||||
|
"attachmentVideoCompressHint": "壓縮一份視頻的副本",
|
||||||
|
"attachmentVideoCompressHintDescription": "你想上傳壓縮視頻 {} 的副本嗎?它將幫助你的觀眾快速預覽視頻,並且他們仍然可以觀看原始視頻。這將會在在你的設備上處理視頻,所以需要一些時間,所以請耐心等待。",
|
||||||
|
"attachmentCompressQuality": "壓縮質量",
|
||||||
|
"attachmentCompressQualityHighest": "最高",
|
||||||
|
"attachmentCompressQualityDefault": "默認",
|
||||||
|
"attachmentCompressQualityMedium": "中等",
|
||||||
|
"attachmentCompressQualityLow": "低",
|
||||||
|
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。",
|
||||||
|
"attachmentUploaded": "已上傳",
|
||||||
|
"attachmentPending": "未上傳",
|
||||||
|
"attachmentCopyCompressed": "有壓縮副本",
|
||||||
|
"attachmentGotBoosted": "有加速傳遞",
|
||||||
|
"attachmentBoost": "加速包",
|
||||||
|
"attachmentCreateBoost": "加速傳遞",
|
||||||
|
"attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。",
|
||||||
|
"attachmentDestinationRegion": "目標節點",
|
||||||
|
"attachmentDestinationRegionAPAC": "亞太地區",
|
||||||
|
"attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波",
|
||||||
|
"attachmentDestinationRegionHKG": "香港",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "無未讀通知",
|
"zero": "無未讀通知",
|
||||||
@ -377,6 +412,9 @@
|
|||||||
"dailyCheckNegativeHint6": "出門",
|
"dailyCheckNegativeHint6": "出門",
|
||||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||||
"celebrateBirthday": "生日快樂,{}!",
|
"celebrateBirthday": "生日快樂,{}!",
|
||||||
|
"celebrateLunarNewYear": "春節快樂,{}!",
|
||||||
|
"celebrateMidAutumn": "中秋節快樂,{}!",
|
||||||
|
"celebrateDragonBoat": "端午節快樂,{}!",
|
||||||
"celebrateMerryXmas": "聖誕快樂,{}!",
|
"celebrateMerryXmas": "聖誕快樂,{}!",
|
||||||
"celebrateNewYear": "新年快樂,{}!",
|
"celebrateNewYear": "新年快樂,{}!",
|
||||||
"celebrateValentineDay": "今天是情人節,{}!",
|
"celebrateValentineDay": "今天是情人節,{}!",
|
||||||
@ -386,6 +424,9 @@
|
|||||||
"celebrateFatherDay": "今天是父親節,{}。",
|
"celebrateFatherDay": "今天是父親節,{}。",
|
||||||
"celebrateHalloween": "快樂在聖誕節,{}!",
|
"celebrateHalloween": "快樂在聖誕節,{}!",
|
||||||
"celebrateThanksgiving": "今天是感恩節,{}!",
|
"celebrateThanksgiving": "今天是感恩節,{}!",
|
||||||
|
"pendingLunarNewYear": "{} 過春節",
|
||||||
|
"pendingMidAutumn": "{} 過中秋節",
|
||||||
|
"pendingDragonBoat": "{} 過端午節",
|
||||||
"pendingBirthday": "{} 過生日",
|
"pendingBirthday": "{} 過生日",
|
||||||
"pendingMerryXmas": "{} 過聖誕節",
|
"pendingMerryXmas": "{} 過聖誕節",
|
||||||
"pendingNewYear": "{} 跨年",
|
"pendingNewYear": "{} 跨年",
|
||||||
@ -429,6 +470,7 @@
|
|||||||
"accountJoinedAt": "加入於 {}",
|
"accountJoinedAt": "加入於 {}",
|
||||||
"accountBirthday": "出生於 {}",
|
"accountBirthday": "出生於 {}",
|
||||||
"accountBadge": "徽章",
|
"accountBadge": "徽章",
|
||||||
|
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||||
"badgeSiteMigration": "Solar Network 原住民",
|
"badgeSiteMigration": "Solar Network 原住民",
|
||||||
"accountStatus": "狀態",
|
"accountStatus": "狀態",
|
||||||
@ -437,6 +479,7 @@
|
|||||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
|
"postLocalDraftRestored": "從本地恢復草稿",
|
||||||
"articleWrittenAt": "發表於 {}",
|
"articleWrittenAt": "發表於 {}",
|
||||||
"articleEditedAt": "編輯於 {}",
|
"articleEditedAt": "編輯於 {}",
|
||||||
"attachmentSaved": "已保存到相冊",
|
"attachmentSaved": "已保存到相冊",
|
||||||
|
@ -7,15 +7,15 @@
|
|||||||
"screenAuthLogin": "登陸",
|
"screenAuthLogin": "登陸",
|
||||||
"screenAuthLoginSubtitle": "使用 Solarpass 登陸 Solar Network",
|
"screenAuthLoginSubtitle": "使用 Solarpass 登陸 Solar Network",
|
||||||
"screenAuthLoginGreeting": "歡迎回來",
|
"screenAuthLoginGreeting": "歡迎回來",
|
||||||
"screenAuthRegister": "建立賬號",
|
"screenAuthRegister": "創建賬號",
|
||||||
"screenAuthRegisterSubtitle": "建立一個 Solarpass 賬號",
|
"screenAuthRegisterSubtitle": "創建一個 Solarpass 賬號",
|
||||||
"screenAccountPublishers": "釋出者",
|
"screenAccountPublishers": "發佈者",
|
||||||
"screenAccountPublisherNew": "新建釋出者",
|
"screenAccountPublisherNew": "新建發佈者",
|
||||||
"screenAccountPublisherEdit": "編輯釋出者",
|
"screenAccountPublisherEdit": "編輯發佈者",
|
||||||
"screenAccountProfileEdit": "編輯資料",
|
"screenAccountProfileEdit": "編輯資料",
|
||||||
"screenAbuseReport": "濫用檢舉",
|
"screenAbuseReport": "濫用檢舉",
|
||||||
"screenSettings": "設定",
|
"screenSettings": "設置",
|
||||||
"screenAlbum": "相簿",
|
"screenAlbum": "相冊",
|
||||||
"screenChat": "聊天",
|
"screenChat": "聊天",
|
||||||
"screenChatManage": "編輯聊天頻道",
|
"screenChatManage": "編輯聊天頻道",
|
||||||
"screenChatNew": "新建聊天頻道",
|
"screenChatNew": "新建聊天頻道",
|
||||||
@ -23,37 +23,37 @@
|
|||||||
"screenRealmManage": "編輯領域",
|
"screenRealmManage": "編輯領域",
|
||||||
"screenRealmNew": "新建領域",
|
"screenRealmNew": "新建領域",
|
||||||
"screenNotification": "通知",
|
"screenNotification": "通知",
|
||||||
"screenPostSearch": "搜尋帖子",
|
"screenPostSearch": "搜索帖子",
|
||||||
"screenFriend": "好友",
|
"screenFriend": "好友",
|
||||||
"dialogOkay": "好的",
|
"dialogOkay": "好的",
|
||||||
"dialogCancel": "取消",
|
"dialogCancel": "取消",
|
||||||
"dialogConfirm": "確認",
|
"dialogConfirm": "確認",
|
||||||
"dialogDismiss": "忽略",
|
"dialogDismiss": "忽略",
|
||||||
"dialogError": "出了點問題",
|
"dialogError": "出了點問題",
|
||||||
"errorRequestBad": "伺服器拒絕了您的請求,請檢查您的輸入。",
|
"errorRequestBad": "服務器拒絕了您的請求,請檢查您的輸入。",
|
||||||
"errorRequestUnauthorized": "未授權的請求,請登入或者嘗試重新登陸。",
|
"errorRequestUnauthorized": "未授權的請求,請登錄或者嘗試重新登陸。",
|
||||||
"errorRequestForbidden": "被禁止的請求,您沒有足夠的許可權去做那件事。",
|
"errorRequestForbidden": "被禁止的請求,您沒有足夠的權限去做那件事。",
|
||||||
"errorRequestNotFound": "您正查詢的資源無法被找到。",
|
"errorRequestNotFound": "您正查找的資源無法被找到。",
|
||||||
"errorRequestConnection": "網路連線錯誤,請檢查您的網路狀態或者檢查我們的服務狀態。",
|
"errorRequestConnection": "網絡連接錯誤,請檢查您的網絡狀態或者檢查我們的服務狀態。",
|
||||||
"errorRequestUnknown": "未知請求錯誤,您可能想將此對話方塊截圖併發送給我們。",
|
"errorRequestUnknown": "未知請求錯誤,您可能想將此對話框截圖併發送給我們。",
|
||||||
"unknown": "未知",
|
"unknown": "未知",
|
||||||
"loading": "載入中…",
|
"loading": "加載中…",
|
||||||
"prev": "上一步",
|
"prev": "上一步",
|
||||||
"next": "下一步",
|
"next": "下一步",
|
||||||
"edit": "編輯",
|
"edit": "編輯",
|
||||||
"apply": "應用",
|
"apply": "應用",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"create": "建立",
|
"create": "創建",
|
||||||
"preview": "預覽",
|
"preview": "預覽",
|
||||||
"delete": "刪除",
|
"delete": "刪除",
|
||||||
"unlink": "解除連結",
|
"unlink": "解除鏈接",
|
||||||
"crop": "裁剪",
|
"crop": "裁剪",
|
||||||
"compress": "壓縮",
|
"compress": "壓縮",
|
||||||
"report": "檢舉",
|
"report": "檢舉",
|
||||||
"repost": "轉帖",
|
"repost": "轉帖",
|
||||||
"replyPost": "回貼",
|
"replyPost": "回貼",
|
||||||
"reply": "回覆",
|
"reply": "回覆",
|
||||||
"unset": "未設定",
|
"unset": "未設置",
|
||||||
"untitled": "無題",
|
"untitled": "無題",
|
||||||
"postDetail": "帖子詳情",
|
"postDetail": "帖子詳情",
|
||||||
"postNoun": "帖子",
|
"postNoun": "帖子",
|
||||||
@ -64,20 +64,20 @@
|
|||||||
"one": "總計 {} 字",
|
"one": "總計 {} 字",
|
||||||
"other": "總計 {} 字"
|
"other": "總計 {} 字"
|
||||||
},
|
},
|
||||||
"fieldUsername": "使用者名稱",
|
"fieldUsername": "用戶名",
|
||||||
"fieldNickname": "顯示名",
|
"fieldNickname": "顯示名",
|
||||||
"fieldEmail": "電子郵箱地址",
|
"fieldEmail": "電子郵箱地址",
|
||||||
"fieldPassword": "密碼",
|
"fieldPassword": "密碼",
|
||||||
"fieldUsernameAlphanumOnly": "使用者名稱只能包含英文大小寫字母和數字。",
|
"fieldUsernameAlphanumOnly": "用戶名只能包含英文大小寫字母和數字。",
|
||||||
"fieldUsernameLengthLimit": "使用者名稱必須在 {} 和 {} 之間。",
|
"fieldUsernameLengthLimit": "用戶名必須在 {} 和 {} 之間。",
|
||||||
"fieldUsernameCannotEditHint": "使用者名稱在建立後無法修改",
|
"fieldUsernameCannotEditHint": "用戶名在創建後無法修改",
|
||||||
"fieldUsernameLookupHint": "支援使用者名稱、電話號碼或郵箱地址",
|
"fieldUsernameLookupHint": "支持用戶名、電話號碼或郵箱地址",
|
||||||
"fieldNicknameLengthLimit": "暱稱必須在 {} 和 {} 之間。",
|
"fieldNicknameLengthLimit": "暱稱必須在 {} 和 {} 之間。",
|
||||||
"fieldEmailAddressMustBeValid": "電子郵箱地址必須是一個電子郵箱地址。",
|
"fieldEmailAddressMustBeValid": "電子郵箱地址必須是一個電子郵箱地址。",
|
||||||
"fieldFirstName": "名",
|
"fieldFirstName": "名",
|
||||||
"fieldLastName": "姓",
|
"fieldLastName": "姓",
|
||||||
"fieldBirthday": "生日",
|
"fieldBirthday": "生日",
|
||||||
"fieldImageHint": "你可以點選這些個人頭像來編輯它們。",
|
"fieldImageHint": "你可以點擊這些個人頭像來編輯它們。",
|
||||||
"fieldDescription": "簡介",
|
"fieldDescription": "簡介",
|
||||||
"forgotPassword": "忘記密碼",
|
"forgotPassword": "忘記密碼",
|
||||||
"loginPickFactor": "選擇方式驗證",
|
"loginPickFactor": "選擇方式驗證",
|
||||||
@ -85,24 +85,24 @@
|
|||||||
"one": "{} 步驗證",
|
"one": "{} 步驗證",
|
||||||
"other": "{} 步驗證"
|
"other": "{} 步驗證"
|
||||||
},
|
},
|
||||||
"loginEnterPassword": "驗證程式碼",
|
"loginEnterPassword": "驗證代碼",
|
||||||
"loginSuccess": "登入為 {}",
|
"loginSuccess": "登錄為 {}",
|
||||||
"authFactorPassword": "密碼",
|
"authFactorPassword": "密碼",
|
||||||
"authFactorEmail": "電郵一次性驗證碼",
|
"authFactorEmail": "電郵一次性驗證碼",
|
||||||
"accountIntroTitle": "喜歡您來!",
|
"accountIntroTitle": "喜歡您來!",
|
||||||
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
"accountIntroSubtitle": "登陸以探索更廣大的世界。",
|
||||||
"accountLogout": "退出登入",
|
"accountLogout": "退出登錄",
|
||||||
"accountLogoutSubtitle": "登出當前賬戶的登陸狀態。",
|
"accountLogoutSubtitle": "註銷當前賬戶的登陸狀態。",
|
||||||
"accountLogoutConfirmTitle": "您確定要退出登入嗎?",
|
"accountLogoutConfirmTitle": "您確定要退出登錄嗎?",
|
||||||
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
"accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。",
|
||||||
"accountPublishers": "你的釋出者",
|
"accountPublishers": "你的發佈者",
|
||||||
"accountPublishersSubtitle": "管理你的公共形象。",
|
"accountPublishersSubtitle": "管理你的公共形象。",
|
||||||
"accountProfileEdit": "編輯資料",
|
"accountProfileEdit": "編輯資料",
|
||||||
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
|
"accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。",
|
||||||
"accountProfileEditApplied": "個人資料修改已被應用。",
|
"accountProfileEditApplied": "個人資料修改已被應用。",
|
||||||
"publishersNew": "新發布者",
|
"publishersNew": "新發布者",
|
||||||
"publisherNewSubtitle": "建立一個新的公共身份。",
|
"publisherNewSubtitle": "創建一個新的公共身份。",
|
||||||
"publisherSyncWithAccount": "同步賬戶資訊",
|
"publisherSyncWithAccount": "同步賬戶信息",
|
||||||
"publisherTotalUpvote": "總頂數",
|
"publisherTotalUpvote": "總頂數",
|
||||||
"publisherTotalDownvote": "總踩數",
|
"publisherTotalDownvote": "總踩數",
|
||||||
"publisherSocialPoint": "社會信用點",
|
"publisherSocialPoint": "社會信用點",
|
||||||
@ -115,10 +115,10 @@
|
|||||||
"publisherAffiliatedBy": "隸屬於 {}",
|
"publisherAffiliatedBy": "隸屬於 {}",
|
||||||
"publisherRunBy": "由 {} 管理",
|
"publisherRunBy": "由 {} 管理",
|
||||||
"fieldPublisherBelongToRealm": "所屬領域",
|
"fieldPublisherBelongToRealm": "所屬領域",
|
||||||
"fieldPublisherBelongToRealmUnset": "未設定釋出者所屬領域",
|
"fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域",
|
||||||
"writePostTypeStory": "發動態",
|
"writePostTypeStory": "發動態",
|
||||||
"writePostTypeArticle": "寫文章",
|
"writePostTypeArticle": "寫文章",
|
||||||
"fieldPostPublisher": "帖子釋出者",
|
"fieldPostPublisher": "帖子發佈者",
|
||||||
"fieldPostContent": "發生什麼事了?!",
|
"fieldPostContent": "發生什麼事了?!",
|
||||||
"fieldPostTitle": "標題",
|
"fieldPostTitle": "標題",
|
||||||
"fieldPostDescription": "描述",
|
"fieldPostDescription": "描述",
|
||||||
@ -126,26 +126,26 @@
|
|||||||
"fieldPostCategories": "分類",
|
"fieldPostCategories": "分類",
|
||||||
"fieldPostAlias": "別名",
|
"fieldPostAlias": "別名",
|
||||||
"fieldPostAliasHint": "可選項,用於在 URL 中表示該帖子,應遵循 URL-Safe 的原則。",
|
"fieldPostAliasHint": "可選項,用於在 URL 中表示該帖子,應遵循 URL-Safe 的原則。",
|
||||||
"postPublish": "釋出",
|
"postPublish": "發佈",
|
||||||
"postPublishedAt": "釋出於",
|
"postPublishedAt": "發佈於",
|
||||||
"postPublishedUntil": "取消釋出於",
|
"postPublishedUntil": "取消發佈於",
|
||||||
"postVisibility": "可見性",
|
"postVisibility": "可見性",
|
||||||
"postVisibilityDescription": "帖子可見性決定了誰能檢視該篇帖子。",
|
"postVisibilityDescription": "帖子可見性決定了誰能查看該篇帖子。",
|
||||||
"postVisibilityAll": "所有人可見",
|
"postVisibilityAll": "所有人可見",
|
||||||
"postVisibilityFriends": "僅限好友可見",
|
"postVisibilityFriends": "僅限好友可見",
|
||||||
"postVisibilitySelected": "選定的使用者可見",
|
"postVisibilitySelected": "選定的用戶可見",
|
||||||
"postVisibilityFiltered": "選定使用者不可見",
|
"postVisibilityFiltered": "選定用戶不可見",
|
||||||
"postVisibilityNone": "僅自己可見",
|
"postVisibilityNone": "僅自己可見",
|
||||||
"postVisibleUsers": "可見的使用者",
|
"postVisibleUsers": "可見的用戶",
|
||||||
"postInvisibleUsers": "不可見的使用者",
|
"postInvisibleUsers": "不可見的用戶",
|
||||||
"postSelectedUsers": {
|
"postSelectedUsers": {
|
||||||
"zero": "未選擇使用者",
|
"zero": "未選擇用戶",
|
||||||
"one": "選擇了 {} 個使用者",
|
"one": "選擇了 {} 個用戶",
|
||||||
"other": "選擇了 {} 個使用者"
|
"other": "選擇了 {} 個用戶"
|
||||||
},
|
},
|
||||||
"postEditingNotice": "你正在修改由 {} 釋出的帖子。",
|
"postEditingNotice": "你正在修改由 {} 發佈的帖子。",
|
||||||
"postReplyingNotice": "你正在回覆由 {} 釋出的帖子。",
|
"postReplyingNotice": "你正在回覆由 {} 發佈的帖子。",
|
||||||
"postRepostingNotice": "你正在轉發由 {} 釋出的帖子。",
|
"postRepostingNotice": "你正在轉發由 {} 發佈的帖子。",
|
||||||
"postReact": "反應",
|
"postReact": "反應",
|
||||||
"postPosted": "帖子已經發表。",
|
"postPosted": "帖子已經發表。",
|
||||||
"postReactions": "帖子的反應",
|
"postReactions": "帖子的反應",
|
||||||
@ -164,7 +164,7 @@
|
|||||||
"one": "{} 點社會信用點變更",
|
"one": "{} 點社會信用點變更",
|
||||||
"other": "{} 點社會信用點變更"
|
"other": "{} 點社會信用點變更"
|
||||||
},
|
},
|
||||||
"postReactCompleted": "反應已被新增。",
|
"postReactCompleted": "反應已被添加。",
|
||||||
"postReactUncompleted": "反應已被移除。",
|
"postReactUncompleted": "反應已被移除。",
|
||||||
"postComments": {
|
"postComments": {
|
||||||
"zero": "評論",
|
"zero": "評論",
|
||||||
@ -178,76 +178,78 @@
|
|||||||
},
|
},
|
||||||
"settingsAppearance": "外觀",
|
"settingsAppearance": "外觀",
|
||||||
"settingsBackgroundImage": "背景圖片",
|
"settingsBackgroundImage": "背景圖片",
|
||||||
"settingsBackgroundImageDescription": "設定應用全域性生效的的背景圖片。",
|
"settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。",
|
||||||
"settingsBackgroundImageClear": "清除現存背景圖",
|
"settingsBackgroundImageClear": "清除現存背景圖",
|
||||||
"settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
|
"settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
|
||||||
"settingsThemeMaterial3": "使用 Material You 設計正規化",
|
"settingsThemeMaterial3": "使用 Material You 設計範式",
|
||||||
"settingsThemeMaterial3Description": "將應用主題設定為 Material 3 設計正規化的主題。",
|
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
||||||
"settingsAppBarTransparent": "透明頂欄",
|
"settingsAppBarTransparent": "透明頂欄",
|
||||||
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
|
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
|
||||||
|
"settingsDrawerPreferCollapse": "側邊欄偏好摺疊",
|
||||||
|
"settingsDrawerPreferCollapseDescription": "將側邊欄優先摺疊,即使屏幕寬度足夠大去放下整個側邊欄。",
|
||||||
"settingsColorScheme": "主題色",
|
"settingsColorScheme": "主題色",
|
||||||
"settingsColorSchemeDescription": "設定應用主題色。",
|
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||||
"settingsColorSeed": "預設色彩主題",
|
"settingsColorSeed": "預設色彩主題",
|
||||||
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
||||||
"settingsNetwork": "網路",
|
"settingsNetwork": "網絡",
|
||||||
"settingsNetworkServer": "HyperNet 伺服器",
|
"settingsNetworkServer": "HyperNet 服務器",
|
||||||
"settingsNetworkServerDescription": "設定 HyperNet 伺服器地址,選擇我們提供的,或者自己搭建。",
|
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||||
"settingsNetworkServerReset": "重設為官方伺服器",
|
"settingsNetworkServerReset": "重設為官方服務器",
|
||||||
"settingsNetworkServerResetDescription": "重設為 Solar Network 的伺服器地址。",
|
"settingsNetworkServerResetDescription": "重設為 Solar Network 的服務器地址。",
|
||||||
"settingsNetworkServerPreset": "預設的 HyperNet 伺服器",
|
"settingsNetworkServerPreset": "預設的 HyperNet 服務器",
|
||||||
"settingsNetworkServerPresetDescription": "你可以在旁邊的列表中選擇我們提供的預設 HyperNet 伺服器地址。",
|
"settingsNetworkServerPresetDescription": "你可以在旁邊的列表中選擇我們提供的預設 HyperNet 服務器地址。",
|
||||||
"settingsNetworkServerSaved": "伺服器地址已儲存。",
|
"settingsNetworkServerSaved": "服務器地址已保存。",
|
||||||
"settingsPerformance": "效能",
|
"settingsPerformance": "性能",
|
||||||
"settingsImageQuality": "圖片預覽質量",
|
"settingsImageQuality": "圖片預覽質量",
|
||||||
"settingsImageQualityDescription": "設定圖片預覽質量,會影響圖片解碼速度。",
|
"settingsImageQualityDescription": "設置圖片預覽質量,會影響圖片解碼速度。",
|
||||||
"settingsImageQualityLowest": "極低",
|
"settingsImageQualityLowest": "極低",
|
||||||
"settingsImageQualityLow": "低",
|
"settingsImageQualityLow": "低",
|
||||||
"settingsImageQualityMedium": "中",
|
"settingsImageQualityMedium": "中",
|
||||||
"settingsImageQualityHigh": "高",
|
"settingsImageQualityHigh": "高",
|
||||||
"settingsMisc": "雜項",
|
"settingsMisc": "雜項",
|
||||||
"settingsMiscAbout": "關於",
|
"settingsMiscAbout": "關於",
|
||||||
"settingsMiscAboutDescription": "檢視 Solian 的版本資訊。",
|
"settingsMiscAboutDescription": "查看 Solian 的版本信息。",
|
||||||
"sensitiveContent": "敏感內容",
|
"sensitiveContent": "敏感內容",
|
||||||
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
"sensitiveContentCollapsed": "敏感內容已摺疊。",
|
||||||
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人檢視。",
|
"sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。",
|
||||||
"sensitiveContentReveal": "顯示內容",
|
"sensitiveContentReveal": "顯示內容",
|
||||||
"serverConnecting": "正在連線伺服器…",
|
"serverConnecting": "正在連接服務器…",
|
||||||
"serverDisconnected": "已與伺服器斷開連線",
|
"serverDisconnected": "已與服務器斷開連接",
|
||||||
"fieldChatAlias": "頻道別名",
|
"fieldChatAlias": "頻道別名",
|
||||||
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
"fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。",
|
||||||
"fieldChatName": "名稱",
|
"fieldChatName": "名稱",
|
||||||
"fieldChatDescription": "描述",
|
"fieldChatDescription": "描述",
|
||||||
"fieldChatBelongToRealm": "所屬領域",
|
"fieldChatBelongToRealm": "所屬領域",
|
||||||
"fieldChatBelongToRealmUnset": "未設定頻道所屬領域",
|
"fieldChatBelongToRealmUnset": "未設置頻道所屬領域",
|
||||||
"channelEditingNotice": "您正在編輯頻道 {}",
|
"channelEditingNotice": "您正在編輯頻道 {}",
|
||||||
"channelDeleted": "聊天頻道 {} 已被刪除",
|
"channelDeleted": "聊天頻道 {} 已被刪除",
|
||||||
"channelDelete": "刪除聊天頻道 {}",
|
"channelDelete": "刪除聊天頻道 {}",
|
||||||
"channelDeleteDescription": "你確定要刪除這個聊天頻道嗎?該操作不可撤銷,其頻道內的所有訊息將被永久刪除。",
|
"channelDeleteDescription": "你確定要刪除這個聊天頻道嗎?該操作不可撤銷,其頻道內的所有消息將被永久刪除。",
|
||||||
"channelDetailPersonalRegion": "個人區域",
|
"channelDetailPersonalRegion": "個人區域",
|
||||||
"channelDetailMemberRegion": "成員管理",
|
"channelDetailMemberRegion": "成員管理",
|
||||||
"channelMemberManage": "管理成員",
|
"channelMemberManage": "管理成員",
|
||||||
"channelMemberManageDescription": "管理頻道內現有成員。",
|
"channelMemberManageDescription": "管理頻道內現有成員。",
|
||||||
"channelMemberAdd": "新增成員",
|
"channelMemberAdd": "添加成員",
|
||||||
"channelMemberAddDescription": "給當前頻道新增新成員。",
|
"channelMemberAddDescription": "給當前頻道添加新成員。",
|
||||||
"channelMemberAdded": "頻道成員已新增。",
|
"channelMemberAdded": "頻道成員已添加。",
|
||||||
"fieldMemberRelatedName": "成員名 / 賬戶 ID",
|
"fieldMemberRelatedName": "成員名 / 賬戶 ID",
|
||||||
"channelDetailAdminRegion": "管理區域",
|
"channelDetailAdminRegion": "管理區域",
|
||||||
"channelEditProfile": "更改頻道身份",
|
"channelEditProfile": "更改頻道身份",
|
||||||
"channelEdit": "編輯頻道",
|
"channelEdit": "編輯頻道",
|
||||||
"channelEditDescription": "更改頻道基本資訊,元資料等。",
|
"channelEditDescription": "更改頻道基本信息,元數據等。",
|
||||||
"channelProfileEdit": "編輯頻道身份",
|
"channelProfileEdit": "編輯頻道身份",
|
||||||
"channelActionDelete": "刪除頻道",
|
"channelActionDelete": "刪除頻道",
|
||||||
"channelActionDeleteDescription": "刪除整個頻道,並且刪除頻道里的所有資訊。",
|
"channelActionDeleteDescription": "刪除整個頻道,並且刪除頻道里的所有信息。",
|
||||||
"channelLeave": "退出頻道 {}",
|
"channelLeave": "退出頻道 {}",
|
||||||
"channelLeaveDescription": "退出該頻道,但是你頻道內的資訊不會被移除。",
|
"channelLeaveDescription": "退出該頻道,但是你頻道內的信息不會被移除。",
|
||||||
"channelActionLeave": "退出頻道",
|
"channelActionLeave": "退出頻道",
|
||||||
"channelActionLeaveDescription": "刪除你在這個頻道的身份。",
|
"channelActionLeaveDescription": "刪除你在這個頻道的身份。",
|
||||||
"channelNotifyLevel": "通知級別",
|
"channelNotifyLevel": "通知級別",
|
||||||
"channelNotifyLevelDescription": "有您決定要接受多少來自這個頻道的訊息。",
|
"channelNotifyLevelDescription": "有您決定要接受多少來自這個頻道的消息。",
|
||||||
"channelNotifyLevelAll": "全部通知",
|
"channelNotifyLevelAll": "全部通知",
|
||||||
"channelNotifyLevelMentioned": "僅提及",
|
"channelNotifyLevelMentioned": "僅提及",
|
||||||
"channelNotifyLevelNone": "全部靜音",
|
"channelNotifyLevelNone": "全部靜音",
|
||||||
"channelNotifyLevelApplied": "已經儲存並應用頻道通知級別配置。",
|
"channelNotifyLevelApplied": "已經保存並應用頻道通知級別配置。",
|
||||||
"fieldChannelProfileNick": "頻道內顯示名",
|
"fieldChannelProfileNick": "頻道內顯示名",
|
||||||
"fieldChannelProfileNickHint": "在頻道內顯示的暱稱,留空則使用賬號顯示名。",
|
"fieldChannelProfileNickHint": "在頻道內顯示的暱稱,留空則使用賬號顯示名。",
|
||||||
"fieldRealmAlias": "領域別名",
|
"fieldRealmAlias": "領域別名",
|
||||||
@ -257,38 +259,71 @@
|
|||||||
"realmEditingNotice": "您正在編輯領域 {}",
|
"realmEditingNotice": "您正在編輯領域 {}",
|
||||||
"realmDeleted": "領域 {} 已被刪除",
|
"realmDeleted": "領域 {} 已被刪除",
|
||||||
"realmDelete": "刪除領域 {}",
|
"realmDelete": "刪除領域 {}",
|
||||||
"realmDeleteDescription": "你確定要刪除這個領域嗎?該操作不可撤銷,其隸屬於該領域的所有資源(帖子、聊天頻道、釋出者、製品等)都將被永久刪除。三思而後行!",
|
"realmDeleteDescription": "你確定要刪除這個領域嗎?該操作不可撤銷,其隸屬於該領域的所有資源(帖子、聊天頻道、發佈者、製品等)都將被永久刪除。三思而後行!",
|
||||||
"realmActionDelete": "刪除領域",
|
"realmActionDelete": "刪除領域",
|
||||||
"realmActionDeleteDescription": "刪除整個領域及其附屬的資源。",
|
"realmActionDeleteDescription": "刪除整個領域及其附屬的資源。",
|
||||||
"realmEdit": "編輯領域",
|
"realmEdit": "編輯領域",
|
||||||
"realmEditDescription": "更改領域基本資訊,元資料等。",
|
"realmEditDescription": "更改領域基本信息,元數據等。",
|
||||||
"realmMemberAdd": "新增成員",
|
"realmMemberAdd": "添加成員",
|
||||||
"realmMemberAddDescription": "給當前領域新增新成員。",
|
"realmMemberAddDescription": "給當前領域添加新成員。",
|
||||||
"realmMemberAdded": "領域成員已新增。",
|
"realmMemberAdded": "領域成員已添加。",
|
||||||
"fieldChatMessage": "在 {} 中發訊息",
|
"fieldChatMessage": "在 {} 中發消息",
|
||||||
"fieldChatMessageDirect": "給 {} 發訊息",
|
"fieldChatMessageDirect": "給 {} 發消息",
|
||||||
"eventResourceTag": "訊息 {}",
|
"eventResourceTag": "消息 {}",
|
||||||
"messageDelete": "刪除訊息 {}",
|
"messageDelete": "刪除消息 {}",
|
||||||
"messageDeleteDescription": "你確定要刪除這個訊息嗎?該操作不可撤銷。同時您將留下一條刪除訊息的記錄。",
|
"messageDeleteDescription": "你確定要刪除這個消息嗎?該操作不可撤銷。同時您將留下一條刪除消息的記錄。",
|
||||||
"messageDeleted": "訊息 {} 已被刪除",
|
"messageDeleted": "消息 {} 已被刪除",
|
||||||
"messageEdited": "訊息 {} 已被編輯",
|
"messageEdited": "消息 {} 已被編輯",
|
||||||
"messageEditedHint": "已編輯",
|
"messageEditedHint": "已編輯",
|
||||||
"messageUnsupported": "不支援的訊息 {}",
|
"messageUnsupported": "不支持的消息 {}",
|
||||||
"messageFileHint": {
|
"messageFileHint": {
|
||||||
"zero": "沒有附件",
|
"zero": "沒有附件",
|
||||||
"one": "{} 個附件",
|
"one": "{} 個附件",
|
||||||
"other": "{} 個附件"
|
"other": "{} 個附件"
|
||||||
},
|
},
|
||||||
"addAttachmentFromAlbum": "從相簿中新增附件",
|
"messageTyping": {
|
||||||
"addAttachmentFromClipboard": "貼上附件",
|
"one": "{} 正在輸入",
|
||||||
|
"other": "{} 正在輸入"
|
||||||
|
},
|
||||||
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
|
"fieldAttachmentAlt": "概述文字",
|
||||||
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
"addAttachmentFromCameraVideo": "拍攝影片",
|
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||||
"attachmentPastedImage": "貼上的圖片",
|
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||||
"attachmentInsertLink": "插入連線",
|
"attachmentPastedImage": "粘貼的圖片",
|
||||||
"attachmentSetAsPostThumbnail": "設定為帖子縮圖",
|
"attachmentInsertLink": "插入連接",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消設定為帖子縮圖",
|
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||||
"attachmentSetThumbnail": "設定縮圖",
|
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||||
|
"attachmentCompressVideo": "重新編碼視頻",
|
||||||
|
"attachmentSetThumbnail": "設置縮略圖",
|
||||||
|
"attachmentSetAlt": "設置概述文字",
|
||||||
|
"attachmentCopyRandomId": "複製訪問 ID",
|
||||||
"attachmentUpload": "上傳",
|
"attachmentUpload": "上傳",
|
||||||
|
"attachmentInputDialog": "上傳附件",
|
||||||
|
"attachmentInputUseRandomId": "使用訪問 ID",
|
||||||
|
"attachmentInputNew": "新上傳附件",
|
||||||
|
"waitingForUpload": "等待上傳",
|
||||||
|
"attachmentVideoCompressHint": "壓縮一份視頻的副本",
|
||||||
|
"attachmentVideoCompressHintDescription": "你想上傳壓縮視頻 {} 的副本嗎?它將幫助你的觀眾快速預覽視頻,並且他們仍然可以觀看原始視頻。這將會在在你的設備上處理視頻,所以需要一些時間,所以請耐心等待。",
|
||||||
|
"attachmentCompressQuality": "壓縮質量",
|
||||||
|
"attachmentCompressQualityHighest": "最高",
|
||||||
|
"attachmentCompressQualityDefault": "默認",
|
||||||
|
"attachmentCompressQualityMedium": "中等",
|
||||||
|
"attachmentCompressQualityLow": "低",
|
||||||
|
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。",
|
||||||
|
"attachmentUploaded": "已上傳",
|
||||||
|
"attachmentPending": "未上傳",
|
||||||
|
"attachmentCopyCompressed": "有壓縮副本",
|
||||||
|
"attachmentGotBoosted": "有加速傳遞",
|
||||||
|
"attachmentBoost": "加速包",
|
||||||
|
"attachmentCreateBoost": "加速傳遞",
|
||||||
|
"attachmentBoostHint": "加速傳遞允許您將附件上傳到更近的受眾或更快的內容網絡。該功能目前處於 Beta 階段。該功能限時免費,當有價格計劃更改時,您將會被通知。",
|
||||||
|
"attachmentDestinationRegion": "目標節點",
|
||||||
|
"attachmentDestinationRegionAPAC": "亞太地區",
|
||||||
|
"attachmentDestinationRegionNGB": "中國 · 浙江 · 寧波",
|
||||||
|
"attachmentDestinationRegionHKG": "香港",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "無未讀通知",
|
"zero": "無未讀通知",
|
||||||
@ -298,18 +333,18 @@
|
|||||||
"notificationUnread": "未讀",
|
"notificationUnread": "未讀",
|
||||||
"notificationRead": "已讀",
|
"notificationRead": "已讀",
|
||||||
"notificationMarkAllRead": "已讀所有通知",
|
"notificationMarkAllRead": "已讀所有通知",
|
||||||
"notificationMarkAllReadDescription": "您確定要將所有通知設定為已讀嗎?該操作不可撤銷。",
|
"notificationMarkAllReadDescription": "您確定要將所有通知設置為已讀嗎?該操作不可撤銷。",
|
||||||
"notificationMarkAllReadPrompt": {
|
"notificationMarkAllReadPrompt": {
|
||||||
"zero": "已將 0 個通知標記為已讀。",
|
"zero": "已將 0 個通知標記為已讀。",
|
||||||
"one": "已將 {} 個通知標記為已讀。",
|
"one": "已將 {} 個通知標記為已讀。",
|
||||||
"other": "已將 {} 個通知標記為已讀。"
|
"other": "已將 {} 個通知標記為已讀。"
|
||||||
},
|
},
|
||||||
"notificationMarkOneReadPrompt": "已將通知 {} 標記為已讀。",
|
"notificationMarkOneReadPrompt": "已將通知 {} 標記為已讀。",
|
||||||
"search": "搜尋",
|
"search": "搜索",
|
||||||
"postSearchResult": {
|
"postSearchResult": {
|
||||||
"zero": "沒有搜尋到結果",
|
"zero": "沒有搜索到結果",
|
||||||
"one": "搜尋到 {} 個結果",
|
"one": "搜索到 {} 個結果",
|
||||||
"other": "搜尋到 {} 個結果"
|
"other": "搜索到 {} 個結果"
|
||||||
},
|
},
|
||||||
"postSearchTook": "耗時 {}",
|
"postSearchTook": "耗時 {}",
|
||||||
"postDelete": "刪除帖子 {}",
|
"postDelete": "刪除帖子 {}",
|
||||||
@ -321,26 +356,26 @@
|
|||||||
"callResume": "恢復",
|
"callResume": "恢復",
|
||||||
"callMicrophone": "麥克風",
|
"callMicrophone": "麥克風",
|
||||||
"callCamera": "攝像頭",
|
"callCamera": "攝像頭",
|
||||||
"callMicrophoneDisabled": "麥克風已停用",
|
"callMicrophoneDisabled": "麥克風已禁用",
|
||||||
"callMicrophoneSelect": "選擇麥克風",
|
"callMicrophoneSelect": "選擇麥克風",
|
||||||
"callCameraDisabled": "攝像頭已停用",
|
"callCameraDisabled": "攝像頭已禁用",
|
||||||
"callCameraSelect": "選擇攝像頭",
|
"callCameraSelect": "選擇攝像頭",
|
||||||
"callDisconnected": "通話已斷開",
|
"callDisconnected": "通話已斷開",
|
||||||
"callEnded": "通話已結束",
|
"callEnded": "通話已結束",
|
||||||
"callStatusConnected": "已連線",
|
"callStatusConnected": "已連接",
|
||||||
"callStatusDisconnected": "未連線",
|
"callStatusDisconnected": "未連接",
|
||||||
"callStatusConnecting": "正在連線",
|
"callStatusConnecting": "正在連接",
|
||||||
"callStatusReconnecting": "正在重連",
|
"callStatusReconnecting": "正在重連",
|
||||||
"callDisconnect": "斷開連線",
|
"callDisconnect": "斷開連接",
|
||||||
"callDisconnectDescription": "您確定要與通話斷開連線嗎?",
|
"callDisconnectDescription": "您確定要與通話斷開連接嗎?",
|
||||||
"callMicrophoneOff": "關閉麥克風",
|
"callMicrophoneOff": "關閉麥克風",
|
||||||
"callMicrophoneOn": "開啟麥克風",
|
"callMicrophoneOn": "打開麥克風",
|
||||||
"callCameraOff": "關閉攝像頭",
|
"callCameraOff": "關閉攝像頭",
|
||||||
"callCameraOn": "開啟攝像頭",
|
"callCameraOn": "打開攝像頭",
|
||||||
"callVideoFlip": "映象畫面",
|
"callVideoFlip": "鏡像畫面",
|
||||||
"callSpeakerphoneToggle": "切換揚聲器",
|
"callSpeakerphoneToggle": "切換揚聲器",
|
||||||
"callScreenOff": "關閉螢幕共享",
|
"callScreenOff": "關閉屏幕共享",
|
||||||
"callScreenOn": "開啟螢幕共享",
|
"callScreenOn": "開啟屏幕共享",
|
||||||
"callMessageEnded": "通話持續了 {}",
|
"callMessageEnded": "通話持續了 {}",
|
||||||
"callMessageStarted": "通話開始了",
|
"callMessageStarted": "通話開始了",
|
||||||
"dailyCheckIn": "每日簽到",
|
"dailyCheckIn": "每日簽到",
|
||||||
@ -377,6 +412,9 @@
|
|||||||
"dailyCheckNegativeHint6": "出門",
|
"dailyCheckNegativeHint6": "出門",
|
||||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||||
"celebrateBirthday": "生日快樂,{}!",
|
"celebrateBirthday": "生日快樂,{}!",
|
||||||
|
"celebrateLunarNewYear": "春節快樂,{}!",
|
||||||
|
"celebrateMidAutumn": "中秋節快樂,{}!",
|
||||||
|
"celebrateDragonBoat": "端午節快樂,{}!",
|
||||||
"celebrateMerryXmas": "聖誕快樂,{}!",
|
"celebrateMerryXmas": "聖誕快樂,{}!",
|
||||||
"celebrateNewYear": "新年快樂,{}!",
|
"celebrateNewYear": "新年快樂,{}!",
|
||||||
"celebrateValentineDay": "今天是情人節,{}!",
|
"celebrateValentineDay": "今天是情人節,{}!",
|
||||||
@ -386,6 +424,9 @@
|
|||||||
"celebrateFatherDay": "今天是父親節,{}。",
|
"celebrateFatherDay": "今天是父親節,{}。",
|
||||||
"celebrateHalloween": "快樂在聖誕節,{}!",
|
"celebrateHalloween": "快樂在聖誕節,{}!",
|
||||||
"celebrateThanksgiving": "今天是感恩節,{}!",
|
"celebrateThanksgiving": "今天是感恩節,{}!",
|
||||||
|
"pendingLunarNewYear": "{} 過春節",
|
||||||
|
"pendingMidAutumn": "{} 過中秋節",
|
||||||
|
"pendingDragonBoat": "{} 過端午節",
|
||||||
"pendingBirthday": "{} 過生日",
|
"pendingBirthday": "{} 過生日",
|
||||||
"pendingMerryXmas": "{} 過聖誕節",
|
"pendingMerryXmas": "{} 過聖誕節",
|
||||||
"pendingNewYear": "{} 跨年",
|
"pendingNewYear": "{} 跨年",
|
||||||
@ -396,27 +437,27 @@
|
|||||||
"pendingFatherDay": "{} 過父親節",
|
"pendingFatherDay": "{} 過父親節",
|
||||||
"pendingHalloween": "{} 過聖誕節",
|
"pendingHalloween": "{} 過聖誕節",
|
||||||
"pendingThanksgiving": "{} 過感恩節",
|
"pendingThanksgiving": "{} 過感恩節",
|
||||||
"friendNew": "新增好友",
|
"friendNew": "添加好友",
|
||||||
"friendRequests": "好友請求",
|
"friendRequests": "好友請求",
|
||||||
"friendRequestsDescription": {
|
"friendRequestsDescription": {
|
||||||
"zero": "你沒有好友請求",
|
"zero": "你沒有好友請求",
|
||||||
"one": "你有 {} 個好友請求",
|
"one": "你有 {} 個好友請求",
|
||||||
"other": "你有 {} 個好友請求"
|
"other": "你有 {} 個好友請求"
|
||||||
},
|
},
|
||||||
"friendBlocklist": "遮蔽列表",
|
"friendBlocklist": "屏蔽列表",
|
||||||
"friendBlocklistDescription": {
|
"friendBlocklistDescription": {
|
||||||
"zero": "你沒有遮蔽任何人",
|
"zero": "你沒有屏蔽任何人",
|
||||||
"one": "你遮蔽了 {} 個使用者",
|
"one": "你屏蔽了 {} 個用戶",
|
||||||
"other": "你遮蔽了 {} 個使用者"
|
"other": "你屏蔽了 {} 個用戶"
|
||||||
},
|
},
|
||||||
"friendStatusPending": "待處理",
|
"friendStatusPending": "待處理",
|
||||||
"friendStatusWaiting": "等待中",
|
"friendStatusWaiting": "等待中",
|
||||||
"friendStatusActive": "正活躍",
|
"friendStatusActive": "正活躍",
|
||||||
"friendStatusBlocked": "已遮蔽",
|
"friendStatusBlocked": "已屏蔽",
|
||||||
"friendRequestSent": "好友請求已傳送。",
|
"friendRequestSent": "好友請求已發送。",
|
||||||
"fieldFriendRelatedName": "好友名 / 賬戶 ID",
|
"fieldFriendRelatedName": "好友名 / 賬戶 ID",
|
||||||
"friendBlock": "遮蔽",
|
"friendBlock": "屏蔽",
|
||||||
"friendUnblock": "解除遮蔽",
|
"friendUnblock": "解除屏蔽",
|
||||||
"friendDeleteAction": "遺忘",
|
"friendDeleteAction": "遺忘",
|
||||||
"friendDelete": "遺忘跟 {} 的關係",
|
"friendDelete": "遺忘跟 {} 的關係",
|
||||||
"friendDeleteDescription": "你確定要遺忘跟 {} 的關係嗎?這個操作無法撤銷。",
|
"friendDeleteDescription": "你確定要遺忘跟 {} 的關係嗎?這個操作無法撤銷。",
|
||||||
@ -429,23 +470,25 @@
|
|||||||
"accountJoinedAt": "加入於 {}",
|
"accountJoinedAt": "加入於 {}",
|
||||||
"accountBirthday": "出生於 {}",
|
"accountBirthday": "出生於 {}",
|
||||||
"accountBadge": "徽章",
|
"accountBadge": "徽章",
|
||||||
|
"accountCheckInNoRecords": "暫無運勢記錄",
|
||||||
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
"badgeCompanyStaff": "索爾辛茨士大夫 · 員工",
|
||||||
"badgeSiteMigration": "Solar Network 原住民",
|
"badgeSiteMigration": "Solar Network 原住民",
|
||||||
"accountStatus": "狀態",
|
"accountStatus": "狀態",
|
||||||
"accountStatusOnline": "線上",
|
"accountStatusOnline": "在線",
|
||||||
"accountStatusOffline": "離線",
|
"accountStatusOffline": "離線",
|
||||||
"accountStatusLastSeen": "最後一次上線於 {}",
|
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
|
"postLocalDraftRestored": "從本地恢復草稿",
|
||||||
"articleWrittenAt": "發表於 {}",
|
"articleWrittenAt": "發表於 {}",
|
||||||
"articleEditedAt": "編輯於 {}",
|
"articleEditedAt": "編輯於 {}",
|
||||||
"attachmentSaved": "已儲存到相簿",
|
"attachmentSaved": "已保存到相冊",
|
||||||
"attachmentSavedDesktop": "已儲存到下載目錄",
|
"attachmentSavedDesktop": "已保存到下載目錄",
|
||||||
"openInAlbum": "在相簿中開啟",
|
"openInAlbum": "在相冊中打開",
|
||||||
"postAbuseReport": "檢舉帖子",
|
"postAbuseReport": "檢舉帖子",
|
||||||
"postAbuseReportDescription": "檢舉不符合我們使用者協議以及社群準則的帖子,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述該帖子如何違反我麼的相關規定。請勿填寫任何敏感資訊。我們將會在 24 小時內處理您的檢舉。",
|
"postAbuseReportDescription": "檢舉不符合我們用戶協議以及社區準則的帖子,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述該帖子如何違反我麼的相關規定。請勿填寫任何敏感信息。我們將會在 24 小時內處理您的檢舉。",
|
||||||
"abuseReport": "檢舉",
|
"abuseReport": "檢舉",
|
||||||
"abuseReportDescription": "檢舉不符合我們使用者協議以及社群準則的任何資源,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述資源的位置(提供資源 ID 為佳)以及如何違反我麼的相關規定。請勿填寫任何敏感資訊。我們將會在 24 小時內處理您的檢舉。",
|
"abuseReportDescription": "檢舉不符合我們用戶協議以及社區準則的任何資源,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述資源的位置(提供資源 ID 為佳)以及如何違反我麼的相關規定。請勿填寫任何敏感信息。我們將會在 24 小時內處理您的檢舉。",
|
||||||
"abuseReportAction": "提交檢舉",
|
"abuseReportAction": "提交檢舉",
|
||||||
"abuseReportActionDescription": "檢舉不合規行為。",
|
"abuseReportActionDescription": "檢舉不合規行為。",
|
||||||
"abuseReportResource": "資源位置 / ID",
|
"abuseReportResource": "資源位置 / ID",
|
||||||
@ -453,35 +496,35 @@
|
|||||||
"abuseReportSubmitted": "檢舉已提交,感謝你的貢獻。",
|
"abuseReportSubmitted": "檢舉已提交,感謝你的貢獻。",
|
||||||
"submit": "提交",
|
"submit": "提交",
|
||||||
"accountDeletion": "刪除帳戶",
|
"accountDeletion": "刪除帳戶",
|
||||||
"accountDeletionDescription": "你確定要刪除這個帳戶嗎?該操作不可撤銷,其隸屬於該帳戶的所有資源(帖子、聊天頻道、釋出者、製品等)都將被永久刪除。三思而後行!",
|
"accountDeletionDescription": "你確定要刪除這個帳戶嗎?該操作不可撤銷,其隸屬於該帳戶的所有資源(帖子、聊天頻道、發佈者、製品等)都將被永久刪除。三思而後行!",
|
||||||
"accountDeletionActionDescription": "刪除你的 Solarpass 帳戶。",
|
"accountDeletionActionDescription": "刪除你的 Solarpass 帳戶。",
|
||||||
"accountDeletionSubmitted": "帳戶刪除申請已發出,你可以檢查你的收件箱並根據郵件內的指示完成刪除操作。",
|
"accountDeletionSubmitted": "帳戶刪除申請已發出,你可以檢查你的收件箱並根據郵件內的指示完成刪除操作。",
|
||||||
"channelNewChannel": "新建頻道",
|
"channelNewChannel": "新建頻道",
|
||||||
"channelNewDirectMessage": "發起私信",
|
"channelNewDirectMessage": "發起私信",
|
||||||
"channelDirectMessageDescription": "與 {} 的私聊",
|
"channelDirectMessageDescription": "與 {} 的私聊",
|
||||||
"fieldCannotBeEmpty": "此欄位不能為空。",
|
"fieldCannotBeEmpty": "此字段不能為空。",
|
||||||
"termAcceptLink": "瀏覽條款",
|
"termAcceptLink": "瀏覽條款",
|
||||||
"termAcceptNextWithAgree": "點選 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
|
"termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。",
|
||||||
"unauthorized": "未登陸",
|
"unauthorized": "未登陸",
|
||||||
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
|
"unauthorizedDescription": "登陸以探索整個 Solar Network。",
|
||||||
"serviceStatus": "服務狀態",
|
"serviceStatus": "服務狀態",
|
||||||
"termRelated": "相關條款",
|
"termRelated": "相關條款",
|
||||||
"appDetails": "應用程式詳情",
|
"appDetails": "應用程序詳情",
|
||||||
"postRecommendation": "推薦帖子",
|
"postRecommendation": "推薦帖子",
|
||||||
"publisherBlockHint": "遮蔽 {}",
|
"publisherBlockHint": "屏蔽 {}",
|
||||||
"publisherBlockHintDescription": "你正要遮蔽此釋出者的運營者,該操作也將遮蔽由同一使用者運營的釋出者。",
|
"publisherBlockHintDescription": "你正要屏蔽此發佈者的運營者,該操作也將屏蔽由同一用戶運營的發佈者。",
|
||||||
"userUnblocked": "已解除遮蔽使用者 {}",
|
"userUnblocked": "已解除屏蔽用戶 {}",
|
||||||
"userBlocked": "已遮蔽使用者 {}",
|
"userBlocked": "已屏蔽用戶 {}",
|
||||||
"postSharingViaPicture": "正在生成帖子截圖,請稍等片刻……",
|
"postSharingViaPicture": "正在生成帖子截圖,請稍等片刻……",
|
||||||
"postImageShareReadMore": "掃描右側 QRCode 檢視全文",
|
"postImageShareReadMore": "掃描右側 QRCode 查看全文",
|
||||||
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
"postImageShareAds": "來 Solar Network 探索更多有趣帖子",
|
||||||
"postShare": "分享",
|
"postShare": "分享",
|
||||||
"postShareImage": "分享帖圖",
|
"postShareImage": "分享帖圖",
|
||||||
"appInitializing": "正在初始化",
|
"appInitializing": "正在初始化",
|
||||||
"poweredBy": "由 {} 提供支援",
|
"poweredBy": "由 {} 提供支持",
|
||||||
"shareIntent": "分享",
|
"shareIntent": "分享",
|
||||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||||
"shareIntentPostStory": "釋出動態",
|
"shareIntentPostStory": "發佈動態",
|
||||||
"updateAvailable": "檢測到更新可用",
|
"updateAvailable": "檢測到更新可用",
|
||||||
"updateOngoing": "正在更新,請稍後……",
|
"updateOngoing": "正在更新,請稍後……",
|
||||||
"custom": "自定義",
|
"custom": "自定義",
|
||||||
|
@ -173,7 +173,7 @@ PODS:
|
|||||||
- in_app_review (2.0.0):
|
- in_app_review (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- Kingfisher (8.1.3)
|
- Kingfisher (8.1.3)
|
||||||
- livekit_client (2.3.2):
|
- livekit_client (2.3.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- WebRTC-SDK (= 125.6422.06)
|
||||||
@ -217,6 +217,8 @@ PODS:
|
|||||||
- SwiftyGif (5.4.5)
|
- SwiftyGif (5.4.5)
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- video_compress (0.3.0):
|
||||||
|
- Flutter
|
||||||
- volume_controller (0.0.1):
|
- volume_controller (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- wakelock_plus (0.0.1):
|
- wakelock_plus (0.0.1):
|
||||||
@ -259,6 +261,7 @@ DEPENDENCIES:
|
|||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
|
- video_compress (from `.symlinks/plugins/video_compress/ios`)
|
||||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||||
- workmanager (from `.symlinks/plugins/workmanager/ios`)
|
- workmanager (from `.symlinks/plugins/workmanager/ios`)
|
||||||
@ -348,6 +351,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
|
video_compress:
|
||||||
|
:path: ".symlinks/plugins/video_compress/ios"
|
||||||
volume_controller:
|
volume_controller:
|
||||||
:path: ".symlinks/plugins/volume_controller/ios"
|
:path: ".symlinks/plugins/volume_controller/ios"
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
@ -375,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
|
||||||
@ -386,7 +391,7 @@ SPEC CHECKSUMS:
|
|||||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||||
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
in_app_review: a31b5257259646ea78e0e35fc914979b0031d011
|
||||||
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
|
Kingfisher: f2af9028b16baf9dc6c07c570072bc41cbf009ef
|
||||||
livekit_client: 6108dad8b77db3142bafd4c630f471d0a54335cd
|
livekit_client: 4eaa7a2968fc7e7c57888f43d90394547cc8d9e9
|
||||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||||
@ -405,6 +410,7 @@ SPEC CHECKSUMS:
|
|||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||||
|
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe
|
||||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||||
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
|
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
|
||||||
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
|
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
|
||||||
|
@ -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;
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package: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';
|
||||||
@ -14,17 +19,11 @@ import 'package:surface/types/attachment.dart';
|
|||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/universal_image.dart';
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
import 'package:video_compress/video_compress.dart';
|
||||||
enum PostWriteMediaType {
|
|
||||||
image,
|
|
||||||
video,
|
|
||||||
audio,
|
|
||||||
file,
|
|
||||||
}
|
|
||||||
|
|
||||||
class PostWriteMedia {
|
class PostWriteMedia {
|
||||||
late String name;
|
late String name;
|
||||||
late PostWriteMediaType type;
|
late SnMediaType type;
|
||||||
final SnAttachment? attachment;
|
final SnAttachment? attachment;
|
||||||
final XFile? file;
|
final XFile? file;
|
||||||
final Uint8List? raw;
|
final Uint8List? raw;
|
||||||
@ -36,16 +35,16 @@ class PostWriteMedia {
|
|||||||
|
|
||||||
switch (attachment?.mimetype.split('/').firstOrNull) {
|
switch (attachment?.mimetype.split('/').firstOrNull) {
|
||||||
case 'image':
|
case 'image':
|
||||||
type = PostWriteMediaType.image;
|
type = SnMediaType.image;
|
||||||
break;
|
break;
|
||||||
case 'video':
|
case 'video':
|
||||||
type = PostWriteMediaType.video;
|
type = SnMediaType.video;
|
||||||
break;
|
break;
|
||||||
case 'audio':
|
case 'audio':
|
||||||
type = PostWriteMediaType.audio;
|
type = SnMediaType.audio;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
type = PostWriteMediaType.file;
|
type = SnMediaType.file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,16 +56,16 @@ class PostWriteMedia {
|
|||||||
|
|
||||||
switch (mimetype?.split('/').firstOrNull) {
|
switch (mimetype?.split('/').firstOrNull) {
|
||||||
case 'image':
|
case 'image':
|
||||||
type = PostWriteMediaType.image;
|
type = SnMediaType.image;
|
||||||
break;
|
break;
|
||||||
case 'video':
|
case 'video':
|
||||||
type = PostWriteMediaType.video;
|
type = SnMediaType.video;
|
||||||
break;
|
break;
|
||||||
case 'audio':
|
case 'audio':
|
||||||
type = PostWriteMediaType.audio;
|
type = SnMediaType.audio;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
type = PostWriteMediaType.file;
|
type = SnMediaType.file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,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,
|
||||||
@ -155,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;
|
||||||
@ -204,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)) {
|
||||||
@ -236,7 +248,8 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media) async {
|
Future<SnAttachment> _uploadAttachment(BuildContext context, PostWriteMedia media,
|
||||||
|
{bool isCompressed = false}) async {
|
||||||
final attach = context.read<SnAttachmentProvider>();
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
|
||||||
final place = await attach.chunkedUploadInitialize(
|
final place = await attach.chunkedUploadInitialize(
|
||||||
@ -244,22 +257,141 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
media.name,
|
media.name,
|
||||||
'interactive',
|
'interactive',
|
||||||
null,
|
null,
|
||||||
mimetype: media.raw != null && media.type == PostWriteMediaType.image ? 'image/png' : null,
|
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
final item = await attach.chunkedUploadParts(
|
var item = await attach.chunkedUploadParts(
|
||||||
media.toFile()!,
|
media.toFile()!,
|
||||||
place.$1,
|
place.$1,
|
||||||
place.$2,
|
place.$2,
|
||||||
onProgress: (progress) {
|
analyzeNow: media.type == SnMediaType.image,
|
||||||
progress = progress;
|
onProgress: (value) {
|
||||||
|
progress = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (media.type == SnMediaType.video && !isCompressed && context.mounted) {
|
||||||
|
try {
|
||||||
|
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||||
|
if (compressedAttachment != null) {
|
||||||
|
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<SnAttachment?> _tryCompressVideoCopy(BuildContext context, PostWriteMedia media) async {
|
||||||
|
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) return null;
|
||||||
|
if (media.type != SnMediaType.video) return null;
|
||||||
|
if (media.file == null) return null;
|
||||||
|
if (VideoCompress.isCompressing) return null;
|
||||||
|
|
||||||
|
final confirm = await context.showConfirmDialog(
|
||||||
|
'attachmentVideoCompressHint'.tr(),
|
||||||
|
'attachmentVideoCompressHintDescription'.tr(args: [media.file!.name]),
|
||||||
|
);
|
||||||
|
if (!confirm) return null;
|
||||||
|
|
||||||
|
progress = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final mediaInfo = await VideoCompress.compressVideo(
|
||||||
|
media.file!.path,
|
||||||
|
quality: VideoQuality.LowQuality,
|
||||||
|
frameRate: 30,
|
||||||
|
deleteOrigin: false,
|
||||||
|
);
|
||||||
|
if (mediaInfo == null) return null;
|
||||||
|
if (!context.mounted) return null;
|
||||||
|
|
||||||
|
final compressedMedia = PostWriteMedia.fromFile(XFile(mediaInfo.path!));
|
||||||
|
final compressedAttachment = await _uploadAttachment(context, compressedMedia, isCompressed: true);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
@ -301,20 +433,31 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
media.name,
|
media.name,
|
||||||
'interactive',
|
'interactive',
|
||||||
null,
|
null,
|
||||||
mimetype: media.raw != null && media.type == PostWriteMediaType.image ? 'image/png' : null,
|
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
final item = await attach.chunkedUploadParts(
|
var item = await attach.chunkedUploadParts(
|
||||||
media.toFile()!,
|
media.toFile()!,
|
||||||
place.$1,
|
place.$1,
|
||||||
place.$2,
|
place.$2,
|
||||||
onProgress: (progress) {
|
onProgress: (value) {
|
||||||
// Calculate overall progress for attachments
|
// Calculate overall progress for attachments
|
||||||
progress = math.max(((i + progress) / attachments.length) * kAttachmentProgressWeight, progress);
|
progress = math.max(((i + value) / attachments.length) * kAttachmentProgressWeight, value);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (context.mounted) {
|
||||||
|
final compressedAttachment = await _tryCompressVideoCopy(context, media);
|
||||||
|
if (compressedAttachment != null) {
|
||||||
|
item = await attach.updateOne(item, compressedId: compressedAttachment.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
|
||||||
progress = (i + 1) / attachments.length * kAttachmentProgressWeight;
|
progress = (i + 1) / attachments.length * kAttachmentProgressWeight;
|
||||||
attachments[i] = PostWriteMedia(item);
|
attachments[i] = PostWriteMedia(item);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@ -368,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);
|
||||||
@ -416,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ class SnAttachmentProvider {
|
|||||||
|
|
||||||
void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) {
|
void putCache(Iterable<SnAttachment> items, {bool noCheck = false}) {
|
||||||
for (final item in items) {
|
for (final item in items) {
|
||||||
if ((item.isAnalyzed && item.isUploaded) || noCheck) {
|
if (item.isAnalyzed || noCheck) {
|
||||||
_cache[item.rid] = item;
|
_cache[item.rid] = item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,7 +34,7 @@ class SnAttachmentProvider {
|
|||||||
|
|
||||||
final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
|
final resp = await _sn.client.get('/cgi/uc/attachments/$rid/meta');
|
||||||
final out = SnAttachment.fromJson(resp.data);
|
final out = SnAttachment.fromJson(resp.data);
|
||||||
if (out.isAnalyzed && out.isUploaded) {
|
if (out.isAnalyzed) {
|
||||||
_cache[rid] = out;
|
_cache[rid] = out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,11 +62,12 @@ class SnAttachmentProvider {
|
|||||||
'id': pendingFetch.join(','),
|
'id': pendingFetch.join(','),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
final out = resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).toList();
|
final List<SnAttachment?> out =
|
||||||
|
resp.data['data'].map((e) => e['id'] == 0 ? null : SnAttachment.fromJson(e)).cast<SnAttachment?>().toList();
|
||||||
|
|
||||||
for (final item in out) {
|
for (final item in out) {
|
||||||
if (item == null) continue;
|
if (item == null) continue;
|
||||||
if (item.isAnalyzed && item.isUploaded) {
|
if (item.isAnalyzed) {
|
||||||
_cache[item.rid] = item;
|
_cache[item.rid] = item;
|
||||||
}
|
}
|
||||||
result[randomMapping[item.rid]!] = item;
|
result[randomMapping[item.rid]!] = item;
|
||||||
@ -85,6 +86,7 @@ class SnAttachmentProvider {
|
|||||||
Map<String, dynamic>? metadata, {
|
Map<String, dynamic>? metadata, {
|
||||||
String? mimetype,
|
String? mimetype,
|
||||||
Function(double progress)? onProgress,
|
Function(double progress)? onProgress,
|
||||||
|
bool analyzeNow = false,
|
||||||
}) async {
|
}) async {
|
||||||
final filePayload = MultipartFile.fromBytes(data, filename: filename);
|
final filePayload = MultipartFile.fromBytes(data, filename: filename);
|
||||||
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
final fileAlt = filename.contains('.') ? filename.substring(0, filename.lastIndexOf('.')) : filename;
|
||||||
@ -107,6 +109,7 @@ class SnAttachmentProvider {
|
|||||||
final resp = await _sn.client.post(
|
final resp = await _sn.client.post(
|
||||||
'/cgi/uc/attachments',
|
'/cgi/uc/attachments',
|
||||||
data: formData,
|
data: formData,
|
||||||
|
queryParameters: {'analyzeNow': analyzeNow},
|
||||||
onSendProgress: (count, total) {
|
onSendProgress: (count, total) {
|
||||||
if (onProgress != null) {
|
if (onProgress != null) {
|
||||||
onProgress(count / total);
|
onProgress(count / total);
|
||||||
@ -117,7 +120,7 @@ class SnAttachmentProvider {
|
|||||||
return SnAttachment.fromJson(resp.data);
|
return SnAttachment.fromJson(resp.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<(SnAttachment, int)> chunkedUploadInitialize(
|
Future<(SnAttachmentFragment, int)> chunkedUploadInitialize(
|
||||||
int size,
|
int size,
|
||||||
String filename,
|
String filename,
|
||||||
String pool,
|
String pool,
|
||||||
@ -134,7 +137,7 @@ class SnAttachmentProvider {
|
|||||||
mimetypeOverride = mimetype;
|
mimetypeOverride = mimetype;
|
||||||
}
|
}
|
||||||
|
|
||||||
final resp = await _sn.client.post('/cgi/uc/attachments/multipart', data: {
|
final resp = await _sn.client.post('/cgi/uc/fragments', data: {
|
||||||
'alt': fileAlt,
|
'alt': fileAlt,
|
||||||
'name': filename,
|
'name': filename,
|
||||||
'pool': pool,
|
'pool': pool,
|
||||||
@ -143,18 +146,20 @@ class SnAttachmentProvider {
|
|||||||
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
|
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (SnAttachment.fromJson(resp.data['meta']), resp.data['chunk_size'] as int);
|
return (SnAttachmentFragment.fromJson(resp.data['meta']), resp.data['chunk_size'] as int);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SnAttachment> _chunkedUploadOnePart(
|
Future<dynamic> _chunkedUploadOnePart(
|
||||||
Uint8List data,
|
Uint8List data,
|
||||||
String rid,
|
String rid,
|
||||||
String cid, {
|
String cid, {
|
||||||
Function(double progress)? onProgress,
|
Function(double progress)? onProgress,
|
||||||
|
bool analyzeNow = false,
|
||||||
}) async {
|
}) async {
|
||||||
final resp = await _sn.client.post(
|
final resp = await _sn.client.post(
|
||||||
'/cgi/uc/attachments/multipart/$rid/$cid',
|
'/cgi/uc/fragments/$rid/$cid',
|
||||||
data: data,
|
data: data,
|
||||||
|
queryParameters: {'analyzeNow': analyzeNow},
|
||||||
options: Options(headers: {'Content-Type': 'application/octet-stream'}),
|
options: Options(headers: {'Content-Type': 'application/octet-stream'}),
|
||||||
onSendProgress: (count, total) {
|
onSendProgress: (count, total) {
|
||||||
if (onProgress != null) {
|
if (onProgress != null) {
|
||||||
@ -163,21 +168,28 @@ class SnAttachmentProvider {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return SnAttachment.fromJson(resp.data);
|
if (resp.data['attachment'] != null) {
|
||||||
|
return SnAttachment.fromJson(resp.data['attachment']);
|
||||||
|
} else {
|
||||||
|
return SnAttachmentFragment.fromJson(resp.data['fragment']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SnAttachment> chunkedUploadParts(
|
Future<SnAttachment> chunkedUploadParts(
|
||||||
XFile file,
|
XFile file,
|
||||||
SnAttachment place,
|
SnAttachmentFragment place,
|
||||||
int chunkSize, {
|
int chunkSize, {
|
||||||
Function(double progress)? onProgress,
|
Function(double progress)? onProgress,
|
||||||
|
bool analyzeNow = false,
|
||||||
}) async {
|
}) async {
|
||||||
final Map<String, dynamic> chunks = place.fileChunks ?? {};
|
final Map<String, dynamic> chunks = place.fileChunks;
|
||||||
var currentTask = 0;
|
var completedTasks = 0;
|
||||||
|
|
||||||
final queue = Queue<Future<void>>();
|
final queue = Queue<Future<void>>();
|
||||||
final activeTasks = <Future<void>>[];
|
final activeTasks = <Future<void>>[];
|
||||||
|
|
||||||
|
late SnAttachment out;
|
||||||
|
|
||||||
for (final entry in chunks.entries) {
|
for (final entry in chunks.entries) {
|
||||||
queue.add(() async {
|
queue.add(() async {
|
||||||
final beginCursor = entry.value * chunkSize;
|
final beginCursor = entry.value * chunkSize;
|
||||||
@ -187,16 +199,26 @@ class SnAttachmentProvider {
|
|||||||
);
|
);
|
||||||
final data = Uint8List.fromList(await file.openRead(beginCursor, endCursor).expand((chunk) => chunk).toList());
|
final data = Uint8List.fromList(await file.openRead(beginCursor, endCursor).expand((chunk) => chunk).toList());
|
||||||
|
|
||||||
place = await _chunkedUploadOnePart(
|
final result = await _chunkedUploadOnePart(
|
||||||
data,
|
data,
|
||||||
place.rid,
|
place.rid,
|
||||||
entry.key,
|
entry.key,
|
||||||
|
analyzeNow: analyzeNow,
|
||||||
|
onProgress: (progress) {
|
||||||
|
final overallProgress = (completedTasks + progress) / chunks.length;
|
||||||
|
onProgress?.call(overallProgress);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final overallProgress = currentTask / chunks.length;
|
completedTasks++;
|
||||||
|
final overallProgress = completedTasks / chunks.length;
|
||||||
onProgress?.call(overallProgress);
|
onProgress?.call(overallProgress);
|
||||||
|
|
||||||
currentTask++;
|
if (result is SnAttachmentFragment) {
|
||||||
|
place = result;
|
||||||
|
} else {
|
||||||
|
out = result as SnAttachment;
|
||||||
|
}
|
||||||
}());
|
}());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,6 +235,24 @@ class SnAttachmentProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return place;
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SnAttachment> updateOne(
|
||||||
|
SnAttachment item, {
|
||||||
|
String? alt,
|
||||||
|
int? thumbnailId,
|
||||||
|
int? compressedId,
|
||||||
|
Map<String, dynamic>? metadata,
|
||||||
|
bool? isIndexable,
|
||||||
|
}) async {
|
||||||
|
final resp = await _sn.client.put('/cgi/uc/attachments/${item.id}', data: {
|
||||||
|
'alt': alt ?? item.alt,
|
||||||
|
'thumbnail': thumbnailId ?? item.thumbnailId,
|
||||||
|
'compressed': compressedId ?? item.compressedId,
|
||||||
|
'metadata': metadata ?? item.usermeta,
|
||||||
|
'is_indexable': isIndexable ?? item.isIndexable,
|
||||||
|
});
|
||||||
|
return SnAttachment.fromJson(resp.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
38
lib/providers/sn_sticker.dart
Normal file
38
lib/providers/sn_sticker.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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': '🏋️',
|
||||||
@ -129,8 +135,50 @@ class SpecialDayProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
double getSpecialDayProgress(DateTime last, DateTime next) {
|
double getSpecialDayProgress(DateTime last, DateTime next) {
|
||||||
final totalDuration = next.difference(last).inSeconds.toDouble();
|
final totalDuration = next.add(-const Duration(days: 1)).difference(last).inSeconds.toDouble();
|
||||||
final elapsedDuration = DateTime.now().difference(last).inSeconds.toDouble();
|
final elapsedDuration = DateTime.now().difference(last).inSeconds.toDouble();
|
||||||
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));
|
||||||
|
}
|
||||||
|
@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.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/widget.dart';
|
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
|
|
||||||
class UserProvider extends ChangeNotifier {
|
class UserProvider extends ChangeNotifier {
|
||||||
@ -13,12 +12,10 @@ class UserProvider extends ChangeNotifier {
|
|||||||
SnAccount? user;
|
SnAccount? user;
|
||||||
|
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
late final HomeWidgetProvider _home;
|
|
||||||
late final ConfigProvider _config;
|
late final ConfigProvider _config;
|
||||||
|
|
||||||
UserProvider(BuildContext context) {
|
UserProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
_home = context.read<HomeWidgetProvider>();
|
|
||||||
_config = context.read<ConfigProvider>();
|
_config = context.read<ConfigProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,9 +28,10 @@ class UserProvider extends ChangeNotifier {
|
|||||||
final value = _config.prefs.getString(kAtkStoreKey);
|
final value = _config.prefs.getString(kAtkStoreKey);
|
||||||
isAuthorized = value != null;
|
isAuthorized = value != null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
refreshUser().then((value) {
|
refreshUser().then((value) async {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
log('Logged in as @${value.name}');
|
log('Logged in as @${value.name}');
|
||||||
|
log('Atk: ${await atk}');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -517,6 +517,12 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
future: _getCheckInRecords(),
|
future: _getCheckInRecords(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData) return const SizedBox.shrink();
|
if (!snapshot.hasData) return const SizedBox.shrink();
|
||||||
|
if (snapshot.data!.length <= 1) {
|
||||||
|
return Text(
|
||||||
|
'accountCheckInNoRecords',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).tr().fontWeight(FontWeight.bold).center().padding(horizontal: 20, vertical: 8);
|
||||||
|
}
|
||||||
final records = snapshot.data!;
|
final records = snapshot.data!;
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
@ -22,15 +22,14 @@ import 'package:surface/widgets/universal_image.dart';
|
|||||||
|
|
||||||
class AccountPublisherEditScreen extends StatefulWidget {
|
class AccountPublisherEditScreen extends StatefulWidget {
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
const AccountPublisherEditScreen({super.key, required this.name});
|
const AccountPublisherEditScreen({super.key, required this.name});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AccountPublisherEditScreen> createState() =>
|
State<AccountPublisherEditScreen> createState() => _AccountPublisherEditScreenState();
|
||||||
_AccountPublisherEditScreenState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AccountPublisherEditScreenState
|
class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen> {
|
||||||
extends State<AccountPublisherEditScreen> {
|
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
|
|
||||||
SnPublisher? _publisher;
|
SnPublisher? _publisher;
|
||||||
@ -54,7 +53,7 @@ class _AccountPublisherEditScreenState
|
|||||||
_publisher = SnPublisher.fromJson(resp.data);
|
_publisher = SnPublisher.fromJson(resp.data);
|
||||||
_syncWidget();
|
_syncWidget();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
context.showErrorDialog(err);
|
if (mounted) context.showErrorDialog(err);
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
}
|
}
|
||||||
@ -75,9 +74,9 @@ class _AccountPublisherEditScreenState
|
|||||||
'name': _nameController.text,
|
'name': _nameController.text,
|
||||||
'description': _descriptionController.text,
|
'description': _descriptionController.text,
|
||||||
});
|
});
|
||||||
Navigator.pop(context, true);
|
if (mounted) Navigator.pop(context, true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
context.showErrorDialog(err);
|
if(mounted) context.showErrorDialog(err);
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
}
|
}
|
||||||
@ -108,11 +107,9 @@ class _AccountPublisherEditScreenState
|
|||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
final ImageProvider imageProvider =
|
final ImageProvider imageProvider = kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
||||||
kIsWeb ? NetworkImage(image.path) : FileImage(File(image.path));
|
final aspectRatios =
|
||||||
final aspectRatios = place == 'banner'
|
place == 'banner' ? [CropAspectRatio(width: 16, height: 7)] : [CropAspectRatio(width: 1, height: 1)];
|
||||||
? [CropAspectRatio(width: 16, height: 7)]
|
|
||||||
: [CropAspectRatio(width: 1, height: 1)];
|
|
||||||
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
final result = (!kIsWeb && (Platform.isIOS || Platform.isMacOS))
|
||||||
? await showCupertinoImageCropper(
|
? await showCupertinoImageCropper(
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
@ -134,10 +131,7 @@ class _AccountPublisherEditScreenState
|
|||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
final rawBytes =
|
final rawBytes = (await result.uiImage.toByteData(format: ImageByteFormat.png))!.buffer.asUint8List();
|
||||||
(await result.uiImage.toByteData(format: ImageByteFormat.png))!
|
|
||||||
.buffer
|
|
||||||
.asUint8List();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final attachment = await attach.directUploadOne(
|
final attachment = await attach.directUploadOne(
|
||||||
@ -199,9 +193,7 @@ class _AccountPublisherEditScreenState
|
|||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: Container(
|
child: Container(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
.colorScheme
|
|
||||||
.surfaceContainerHigh,
|
|
||||||
child: _banner != null
|
child: _banner != null
|
||||||
? AutoResizeUniversalImage(
|
? AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(_banner!),
|
sn.getAttachmentUrl(_banner!),
|
||||||
@ -240,8 +232,7 @@ class _AccountPublisherEditScreenState
|
|||||||
labelText: 'fieldUsername'.tr(),
|
labelText: 'fieldUsername'.tr(),
|
||||||
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
helperText: 'fieldUsernameCannotEditHint'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) =>
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
TextField(
|
TextField(
|
||||||
@ -249,8 +240,7 @@ class _AccountPublisherEditScreenState
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'fieldNickname'.tr(),
|
labelText: 'fieldNickname'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) =>
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
TextField(
|
TextField(
|
||||||
@ -260,8 +250,7 @@ class _AccountPublisherEditScreenState
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'fieldDescription'.tr(),
|
labelText: 'fieldDescription'.tr(),
|
||||||
),
|
),
|
||||||
onTapOutside: (_) =>
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
|
||||||
),
|
),
|
||||||
const Gap(12),
|
const Gap(12),
|
||||||
Row(
|
Row(
|
||||||
|
@ -201,7 +201,7 @@ class _PublisherNewPersonalState extends State<_PublisherNewPersonal> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PublisherNewOrganization extends StatefulWidget {
|
class _PublisherNewOrganization extends StatefulWidget {
|
||||||
const _PublisherNewOrganization({super.key});
|
const _PublisherNewOrganization();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_PublisherNewOrganization> createState() =>
|
State<_PublisherNewOrganization> createState() =>
|
||||||
|
@ -105,6 +105,7 @@ class _LoginCheckScreen extends StatefulWidget {
|
|||||||
final SnAuthFactor? factor;
|
final SnAuthFactor? factor;
|
||||||
final Function(SnAuthTicket?) onTicket;
|
final Function(SnAuthTicket?) onTicket;
|
||||||
final Function onNext;
|
final Function onNext;
|
||||||
|
|
||||||
const _LoginCheckScreen({
|
const _LoginCheckScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.ticket,
|
required this.ticket,
|
||||||
@ -204,9 +205,7 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
|
|||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
autofillHints: [
|
autofillHints: [
|
||||||
(_factorLabelMap[widget.factor!.type]?.$3 ?? true)
|
(_factorLabelMap[widget.factor!.type]?.$3 ?? true) ? AutofillHints.password : AutofillHints.oneTimeCode
|
||||||
? AutofillHints.password
|
|
||||||
: AutofillHints.oneTimeCode
|
|
||||||
],
|
],
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
@ -243,6 +242,7 @@ class _LoginPickerScreen extends StatefulWidget {
|
|||||||
final Function(SnAuthTicket?) onTicket;
|
final Function(SnAuthTicket?) onTicket;
|
||||||
final Function(SnAuthFactor) onPickFactor;
|
final Function(SnAuthFactor) onPickFactor;
|
||||||
final Function onNext;
|
final Function onNext;
|
||||||
|
|
||||||
const _LoginPickerScreen({
|
const _LoginPickerScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.ticket,
|
required this.ticket,
|
||||||
@ -260,8 +260,7 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
|
|||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
int? _factorPicked;
|
int? _factorPicked;
|
||||||
|
|
||||||
Color get _unFocusColor =>
|
Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
||||||
Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
|
|
||||||
|
|
||||||
void _performGetFactorCode() async {
|
void _performGetFactorCode() async {
|
||||||
if (_factorPicked == null) return;
|
if (_factorPicked == null) return;
|
||||||
@ -373,6 +372,7 @@ class _LoginLookupScreen extends StatefulWidget {
|
|||||||
final Function(SnAuthTicket?) onTicket;
|
final Function(SnAuthTicket?) onTicket;
|
||||||
final Function(List<SnAuthFactor>?) onFactor;
|
final Function(List<SnAuthFactor>?) onFactor;
|
||||||
final Function onNext;
|
final Function onNext;
|
||||||
|
|
||||||
const _LoginLookupScreen({
|
const _LoginLookupScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.ticket,
|
required this.ticket,
|
||||||
@ -401,14 +401,13 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final lookupResp =
|
final lookupResp = await sn.client.get('/cgi/id/users/lookup?probe=$username');
|
||||||
await sn.client.get('/cgi/id/users/lookup?probe=$username');
|
|
||||||
await sn.client.post('/cgi/id/users/me/password-reset', data: {
|
await sn.client.post('/cgi/id/users/me/password-reset', data: {
|
||||||
'user_id': lookupResp.data['id'],
|
'user_id': lookupResp.data['id'],
|
||||||
});
|
});
|
||||||
context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
if (mounted) context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
context.showErrorDialog(err);
|
if (mounted) context.showErrorDialog(err);
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
}
|
}
|
||||||
@ -431,8 +430,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
|||||||
widget.onTicket(result.ticket);
|
widget.onTicket(result.ticket);
|
||||||
|
|
||||||
// Pull factors
|
// Pull factors
|
||||||
final factorResp =
|
final factorResp = await sn.client.get('/cgi/id/auth/factors', queryParameters: {
|
||||||
await sn.client.get('/cgi/id/auth/factors', queryParameters: {
|
|
||||||
'ticketId': result.ticket!.id.toString(),
|
'ticketId': result.ticket!.id.toString(),
|
||||||
});
|
});
|
||||||
widget.onFactor(
|
widget.onFactor(
|
||||||
@ -443,7 +441,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
|||||||
|
|
||||||
widget.onNext();
|
widget.onNext();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
context.showErrorDialog(err);
|
if(mounted) context.showErrorDialog(err);
|
||||||
return;
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
@ -526,10 +524,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
|
|||||||
'termAcceptNextWithAgree'.tr(),
|
'termAcceptNextWithAgree'.tr(),
|
||||||
textAlign: TextAlign.end,
|
textAlign: TextAlign.end,
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
|
||||||
.colorScheme
|
|
||||||
.onSurface
|
|
||||||
.withAlpha((255 * 0.75).round()),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Material(
|
Material(
|
||||||
|
@ -443,7 +443,7 @@ class _ChannelProfileDetailDialogState
|
|||||||
|
|
||||||
class _ChannelMemberListWidget extends StatefulWidget {
|
class _ChannelMemberListWidget extends StatefulWidget {
|
||||||
final SnChannel channel;
|
final SnChannel channel;
|
||||||
const _ChannelMemberListWidget({super.key, required this.channel});
|
const _ChannelMemberListWidget({required this.channel});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_ChannelMemberListWidget> createState() =>
|
State<_ChannelMemberListWidget> createState() =>
|
||||||
@ -580,7 +580,7 @@ class _ChannelMemberListWidgetState extends State<_ChannelMemberListWidget> {
|
|||||||
|
|
||||||
class _NewChannelMemberWidget extends StatefulWidget {
|
class _NewChannelMemberWidget extends StatefulWidget {
|
||||||
final SnChannel channel;
|
final SnChannel channel;
|
||||||
const _NewChannelMemberWidget({super.key, required this.channel});
|
const _NewChannelMemberWidget({required this.channel});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_NewChannelMemberWidget> createState() =>
|
State<_NewChannelMemberWidget> createState() =>
|
||||||
|
@ -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(
|
||||||
|
@ -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';
|
||||||
@ -97,7 +98,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
print((err as DioException).response?.data);
|
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isCalling = false);
|
setState(() => _isCalling = false);
|
||||||
@ -281,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,
|
||||||
@ -311,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);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -336,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),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -289,7 +289,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _NewFriendWidget extends StatefulWidget {
|
class _NewFriendWidget extends StatefulWidget {
|
||||||
const _NewFriendWidget({super.key});
|
const _NewFriendWidget();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_NewFriendWidget> createState() => _NewFriendWidgetState();
|
State<_NewFriendWidget> createState() => _NewFriendWidgetState();
|
||||||
@ -365,7 +365,7 @@ class _NewFriendWidgetState extends State<_NewFriendWidget> {
|
|||||||
|
|
||||||
class _FriendshipListWidget extends StatefulWidget {
|
class _FriendshipListWidget extends StatefulWidget {
|
||||||
final List<SnRelationship> relations;
|
final List<SnRelationship> relations;
|
||||||
const _FriendshipListWidget({super.key, required this.relations});
|
const _FriendshipListWidget({required this.relations});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_FriendshipListWidget> createState() => _FriendshipListWidgetState();
|
State<_FriendshipListWidget> createState() => _FriendshipListWidgetState();
|
||||||
|
@ -12,6 +12,7 @@ import 'package:google_fonts/google_fonts.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
|
import 'package:slide_countdown/slide_countdown.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
@ -109,7 +110,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
class _HomeDashUpdateWidget extends StatelessWidget {
|
class _HomeDashUpdateWidget extends StatelessWidget {
|
||||||
final EdgeInsets? padding;
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
const _HomeDashUpdateWidget({super.key, this.padding});
|
const _HomeDashUpdateWidget({this.padding});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -152,9 +153,14 @@ class _HomeDashUpdateWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeDashSpecialDayWidget extends StatelessWidget {
|
class _HomeDashSpecialDayWidget extends StatefulWidget {
|
||||||
const _HomeDashSpecialDayWidget({super.key});
|
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>();
|
||||||
@ -164,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();
|
||||||
@ -188,16 +193,26 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
var (name, date) = nextOne;
|
var (name, date) = nextOne;
|
||||||
date = date.add(Duration(days: 1));
|
date = date.add(Duration(days: 1));
|
||||||
final progress = dayz.getSpecialDayProgress(lastOne.$2, date);
|
final progress = dayz.getSpecialDayProgress(lastOne.$2, date);
|
||||||
final diff = nextOne.$2.add(-const Duration(days: 1)).difference(lastOne.$2);
|
final diff = nextOne.$2.difference(DateTime.now());
|
||||||
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: [
|
||||||
Text('${diff.inDays}d · ${(progress * 100).toStringAsFixed(2)}%'),
|
SlideCountdown(
|
||||||
const Gap(8),
|
duration: diff,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
separatorStyle: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
separatorType: SeparatorType.symbol,
|
||||||
|
decoration: BoxDecoration(),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onDone: () {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: progress,
|
value: progress,
|
||||||
@ -215,7 +230,7 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeDashCheckInWidget extends StatefulWidget {
|
class _HomeDashCheckInWidget extends StatefulWidget {
|
||||||
const _HomeDashCheckInWidget({super.key});
|
const _HomeDashCheckInWidget();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_HomeDashCheckInWidget> createState() => _HomeDashCheckInWidgetState();
|
State<_HomeDashCheckInWidget> createState() => _HomeDashCheckInWidgetState();
|
||||||
@ -433,7 +448,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeDashNotificationWidget extends StatefulWidget {
|
class _HomeDashNotificationWidget extends StatefulWidget {
|
||||||
const _HomeDashNotificationWidget({super.key});
|
const _HomeDashNotificationWidget();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_HomeDashNotificationWidget> createState() => _HomeDashNotificationWidgetState();
|
State<_HomeDashNotificationWidget> createState() => _HomeDashNotificationWidgetState();
|
||||||
@ -504,7 +519,7 @@ class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeDashRecommendationPostWidget extends StatefulWidget {
|
class _HomeDashRecommendationPostWidget extends StatefulWidget {
|
||||||
const _HomeDashRecommendationPostWidget({super.key});
|
const _HomeDashRecommendationPostWidget();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_HomeDashRecommendationPostWidget> createState() => _HomeDashRecommendationPostWidgetState();
|
State<_HomeDashRecommendationPostWidget> createState() => _HomeDashRecommendationPostWidgetState();
|
||||||
|
@ -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',
|
||||||
|
@ -96,6 +96,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
)
|
)
|
||||||
: Text('postDetail').tr(),
|
: Text('postDetail').tr(),
|
||||||
),
|
),
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:pasteboard/pasteboard.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
@ -59,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;
|
||||||
|
|
||||||
@ -96,38 +93,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final _imagePicker = ImagePicker();
|
|
||||||
|
|
||||||
void _takeMedia(bool isVideo) async {
|
|
||||||
final result = isVideo
|
|
||||||
? await _imagePicker.pickVideo(source: ImageSource.camera)
|
|
||||||
: await _imagePicker.pickImage(source: ImageSource.camera);
|
|
||||||
if (result == null) return;
|
|
||||||
_writeController.addAttachments([
|
|
||||||
PostWriteMedia.fromFile(result),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _selectMedia() async {
|
|
||||||
final result = await _imagePicker.pickMultipleMedia();
|
|
||||||
if (result.isEmpty) return;
|
|
||||||
_writeController.addAttachments(
|
|
||||||
result.map((e) => PostWriteMedia.fromFile(e)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _pasteMedia() async {
|
|
||||||
final imageBytes = await Pasteboard.image;
|
|
||||||
if (imageBytes == null) return;
|
|
||||||
_writeController.addAttachments([
|
|
||||||
PostWriteMedia.fromBytes(
|
|
||||||
imageBytes,
|
|
||||||
'attachmentPastedImage'.tr(),
|
|
||||||
PostWriteMediaType.image,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_writeController.dispose();
|
_writeController.dispose();
|
||||||
@ -292,18 +257,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
if (_writeController.replyingPost != null)
|
if (_writeController.replyingPost != null)
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Theme(
|
ExpansionTile(
|
||||||
data: Theme.of(context).copyWith(
|
minTileHeight: 48,
|
||||||
dividerColor: Colors.transparent,
|
leading: const Icon(Symbols.reply).padding(left: 4),
|
||||||
),
|
title: Text('postReplyingNotice')
|
||||||
child: ExpansionTile(
|
.fontSize(15)
|
||||||
minTileHeight: 48,
|
.tr(args: ['@${_writeController.replyingPost!.publisher.name}']),
|
||||||
leading: const Icon(Symbols.reply).padding(left: 4),
|
children: <Widget>[PostItem(data: _writeController.replyingPost!)],
|
||||||
title: Text('postReplyingNotice')
|
|
||||||
.fontSize(15)
|
|
||||||
.tr(args: ['@${_writeController.replyingPost!.publisher.name}']),
|
|
||||||
children: <Widget>[PostItem(data: _writeController.replyingPost!)],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
],
|
],
|
||||||
@ -312,22 +272,17 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
if (_writeController.repostingPost != null)
|
if (_writeController.repostingPost != null)
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Theme(
|
ExpansionTile(
|
||||||
data: Theme.of(context).copyWith(
|
minTileHeight: 48,
|
||||||
dividerColor: Colors.transparent,
|
leading: const Icon(Symbols.forward).padding(left: 4),
|
||||||
),
|
title: Text('postRepostingNotice')
|
||||||
child: ExpansionTile(
|
.fontSize(15)
|
||||||
minTileHeight: 48,
|
.tr(args: ['@${_writeController.repostingPost!.publisher.name}']),
|
||||||
leading: const Icon(Symbols.forward).padding(left: 4),
|
children: <Widget>[
|
||||||
title: Text('postRepostingNotice')
|
PostItem(
|
||||||
.fontSize(15)
|
data: _writeController.repostingPost!,
|
||||||
.tr(args: ['@${_writeController.repostingPost!.publisher.name}']),
|
)
|
||||||
children: <Widget>[
|
],
|
||||||
PostItem(
|
|
||||||
data: _writeController.repostingPost!,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
],
|
],
|
||||||
@ -336,18 +291,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
if (_writeController.editingPost != null)
|
if (_writeController.editingPost != null)
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Theme(
|
ExpansionTile(
|
||||||
data: Theme.of(context).copyWith(
|
minTileHeight: 48,
|
||||||
dividerColor: Colors.transparent,
|
leading: const Icon(Symbols.edit_note).padding(left: 4),
|
||||||
),
|
title: Text('postEditingNotice')
|
||||||
child: ExpansionTile(
|
.fontSize(15)
|
||||||
minTileHeight: 48,
|
.tr(args: ['@${_writeController.editingPost!.publisher.name}']),
|
||||||
leading: const Icon(Symbols.edit_note).padding(left: 4),
|
children: <Widget>[PostItem(data: _writeController.editingPost!)],
|
||||||
title: Text('postEditingNotice')
|
|
||||||
.fontSize(15)
|
|
||||||
.tr(args: ['@${_writeController.editingPost!.publisher.name}']),
|
|
||||||
children: <Widget>[PostItem(data: _writeController.editingPost!)],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
],
|
],
|
||||||
@ -416,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>(
|
||||||
@ -435,63 +415,12 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
PopupMenuButton(
|
AddPostMediaButton(
|
||||||
icon: Icon(
|
onAdd: (items) {
|
||||||
Symbols.add_photo_alternate,
|
setState(() {
|
||||||
color: Theme.of(context).colorScheme.primary,
|
_writeController.addAttachments(items);
|
||||||
),
|
});
|
||||||
itemBuilder: (context) => [
|
},
|
||||||
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.photo_camera),
|
|
||||||
const Gap(16),
|
|
||||||
Text('addAttachmentFromCameraPhoto').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_takeMedia(false);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.videocam),
|
|
||||||
const Gap(16),
|
|
||||||
Text('addAttachmentFromCameraVideo').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_takeMedia(true);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.photo_library),
|
|
||||||
const Gap(16),
|
|
||||||
Text('addAttachmentFromAlbum').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_selectMedia();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.content_paste),
|
|
||||||
const Gap(16),
|
|
||||||
Text('addAttachmentFromClipboard').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_pasteMedia();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -580,7 +580,6 @@ class _PublisherPostList extends StatelessWidget {
|
|||||||
final void Function() onDeleted;
|
final void Function() onDeleted;
|
||||||
|
|
||||||
const _PublisherPostList({
|
const _PublisherPostList({
|
||||||
super.key,
|
|
||||||
required this.isBusy,
|
required this.isBusy,
|
||||||
required this.postCount,
|
required this.postCount,
|
||||||
required this.posts,
|
required this.posts,
|
||||||
|
@ -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)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -119,7 +119,7 @@ class _RealmDetailHomeWidget extends StatelessWidget {
|
|||||||
final SnRealm? realm;
|
final SnRealm? realm;
|
||||||
final List<SnPublisher>? publishers;
|
final List<SnPublisher>? publishers;
|
||||||
|
|
||||||
const _RealmDetailHomeWidget({super.key, required this.realm, this.publishers});
|
const _RealmDetailHomeWidget({required this.realm, this.publishers});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -175,7 +175,7 @@ class _RealmDetailHomeWidget extends StatelessWidget {
|
|||||||
class _RealmMemberListWidget extends StatefulWidget {
|
class _RealmMemberListWidget extends StatefulWidget {
|
||||||
final SnRealm? realm;
|
final SnRealm? realm;
|
||||||
|
|
||||||
const _RealmMemberListWidget({super.key, this.realm});
|
const _RealmMemberListWidget({this.realm});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_RealmMemberListWidget> createState() => _RealmMemberListWidgetState();
|
State<_RealmMemberListWidget> createState() => _RealmMemberListWidgetState();
|
||||||
@ -304,7 +304,7 @@ class _RealmMemberListWidgetState extends State<_RealmMemberListWidget> {
|
|||||||
class _NewRealmMemberWidget extends StatefulWidget {
|
class _NewRealmMemberWidget extends StatefulWidget {
|
||||||
final SnRealm realm;
|
final SnRealm realm;
|
||||||
|
|
||||||
const _NewRealmMemberWidget({super.key, required this.realm});
|
const _NewRealmMemberWidget({required this.realm});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_NewRealmMemberWidget> createState() => _NewRealmMemberWidgetState();
|
State<_NewRealmMemberWidget> createState() => _NewRealmMemberWidgetState();
|
||||||
@ -384,7 +384,7 @@ class _RealmSettingsWidget extends StatefulWidget {
|
|||||||
final SnRealm? realm;
|
final SnRealm? realm;
|
||||||
final Function() onUpdate;
|
final Function() onUpdate;
|
||||||
|
|
||||||
const _RealmSettingsWidget({super.key, required this.realm, required this.onUpdate});
|
const _RealmSettingsWidget({required this.realm, required this.onUpdate});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_RealmSettingsWidget> createState() => _RealmSettingsWidgetState();
|
State<_RealmSettingsWidget> createState() => _RealmSettingsWidgetState();
|
||||||
|
@ -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(
|
||||||
|
@ -33,7 +33,6 @@ Future<ThemeData> createAppTheme(
|
|||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
);
|
);
|
||||||
|
|
||||||
final hasBackground = prefs.getBool(kAppBackgroundStoreKey) ?? false;
|
|
||||||
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
@ -49,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,
|
||||||
|
@ -1,15 +1,25 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'attachment.freezed.dart';
|
part 'attachment.freezed.dart';
|
||||||
|
|
||||||
part 'attachment.g.dart';
|
part 'attachment.g.dart';
|
||||||
|
|
||||||
|
enum SnMediaType {
|
||||||
|
image,
|
||||||
|
video,
|
||||||
|
audio,
|
||||||
|
file,
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class SnAttachment with _$SnAttachment {
|
class SnAttachment with _$SnAttachment {
|
||||||
|
const SnAttachment._();
|
||||||
|
|
||||||
const factory SnAttachment({
|
const factory SnAttachment({
|
||||||
required int id,
|
required int id,
|
||||||
required DateTime createdAt,
|
required DateTime createdAt,
|
||||||
required DateTime updatedAt,
|
required DateTime updatedAt,
|
||||||
required dynamic deletedAt,
|
required DateTime? deletedAt,
|
||||||
required String rid,
|
required String rid,
|
||||||
required String uuid,
|
required String uuid,
|
||||||
required int size,
|
required int size,
|
||||||
@ -19,22 +29,70 @@ class SnAttachment with _$SnAttachment {
|
|||||||
required String hash,
|
required String hash,
|
||||||
required int destination,
|
required int destination,
|
||||||
required int refCount,
|
required int refCount,
|
||||||
required dynamic fileChunks,
|
@Default(0) int contentRating,
|
||||||
required dynamic cleanedAt,
|
@Default(0) int qualityRating,
|
||||||
required bool isMature,
|
required DateTime? cleanedAt,
|
||||||
required bool isAnalyzed,
|
required bool isAnalyzed,
|
||||||
required bool isUploaded,
|
|
||||||
required bool isSelfRef,
|
required bool isSelfRef,
|
||||||
required dynamic ref,
|
required bool isIndexable,
|
||||||
required dynamic refId,
|
required SnAttachment? ref,
|
||||||
|
required int? refId,
|
||||||
required SnAttachmentPool? pool,
|
required SnAttachmentPool? pool,
|
||||||
required int poolId,
|
required int? poolId,
|
||||||
required int accountId,
|
required int accountId,
|
||||||
|
int? thumbnailId,
|
||||||
|
SnAttachment? thumbnail,
|
||||||
|
int? compressedId,
|
||||||
|
SnAttachment? compressed,
|
||||||
|
@Default([]) List<SnAttachmentBoost> boosts,
|
||||||
|
@Default({}) Map<String, dynamic> usermeta,
|
||||||
@Default({}) Map<String, dynamic> metadata,
|
@Default({}) Map<String, dynamic> metadata,
|
||||||
}) = _SnAttachment;
|
}) = _SnAttachment;
|
||||||
|
|
||||||
factory SnAttachment.fromJson(Map<String, Object?> json) =>
|
factory SnAttachment.fromJson(Map<String, Object?> json) => _$SnAttachmentFromJson(json);
|
||||||
_$SnAttachmentFromJson(json);
|
|
||||||
|
Map<String, dynamic> get data => {
|
||||||
|
...metadata,
|
||||||
|
...usermeta,
|
||||||
|
};
|
||||||
|
|
||||||
|
SnMediaType get mediaType => switch (mimetype.split('/').firstOrNull) {
|
||||||
|
'image' => SnMediaType.image,
|
||||||
|
'video' => SnMediaType.video,
|
||||||
|
'audio' => SnMediaType.audio,
|
||||||
|
_ => SnMediaType.file,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnAttachmentFragment with _$SnAttachmentFragment {
|
||||||
|
const SnAttachmentFragment._();
|
||||||
|
|
||||||
|
const factory SnAttachmentFragment({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required String rid,
|
||||||
|
required String uuid,
|
||||||
|
required int size,
|
||||||
|
required String name,
|
||||||
|
required String alt,
|
||||||
|
required String mimetype,
|
||||||
|
required String hash,
|
||||||
|
String? fingerprint,
|
||||||
|
@Default({}) Map<String, int> fileChunks,
|
||||||
|
@Default([]) List<String> fileChunksMissing,
|
||||||
|
}) = _SnAttachmentFragment;
|
||||||
|
|
||||||
|
factory SnAttachmentFragment.fromJson(Map<String, Object?> json) => _$SnAttachmentFragmentFromJson(json);
|
||||||
|
|
||||||
|
SnMediaType get mediaType => switch (mimetype.split('/').firstOrNull) {
|
||||||
|
'image' => SnMediaType.image,
|
||||||
|
'video' => SnMediaType.video,
|
||||||
|
'audio' => SnMediaType.audio,
|
||||||
|
_ => SnMediaType.file,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@ -51,6 +109,71 @@ class SnAttachmentPool with _$SnAttachmentPool {
|
|||||||
required int? accountId,
|
required int? accountId,
|
||||||
}) = _SnAttachmentPool;
|
}) = _SnAttachmentPool;
|
||||||
|
|
||||||
factory SnAttachmentPool.fromJson(Map<String, Object?> json) =>
|
factory SnAttachmentPool.fromJson(Map<String, Object?> json) => _$SnAttachmentPoolFromJson(json);
|
||||||
_$SnAttachmentPoolFromJson(json);
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnAttachmentDestination with _$SnAttachmentDestination {
|
||||||
|
const factory SnAttachmentDestination({
|
||||||
|
@Default(0) int id,
|
||||||
|
required String type,
|
||||||
|
required String label,
|
||||||
|
required String region,
|
||||||
|
required bool isBoost,
|
||||||
|
}) = _SnAttachmentDestination;
|
||||||
|
|
||||||
|
factory SnAttachmentDestination.fromJson(Map<String, Object?> json) => _$SnAttachmentDestinationFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnAttachmentBoost with _$SnAttachmentBoost {
|
||||||
|
const factory SnAttachmentBoost({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required int status,
|
||||||
|
required int destination,
|
||||||
|
required int attachmentId,
|
||||||
|
required SnAttachment attachment,
|
||||||
|
required int account,
|
||||||
|
}) = _SnAttachmentBoost;
|
||||||
|
|
||||||
|
factory SnAttachmentBoost.fromJson(Map<String, Object?> json) => _$SnAttachmentBoostFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnSticker with _$SnSticker {
|
||||||
|
const factory SnSticker({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required String alias,
|
||||||
|
required String name,
|
||||||
|
required int attachmentId,
|
||||||
|
required SnAttachment attachment,
|
||||||
|
required int packId,
|
||||||
|
required SnStickerPack pack,
|
||||||
|
required int accountId,
|
||||||
|
}) = _SnSticker;
|
||||||
|
|
||||||
|
factory SnSticker.fromJson(Map<String, Object?> json) => _$SnStickerFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnStickerPack with _$SnStickerPack {
|
||||||
|
const factory SnStickerPack({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required DateTime? deletedAt,
|
||||||
|
required String prefix,
|
||||||
|
required String name,
|
||||||
|
required String description,
|
||||||
|
required List<SnSticker>? stickers,
|
||||||
|
required int accountId,
|
||||||
|
}) = _SnStickerPack;
|
||||||
|
|
||||||
|
factory SnStickerPack.fromJson(Map<String, Object?> json) => _$SnStickerPackFromJson(json);
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,9 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
|||||||
id: (json['id'] as num).toInt(),
|
id: (json['id'] as num).toInt(),
|
||||||
createdAt: DateTime.parse(json['created_at'] as String),
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
deletedAt: json['deleted_at'],
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
rid: json['rid'] as String,
|
rid: json['rid'] as String,
|
||||||
uuid: json['uuid'] as String,
|
uuid: json['uuid'] as String,
|
||||||
size: (json['size'] as num).toInt(),
|
size: (json['size'] as num).toInt(),
|
||||||
@ -21,19 +23,37 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> json) =>
|
|||||||
hash: json['hash'] as String,
|
hash: json['hash'] as String,
|
||||||
destination: (json['destination'] as num).toInt(),
|
destination: (json['destination'] as num).toInt(),
|
||||||
refCount: (json['ref_count'] as num).toInt(),
|
refCount: (json['ref_count'] as num).toInt(),
|
||||||
fileChunks: json['file_chunks'],
|
contentRating: (json['content_rating'] as num?)?.toInt() ?? 0,
|
||||||
cleanedAt: json['cleaned_at'],
|
qualityRating: (json['quality_rating'] as num?)?.toInt() ?? 0,
|
||||||
isMature: json['is_mature'] as bool,
|
cleanedAt: json['cleaned_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['cleaned_at'] as String),
|
||||||
isAnalyzed: json['is_analyzed'] as bool,
|
isAnalyzed: json['is_analyzed'] as bool,
|
||||||
isUploaded: json['is_uploaded'] as bool,
|
|
||||||
isSelfRef: json['is_self_ref'] as bool,
|
isSelfRef: json['is_self_ref'] as bool,
|
||||||
ref: json['ref'],
|
isIndexable: json['is_indexable'] as bool,
|
||||||
refId: json['ref_id'],
|
ref: json['ref'] == null
|
||||||
|
? null
|
||||||
|
: SnAttachment.fromJson(json['ref'] as Map<String, dynamic>),
|
||||||
|
refId: (json['ref_id'] as num?)?.toInt(),
|
||||||
pool: json['pool'] == null
|
pool: json['pool'] == null
|
||||||
? null
|
? null
|
||||||
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
|
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
|
||||||
poolId: (json['pool_id'] as num).toInt(),
|
poolId: (json['pool_id'] as num?)?.toInt(),
|
||||||
accountId: (json['account_id'] as num).toInt(),
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
|
thumbnailId: (json['thumbnail_id'] as num?)?.toInt(),
|
||||||
|
thumbnail: json['thumbnail'] == null
|
||||||
|
? null
|
||||||
|
: SnAttachment.fromJson(json['thumbnail'] as Map<String, dynamic>),
|
||||||
|
compressedId: (json['compressed_id'] as num?)?.toInt(),
|
||||||
|
compressed: json['compressed'] == null
|
||||||
|
? null
|
||||||
|
: SnAttachment.fromJson(json['compressed'] as Map<String, dynamic>),
|
||||||
|
boosts: (json['boosts'] as List<dynamic>?)
|
||||||
|
?.map(
|
||||||
|
(e) => SnAttachmentBoost.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
usermeta: json['usermeta'] as Map<String, dynamic>? ?? const {},
|
||||||
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
|
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -42,7 +62,7 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
|
|||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'created_at': instance.createdAt.toIso8601String(),
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
'updated_at': instance.updatedAt.toIso8601String(),
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
'deleted_at': instance.deletedAt,
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
'rid': instance.rid,
|
'rid': instance.rid,
|
||||||
'uuid': instance.uuid,
|
'uuid': instance.uuid,
|
||||||
'size': instance.size,
|
'size': instance.size,
|
||||||
@ -52,20 +72,72 @@ Map<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
|
|||||||
'hash': instance.hash,
|
'hash': instance.hash,
|
||||||
'destination': instance.destination,
|
'destination': instance.destination,
|
||||||
'ref_count': instance.refCount,
|
'ref_count': instance.refCount,
|
||||||
'file_chunks': instance.fileChunks,
|
'content_rating': instance.contentRating,
|
||||||
'cleaned_at': instance.cleanedAt,
|
'quality_rating': instance.qualityRating,
|
||||||
'is_mature': instance.isMature,
|
'cleaned_at': instance.cleanedAt?.toIso8601String(),
|
||||||
'is_analyzed': instance.isAnalyzed,
|
'is_analyzed': instance.isAnalyzed,
|
||||||
'is_uploaded': instance.isUploaded,
|
|
||||||
'is_self_ref': instance.isSelfRef,
|
'is_self_ref': instance.isSelfRef,
|
||||||
'ref': instance.ref,
|
'is_indexable': instance.isIndexable,
|
||||||
|
'ref': instance.ref?.toJson(),
|
||||||
'ref_id': instance.refId,
|
'ref_id': instance.refId,
|
||||||
'pool': instance.pool?.toJson(),
|
'pool': instance.pool?.toJson(),
|
||||||
'pool_id': instance.poolId,
|
'pool_id': instance.poolId,
|
||||||
'account_id': instance.accountId,
|
'account_id': instance.accountId,
|
||||||
|
'thumbnail_id': instance.thumbnailId,
|
||||||
|
'thumbnail': instance.thumbnail?.toJson(),
|
||||||
|
'compressed_id': instance.compressedId,
|
||||||
|
'compressed': instance.compressed?.toJson(),
|
||||||
|
'boosts': instance.boosts.map((e) => e.toJson()).toList(),
|
||||||
|
'usermeta': instance.usermeta,
|
||||||
'metadata': instance.metadata,
|
'metadata': instance.metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_$SnAttachmentFragmentImpl _$$SnAttachmentFragmentImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SnAttachmentFragmentImpl(
|
||||||
|
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),
|
||||||
|
rid: json['rid'] as String,
|
||||||
|
uuid: json['uuid'] as String,
|
||||||
|
size: (json['size'] as num).toInt(),
|
||||||
|
name: json['name'] as String,
|
||||||
|
alt: json['alt'] as String,
|
||||||
|
mimetype: json['mimetype'] as String,
|
||||||
|
hash: json['hash'] as String,
|
||||||
|
fingerprint: json['fingerprint'] as String?,
|
||||||
|
fileChunks: (json['file_chunks'] as Map<String, dynamic>?)?.map(
|
||||||
|
(k, e) => MapEntry(k, (e as num).toInt()),
|
||||||
|
) ??
|
||||||
|
const {},
|
||||||
|
fileChunksMissing: (json['file_chunks_missing'] as List<dynamic>?)
|
||||||
|
?.map((e) => e as String)
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnAttachmentFragmentImplToJson(
|
||||||
|
_$SnAttachmentFragmentImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'rid': instance.rid,
|
||||||
|
'uuid': instance.uuid,
|
||||||
|
'size': instance.size,
|
||||||
|
'name': instance.name,
|
||||||
|
'alt': instance.alt,
|
||||||
|
'mimetype': instance.mimetype,
|
||||||
|
'hash': instance.hash,
|
||||||
|
'fingerprint': instance.fingerprint,
|
||||||
|
'file_chunks': instance.fileChunks,
|
||||||
|
'file_chunks_missing': instance.fileChunksMissing,
|
||||||
|
};
|
||||||
|
|
||||||
_$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson(
|
_$SnAttachmentPoolImpl _$$SnAttachmentPoolImplFromJson(
|
||||||
Map<String, dynamic> json) =>
|
Map<String, dynamic> json) =>
|
||||||
_$SnAttachmentPoolImpl(
|
_$SnAttachmentPoolImpl(
|
||||||
@ -95,3 +167,117 @@ Map<String, dynamic> _$$SnAttachmentPoolImplToJson(
|
|||||||
'config': instance.config,
|
'config': instance.config,
|
||||||
'account_id': instance.accountId,
|
'account_id': instance.accountId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_$SnAttachmentDestinationImpl _$$SnAttachmentDestinationImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SnAttachmentDestinationImpl(
|
||||||
|
id: (json['id'] as num?)?.toInt() ?? 0,
|
||||||
|
type: json['type'] as String,
|
||||||
|
label: json['label'] as String,
|
||||||
|
region: json['region'] as String,
|
||||||
|
isBoost: json['is_boost'] as bool,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnAttachmentDestinationImplToJson(
|
||||||
|
_$SnAttachmentDestinationImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'type': instance.type,
|
||||||
|
'label': instance.label,
|
||||||
|
'region': instance.region,
|
||||||
|
'is_boost': instance.isBoost,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SnAttachmentBoostImpl _$$SnAttachmentBoostImplFromJson(
|
||||||
|
Map<String, dynamic> json) =>
|
||||||
|
_$SnAttachmentBoostImpl(
|
||||||
|
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),
|
||||||
|
status: (json['status'] as num).toInt(),
|
||||||
|
destination: (json['destination'] as num).toInt(),
|
||||||
|
attachmentId: (json['attachment_id'] as num).toInt(),
|
||||||
|
attachment:
|
||||||
|
SnAttachment.fromJson(json['attachment'] as Map<String, dynamic>),
|
||||||
|
account: (json['account'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnAttachmentBoostImplToJson(
|
||||||
|
_$SnAttachmentBoostImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'status': instance.status,
|
||||||
|
'destination': instance.destination,
|
||||||
|
'attachment_id': instance.attachmentId,
|
||||||
|
'attachment': instance.attachment.toJson(),
|
||||||
|
'account': instance.account,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SnStickerImpl _$$SnStickerImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnStickerImpl(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
alias: json['alias'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
attachmentId: (json['attachment_id'] as num).toInt(),
|
||||||
|
attachment:
|
||||||
|
SnAttachment.fromJson(json['attachment'] as Map<String, dynamic>),
|
||||||
|
packId: (json['pack_id'] as num).toInt(),
|
||||||
|
pack: SnStickerPack.fromJson(json['pack'] as Map<String, dynamic>),
|
||||||
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnStickerImplToJson(_$SnStickerImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'alias': instance.alias,
|
||||||
|
'name': instance.name,
|
||||||
|
'attachment_id': instance.attachmentId,
|
||||||
|
'attachment': instance.attachment.toJson(),
|
||||||
|
'pack_id': instance.packId,
|
||||||
|
'pack': instance.pack.toJson(),
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
};
|
||||||
|
|
||||||
|
_$SnStickerPackImpl _$$SnStickerPackImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnStickerPackImpl(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
|
prefix: json['prefix'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
description: json['description'] as String,
|
||||||
|
stickers: (json['stickers'] as List<dynamic>?)
|
||||||
|
?.map((e) => SnSticker.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
accountId: (json['account_id'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnStickerPackImplToJson(_$SnStickerPackImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||||
|
'prefix': instance.prefix,
|
||||||
|
'name': instance.name,
|
||||||
|
'description': instance.description,
|
||||||
|
'stickers': instance.stickers?.map((e) => e.toJson()).toList(),
|
||||||
|
'account_id': instance.accountId,
|
||||||
|
};
|
||||||
|
@ -32,7 +32,7 @@ class _AccountSelectState extends State<AccountSelect> {
|
|||||||
final List<SnAccount> _pendingUsers = List.empty(growable: true);
|
final List<SnAccount> _pendingUsers = List.empty(growable: true);
|
||||||
final List<SnAccount> _selectedUsers = List.empty(growable: true);
|
final List<SnAccount> _selectedUsers = List.empty(growable: true);
|
||||||
|
|
||||||
int _accountId = 0;
|
final int _accountId = 0;
|
||||||
|
|
||||||
Future<void> _revertSelectedUsers() async {
|
Future<void> _revertSelectedUsers() async {
|
||||||
if (widget.initialSelection?.isEmpty ?? true) return;
|
if (widget.initialSelection?.isEmpty ?? true) return;
|
||||||
|
116
lib/widgets/attachment/attachment_input.dart
Normal file
116
lib/widgets/attachment/attachment_input.dart
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
|
||||||
|
class AttachmentInputDialog extends StatefulWidget {
|
||||||
|
final String? title;
|
||||||
|
final bool? analyzeNow;
|
||||||
|
const AttachmentInputDialog({super.key, required this.title, this.analyzeNow = false});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AttachmentInputDialog> createState() => _AttachmentInputDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
|
||||||
|
final _randomIdController = TextEditingController();
|
||||||
|
|
||||||
|
XFile? _thumbnailFile;
|
||||||
|
|
||||||
|
void _pickImage() async {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final result = await picker.pickImage(source: ImageSource.gallery);
|
||||||
|
if (result == null) return;
|
||||||
|
setState(() => _thumbnailFile = result);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
void _finishUp() async {
|
||||||
|
if (_isBusy) return;
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
|
||||||
|
if (_randomIdController.text.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final attachment = await attach.getOne(_randomIdController.text);
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, attachment);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
} else if (_thumbnailFile != null) {
|
||||||
|
try {
|
||||||
|
final attachment = await attach.directUploadOne(
|
||||||
|
(await _thumbnailFile!.readAsBytes()).buffer.asUint8List(),
|
||||||
|
_thumbnailFile!.path,
|
||||||
|
'interactive',
|
||||||
|
null,
|
||||||
|
analyzeNow: widget.analyzeNow ?? false,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, attachment);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(widget.title ?? 'attachmentInputDialog').tr(),
|
||||||
|
content: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text('attachmentInputUseRandomId').tr().fontSize(14),
|
||||||
|
const Gap(8),
|
||||||
|
TextField(
|
||||||
|
controller: _randomIdController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'fieldAttachmentRandomId'.tr(),
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const Gap(24),
|
||||||
|
Text('attachmentInputNew').tr().fontSize(14),
|
||||||
|
Card(
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
leading: const Icon(Symbols.add_photo_alternate),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
title: Text('addAttachmentFromAlbum').tr(),
|
||||||
|
subtitle: _thumbnailFile == null ? Text('unset').tr() : Text('waitingForUpload').tr(),
|
||||||
|
onTap: () {
|
||||||
|
_pickImage();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text('dialogDismiss').tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _finishUp(),
|
||||||
|
child: Text('dialogConfirm').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
@ -18,8 +20,11 @@ import 'package:uuid/uuid.dart';
|
|||||||
class AttachmentItem extends StatelessWidget {
|
class AttachmentItem extends StatelessWidget {
|
||||||
final SnAttachment? data;
|
final SnAttachment? data;
|
||||||
final String? heroTag;
|
final String? heroTag;
|
||||||
|
final BoxFit fit;
|
||||||
|
|
||||||
const AttachmentItem({
|
const AttachmentItem({
|
||||||
super.key,
|
super.key,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
required this.data,
|
required this.data,
|
||||||
required this.heroTag,
|
required this.heroTag,
|
||||||
});
|
});
|
||||||
@ -40,7 +45,7 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
child: AutoResizeUniversalImage(
|
child: AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(data!.rid),
|
sn.getAttachmentUrl(data!.rid),
|
||||||
key: Key('attachment-${data!.rid}-$tag'),
|
key: Key('attachment-${data!.rid}-$tag'),
|
||||||
fit: BoxFit.cover,
|
fit: fit,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
case 'video':
|
case 'video':
|
||||||
@ -60,10 +65,13 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (data!.isMature) {
|
if (data!.contentRating > 0) {
|
||||||
return _AttachmentItemSensitiveBlur(
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
child: _buildContent(context),
|
return _AttachmentItemSensitiveBlur(
|
||||||
);
|
isCompact: constraints.maxHeight < 360,
|
||||||
|
child: _buildContent(context),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buildContent(context);
|
return _buildContent(context);
|
||||||
@ -72,15 +80,15 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
|
|
||||||
class _AttachmentItemSensitiveBlur extends StatefulWidget {
|
class _AttachmentItemSensitiveBlur extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
const _AttachmentItemSensitiveBlur({super.key, required this.child});
|
final bool isCompact;
|
||||||
|
|
||||||
|
const _AttachmentItemSensitiveBlur({required this.child, this.isCompact = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_AttachmentItemSensitiveBlur> createState() =>
|
State<_AttachmentItemSensitiveBlur> createState() => _AttachmentItemSensitiveBlurState();
|
||||||
_AttachmentItemSensitiveBlurState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AttachmentItemSensitiveBlurState
|
class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBlur> {
|
||||||
extends State<_AttachmentItemSensitiveBlur> {
|
|
||||||
bool _doesShow = false;
|
bool _doesShow = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -104,24 +112,21 @@ class _AttachmentItemSensitiveBlurState
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 32,
|
size: 32,
|
||||||
),
|
),
|
||||||
const Gap(8),
|
if (!widget.isCompact) const Gap(8),
|
||||||
Text('sensitiveContent', textAlign: TextAlign.center)
|
if (!widget.isCompact)
|
||||||
.tr()
|
Text('sensitiveContent', textAlign: TextAlign.center)
|
||||||
.fontSize(20)
|
|
||||||
.textColor(Colors.white)
|
|
||||||
.bold(),
|
|
||||||
Text(
|
|
||||||
'sensitiveContentDescription',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
)
|
|
||||||
.tr()
|
|
||||||
.fontSize(14)
|
|
||||||
.textColor(Colors.white.withOpacity(0.8)),
|
|
||||||
const Gap(16),
|
|
||||||
InkWell(
|
|
||||||
child: Text('sensitiveContentReveal')
|
|
||||||
.tr()
|
.tr()
|
||||||
.textColor(Colors.white),
|
.fontSize(20)
|
||||||
|
.textColor(Colors.white)
|
||||||
|
.bold(),
|
||||||
|
if (!widget.isCompact)
|
||||||
|
Text(
|
||||||
|
'sensitiveContentDescription',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).tr().fontSize(14).textColor(Colors.white.withOpacity(0.8)),
|
||||||
|
if (!widget.isCompact) const Gap(16),
|
||||||
|
InkWell(
|
||||||
|
child: Text('sensitiveContentReveal').tr().textColor(Colors.white),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() => _doesShow = !_doesShow);
|
setState(() => _doesShow = !_doesShow);
|
||||||
},
|
},
|
||||||
@ -131,9 +136,7 @@ class _AttachmentItemSensitiveBlurState
|
|||||||
).center(),
|
).center(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
).opacity(_doesShow ? 0 : 1, animate: true).animate(const Duration(milliseconds: 300), Curves.easeInOut),
|
||||||
.opacity(_doesShow ? 0 : 1, animate: true)
|
|
||||||
.animate(const Duration(milliseconds: 300), Curves.easeInOut),
|
|
||||||
if (_doesShow)
|
if (_doesShow)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -163,20 +166,19 @@ class _AttachmentItemSensitiveBlurState
|
|||||||
class _AttachmentItemContentVideo extends StatefulWidget {
|
class _AttachmentItemContentVideo extends StatefulWidget {
|
||||||
final SnAttachment data;
|
final SnAttachment data;
|
||||||
final bool isAutoload;
|
final bool isAutoload;
|
||||||
|
|
||||||
const _AttachmentItemContentVideo({
|
const _AttachmentItemContentVideo({
|
||||||
super.key,
|
|
||||||
required this.data,
|
required this.data,
|
||||||
this.isAutoload = false,
|
this.isAutoload = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_AttachmentItemContentVideo> createState() =>
|
State<_AttachmentItemContentVideo> createState() => _AttachmentItemContentVideoState();
|
||||||
_AttachmentItemContentVideoState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AttachmentItemContentVideoState
|
class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo> {
|
||||||
extends State<_AttachmentItemContentVideo> {
|
|
||||||
bool _showContent = false;
|
bool _showContent = false;
|
||||||
|
bool _showOriginal = false;
|
||||||
|
|
||||||
Player? _videoPlayer;
|
Player? _videoPlayer;
|
||||||
VideoController? _videoController;
|
VideoController? _videoController;
|
||||||
@ -185,15 +187,29 @@ class _AttachmentItemContentVideoState
|
|||||||
setState(() => _showContent = true);
|
setState(() => _showContent = true);
|
||||||
MediaKit.ensureInitialized();
|
MediaKit.ensureInitialized();
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final url = sn.getAttachmentUrl(widget.data.rid);
|
final url = _showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid);
|
||||||
_videoPlayer = Player();
|
_videoPlayer = Player();
|
||||||
_videoController = VideoController(_videoPlayer!);
|
_videoController = VideoController(_videoPlayer!);
|
||||||
_videoPlayer!.open(Media(url), play: !widget.isAutoload);
|
_videoPlayer!.open(Media(url), play: !widget.isAutoload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _toggleOriginal() {
|
||||||
|
if (!mounted) return;
|
||||||
|
if (widget.data.compressedId == null) return;
|
||||||
|
setState(() => _showOriginal = !_showOriginal);
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
_videoPlayer?.open(
|
||||||
|
Media(
|
||||||
|
_showOriginal ? sn.getAttachmentUrl(widget.data.rid) : sn.getAttachmentUrl(widget.data.compressed!.rid),
|
||||||
|
),
|
||||||
|
play: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_showOriginal = widget.data.compressedId == null;
|
||||||
if (widget.isAutoload) _startLoad();
|
if (widget.isAutoload) _startLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +223,7 @@ class _AttachmentItemContentVideoState
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
final ratio = widget.data.metadata['ratio'] ?? 16 / 9;
|
final ratio = widget.data.data['ratio'] ?? 16 / 9;
|
||||||
|
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
@ -216,9 +232,9 @@ class _AttachmentItemContentVideoState
|
|||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
if (widget.data.metadata['thumbnail'] != null)
|
if (widget.data.thumbnail != null)
|
||||||
AutoResizeUniversalImage(
|
AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(widget.data.metadata['thumbnail']),
|
sn.getAttachmentUrl(widget.data.thumbnail!.rid),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@ -266,10 +282,7 @@ class _AttachmentItemContentVideoState
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
Duration(
|
Duration(
|
||||||
milliseconds:
|
milliseconds: (widget.data.data['duration'] ?? 0).toInt() * 1000,
|
||||||
(widget.data.metadata['duration'] ?? 0)
|
|
||||||
.toInt() *
|
|
||||||
1000,
|
|
||||||
).toString(),
|
).toString(),
|
||||||
style: GoogleFonts.robotoMono(
|
style: GoogleFonts.robotoMono(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@ -301,9 +314,48 @@ class _AttachmentItemContentVideoState
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Video(
|
return MaterialDesktopVideoControlsTheme(
|
||||||
controller: _videoController!,
|
key: Key('material-desktop-video-controls-theme-$_showOriginal'),
|
||||||
aspectRatio: ratio,
|
normal: MaterialDesktopVideoControlsThemeData(
|
||||||
|
buttonBarButtonSize: 24,
|
||||||
|
buttonBarButtonColor: Colors.white,
|
||||||
|
topButtonBarMargin: EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||||
|
topButtonBar: [
|
||||||
|
const Spacer(),
|
||||||
|
MaterialDesktopCustomButton(
|
||||||
|
iconSize: 24,
|
||||||
|
onPressed: _toggleOriginal,
|
||||||
|
icon: Icon(
|
||||||
|
_showOriginal ? Symbols.high_quality : Symbols.sd,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
fullscreen: const MaterialDesktopVideoControlsThemeData(),
|
||||||
|
child: MaterialVideoControlsTheme(
|
||||||
|
key: Key('material-video-controls-theme-$_showOriginal'),
|
||||||
|
normal: MaterialVideoControlsThemeData(
|
||||||
|
buttonBarButtonSize: 24,
|
||||||
|
buttonBarButtonColor: Colors.white,
|
||||||
|
topButtonBarMargin: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
topButtonBar: [
|
||||||
|
const Spacer(),
|
||||||
|
MaterialDesktopCustomButton(
|
||||||
|
iconSize: 24,
|
||||||
|
onPressed: _toggleOriginal,
|
||||||
|
icon: _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
fullscreen: const MaterialVideoControlsThemeData(),
|
||||||
|
child: Video(
|
||||||
|
controller: _videoController!,
|
||||||
|
aspectRatio: ratio,
|
||||||
|
controls:
|
||||||
|
!kIsWeb && (Platform.isAndroid || Platform.isIOS) ? MaterialVideoControls : MaterialDesktopVideoControls,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,19 +369,17 @@ class _AttachmentItemContentVideoState
|
|||||||
class _AttachmentItemContentAudio extends StatefulWidget {
|
class _AttachmentItemContentAudio extends StatefulWidget {
|
||||||
final SnAttachment data;
|
final SnAttachment data;
|
||||||
final bool isAutoload;
|
final bool isAutoload;
|
||||||
|
|
||||||
const _AttachmentItemContentAudio({
|
const _AttachmentItemContentAudio({
|
||||||
super.key,
|
|
||||||
required this.data,
|
required this.data,
|
||||||
this.isAutoload = false,
|
this.isAutoload = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_AttachmentItemContentAudio> createState() =>
|
State<_AttachmentItemContentAudio> createState() => _AttachmentItemContentAudioState();
|
||||||
_AttachmentItemContentAudioState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AttachmentItemContentAudioState
|
class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio> {
|
||||||
extends State<_AttachmentItemContentAudio> {
|
|
||||||
bool _showContent = false;
|
bool _showContent = false;
|
||||||
|
|
||||||
double? _draggingValue;
|
double? _draggingValue;
|
||||||
@ -378,11 +428,11 @@ class _AttachmentItemContentAudioState
|
|||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
if (widget.data.metadata['thumbnail'] != null)
|
if (widget.data.thumbnail != null)
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: AutoResizeUniversalImage(
|
child: AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(widget.data.metadata['thumbnail']),
|
sn.getAttachmentUrl(widget.data.data['thumbnail']),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -463,11 +513,11 @@ class _AttachmentItemContentAudioState
|
|||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
if (widget.data.metadata['thumbnail'] != null)
|
if (widget.data.data['thumbnail'] != null)
|
||||||
AspectRatio(
|
AspectRatio(
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
child: AutoResizeUniversalImage(
|
child: AutoResizeUniversalImage(
|
||||||
sn.getAttachmentUrl(widget.data.metadata['thumbnail']),
|
sn.getAttachmentUrl(widget.data.data['thumbnail']),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -499,12 +549,8 @@ class _AttachmentItemContentAudioState
|
|||||||
overlayShape: SliderComponentShape.noOverlay,
|
overlayShape: SliderComponentShape.noOverlay,
|
||||||
),
|
),
|
||||||
child: Slider(
|
child: Slider(
|
||||||
secondaryTrackValue: _bufferedPosition
|
secondaryTrackValue: _bufferedPosition.inMilliseconds.abs().toDouble(),
|
||||||
.inMilliseconds
|
value: _draggingValue?.abs() ?? _position.inMilliseconds.toDouble().abs(),
|
||||||
.abs()
|
|
||||||
.toDouble(),
|
|
||||||
value: _draggingValue?.abs() ??
|
|
||||||
_position.inMilliseconds.toDouble().abs(),
|
|
||||||
min: 0,
|
min: 0,
|
||||||
max: math
|
max: math
|
||||||
.max(
|
.max(
|
||||||
@ -544,9 +590,7 @@ class _AttachmentItemContentAudioState
|
|||||||
),
|
),
|
||||||
const Gap(16),
|
const Gap(16),
|
||||||
IconButton.filled(
|
IconButton.filled(
|
||||||
icon: _isPlaying
|
icon: _isPlaying ? const Icon(Symbols.pause) : const Icon(Symbols.play_arrow),
|
||||||
? const Icon(Symbols.pause)
|
|
||||||
: const Icon(Symbols.play_arrow),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_audioPlayer!.playOrPause();
|
_audioPlayer!.playOrPause();
|
||||||
},
|
},
|
||||||
|
@ -4,8 +4,8 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:responsive_framework/responsive_framework.dart';
|
|
||||||
import 'package:surface/types/attachment.dart';
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
import 'package:surface/widgets/attachment/attachment_zoom.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_item.dart';
|
import 'package:surface/widgets/attachment/attachment_item.dart';
|
||||||
@ -14,19 +14,23 @@ import 'package:uuid/uuid.dart';
|
|||||||
class AttachmentList extends StatefulWidget {
|
class AttachmentList extends StatefulWidget {
|
||||||
final List<SnAttachment?> data;
|
final List<SnAttachment?> data;
|
||||||
final bool bordered;
|
final bool bordered;
|
||||||
final bool noGrow;
|
final bool gridded;
|
||||||
final bool isFlatted;
|
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.noGrow = false,
|
this.gridded = false,
|
||||||
this.isFlatted = false,
|
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));
|
||||||
@ -41,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(
|
||||||
@ -51,14 +53,13 @@ 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,
|
||||||
maxWidth: layoutConstraints.maxWidth - 20,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.data.isEmpty) return const SizedBox.shrink();
|
if (widget.data.isEmpty) return const SizedBox.shrink();
|
||||||
if (widget.data.length == 1) {
|
if (widget.data.length == 1) {
|
||||||
final singleAspectRatio = widget.data[0]?.metadata['ratio']?.toDouble() ??
|
final singleAspectRatio = widget.data[0]?.data['ratio']?.toDouble() ??
|
||||||
switch (widget.data[0]?.mimetype.split('/').firstOrNull) {
|
switch (widget.data[0]?.mimetype.split('/').firstOrNull) {
|
||||||
'audio' => 16 / 9,
|
'audio' => 16 / 9,
|
||||||
'video' => 16 / 9,
|
'video' => 16 / 9,
|
||||||
@ -67,115 +68,73 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
.toDouble();
|
.toDouble();
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
constraints: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
padding: widget.padding ?? EdgeInsets.zero,
|
||||||
? constraints.copyWith(
|
constraints: constraints,
|
||||||
maxWidth: math.min(
|
child: GestureDetector(
|
||||||
constraints.maxWidth,
|
child: AspectRatio(
|
||||||
kAttachmentMaxWidth,
|
aspectRatio: singleAspectRatio,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
border: Border.fromBorderSide(borderSide),
|
||||||
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
|
child: AttachmentItem(
|
||||||
|
data: widget.data[0],
|
||||||
|
heroTag: heroTags[0],
|
||||||
|
fit: widget.fit,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: null,
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: singleAspectRatio,
|
|
||||||
child: GestureDetector(
|
|
||||||
child: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) || widget.noGrow) {
|
|
||||||
return Padding(
|
|
||||||
// Single child list-like displaying
|
|
||||||
padding: widget.listPadding ?? EdgeInsets.zero,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: backgroundColor,
|
|
||||||
border: Border(top: borderSide, bottom: borderSide),
|
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
|
||||||
child: AttachmentItem(
|
|
||||||
data: widget.data[0],
|
|
||||||
heroTag: heroTags[0],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: backgroundColor,
|
|
||||||
border: Border(top: borderSide, bottom: borderSide),
|
|
||||||
),
|
|
||||||
child: AttachmentItem(
|
|
||||||
data: widget.data[0],
|
|
||||||
heroTag: heroTags.first,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
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.isFlatted) {
|
if (widget.gridded) {
|
||||||
return Wrap(
|
return Container(
|
||||||
spacing: 4,
|
margin: widget.padding ?? EdgeInsets.zero,
|
||||||
runSpacing: 4,
|
decoration: BoxDecoration(
|
||||||
children: widget.data
|
color: backgroundColor,
|
||||||
.mapIndexed(
|
border: Border(
|
||||||
(idx, ele) => AspectRatio(
|
top: borderSide,
|
||||||
aspectRatio: (ele?.metadata['ratio'] ?? 1).toDouble(),
|
bottom: borderSide,
|
||||||
child: Container(
|
),
|
||||||
decoration: BoxDecoration(
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
color: backgroundColor,
|
),
|
||||||
border: Border(
|
child: ClipRRect(
|
||||||
top: borderSide,
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
bottom: borderSide,
|
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: BoxFit.cover,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
|
||||||
child: AttachmentItem(
|
|
||||||
data: ele,
|
|
||||||
heroTag: heroTags[idx],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AspectRatio(
|
|
||||||
aspectRatio: (widget.data.firstOrNull?.metadata['ratio'] ?? 1).toDouble(),
|
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
|
||||||
child: ScrollConfiguration(
|
|
||||||
behavior: _AttachmentListScrollBehavior(),
|
|
||||||
child: ListView.separated(
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: widget.data.length,
|
|
||||||
itemBuilder: (context, idx) {
|
|
||||||
return Container(
|
|
||||||
constraints: constraints,
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: (widget.data[idx]?.metadata['ratio'] ?? 1).toDouble(),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
AttachmentZoomView(
|
AttachmentZoomView(
|
||||||
data: widget.data.where((ele) => ele != null).cast(),
|
data: widget.data.where((ele) => ele != null).cast(),
|
||||||
@ -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,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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)
|
||||||
|
86
lib/widgets/attachment/pending_attachment_alt.dart
Normal file
86
lib/widgets/attachment/pending_attachment_alt.dart
Normal 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()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
120
lib/widgets/attachment/pending_attachment_boost.dart
Normal file
120
lib/widgets/attachment/pending_attachment_boost.dart
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/types/attachment.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
|
||||||
|
class PendingAttachmentBoostDialog extends StatefulWidget {
|
||||||
|
final PostWriteMedia media;
|
||||||
|
|
||||||
|
const PendingAttachmentBoostDialog({super.key, required this.media});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PendingAttachmentBoostDialog> createState() => _PendingAttachmentBoostDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PendingAttachmentBoostDialogState extends State<PendingAttachmentBoostDialog> {
|
||||||
|
List<SnAttachmentDestination>? _regions;
|
||||||
|
SnAttachmentDestination? _selectedRegion;
|
||||||
|
|
||||||
|
Future<void> _fetchRegions() async {
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/uc/destinations');
|
||||||
|
setState(() {
|
||||||
|
_regions = List<SnAttachmentDestination>.from(
|
||||||
|
resp.data?.map((e) => SnAttachmentDestination.fromJson(e)) ?? [],
|
||||||
|
).cast<SnAttachmentDestination>().where((ele) => ele.isBoost).toList();
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
|
||||||
|
Future<void> _performAction() async {
|
||||||
|
if (_isBusy) return;
|
||||||
|
if (_selectedRegion == null) return;
|
||||||
|
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.post('/cgi/uc/boosts', data: {
|
||||||
|
'attachment': widget.media.attachment!.id,
|
||||||
|
'destination': _selectedRegion!.id,
|
||||||
|
});
|
||||||
|
if (!mounted) return;
|
||||||
|
Navigator.pop(context, SnAttachmentBoost.fromJson(resp.data));
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
setState(() => _isBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_fetchRegions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('attachmentBoost').tr(),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('attachmentBoostHint').tr(),
|
||||||
|
const Gap(16),
|
||||||
|
Text('attachmentDestinationRegion').tr().fontSize(18),
|
||||||
|
const Gap(8),
|
||||||
|
Card(
|
||||||
|
child: _regions == null
|
||||||
|
? const CircularProgressIndicator().center().padding(all: 16)
|
||||||
|
: Column(
|
||||||
|
children: _regions!.map(
|
||||||
|
(ele) {
|
||||||
|
return RadioListTile(
|
||||||
|
title: Text(ele.label).tr(),
|
||||||
|
subtitle: Text(
|
||||||
|
'attachmentDestinationRegion${ele.region}'.trExists()
|
||||||
|
? 'attachmentDestinationRegion${ele.region}'.tr()
|
||||||
|
: ele.region,
|
||||||
|
),
|
||||||
|
selected: _selectedRegion == ele,
|
||||||
|
value: ele,
|
||||||
|
groupValue: _selectedRegion,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) setState(() => _selectedRegion = value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text('dialogDismiss'.tr()),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : () => _performAction(),
|
||||||
|
child: Text('dialogConfirm'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
163
lib/widgets/attachment/pending_attachment_compress.dart
Normal file
163
lib/widgets/attachment/pending_attachment_compress.dart
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:cross_file/cross_file.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:video_compress/video_compress.dart';
|
||||||
|
|
||||||
|
class PendingVideoCompressDialog extends StatefulWidget {
|
||||||
|
final PostWriteMedia media;
|
||||||
|
|
||||||
|
const PendingVideoCompressDialog({super.key, required this.media});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PendingVideoCompressDialog> createState() => _PendingVideoCompressDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PendingVideoCompressDialogState extends State<PendingVideoCompressDialog> {
|
||||||
|
VideoQuality _quality = VideoQuality.DefaultQuality;
|
||||||
|
|
||||||
|
bool _isBusy = false;
|
||||||
|
double? _progress;
|
||||||
|
MediaInfo? _mediaInfo;
|
||||||
|
|
||||||
|
Subscription? _progressSubscription;
|
||||||
|
|
||||||
|
Future<void> _startCompress() async {
|
||||||
|
_mediaInfo = await VideoCompress.compressVideo(
|
||||||
|
widget.media.file!.path,
|
||||||
|
quality: _quality,
|
||||||
|
deleteOrigin: false,
|
||||||
|
frameRate: switch (_quality) {
|
||||||
|
VideoQuality.HighestQuality => 60,
|
||||||
|
VideoQuality.DefaultQuality => 60,
|
||||||
|
_ => 30,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (_mediaInfo == null) return;
|
||||||
|
setState(() => _isBusy = true);
|
||||||
|
if (!mounted || _mediaInfo == null) return;
|
||||||
|
Navigator.pop(context, PostWriteMedia.fromFile(XFile(_mediaInfo!.path!)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_progressSubscription = VideoCompress.compressProgress$.subscribe((event) {
|
||||||
|
log('[Compress] Progress: $event');
|
||||||
|
setState(() {
|
||||||
|
_progress = event / 100;
|
||||||
|
_isBusy = event < 100;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_progressSubscription?.unsubscribe();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('attachmentCompressVideo').tr(),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
FutureBuilder(
|
||||||
|
future: widget.media.file?.length(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) return const SizedBox.shrink();
|
||||||
|
return Text(
|
||||||
|
snapshot.data!.formatBytes(),
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text('attachmentCompressQuality').tr(),
|
||||||
|
const Gap(8),
|
||||||
|
Card(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
RadioListTile(
|
||||||
|
title: Text('attachmentCompressQualityHighest').tr(),
|
||||||
|
value: VideoQuality.HighestQuality,
|
||||||
|
groupValue: _quality,
|
||||||
|
selected: _quality == VideoQuality.HighestQuality,
|
||||||
|
onChanged: (val) {
|
||||||
|
if (val != null) {
|
||||||
|
setState(() => _quality = val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RadioListTile(
|
||||||
|
title: Text('attachmentCompressQualityDefault').tr(),
|
||||||
|
value: VideoQuality.DefaultQuality,
|
||||||
|
groupValue: _quality,
|
||||||
|
onChanged: (val) {
|
||||||
|
if (val != null) {
|
||||||
|
setState(() => _quality = val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RadioListTile(
|
||||||
|
title: Text('attachmentCompressQualityMedium').tr(),
|
||||||
|
value: VideoQuality.MediumQuality,
|
||||||
|
groupValue: _quality,
|
||||||
|
onChanged: (val) {
|
||||||
|
if (val != null) {
|
||||||
|
setState(() => _quality = val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
RadioListTile(
|
||||||
|
title: Text('attachmentCompressQualityLow').tr(),
|
||||||
|
value: VideoQuality.LowQuality,
|
||||||
|
groupValue: _quality,
|
||||||
|
onChanged: (val) {
|
||||||
|
if (val != null) {
|
||||||
|
setState(() => _quality = val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Text('attachmentCompressQualityHint', style: Theme.of(context).textTheme.bodySmall!).tr(),
|
||||||
|
if (_isBusy)
|
||||||
|
TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween(begin: 0, end: _progress ?? 0),
|
||||||
|
duration: Duration(milliseconds: 100),
|
||||||
|
builder: (context, value, _) => LinearProgressIndicator(
|
||||||
|
value: value,
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
|
),
|
||||||
|
).padding(top: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Text('dialogDismiss').tr(),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isBusy ? null : _startCompress,
|
||||||
|
child: Text('dialogConfirm').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ import 'package:surface/providers/userinfo.dart';
|
|||||||
import 'package:surface/types/chat.dart';
|
import 'package:surface/types/chat.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/attachment/attachment_list.dart';
|
import 'package:surface/widgets/attachment/attachment_list.dart';
|
||||||
|
import 'package:surface/widgets/context_menu.dart';
|
||||||
import 'package:surface/widgets/link_preview.dart';
|
import 'package:surface/widgets/link_preview.dart';
|
||||||
import 'package:surface/widgets/markdown_content.dart';
|
import 'package:surface/widgets/markdown_content.dart';
|
||||||
import 'package:swipe_to/swipe_to.dart';
|
import 'package:swipe_to/swipe_to.dart';
|
||||||
@ -23,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,
|
||||||
@ -34,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
|
||||||
@ -52,8 +55,8 @@ 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: ContextMenuRegion(
|
child: ContextMenuArea(
|
||||||
contextMenu: ContextMenu(
|
contextMenu: ContextMenu(
|
||||||
entries: [
|
entries: [
|
||||||
MenuHeader(text: "eventResourceTag".tr(args: ['#${data.id}'])),
|
MenuHeader(text: "eventResourceTag".tr(args: ['#${data.id}'])),
|
||||||
@ -86,83 +89,94 @@ 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(
|
||||||
|
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,
|
||||||
noGrow: true,
|
maxHeight: 560,
|
||||||
maxHeight: 520,
|
maxWidth: 480,
|
||||||
listPadding: const EdgeInsets.only(top: 8),
|
minWidth: 480,
|
||||||
|
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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -172,19 +186,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({super.key, 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(
|
||||||
@ -213,7 +280,7 @@ class _ChatMessageText extends StatelessWidget {
|
|||||||
class _ChatMessageSystemNotify extends StatelessWidget {
|
class _ChatMessageSystemNotify extends StatelessWidget {
|
||||||
final SnChatMessage data;
|
final SnChatMessage data;
|
||||||
|
|
||||||
const _ChatMessageSystemNotify({super.key, required this.data});
|
const _ChatMessageSystemNotify({required this.data});
|
||||||
|
|
||||||
String _formatDuration(Duration duration) {
|
String _formatDuration(Duration duration) {
|
||||||
String negativeSign = duration.isNegative ? '-' : '';
|
String negativeSign = duration.isNegative ? '-' : '';
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
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:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:pasteboard/pasteboard.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/chat_message_controller.dart';
|
import 'package:surface/controllers/chat_message_controller.dart';
|
||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.dart';
|
||||||
import 'package:surface/providers/sn_attachment.dart';
|
import 'package:surface/providers/sn_attachment.dart';
|
||||||
import 'package:surface/providers/user_directory.dart';
|
import 'package:surface/providers/user_directory.dart';
|
||||||
|
import 'package:surface/types/attachment.dart';
|
||||||
import 'package:surface/types/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 {
|
||||||
@ -37,6 +32,16 @@ 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);
|
||||||
}
|
}
|
||||||
@ -80,13 +85,14 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
media.name,
|
media.name,
|
||||||
'messaging',
|
'messaging',
|
||||||
null,
|
null,
|
||||||
mimetype: media.raw != null && media.type == PostWriteMediaType.image ? 'image/png' : null,
|
mimetype: media.raw != null && media.type == SnMediaType.image ? 'image/png' : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
final item = await attach.chunkedUploadParts(
|
final item = await attach.chunkedUploadParts(
|
||||||
media.toFile()!,
|
media.toFile()!,
|
||||||
place.$1,
|
place.$1,
|
||||||
place.$2,
|
place.$2,
|
||||||
|
analyzeNow: media.type == SnMediaType.image,
|
||||||
onProgress: (progress) {
|
onProgress: (progress) {
|
||||||
// Calculate overall progress for attachments
|
// Calculate overall progress for attachments
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -123,40 +129,6 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final List<PostWriteMedia> _attachments = List.empty(growable: true);
|
final List<PostWriteMedia> _attachments = List.empty(growable: true);
|
||||||
final _imagePicker = ImagePicker();
|
|
||||||
|
|
||||||
void _takeMedia(bool isVideo) async {
|
|
||||||
final result = isVideo
|
|
||||||
? await _imagePicker.pickVideo(source: ImageSource.camera)
|
|
||||||
: await _imagePicker.pickImage(source: ImageSource.camera);
|
|
||||||
if (result == null) return;
|
|
||||||
_attachments.add(
|
|
||||||
PostWriteMedia.fromFile(result),
|
|
||||||
);
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _selectMedia() async {
|
|
||||||
final result = await _imagePicker.pickMultipleMedia();
|
|
||||||
if (result.isEmpty) return;
|
|
||||||
_attachments.addAll(
|
|
||||||
result.map((e) => PostWriteMedia.fromFile(e)),
|
|
||||||
);
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _pasteMedia() async {
|
|
||||||
final imageBytes = await Pasteboard.image;
|
|
||||||
if (imageBytes == null) return;
|
|
||||||
_attachments.add(
|
|
||||||
PostWriteMedia.fromBytes(
|
|
||||||
imageBytes,
|
|
||||||
'attachmentPastedImage'.tr(),
|
|
||||||
PostWriteMediaType.image,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -198,75 +170,82 @@ 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: () {
|
||||||
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: () {
|
||||||
|
_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,
|
||||||
@ -294,63 +273,12 @@ class ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
PopupMenuButton(
|
AddPostMediaButton(
|
||||||
icon: Icon(
|
onAdd: (items) {
|
||||||
Symbols.add_photo_alternate,
|
setState(() {
|
||||||
color: Theme.of(context).colorScheme.primary,
|
_attachments.addAll(items);
|
||||||
),
|
});
|
||||||
itemBuilder: (context) => [
|
},
|
||||||
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.photo_camera),
|
|
||||||
const Gap(16),
|
|
||||||
Text('addAttachmentFromCameraPhoto').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_takeMedia(false);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.videocam),
|
|
||||||
const Gap(16),
|
|
||||||
Text('addAttachmentFromCameraVideo').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_takeMedia(true);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.photo_library),
|
|
||||||
const Gap(16),
|
|
||||||
Text('addAttachmentFromAlbum').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_selectMedia();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.content_paste),
|
|
||||||
const Gap(16),
|
|
||||||
Text('addAttachmentFromClipboard').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_pasteMedia();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: _isBusy ? null : _sendMessage,
|
onPressed: _isBusy ? null : _sendMessage,
|
||||||
|
53
lib/widgets/chat/chat_typing_indicator.dart
Normal file
53
lib/widgets/chat/chat_typing_indicator.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
47
lib/widgets/context_menu.dart
Normal file
47
lib/widgets/context_menu.dart
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/config.dart';
|
||||||
|
|
||||||
|
class ContextMenuArea extends StatelessWidget {
|
||||||
|
final ContextMenu contextMenu;
|
||||||
|
final Widget child;
|
||||||
|
final ValueChanged<dynamic>? onItemSelected;
|
||||||
|
|
||||||
|
const ContextMenuArea({
|
||||||
|
super.key,
|
||||||
|
required this.contextMenu,
|
||||||
|
required this.child,
|
||||||
|
this.onItemSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Offset mousePosition = Offset.zero;
|
||||||
|
|
||||||
|
return Listener(
|
||||||
|
onPointerDown: (event) {
|
||||||
|
mousePosition = event.position;
|
||||||
|
final cfg = context.read<ConfigProvider>();
|
||||||
|
if (!cfg.drawerIsCollapsed) {
|
||||||
|
// Leave padding for side navigation
|
||||||
|
mousePosition = cfg.drawerIsExpanded
|
||||||
|
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
||||||
|
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: GestureDetector(
|
||||||
|
onLongPress: () => _showMenu(context, mousePosition),
|
||||||
|
onSecondaryTap: () => _showMenu(context, mousePosition),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showMenu(BuildContext context, Offset mousePosition) async {
|
||||||
|
final menu = contextMenu.copyWith(position: contextMenu.position ?? mousePosition);
|
||||||
|
final value = await showContextMenu(context, contextMenu: menu);
|
||||||
|
onItemSelected?.call(value);
|
||||||
|
}
|
||||||
|
}
|
@ -60,7 +60,6 @@ class _LinkPreviewEntry extends StatelessWidget {
|
|||||||
final SnLinkMeta meta;
|
final SnLinkMeta meta;
|
||||||
|
|
||||||
const _LinkPreviewEntry({
|
const _LinkPreviewEntry({
|
||||||
super.key,
|
|
||||||
required this.meta,
|
required this.meta,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -18,9 +18,9 @@ import 'package:screenshot/screenshot.dart';
|
|||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.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/config.dart';
|
||||||
import 'package:surface/providers/link_preview.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';
|
||||||
@ -85,7 +85,6 @@ class PostItem extends StatelessWidget {
|
|||||||
child: MultiProvider(
|
child: MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
Provider<SnNetworkProvider>(create: (_) => context.read()),
|
Provider<SnNetworkProvider>(create: (_) => context.read()),
|
||||||
Provider<SnLinkPreviewProvider>(create: (_) => context.read()),
|
|
||||||
ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()),
|
ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()),
|
||||||
],
|
],
|
||||||
child: ResponsiveBreakpoints.builder(
|
child: ResponsiveBreakpoints.builder(
|
||||||
@ -114,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();
|
||||||
@ -200,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: [
|
||||||
@ -249,12 +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,
|
||||||
maxHeight: 560,
|
gridded: true,
|
||||||
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
maxHeight: showFullPost ? null : 480,
|
||||||
|
minWidth: 640,
|
||||||
|
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
if (data.body['content'] != null)
|
if (data.body['content'] != null)
|
||||||
LinkPreviewWidget(
|
LinkPreviewWidget(
|
||||||
@ -334,17 +340,12 @@ class PostShareImageWidget extends StatelessWidget {
|
|||||||
_PostQuoteContent(
|
_PostQuoteContent(
|
||||||
child: data.repostTo!,
|
child: data.repostTo!,
|
||||||
isRelativeDate: false,
|
isRelativeDate: false,
|
||||||
isFlatted: true,
|
|
||||||
).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!,
|
||||||
isFlatted: true,
|
gridded: true,
|
||||||
).padding(horizontal: 16, bottom: 8),
|
)).padding(horizontal: 16, bottom: 8),
|
||||||
if (data.body['content'] != null)
|
|
||||||
LinkPreviewWidget(
|
|
||||||
text: data.body['content'],
|
|
||||||
).padding(horizontal: 4),
|
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -549,7 +550,6 @@ class _PostHeadline extends StatelessWidget {
|
|||||||
final bool isEnlarge;
|
final bool isEnlarge;
|
||||||
|
|
||||||
const _PostHeadline({
|
const _PostHeadline({
|
||||||
super.key,
|
|
||||||
required this.data,
|
required this.data,
|
||||||
this.isEnlarge = false,
|
this.isEnlarge = false,
|
||||||
});
|
});
|
||||||
@ -879,24 +879,26 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PostQuoteContent extends StatelessWidget {
|
class _PostQuoteContent extends StatelessWidget {
|
||||||
final SnPost child;
|
final SnPost child;
|
||||||
final bool isRelativeDate;
|
final bool isRelativeDate;
|
||||||
final bool isFlatted;
|
|
||||||
|
|
||||||
const _PostQuoteContent({
|
const _PostQuoteContent({
|
||||||
super.key,
|
|
||||||
this.isRelativeDate = true,
|
this.isRelativeDate = true,
|
||||||
this.isFlatted = false,
|
|
||||||
required this.child,
|
required this.child,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -938,12 +940,15 @@ class _PostQuoteContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: AttachmentList(
|
child: AttachmentList(
|
||||||
data: child.preload!.attachments!,
|
data: child.preload!.attachments!,
|
||||||
isFlatted: isFlatted,
|
maxHeight: 360,
|
||||||
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
minWidth: 640,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
gridded: true,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
).padding(
|
).padding(
|
||||||
top: 8,
|
top: 8,
|
||||||
bottom: (child.preload?.attachments?.length ?? 0) > 1 ? 12 : 0,
|
bottom: 12,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@ -962,7 +967,7 @@ class _PostQuoteContent extends StatelessWidget {
|
|||||||
class _PostTagsList extends StatelessWidget {
|
class _PostTagsList extends StatelessWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
|
|
||||||
const _PostTagsList({super.key, required this.data});
|
const _PostTagsList({required this.data});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -1035,7 +1040,7 @@ class _PostTagsList extends StatelessWidget {
|
|||||||
class _PostVisibilityHint extends StatelessWidget {
|
class _PostVisibilityHint extends StatelessWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
|
|
||||||
const _PostVisibilityHint({super.key, required this.data});
|
const _PostVisibilityHint({required this.data});
|
||||||
|
|
||||||
static const List<IconData> kVisibilityIcons = [
|
static const List<IconData> kVisibilityIcons = [
|
||||||
Symbols.public,
|
Symbols.public,
|
||||||
@ -1060,7 +1065,7 @@ class _PostVisibilityHint extends StatelessWidget {
|
|||||||
class _PostTruncatedHint extends StatelessWidget {
|
class _PostTruncatedHint extends StatelessWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
|
|
||||||
const _PostTruncatedHint({super.key, required this.data});
|
const _PostTruncatedHint({required this.data});
|
||||||
|
|
||||||
static const int kHumanReadSpeed = 238;
|
static const int kHumanReadSpeed = 238;
|
||||||
|
|
||||||
@ -1102,7 +1107,7 @@ class _PostTruncatedHint extends StatelessWidget {
|
|||||||
class _PostAbuseReportDialog extends StatefulWidget {
|
class _PostAbuseReportDialog extends StatefulWidget {
|
||||||
final SnPost data;
|
final SnPost data;
|
||||||
|
|
||||||
const _PostAbuseReportDialog({super.key, required this.data});
|
const _PostAbuseReportDialog({required this.data});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_PostAbuseReportDialog> createState() => _PostAbuseReportDialogState();
|
State<_PostAbuseReportDialog> createState() => _PostAbuseReportDialogState();
|
||||||
|
@ -6,15 +6,28 @@ import 'package:dismissible_page/dismissible_page.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:pasteboard/pasteboard.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/controllers/post_write_controller.dart';
|
import 'package:surface/controllers/post_write_controller.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/types/attachment.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/context_menu.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
import 'package:surface/widgets/universal_image.dart';
|
||||||
|
|
||||||
|
import '../attachment/pending_attachment_compress.dart';
|
||||||
|
|
||||||
class PostMediaPendingList extends StatelessWidget {
|
class PostMediaPendingList extends StatelessWidget {
|
||||||
final PostWriteMedia? thumbnail;
|
final PostWriteMedia? thumbnail;
|
||||||
@ -70,6 +83,37 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _setThumbnail(BuildContext context, int idx) async {
|
||||||
|
if (idx == -1) {
|
||||||
|
// Thumbnail only can set on video or audio. And thumbnail of the post must be an image, so it's not possible to set thumbnail on the post thumbnail.
|
||||||
|
return;
|
||||||
|
} else if (attachments[idx].attachment == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final thumbnail = await showDialog<SnAttachment?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AttachmentInputDialog(
|
||||||
|
title: 'attachmentSetThumbnail'.tr(),
|
||||||
|
analyzeNow: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (thumbnail == null) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
final newAttach = await attach.updateOne(
|
||||||
|
attachments[idx].attachment!,
|
||||||
|
thumbnailId: thumbnail.id,
|
||||||
|
);
|
||||||
|
onUpdate!(idx, PostWriteMedia(newAttach));
|
||||||
|
} catch (err) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _deleteAttachment(BuildContext context, int idx) async {
|
Future<void> _deleteAttachment(BuildContext context, int idx) async {
|
||||||
final media = idx == -1 ? thumbnail! : attachments[idx];
|
final media = idx == -1 ? thumbnail! : attachments[idx];
|
||||||
if (media.attachment == null) return;
|
if (media.attachment == null) return;
|
||||||
@ -87,9 +131,79 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextMenu _buildContextMenu(BuildContext context, int idx, PostWriteMedia media) {
|
Future<void> _createBoost(BuildContext context, int idx) async {
|
||||||
|
if (attachments[idx].attachment == null) return;
|
||||||
|
|
||||||
|
final result = await showDialog<SnAttachmentBoost?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PendingAttachmentBoostDialog(media: attachments[idx]),
|
||||||
|
);
|
||||||
|
if (result == null) return;
|
||||||
|
|
||||||
|
final newAttach = attachments[idx].attachment!.copyWith(
|
||||||
|
boosts: [...attachments[idx].attachment!.boosts, result],
|
||||||
|
);
|
||||||
|
final newMedia = PostWriteMedia(newAttach);
|
||||||
|
|
||||||
|
onUpdate!(idx, newMedia);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _compressVideo(BuildContext context, int idx) async {
|
||||||
|
final result = await showDialog<PostWriteMedia?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => PendingVideoCompressDialog(media: attachments[idx]),
|
||||||
|
);
|
||||||
|
if (result == null) return;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
final canCompressVideo = !kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS);
|
||||||
return ContextMenu(
|
return ContextMenu(
|
||||||
entries: [
|
entries: [
|
||||||
|
if (media.attachment == null && media.type == SnMediaType.video && canCompressVideo)
|
||||||
|
MenuItem(
|
||||||
|
label: 'attachmentCompressVideo'.tr(),
|
||||||
|
icon: Symbols.compress,
|
||||||
|
onSelected: () {
|
||||||
|
_compressVideo(context, idx);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (media.attachment != null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'attachmentSetAlt'.tr(),
|
||||||
|
icon: Symbols.description,
|
||||||
|
onSelected: () {
|
||||||
|
_setAlt(context, idx);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (media.attachment != null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'attachmentBoost'.tr(),
|
||||||
|
icon: Symbols.bolt,
|
||||||
|
onSelected: () {
|
||||||
|
_createBoost(context, idx);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (media.attachment != null && media.type == SnMediaType.video)
|
||||||
|
MenuItem(
|
||||||
|
label: 'attachmentSetThumbnail'.tr(),
|
||||||
|
icon: Symbols.image,
|
||||||
|
onSelected: () {
|
||||||
|
_setThumbnail(context, idx);
|
||||||
|
},
|
||||||
|
),
|
||||||
if (media.attachment == null && onUpload != null)
|
if (media.attachment == null && onUpload != null)
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'attachmentUpload'.tr(),
|
label: 'attachmentUpload'.tr(),
|
||||||
@ -97,7 +211,7 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
onSelected: () {
|
onSelected: () {
|
||||||
onUpload!(idx);
|
onUpload!(idx);
|
||||||
}),
|
}),
|
||||||
if (media.attachment != null && onPostSetThumbnail != null && idx != -1)
|
if (media.attachment != null && media.type == SnMediaType.image && onPostSetThumbnail != null && idx != -1)
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'attachmentSetAsPostThumbnail'.tr(),
|
label: 'attachmentSetAsPostThumbnail'.tr(),
|
||||||
icon: Symbols.gallery_thumbnail,
|
icon: Symbols.gallery_thumbnail,
|
||||||
@ -105,7 +219,7 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
onPostSetThumbnail!(idx);
|
onPostSetThumbnail!(idx);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else if (media.attachment != null && onPostSetThumbnail != null)
|
else if (media.attachment != null && media.type == SnMediaType.image && onPostSetThumbnail != null)
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'attachmentUnsetAsPostThumbnail'.tr(),
|
label: 'attachmentUnsetAsPostThumbnail'.tr(),
|
||||||
icon: Symbols.cancel,
|
icon: Symbols.cancel,
|
||||||
@ -121,7 +235,7 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
onInsertLink!(idx);
|
onInsertLink!(idx);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (media.type == PostWriteMediaType.image && media.attachment != null)
|
if (media.type == SnMediaType.image && media.attachment != null)
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'preview'.tr(),
|
label: 'preview'.tr(),
|
||||||
icon: Symbols.preview,
|
icon: Symbols.preview,
|
||||||
@ -132,12 +246,20 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (media.type == PostWriteMediaType.image && media.attachment == null)
|
if (media.type == SnMediaType.image && media.attachment == null)
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'crop'.tr(),
|
label: 'crop'.tr(),
|
||||||
icon: Symbols.crop,
|
icon: Symbols.crop,
|
||||||
onSelected: () => _cropImage(context, idx),
|
onSelected: () => _cropImage(context, idx),
|
||||||
),
|
),
|
||||||
|
if (media.attachment != null)
|
||||||
|
MenuItem(
|
||||||
|
label: 'attachmentCopyRandomId'.tr(),
|
||||||
|
icon: Symbols.content_copy,
|
||||||
|
onSelected: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: media.attachment!.rid));
|
||||||
|
},
|
||||||
|
),
|
||||||
if (media.attachment != null && onRemove != null)
|
if (media.attachment != null && onRemove != null)
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'delete'.tr(),
|
label: 'delete'.tr(),
|
||||||
@ -166,50 +288,15 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
constraints: const BoxConstraints(maxHeight: 120),
|
constraints: const BoxConstraints(maxHeight: 120),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
if (thumbnail != null)
|
if (thumbnail != null)
|
||||||
ContextMenuRegion(
|
ContextMenuArea(
|
||||||
contextMenu: _buildContextMenu(context, -1, thumbnail!),
|
contextMenu: _createContextMenu(context, -1, thumbnail!),
|
||||||
child: Container(
|
child: _PostMediaPendingItem(media: thumbnail!),
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: switch (thumbnail!.type) {
|
|
||||||
PostWriteMediaType.image => Container(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
|
||||||
return Image(
|
|
||||||
image: thumbnail!.getImageProvider(
|
|
||||||
context,
|
|
||||||
width: (constraints.maxWidth * devicePixelRatio).round(),
|
|
||||||
height: (constraints.maxHeight * devicePixelRatio).round(),
|
|
||||||
)!,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
_ => Container(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: const Icon(Symbols.docs).center(),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (thumbnail != null)
|
if (thumbnail != null)
|
||||||
const VerticalDivider(width: 1, thickness: 1).padding(
|
const VerticalDivider(width: 1, thickness: 1).padding(
|
||||||
@ -224,42 +311,9 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
itemCount: attachments.length,
|
itemCount: attachments.length,
|
||||||
itemBuilder: (context, idx) {
|
itemBuilder: (context, idx) {
|
||||||
final media = attachments[idx];
|
final media = attachments[idx];
|
||||||
return ContextMenuRegion(
|
return ContextMenuArea(
|
||||||
contextMenu: _buildContextMenu(context, idx, media),
|
contextMenu: _createContextMenu(context, idx, media),
|
||||||
child: Container(
|
child: _PostMediaPendingItem(media: media),
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: 1,
|
|
||||||
child: switch (media.type) {
|
|
||||||
PostWriteMediaType.image => Container(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
|
||||||
return Image(
|
|
||||||
image: media.getImageProvider(
|
|
||||||
context,
|
|
||||||
width: (constraints.maxWidth * devicePixelRatio).round(),
|
|
||||||
height: (constraints.maxHeight * devicePixelRatio).round(),
|
|
||||||
)!,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
_ => Container(
|
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
|
||||||
child: const Icon(Symbols.docs).center(),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -269,3 +323,319 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PostMediaPendingItem extends StatelessWidget {
|
||||||
|
final PostWriteMedia media;
|
||||||
|
|
||||||
|
const _PostMediaPendingItem({
|
||||||
|
required this.media,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: switch (media.type) {
|
||||||
|
SnMediaType.image => LayoutBuilder(builder: (context, constraints) {
|
||||||
|
return Image(
|
||||||
|
image: media.getImageProvider(
|
||||||
|
context,
|
||||||
|
width: (constraints.maxWidth * devicePixelRatio).round(),
|
||||||
|
height: (constraints.maxHeight * devicePixelRatio).round(),
|
||||||
|
)!,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
SnMediaType.video => Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
if (media.attachment?.thumbnail != null)
|
||||||
|
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
|
||||||
|
const Icon(Symbols.videocam, color: Colors.white, shadows: [
|
||||||
|
Shadow(
|
||||||
|
offset: Offset(1, 1),
|
||||||
|
blurRadius: 8.0,
|
||||||
|
color: Color.fromARGB(255, 0, 0, 0),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SnMediaType.audio => Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
if (media.attachment?.thumbnail != null)
|
||||||
|
AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment!.thumbnail!.rid)),
|
||||||
|
const Icon(Symbols.audio_file, color: Colors.white, shadows: [
|
||||||
|
Shadow(
|
||||||
|
offset: Offset(1, 1),
|
||||||
|
blurRadius: 8.0,
|
||||||
|
color: Color.fromARGB(255, 0, 0, 0),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_ => Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: const Icon(Symbols.docs).center(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (media.type != SnMediaType.image) const VerticalDivider(width: 1, thickness: 1),
|
||||||
|
if (media.type != SnMediaType.image)
|
||||||
|
SizedBox(
|
||||||
|
width: 160,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (media.attachment != null)
|
||||||
|
Text(
|
||||||
|
media.attachment!.alt,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
else if (media.file != null)
|
||||||
|
Text(media.file!.name, maxLines: 1, overflow: TextOverflow.ellipsis)
|
||||||
|
else
|
||||||
|
Text('unknown'.tr()),
|
||||||
|
if (media.attachment != null)
|
||||||
|
Text(
|
||||||
|
media.attachment!.size.formatBytes(),
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
maxLines: 1,
|
||||||
|
)
|
||||||
|
else if (media.file != null)
|
||||||
|
FutureBuilder<int?>(
|
||||||
|
future: media.length(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) return const SizedBox.shrink();
|
||||||
|
return Text(
|
||||||
|
snapshot.data!.formatBytes(),
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
maxLines: 1,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (media.attachment != null && media.attachment!.boosts.isNotEmpty)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.bolt, size: 16),
|
||||||
|
const Gap(4),
|
||||||
|
Text('attachmentGotBoosted').tr().fontSize(13),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (media.attachment != null && media.attachment!.compressedId != null)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.compress, size: 16),
|
||||||
|
const Gap(4),
|
||||||
|
Text('attachmentCopyCompressed').tr().fontSize(13),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (media.attachment != null)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.cloud, size: 16),
|
||||||
|
const Gap(4),
|
||||||
|
Text('attachmentUploaded').tr().fontSize(13),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Symbols.cloud_off, size: 16),
|
||||||
|
const Gap(4),
|
||||||
|
Text('attachmentPending').tr().fontSize(13),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).padding(horizontal: 12, vertical: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddPostMediaButton extends StatelessWidget {
|
||||||
|
final Function(Iterable<PostWriteMedia>) onAdd;
|
||||||
|
|
||||||
|
const AddPostMediaButton({super.key, required this.onAdd});
|
||||||
|
|
||||||
|
void _takeMedia(bool isVideo) async {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final result = isVideo
|
||||||
|
? await picker.pickVideo(source: ImageSource.camera)
|
||||||
|
: await picker.pickImage(source: ImageSource.camera);
|
||||||
|
if (result == null) return;
|
||||||
|
onAdd([PostWriteMedia.fromFile(result)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectMedia() async {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final result = await picker.pickMultipleMedia();
|
||||||
|
if (result.isEmpty) return;
|
||||||
|
onAdd(
|
||||||
|
result.map((e) => PostWriteMedia.fromFile(e)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pasteMedia() async {
|
||||||
|
final imageBytes = await Pasteboard.image;
|
||||||
|
if (imageBytes == null) return;
|
||||||
|
onAdd([
|
||||||
|
PostWriteMedia.fromBytes(
|
||||||
|
imageBytes,
|
||||||
|
'attachmentPastedImage'.tr(),
|
||||||
|
SnMediaType.image,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _linkRandomId(BuildContext context) async {
|
||||||
|
final randomIdController = TextEditingController();
|
||||||
|
final randomId = await showDialog<String?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text('addAttachmentFromRandomId').tr(),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: randomIdController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'fieldAttachmentRandomId'.tr(),
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: Text('dialogDismiss').tr(),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: Text('dialogConfirm').tr(),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, randomIdController.text);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
randomIdController.dispose();
|
||||||
|
});
|
||||||
|
if (randomId == null || randomId.isEmpty) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
final attachment = await attach.getOne(randomId);
|
||||||
|
|
||||||
|
onAdd([
|
||||||
|
PostWriteMedia(attachment),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopupMenuButton(
|
||||||
|
icon: Icon(
|
||||||
|
Symbols.add_photo_alternate,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.photo_camera),
|
||||||
|
const Gap(16),
|
||||||
|
Text('addAttachmentFromCameraPhoto').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_takeMedia(false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.videocam),
|
||||||
|
const Gap(16),
|
||||||
|
Text('addAttachmentFromCameraVideo').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_takeMedia(true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.photo_library),
|
||||||
|
const Gap(16),
|
||||||
|
Text('addAttachmentFromAlbum').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_selectMedia();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.link),
|
||||||
|
const Gap(16),
|
||||||
|
Text('addAttachmentFromRandomId').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_linkRandomId(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.content_paste),
|
||||||
|
const Gap(16),
|
||||||
|
Text('addAttachmentFromClipboard').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_pasteMedia();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -28,6 +28,7 @@ import share_plus
|
|||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
import video_compress
|
||||||
import wakelock_plus
|
import wakelock_plus
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
@ -54,5 +55,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin"))
|
||||||
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
|
||||||
}
|
}
|
||||||
|
@ -134,7 +134,7 @@ PODS:
|
|||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- in_app_review (2.0.0):
|
- in_app_review (2.0.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- livekit_client (2.3.2):
|
- livekit_client (2.3.4):
|
||||||
- flutter_webrtc
|
- flutter_webrtc
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- WebRTC-SDK (= 125.6422.06)
|
- WebRTC-SDK (= 125.6422.06)
|
||||||
@ -170,6 +170,8 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- url_launcher_macos (0.0.1):
|
- url_launcher_macos (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- video_compress (0.3.0):
|
||||||
|
- FlutterMacOS
|
||||||
- wakelock_plus (0.0.1):
|
- wakelock_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- WebRTC-SDK (125.6422.06)
|
- WebRTC-SDK (125.6422.06)
|
||||||
@ -201,6 +203,7 @@ DEPENDENCIES:
|
|||||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||||
|
- video_compress (from `Flutter/ephemeral/.symlinks/plugins/video_compress/macos`)
|
||||||
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
@ -272,6 +275,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||||
|
video_compress:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/video_compress/macos
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos
|
||||||
|
|
||||||
@ -299,7 +304,7 @@ SPEC CHECKSUMS:
|
|||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||||
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
in_app_review: a6a031b9acd03c7d103e341aa334adf2c493fb93
|
||||||
livekit_client: 9fdcb22df3de55e6d4b24bdc3b5eb1c0269d774a
|
livekit_client: b7ab91e79e657d7d40da16cb2f90d517cb72d406
|
||||||
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
|
||||||
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
|
||||||
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
|
||||||
@ -314,6 +319,7 @@ SPEC CHECKSUMS:
|
|||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||||
|
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f
|
||||||
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269
|
||||||
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
|
WebRTC-SDK: 79942c006ea64f6fb48d7da8a4786dfc820bc1db
|
||||||
|
|
||||||
|
86
pubspec.lock
86
pubspec.lock
@ -490,10 +490,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: c2376a6aae82358a9f9ccdd7d1f4006d08faa39a2767cce01031d9f593a8bd3b
|
sha256: c904b4ab56d53385563c7c39d8e9fa9af086f91495dfc48717ad84a42c3cf204
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.6"
|
version: "8.1.7"
|
||||||
file_saver:
|
file_saver:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -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:
|
||||||
@ -753,23 +753,23 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_udid
|
name: flutter_udid
|
||||||
sha256: be464dc5b1fb7ee894f6a32d65c086ca5e177fdcf9375ac08d77495b98150f84
|
sha256: "166bee5989a58c66b8b62000ea65edccc7c8167bbafdbb08022638db330dd030"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "4.0.0"
|
||||||
flutter_web_plugins:
|
flutter_web_plugins:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_webrtc:
|
flutter_webrtc:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_webrtc
|
name: flutter_webrtc
|
||||||
sha256: "430859fb5b763d7556d06ef287cfca582e17d9a2dc36da26017f25a5c0b2523e"
|
sha256: e82ffd0d0b79621c5554eed73509d7f5bd286d57fef29a573846785c65237fb1
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.4"
|
version: "0.12.5+hotfix.2"
|
||||||
freezed:
|
freezed:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
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:
|
||||||
@ -934,10 +934,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: fa8141602fde3f7e2f81dbf043613eb44dfa325fa0bcf93c0f142c9f7a2c193e
|
sha256: aa6f1280b670861ac45220cc95adc59bb6ae130259d36f980ccb62220dc5e59f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.12+18"
|
version: "0.8.12+19"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
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: "7802b5de1cae2ee3439db730d24d31c6dcbce173c5e6db2fc5774039a290bc2d"
|
sha256: a19bcf8640b45e0730b1e3e3e78be7882dad680c6ebe8ae75294fd8d4612450d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
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:
|
||||||
@ -1354,6 +1354,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
pausable_timer:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pausable_timer
|
||||||
|
sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.0+3"
|
||||||
permission_handler:
|
permission_handler:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1638,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:
|
||||||
@ -1711,6 +1719,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
slide_countdown:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: slide_countdown
|
||||||
|
sha256: "363914f96389502467d4dc9c0f26e88f93df3d8e37de2d5ff05b16d981fe973d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
sliver_tools:
|
sliver_tools:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1779,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:
|
||||||
@ -1847,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:
|
||||||
@ -2031,6 +2039,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.0"
|
version: "0.9.0"
|
||||||
|
video_compress:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: video_compress
|
||||||
|
sha256: "5b42d89f3970c956bad7a86c29682b0892c11a4ddf95ae6e29897ee28788e377"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -2107,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:
|
||||||
|
10
pubspec.yaml
10
pubspec.yaml
@ -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.1.1+39
|
version: 2.2.2+50
|
||||||
|
|
||||||
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
|
||||||
@ -80,7 +78,7 @@ dependencies:
|
|||||||
firebase_core: ^3.8.0
|
firebase_core: ^3.8.0
|
||||||
firebase_messaging: ^15.1.5
|
firebase_messaging: ^15.1.5
|
||||||
firebase_analytics: ^11.3.5
|
firebase_analytics: ^11.3.5
|
||||||
flutter_udid: ^3.0.0
|
flutter_udid: ^4.0.0
|
||||||
media_kit: ^1.1.11
|
media_kit: ^1.1.11
|
||||||
media_kit_video: ^1.2.5
|
media_kit_video: ^1.2.5
|
||||||
media_kit_libs_video: ^1.0.5
|
media_kit_libs_video: ^1.0.5
|
||||||
@ -113,6 +111,10 @@ dependencies:
|
|||||||
version: ^3.0.2
|
version: ^3.0.2
|
||||||
flutter_colorpicker: ^1.1.0
|
flutter_colorpicker: ^1.1.0
|
||||||
fl_chart: ^0.70.0
|
fl_chart: ^0.70.0
|
||||||
|
flutter_webrtc: ^0.12.5+hotfix.1
|
||||||
|
slide_countdown: ^2.0.2
|
||||||
|
video_compress: ^3.1.3
|
||||||
|
cached_network_image: ^3.4.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
221
web/index.html
221
web/index.html
@ -1,130 +1,133 @@
|
|||||||
<!DOCTYPE html><html><head>
|
<!DOCTYPE html>
|
||||||
<!--
|
<html lang="en" oncontextmenu="event.preventDefault();">
|
||||||
If you are serving your web app in a path other than the root, change the
|
<head>
|
||||||
href value below to reflect the base path you are serving from.
|
<!--
|
||||||
|
If you are serving your web app in a path other than the root, change the
|
||||||
|
href value below to reflect the base path you are serving from.
|
||||||
|
|
||||||
The path provided below has to start and end with a slash "/" in order for
|
The path provided below has to start and end with a slash "/" in order for
|
||||||
it to work correctly.
|
it to work correctly.
|
||||||
|
|
||||||
For more details:
|
For more details:
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||||
|
|
||||||
This is a placeholder for base href that will be replaced by the value of
|
This is a placeholder for base href that will be replaced by the value of
|
||||||
the `--base-href` argument provided to `flutter build`.
|
the `--base-href` argument provided to `flutter build`.
|
||||||
-->
|
-->
|
||||||
<base href="$FLUTTER_BASE_HREF">
|
<base href="$FLUTTER_BASE_HREF">
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="A new Flutter project.">
|
<meta name="description" content="A new Flutter project.">
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="surface">
|
<meta name="apple-mobile-web-app-title" content="surface">
|
||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png">
|
<link rel="icon" type="image/png" href="favicon.png">
|
||||||
|
|
||||||
<title>Solian</title>
|
<title>Solian</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
|
|
||||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style id="splash-screen-style">
|
|
||||||
html {
|
|
||||||
height: 100%
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100%;
|
|
||||||
background-color: #ffffff;
|
|
||||||
background-size: 100% 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
margin: 0;
|
name="viewport">
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
-ms-transform: translate(-50%, -50%);
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.contain {
|
|
||||||
display:block;
|
|
||||||
width:100%; height:100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stretch {
|
<style id="splash-screen-style">
|
||||||
display:block;
|
html {
|
||||||
width:100%; height:100%;
|
height: 100%
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover {
|
body {
|
||||||
display:block;
|
margin: 0;
|
||||||
width:100%; height:100%;
|
min-height: 100%;
|
||||||
object-fit: cover;
|
background-color: #ffffff;
|
||||||
}
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.bottom {
|
.center {
|
||||||
position: absolute;
|
margin: 0;
|
||||||
bottom: 0;
|
position: absolute;
|
||||||
left: 50%;
|
top: 50%;
|
||||||
-ms-transform: translate(-50%, 0);
|
left: 50%;
|
||||||
transform: translate(-50%, 0);
|
-ms-transform: translate(-50%, -50%);
|
||||||
}
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
.bottomLeft {
|
.contain {
|
||||||
position: absolute;
|
display:block;
|
||||||
bottom: 0;
|
width:100%; height:100%;
|
||||||
left: 0;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottomRight {
|
.stretch {
|
||||||
position: absolute;
|
display:block;
|
||||||
bottom: 0;
|
width:100%; height:100%;
|
||||||
right: 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
.cover {
|
||||||
body {
|
display:block;
|
||||||
background-color: #000000;
|
width:100%; height:100%;
|
||||||
}
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
<script id="splash-screen-script">
|
.bottom {
|
||||||
function removeSplashFromWeb() {
|
position: absolute;
|
||||||
document.getElementById("splash")?.remove();
|
bottom: 0;
|
||||||
document.getElementById("splash-branding")?.remove();
|
left: 50%;
|
||||||
document.body.style.background = "transparent";
|
-ms-transform: translate(-50%, 0);
|
||||||
}
|
transform: translate(-50%, 0);
|
||||||
</script>
|
}
|
||||||
|
|
||||||
|
.bottomLeft {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomRight {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: #000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script id="splash-screen-script">
|
||||||
|
function removeSplashFromWeb() {
|
||||||
|
document.getElementById("splash")?.remove();
|
||||||
|
document.getElementById("splash-branding")?.remove();
|
||||||
|
document.body.style.background = "transparent";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<picture id="splash-branding">
|
<picture id="splash-branding">
|
||||||
<source srcset="splash/img/branding-1x.png 1x, splash/img/branding-2x.png 2x, splash/img/branding-3x.png 3x, splash/img/branding-4x.png 4x" media="(prefers-color-scheme: light)">
|
<source srcset="splash/img/branding-1x.png 1x, splash/img/branding-2x.png 2x, splash/img/branding-3x.png 3x, splash/img/branding-4x.png 4x"
|
||||||
<source srcset="splash/img/branding-dark-1x.png 1x, splash/img/branding-dark-2x.png 2x, splash/img/branding-dark-3x.png 3x, splash/img/branding-dark-4x.png 4x" media="(prefers-color-scheme: dark)">
|
media="(prefers-color-scheme: light)">
|
||||||
|
<source srcset="splash/img/branding-dark-1x.png 1x, splash/img/branding-dark-2x.png 2x, splash/img/branding-dark-3x.png 3x, splash/img/branding-dark-4x.png 4x"
|
||||||
|
media="(prefers-color-scheme: dark)">
|
||||||
<img class="bottom" aria-hidden="true" src="splash/img/branding-1x.png" alt="">
|
<img class="bottom" aria-hidden="true" src="splash/img/branding-1x.png" alt="">
|
||||||
</picture>
|
</picture>
|
||||||
<picture id="splash">
|
<picture id="splash">
|
||||||
<source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x" media="(prefers-color-scheme: light)">
|
<source srcset="splash/img/light-1x.png 1x, splash/img/light-2x.png 2x, splash/img/light-3x.png 3x, splash/img/light-4x.png 4x"
|
||||||
<source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x" media="(prefers-color-scheme: dark)">
|
media="(prefers-color-scheme: light)">
|
||||||
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
|
<source srcset="splash/img/dark-1x.png 1x, splash/img/dark-2x.png 2x, splash/img/dark-3x.png 3x, splash/img/dark-4x.png 4x"
|
||||||
</picture>
|
media="(prefers-color-scheme: dark)">
|
||||||
|
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script src="flutter_bootstrap.js" async=""></script>
|
|
||||||
|
|
||||||
|
|
||||||
</body></html>
|
<script src="flutter_bootstrap.js" async=""></script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in New Issue
Block a user