Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
7fbd4e9647 | |||
95d926b29f | |||
f6cf6d0440 | |||
e503c3f02f | |||
d4fbdd397e | |||
03943a7138 | |||
44f2c5fe0e | |||
bb66d5b684 | |||
1fca36293d | |||
2c7dc8c2ea | |||
cf0df91d8c | |||
91c85e8a58 | |||
2851780dda | |||
00fd58fb97 | |||
ee7d0ddd25 | |||
7656c08832 | |||
619c90cdd9 | |||
168d51c9fe | |||
d4b831f98e | |||
4d96a15c31 | |||
06dd3e092a | |||
82fe9e287a | |||
dc1c285de1 | |||
5a3313e94f | |||
61032c84f1 | |||
36a5b8fb39 | |||
3eda464e03 |
@ -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
|
||||||
|
30
api/Paperclip/Activate Boost.bru
Normal file
30
api/Paperclip/Activate Boost.bru
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
meta {
|
||||||
|
name: Activate Boost
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/uc/boosts/1/activate
|
||||||
|
body: none
|
||||||
|
auth: bearer
|
||||||
|
}
|
||||||
|
|
||||||
|
auth:bearer {
|
||||||
|
token: {{atk}}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
30
api/Passport/Developer Notify All Users.bru
Normal file
30
api/Passport/Developer Notify All Users.bru
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
meta {
|
||||||
|
name: Developer Notify All Users
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/id/dev/notify/all
|
||||||
|
body: json
|
||||||
|
auth: bearer
|
||||||
|
}
|
||||||
|
|
||||||
|
auth:bearer {
|
||||||
|
token: {{atk}}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
]
|
||||||
|
}
|
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
|
||||||
|
]
|
@ -281,16 +281,43 @@
|
|||||||
"one": "{} attachment",
|
"one": "{} attachment",
|
||||||
"other": "{} attachments"
|
"other": "{} attachments"
|
||||||
},
|
},
|
||||||
|
"fieldAttachmentRandomId": "Random ID",
|
||||||
"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",
|
||||||
|
"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",
|
||||||
@ -378,9 +405,26 @@
|
|||||||
"dailyCheckNegativeHint5Description": "Lost connection at a crucial moment",
|
"dailyCheckNegativeHint5Description": "Lost connection at a crucial moment",
|
||||||
"dailyCheckNegativeHint6": "Going out",
|
"dailyCheckNegativeHint6": "Going out",
|
||||||
"dailyCheckNegativeHint6Description": "Forgot your umbrella and got caught in the rain",
|
"dailyCheckNegativeHint6Description": "Forgot your umbrella and got caught in the rain",
|
||||||
"happyBirthday": "Happy birthday, {}!",
|
"celebrateBirthday": "Happy birthday, {}!",
|
||||||
"celebrateMerryXmas": "Merry christmas, {}!",
|
"celebrateMerryXmas": "Merry christmas, {}!",
|
||||||
"celebrateNewYear": "Happy new year, {}!",
|
"celebrateNewYear": "Happy new year, {}!",
|
||||||
|
"celebrateValentineDay": "Today is valentine's day, {}!",
|
||||||
|
"celebrateLaborDay": "Today is labor day, {}.",
|
||||||
|
"celebrateMotherDay": "Today is mother's day, {}.",
|
||||||
|
"celebrateChildrenDay": "Today is children's day, {}!",
|
||||||
|
"celebrateFatherDay": "Today is father's day, {}.",
|
||||||
|
"celebrateHalloween": "Happy halloween, {}!",
|
||||||
|
"celebrateThanksgiving": "Today is thanksgiving day, {}!",
|
||||||
|
"pendingBirthday": "Birthday in {}",
|
||||||
|
"pendingMerryXmas": "Christmas in {}",
|
||||||
|
"pendingNewYear": "New year in {}",
|
||||||
|
"pendingValentineDay": "Valentine's day in {}",
|
||||||
|
"pendingLaborDay": "Labor day in {}",
|
||||||
|
"pendingMotherDay": "Mother's day in {}",
|
||||||
|
"pendingChildrenDay": "Children's day in {}",
|
||||||
|
"pendingFatherDay": "Father's day in {}",
|
||||||
|
"pendingHalloween": "Halloween in {}",
|
||||||
|
"pendingThanksgiving": "Thanksgiving day in {}",
|
||||||
"friendNew": "Add Friend",
|
"friendNew": "Add Friend",
|
||||||
"friendRequests": "Friend Requests",
|
"friendRequests": "Friend Requests",
|
||||||
"friendRequestsDescription": {
|
"friendRequestsDescription": {
|
||||||
@ -414,6 +458,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",
|
||||||
@ -465,7 +510,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...",
|
||||||
@ -488,5 +533,6 @@
|
|||||||
"postCategoryNews": "News",
|
"postCategoryNews": "News",
|
||||||
"postCategoryKnowledge": "Knowledge",
|
"postCategoryKnowledge": "Knowledge",
|
||||||
"postCategoryLiterature": "Literature",
|
"postCategoryLiterature": "Literature",
|
||||||
|
"postCategoryFunny": "Funny",
|
||||||
"postCategoryUncategorized": "Uncategorized"
|
"postCategoryUncategorized": "Uncategorized"
|
||||||
}
|
}
|
||||||
|
@ -279,16 +279,43 @@
|
|||||||
"one": "{} 个附件",
|
"one": "{} 个附件",
|
||||||
"other": "{} 个附件"
|
"other": "{} 个附件"
|
||||||
},
|
},
|
||||||
|
"fieldAttachmentRandomId": "访问 ID",
|
||||||
"addAttachmentFromAlbum": "从相册中添加附件",
|
"addAttachmentFromAlbum": "从相册中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘贴附件",
|
"addAttachmentFromClipboard": "粘贴附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍摄照片",
|
"addAttachmentFromCameraPhoto": "拍摄照片",
|
||||||
"addAttachmentFromCameraVideo": "拍摄视频",
|
"addAttachmentFromCameraVideo": "拍摄视频",
|
||||||
|
"addAttachmentFromRandomId": "通过访问 ID 链接",
|
||||||
"attachmentPastedImage": "粘贴的图片",
|
"attachmentPastedImage": "粘贴的图片",
|
||||||
"attachmentInsertLink": "插入连接",
|
"attachmentInsertLink": "插入连接",
|
||||||
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
"attachmentSetAsPostThumbnail": "设置为帖子缩略图",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
"attachmentUnsetAsPostThumbnail": "取消设置为帖子缩略图",
|
||||||
|
"attachmentCompressVideo": "重新编码视频",
|
||||||
"attachmentSetThumbnail": "设置缩略图",
|
"attachmentSetThumbnail": "设置缩略图",
|
||||||
|
"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": "无未读通知",
|
||||||
@ -376,9 +403,26 @@
|
|||||||
"dailyCheckNegativeHint5Description": "关键时刻断网",
|
"dailyCheckNegativeHint5Description": "关键时刻断网",
|
||||||
"dailyCheckNegativeHint6": "出门",
|
"dailyCheckNegativeHint6": "出门",
|
||||||
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
|
||||||
"happyBirthday": "生日快乐,{}!",
|
"celebrateBirthday": "生日快乐,{}!",
|
||||||
"celebrateMerryXmas": "圣诞快乐,{}!",
|
"celebrateMerryXmas": "圣诞快乐,{}!",
|
||||||
"celebrateNewYear": "新年快乐,{}!",
|
"celebrateNewYear": "新年快乐,{}!",
|
||||||
|
"celebrateValentineDay": "今天是情人节,{}!",
|
||||||
|
"celebrateLaborDay": "今天是劳动节,{}。",
|
||||||
|
"celebrateMotherDay": "今天是母亲节,{}。",
|
||||||
|
"celebrateChildrenDay": "今天是儿童节,{}!",
|
||||||
|
"celebrateFatherDay": "今天是父亲节,{}。",
|
||||||
|
"celebrateHalloween": "快乐在圣诞节,{}!",
|
||||||
|
"celebrateThanksgiving": "今天是感恩节,{}!",
|
||||||
|
"pendingBirthday": "{} 过生日",
|
||||||
|
"pendingMerryXmas": "{} 过圣诞节",
|
||||||
|
"pendingNewYear": "{} 跨年",
|
||||||
|
"pendingValentineDay": "{} 过情人节",
|
||||||
|
"pendingLaborDay": "{} 过劳动节",
|
||||||
|
"pendingMotherDay": "{} 过母亲节",
|
||||||
|
"pendingChildrenDay": "{} 过儿童节",
|
||||||
|
"pendingFatherDay": "{} 过父亲节",
|
||||||
|
"pendingHalloween": "{} 过圣诞节",
|
||||||
|
"pendingThanksgiving": "{} 过感恩节",
|
||||||
"friendNew": "添加好友",
|
"friendNew": "添加好友",
|
||||||
"friendRequests": "好友请求",
|
"friendRequests": "好友请求",
|
||||||
"friendRequestsDescription": {
|
"friendRequestsDescription": {
|
||||||
@ -412,6 +456,7 @@
|
|||||||
"accountJoinedAt": "加入于 {}",
|
"accountJoinedAt": "加入于 {}",
|
||||||
"accountBirthday": "出生于 {}",
|
"accountBirthday": "出生于 {}",
|
||||||
"accountBadge": "徽章",
|
"accountBadge": "徽章",
|
||||||
|
"accountCheckInNoRecords": "暂无运势记录",
|
||||||
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
|
"badgeCompanyStaff": "索尔辛茨士大夫 · 员工",
|
||||||
"badgeSiteMigration": "Solar Network 原住民",
|
"badgeSiteMigration": "Solar Network 原住民",
|
||||||
"accountStatus": "状态",
|
"accountStatus": "状态",
|
||||||
@ -486,5 +531,6 @@
|
|||||||
"postCategoryNews": "新闻",
|
"postCategoryNews": "新闻",
|
||||||
"postCategoryKnowledge": "知识",
|
"postCategoryKnowledge": "知识",
|
||||||
"postCategoryLiterature": "文学",
|
"postCategoryLiterature": "文学",
|
||||||
|
"postCategoryFunny": "搞笑",
|
||||||
"postCategoryUncategorized": "未分类"
|
"postCategoryUncategorized": "未分类"
|
||||||
}
|
}
|
||||||
|
@ -279,16 +279,43 @@
|
|||||||
"one": "{} 個附件",
|
"one": "{} 個附件",
|
||||||
"other": "{} 個附件"
|
"other": "{} 個附件"
|
||||||
},
|
},
|
||||||
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
"addAttachmentFromAlbum": "從相冊中添加附件",
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
"addAttachmentFromClipboard": "粘貼附件",
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
"addAttachmentFromCameraVideo": "拍攝視頻",
|
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||||
|
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||||
"attachmentPastedImage": "粘貼的圖片",
|
"attachmentPastedImage": "粘貼的圖片",
|
||||||
"attachmentInsertLink": "插入連接",
|
"attachmentInsertLink": "插入連接",
|
||||||
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||||
|
"attachmentCompressVideo": "重新編碼視頻",
|
||||||
"attachmentSetThumbnail": "設置縮略圖",
|
"attachmentSetThumbnail": "設置縮略圖",
|
||||||
|
"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": "無未讀通知",
|
||||||
@ -376,9 +403,26 @@
|
|||||||
"dailyCheckNegativeHint5Description": "關鍵時刻斷網",
|
"dailyCheckNegativeHint5Description": "關鍵時刻斷網",
|
||||||
"dailyCheckNegativeHint6": "出門",
|
"dailyCheckNegativeHint6": "出門",
|
||||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||||
"happyBirthday": "生日快樂,{}!",
|
"celebrateBirthday": "生日快樂,{}!",
|
||||||
"celebrateMerryXmas": "聖誕快樂,{}!",
|
"celebrateMerryXmas": "聖誕快樂,{}!",
|
||||||
"celebrateNewYear": "新年快樂,{}!",
|
"celebrateNewYear": "新年快樂,{}!",
|
||||||
|
"celebrateValentineDay": "今天是情人節,{}!",
|
||||||
|
"celebrateLaborDay": "今天是勞動節,{}。",
|
||||||
|
"celebrateMotherDay": "今天是母親節,{}。",
|
||||||
|
"celebrateChildrenDay": "今天是兒童節,{}!",
|
||||||
|
"celebrateFatherDay": "今天是父親節,{}。",
|
||||||
|
"celebrateHalloween": "快樂在聖誕節,{}!",
|
||||||
|
"celebrateThanksgiving": "今天是感恩節,{}!",
|
||||||
|
"pendingBirthday": "{} 過生日",
|
||||||
|
"pendingMerryXmas": "{} 過聖誕節",
|
||||||
|
"pendingNewYear": "{} 跨年",
|
||||||
|
"pendingValentineDay": "{} 過情人節",
|
||||||
|
"pendingLaborDay": "{} 過勞動節",
|
||||||
|
"pendingMotherDay": "{} 過母親節",
|
||||||
|
"pendingChildrenDay": "{} 過兒童節",
|
||||||
|
"pendingFatherDay": "{} 過父親節",
|
||||||
|
"pendingHalloween": "{} 過聖誕節",
|
||||||
|
"pendingThanksgiving": "{} 過感恩節",
|
||||||
"friendNew": "添加好友",
|
"friendNew": "添加好友",
|
||||||
"friendRequests": "好友請求",
|
"friendRequests": "好友請求",
|
||||||
"friendRequestsDescription": {
|
"friendRequestsDescription": {
|
||||||
@ -486,5 +530,6 @@
|
|||||||
"postCategoryNews": "新聞",
|
"postCategoryNews": "新聞",
|
||||||
"postCategoryKnowledge": "知識",
|
"postCategoryKnowledge": "知識",
|
||||||
"postCategoryLiterature": "文學",
|
"postCategoryLiterature": "文學",
|
||||||
|
"postCategoryFunny": "搞笑",
|
||||||
"postCategoryUncategorized": "未分類"
|
"postCategoryUncategorized": "未分類"
|
||||||
}
|
}
|
||||||
|
@ -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,76 @@
|
|||||||
},
|
},
|
||||||
"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": "為頂欄啟用透明效果。",
|
||||||
"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 +257,65 @@
|
|||||||
"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": "從相簿中新增附件",
|
"fieldAttachmentRandomId": "訪問 ID",
|
||||||
"addAttachmentFromClipboard": "貼上附件",
|
"addAttachmentFromAlbum": "從相冊中添加附件",
|
||||||
|
"addAttachmentFromClipboard": "粘貼附件",
|
||||||
"addAttachmentFromCameraPhoto": "拍攝照片",
|
"addAttachmentFromCameraPhoto": "拍攝照片",
|
||||||
"addAttachmentFromCameraVideo": "拍攝影片",
|
"addAttachmentFromCameraVideo": "拍攝視頻",
|
||||||
"attachmentPastedImage": "貼上的圖片",
|
"addAttachmentFromRandomId": "通過訪問 ID 鏈接",
|
||||||
"attachmentInsertLink": "插入連線",
|
"attachmentPastedImage": "粘貼的圖片",
|
||||||
"attachmentSetAsPostThumbnail": "設定為帖子縮圖",
|
"attachmentInsertLink": "插入連接",
|
||||||
"attachmentUnsetAsPostThumbnail": "取消設定為帖子縮圖",
|
"attachmentSetAsPostThumbnail": "設置為帖子縮略圖",
|
||||||
"attachmentSetThumbnail": "設定縮圖",
|
"attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖",
|
||||||
|
"attachmentCompressVideo": "重新編碼視頻",
|
||||||
|
"attachmentSetThumbnail": "設置縮略圖",
|
||||||
|
"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 +325,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 +348,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": "每日簽到",
|
||||||
@ -376,30 +403,47 @@
|
|||||||
"dailyCheckNegativeHint5Description": "關鍵時刻斷網",
|
"dailyCheckNegativeHint5Description": "關鍵時刻斷網",
|
||||||
"dailyCheckNegativeHint6": "出門",
|
"dailyCheckNegativeHint6": "出門",
|
||||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||||
"happyBirthday": "生日快樂,{}!",
|
"celebrateBirthday": "生日快樂,{}!",
|
||||||
"celebrateMerryXmas": "聖誕快樂,{}!",
|
"celebrateMerryXmas": "聖誕快樂,{}!",
|
||||||
"celebrateNewYear": "新年快樂,{}!",
|
"celebrateNewYear": "新年快樂,{}!",
|
||||||
"friendNew": "新增好友",
|
"celebrateValentineDay": "今天是情人節,{}!",
|
||||||
|
"celebrateLaborDay": "今天是勞動節,{}。",
|
||||||
|
"celebrateMotherDay": "今天是母親節,{}。",
|
||||||
|
"celebrateChildrenDay": "今天是兒童節,{}!",
|
||||||
|
"celebrateFatherDay": "今天是父親節,{}。",
|
||||||
|
"celebrateHalloween": "快樂在聖誕節,{}!",
|
||||||
|
"celebrateThanksgiving": "今天是感恩節,{}!",
|
||||||
|
"pendingBirthday": "{} 過生日",
|
||||||
|
"pendingMerryXmas": "{} 過聖誕節",
|
||||||
|
"pendingNewYear": "{} 跨年",
|
||||||
|
"pendingValentineDay": "{} 過情人節",
|
||||||
|
"pendingLaborDay": "{} 過勞動節",
|
||||||
|
"pendingMotherDay": "{} 過母親節",
|
||||||
|
"pendingChildrenDay": "{} 過兒童節",
|
||||||
|
"pendingFatherDay": "{} 過父親節",
|
||||||
|
"pendingHalloween": "{} 過聖誕節",
|
||||||
|
"pendingThanksgiving": "{} 過感恩節",
|
||||||
|
"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": "你確定要遺忘跟 {} 的關係嗎?這個操作無法撤銷。",
|
||||||
@ -415,20 +459,20 @@
|
|||||||
"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 上的故事",
|
||||||
"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",
|
||||||
@ -436,35 +480,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": "自定義",
|
||||||
@ -486,5 +530,6 @@
|
|||||||
"postCategoryNews": "新聞",
|
"postCategoryNews": "新聞",
|
||||||
"postCategoryKnowledge": "知識",
|
"postCategoryKnowledge": "知識",
|
||||||
"postCategoryLiterature": "文學",
|
"postCategoryLiterature": "文學",
|
||||||
|
"postCategoryFunny": "搞笑",
|
||||||
"postCategoryUncategorized": "未分類"
|
"postCategoryUncategorized": "未分類"
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
@ -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
|
||||||
|
@ -80,7 +80,11 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
|
|
||||||
let metadataCopy = metadata as? [String: String] ?? [:]
|
let metadataCopy = metadata as? [String: String] ?? [:]
|
||||||
let avatarUrl = getAttachmentUrl(for: avatarIdentifier)
|
let avatarUrl = getAttachmentUrl(for: avatarIdentifier)
|
||||||
KingfisherManager.shared.retrieveImage(with: URL(string: avatarUrl)!, completionHandler: { result in
|
|
||||||
|
let targetSize = 640
|
||||||
|
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
||||||
|
|
||||||
|
KingfisherManager.shared.retrieveImage(with: URL(string: avatarUrl)!, options: [.processor(scaleProcessor)], completionHandler: { result in
|
||||||
var image: Data?
|
var image: Data?
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let value):
|
case .success(let value):
|
||||||
|
@ -2,6 +2,7 @@ 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';
|
||||||
@ -14,17 +15,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 +31,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 +52,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +231,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 +240,64 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
|
Future<void> uploadSingleAttachment(BuildContext context, int idx) async {
|
||||||
if (isBusy) return;
|
if (isBusy) return;
|
||||||
|
|
||||||
@ -301,20 +339,29 @@ 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 {
|
||||||
|
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();
|
||||||
|
@ -30,6 +30,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/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';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
@ -148,6 +149,9 @@ class SolianApp extends StatelessWidget {
|
|||||||
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => NotificationProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => ChatChannelProvider(ctx)),
|
||||||
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
|
ChangeNotifierProvider(create: (ctx) => ChatCallProvider(ctx)),
|
||||||
|
|
||||||
|
// Additional helper layer
|
||||||
|
Provider(create: (ctx) => SpecialDayProvider(ctx)),
|
||||||
],
|
],
|
||||||
child: _AppDelegate(),
|
child: _AppDelegate(),
|
||||||
),
|
),
|
||||||
@ -265,6 +269,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
|
|||||||
// The Network initialization will also save initialize the Config, so it not need to be initialized again
|
// The Network initialization will also save initialize the Config, so it not need to be initialized again
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
await sn.initializeUserAgent();
|
await sn.initializeUserAgent();
|
||||||
|
await sn.setConfigWithNative();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
await ua.initialize();
|
await ua.initialize();
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,9 +68,8 @@ class SnNetworkProvider {
|
|||||||
_config.initialize().then((_) {
|
_config.initialize().then((_) {
|
||||||
_prefs = _config.prefs;
|
_prefs = _config.prefs;
|
||||||
client.options.baseUrl = _config.serverUrl;
|
client.options.baseUrl = _config.serverUrl;
|
||||||
if (!context.mounted) return;
|
|
||||||
_home.saveWidgetData("nex_server_url", client.options.baseUrl);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Dio> createOffContextClient() async {
|
static Future<Dio> createOffContextClient() async {
|
||||||
@ -109,6 +108,10 @@ class SnNetworkProvider {
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setConfigWithNative() async {
|
||||||
|
_home.saveWidgetData("nex_server_url", client.options.baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<String> _getUserAgent() async {
|
static Future<String> _getUserAgent() async {
|
||||||
final String platformInfo;
|
final String platformInfo;
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
|
136
lib/providers/special_day.dart
Normal file
136
lib/providers/special_day.dart
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
|
||||||
|
// Stored as key: month, day
|
||||||
|
const Map<String, (int, int)> kSpecialDays = {
|
||||||
|
// Birthday is dynamically generated according to the user's profile
|
||||||
|
'NewYear': (1, 1),
|
||||||
|
'ValentineDay': (2, 14),
|
||||||
|
'LaborDay': (5, 1),
|
||||||
|
'MotherDay': (5, 11),
|
||||||
|
'ChildrenDay': (6, 1),
|
||||||
|
'FatherDay': (8, 8),
|
||||||
|
'Halloween': (10, 31),
|
||||||
|
'Thanksgiving': (11, 28),
|
||||||
|
'MerryXmas': (12, 25),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Map<String, String> kSpecialDaysSymbol = {
|
||||||
|
'Birthday': '🎂',
|
||||||
|
'NewYear': '🎉',
|
||||||
|
'MerryXmas': '🎄',
|
||||||
|
'ValentineDay': '💑',
|
||||||
|
'LaborDay': '🏋️',
|
||||||
|
'MotherDay': '👩',
|
||||||
|
'ChildrenDay': '👶',
|
||||||
|
'FatherDay': '👨',
|
||||||
|
'Halloween': '🎃',
|
||||||
|
'Thanksgiving': '🎅',
|
||||||
|
};
|
||||||
|
|
||||||
|
class SpecialDayProvider {
|
||||||
|
late final UserProvider _user;
|
||||||
|
|
||||||
|
SpecialDayProvider(BuildContext context) {
|
||||||
|
_user = context.read<UserProvider>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> getSpecialDays() {
|
||||||
|
final now = DateTime.now().toLocal();
|
||||||
|
final birthday = _user.user?.profile?.birthday?.toLocal();
|
||||||
|
final isBirthday = birthday != null && birthday.day == now.day && birthday.month == now.month;
|
||||||
|
|
||||||
|
return [
|
||||||
|
if (isBirthday) 'Birthday',
|
||||||
|
...kSpecialDays.keys.where(
|
||||||
|
(key) => kSpecialDays[key]!.$1 == now.month && kSpecialDays[key]!.$2 == now.day,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
(String, DateTime)? getLastSpecialDay() {
|
||||||
|
final now = DateTime.now().toLocal();
|
||||||
|
final birthday = _user.user?.profile?.birthday?.toLocal();
|
||||||
|
|
||||||
|
final Map<String, (int, int)> specialDays = {
|
||||||
|
if (birthday != null) 'Birthday': (birthday.month, birthday.day),
|
||||||
|
...kSpecialDays,
|
||||||
|
};
|
||||||
|
|
||||||
|
DateTime? lastDate;
|
||||||
|
String? lastEvent;
|
||||||
|
|
||||||
|
for (final entry in specialDays.entries) {
|
||||||
|
final eventName = entry.key;
|
||||||
|
final (month, day) = entry.value;
|
||||||
|
|
||||||
|
var specialDayThisYear = DateTime(now.year, month, day);
|
||||||
|
var specialDayLastYear = DateTime(now.year - 1, month, day);
|
||||||
|
|
||||||
|
if (specialDayThisYear.isBefore(now)) {
|
||||||
|
if (lastDate == null || specialDayThisYear.isAfter(lastDate)) {
|
||||||
|
lastDate = specialDayThisYear;
|
||||||
|
lastEvent = eventName;
|
||||||
|
}
|
||||||
|
} else if (specialDayLastYear.isBefore(now)) {
|
||||||
|
if (lastDate == null || specialDayLastYear.isAfter(lastDate)) {
|
||||||
|
lastDate = specialDayLastYear;
|
||||||
|
lastEvent = eventName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastEvent != null && lastDate != null) {
|
||||||
|
return (lastEvent, lastDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
(String, DateTime)? getNextSpecialDay() {
|
||||||
|
final now = DateTime.now().toLocal();
|
||||||
|
final birthday = _user.user?.profile?.birthday?.toLocal();
|
||||||
|
|
||||||
|
// Stored as key: month, day
|
||||||
|
final Map<String, (int, int)> specialDays = {
|
||||||
|
if (birthday != null) 'Birthday': (birthday.month, birthday.day),
|
||||||
|
...kSpecialDays,
|
||||||
|
};
|
||||||
|
|
||||||
|
DateTime? closestDate;
|
||||||
|
String? closestEvent;
|
||||||
|
|
||||||
|
for (final entry in specialDays.entries) {
|
||||||
|
final eventName = entry.key;
|
||||||
|
final (month, day) = entry.value;
|
||||||
|
|
||||||
|
// Calculate the special day's DateTime in the current year
|
||||||
|
var specialDay = DateTime(now.year, month, day);
|
||||||
|
|
||||||
|
// If the special day has already passed this year, consider it for the next year
|
||||||
|
if (specialDay.isBefore(now)) {
|
||||||
|
specialDay = DateTime(now.year + 1, month, day);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this special day is closer than the previously found one
|
||||||
|
if (closestDate == null || specialDay.isBefore(closestDate)) {
|
||||||
|
closestDate = specialDay;
|
||||||
|
closestEvent = eventName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closestEvent != null && closestDate != null) {
|
||||||
|
return (closestEvent, closestDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No special day found
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
double getSpecialDayProgress(DateTime last, DateTime next) {
|
||||||
|
final totalDuration = next.add(-const Duration(days: 1)).difference(last).inSeconds.toDouble();
|
||||||
|
final elapsedDuration = DateTime.now().difference(last).inSeconds.toDouble();
|
||||||
|
return (elapsedDuration / totalDuration).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
}
|
@ -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,10 +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}');
|
||||||
_home.saveWidgetData('user', value.toJson());
|
log('Atk: ${await atk}');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ class HomeWidgetProvider {
|
|||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return;
|
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return;
|
||||||
if (!kIsWeb && Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
await HomeWidget.setAppGroupId("group.solsynth.solian");
|
await HomeWidget.setAppGroupId("group.solsynth.solian");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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() =>
|
||||||
|
@ -97,7 +97,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);
|
||||||
|
@ -22,8 +22,9 @@ const Map<String, IconData> kCategoryIcons = {
|
|||||||
'sports': Symbols.sports_soccer,
|
'sports': Symbols.sports_soccer,
|
||||||
'music': Symbols.music_note,
|
'music': Symbols.music_note,
|
||||||
'news': Symbols.newspaper,
|
'news': Symbols.newspaper,
|
||||||
'knowledge': Symbols.book,
|
'knowledge': Symbols.library_books,
|
||||||
'literature': Symbols.book,
|
'literature': Symbols.book,
|
||||||
|
'funny': Symbols.attractions,
|
||||||
};
|
};
|
||||||
|
|
||||||
class ExploreScreen extends StatefulWidget {
|
class ExploreScreen extends StatefulWidget {
|
||||||
@ -184,26 +185,27 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
preferredSize: const Size.fromHeight(50),
|
preferredSize: const Size.fromHeight(50),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 50,
|
height: 50,
|
||||||
child: ListView.builder(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 12),
|
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: _categories.length,
|
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 12),
|
||||||
itemBuilder: (context, idx) {
|
child: Row(
|
||||||
final ele = _categories[idx];
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
return StyledWidget(ChoiceChip(
|
children: _categories.map((ele) {
|
||||||
avatar: Icon(kCategoryIcons[ele.alias] ?? Symbols.question_mark),
|
return StyledWidget(ChoiceChip(
|
||||||
label: Text(
|
avatar: Icon(kCategoryIcons[ele.alias] ?? Symbols.question_mark),
|
||||||
'postCategory${ele.alias.capitalize()}'.trExists()
|
label: Text(
|
||||||
? 'postCategory${ele.alias.capitalize()}'.tr()
|
'postCategory${ele.alias.capitalize()}'.trExists()
|
||||||
: ele.name,
|
? 'postCategory${ele.alias.capitalize()}'.tr()
|
||||||
),
|
: ele.name,
|
||||||
selected: _selectedCategory == ele.alias,
|
),
|
||||||
onSelected: (value) {
|
selected: _selectedCategory == ele.alias,
|
||||||
_selectedCategory = value ? ele.alias : null;
|
onSelected: (value) {
|
||||||
_refreshPosts();
|
_selectedCategory = value ? ele.alias : null;
|
||||||
},
|
_refreshPosts();
|
||||||
)).padding(horizontal: 4);
|
},
|
||||||
},
|
)).padding(horizontal: 4);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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();
|
||||||
|
@ -11,11 +11,14 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package: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';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/providers/special_day.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/widget.dart';
|
import 'package:surface/providers/widget.dart';
|
||||||
import 'package:surface/types/check_in.dart';
|
import 'package:surface/types/check_in.dart';
|
||||||
@ -79,8 +82,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start,
|
mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_HomeDashSpecialDayWidget().padding(bottom: 8, horizontal: 8),
|
|
||||||
_HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)),
|
_HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)),
|
||||||
|
_HomeDashSpecialDayWidget().padding(horizontal: 8),
|
||||||
StaggeredGrid.extent(
|
StaggeredGrid.extent(
|
||||||
maxCrossAxisExtent: 280,
|
maxCrossAxisExtent: 280,
|
||||||
mainAxisSpacing: 8,
|
mainAxisSpacing: 8,
|
||||||
@ -107,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) {
|
||||||
@ -151,46 +154,76 @@ class _HomeDashUpdateWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeDashSpecialDayWidget extends StatelessWidget {
|
class _HomeDashSpecialDayWidget extends StatelessWidget {
|
||||||
const _HomeDashSpecialDayWidget({super.key});
|
const _HomeDashSpecialDayWidget();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ua = context.watch<UserProvider>();
|
final ua = context.watch<UserProvider>();
|
||||||
final today = DateTime.now();
|
final dayz = context.watch<SpecialDayProvider>();
|
||||||
final birthday = ua.user?.profile?.birthday?.toLocal();
|
|
||||||
final isBirthday = birthday != null && birthday.day == today.day && birthday.month == today.month;
|
|
||||||
|
|
||||||
return Column(
|
final days = dayz.getSpecialDays();
|
||||||
spacing: 8,
|
|
||||||
children: [
|
if (days.isNotEmpty) {
|
||||||
if (isBirthday)
|
return Column(
|
||||||
Card(
|
spacing: 8,
|
||||||
child: ListTile(
|
children: days.map((ele) {
|
||||||
leading: Text('🎂').fontSize(24),
|
return Card(
|
||||||
title: Text('happyBirthday').tr(args: [ua.user?.nick ?? 'user']),
|
child: ListTile(
|
||||||
),
|
leading: Text(kSpecialDaysSymbol[ele] ?? '🎉').fontSize(24),
|
||||||
).padding(bottom: 8),
|
title: Text('celebrate$ele').tr(args: [ua.user?.nick ?? 'user']),
|
||||||
if (today.month == 12 && today.day == 25)
|
subtitle: Text(
|
||||||
Card(
|
DateFormat('y/M/d').format(DateTime.now().copyWith(
|
||||||
child: ListTile(
|
month: kSpecialDays[ele]!.$1,
|
||||||
leading: Text('🎄').fontSize(24),
|
day: kSpecialDays[ele]!.$2,
|
||||||
title: Text('celebrateMerryXmas').tr(args: [ua.user?.nick ?? 'user']),
|
)),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
).padding(bottom: 8);
|
||||||
|
}).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
final nextOne = dayz.getNextSpecialDay();
|
||||||
|
final lastOne = dayz.getLastSpecialDay();
|
||||||
|
|
||||||
|
if (nextOne != null && lastOne != null) {
|
||||||
|
var (name, date) = nextOne;
|
||||||
|
date = date.add(Duration(days: 1));
|
||||||
|
final progress = dayz.getSpecialDayProgress(lastOne.$2, date);
|
||||||
|
final diff = nextOne.$2.difference(DateTime.now());
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
||||||
|
title: Text('pending$name').tr(args: [RelativeTime(context).format(date)]),
|
||||||
|
subtitle: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SlideCountdown(
|
||||||
|
duration: diff,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
separatorStyle: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
separatorType: SeparatorType.symbol,
|
||||||
|
decoration: BoxDecoration(),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Expanded(
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (today.month == 1 && today.day == 1)
|
),
|
||||||
Card(
|
).padding(bottom: 8);
|
||||||
child: ListTile(
|
}
|
||||||
leading: Text('🎉').fontSize(24),
|
|
||||||
title: Text('celebrateNewYear').tr(args: [ua.user?.nick ?? 'user']),
|
return const SizedBox.shrink();
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeDashCheckInWidget extends StatefulWidget {
|
class _HomeDashCheckInWidget extends StatefulWidget {
|
||||||
const _HomeDashCheckInWidget({super.key});
|
const _HomeDashCheckInWidget();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_HomeDashCheckInWidget> createState() => _HomeDashCheckInWidgetState();
|
State<_HomeDashCheckInWidget> createState() => _HomeDashCheckInWidgetState();
|
||||||
@ -408,7 +441,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();
|
||||||
@ -479,7 +512,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();
|
||||||
@ -493,9 +526,7 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
|
|||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
try {
|
try {
|
||||||
final pt = context.read<SnPostContentProvider>();
|
final pt = context.read<SnPostContentProvider>();
|
||||||
final home = context.read<HomeWidgetProvider>();
|
|
||||||
_posts = await pt.listRecommendations();
|
_posts = await pt.listRecommendations();
|
||||||
home.saveWidgetData('post_featured', _posts!.first.toJson());
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
|
@ -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';
|
||||||
@ -96,38 +91,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 +255,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 +270,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 +289,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),
|
||||||
],
|
],
|
||||||
@ -435,63 +383,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();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -99,11 +99,16 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
],
|
],
|
||||||
).padding(horizontal: 24, vertical: 16),
|
).padding(horizontal: 24, vertical: 16),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
_posts.clear();
|
_refreshPosts();
|
||||||
_fetchPosts();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshPosts() {
|
||||||
|
_postCount = null;
|
||||||
|
_posts.clear();
|
||||||
|
return _fetchPosts();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const labelShadows = <Shadow>[
|
const labelShadows = <Shadow>[
|
||||||
@ -144,8 +149,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
setState(() => _posts[idx] = data);
|
setState(() => _posts[idx] = data);
|
||||||
},
|
},
|
||||||
onDeleted: () {
|
onDeleted: () {
|
||||||
_posts.clear();
|
_refreshPosts();
|
||||||
_fetchPosts();
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -176,10 +180,8 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
_searchTerm = value;
|
_searchTerm = value;
|
||||||
},
|
},
|
||||||
onSubmitted: (value) {
|
onSubmitted: (value) {
|
||||||
setState(() => _posts.clear());
|
|
||||||
|
|
||||||
_searchTerm = value;
|
_searchTerm = value;
|
||||||
_fetchPosts();
|
_refreshPosts();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (_lastTook != null)
|
if (_lastTook != null)
|
||||||
|
@ -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,
|
||||||
|
@ -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();
|
||||||
|
@ -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(
|
||||||
|
@ -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,35 @@ 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);
|
||||||
}
|
}
|
||||||
|
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,54 @@ 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,
|
||||||
|
};
|
||||||
|
@ -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,45 @@ class _AttachmentItemContentVideoState
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Video(
|
return MaterialDesktopVideoControlsTheme(
|
||||||
controller: _videoController!,
|
normal: MaterialDesktopVideoControlsThemeData(
|
||||||
aspectRatio: ratio,
|
buttonBarButtonSize: 24,
|
||||||
|
buttonBarButtonColor: Colors.white,
|
||||||
|
topButtonBarMargin: EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||||
|
topButtonBar: [
|
||||||
|
const Spacer(),
|
||||||
|
MaterialDesktopCustomButton(
|
||||||
|
iconSize: 24,
|
||||||
|
onPressed: _toggleOriginal,
|
||||||
|
icon: Builder(builder: (context) {
|
||||||
|
return _showOriginal ? const Icon(Symbols.high_quality, size: 24) : const Icon(Symbols.sd, size: 24);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
fullscreen: const MaterialDesktopVideoControlsThemeData(),
|
||||||
|
child: MaterialVideoControlsTheme(
|
||||||
|
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 +366,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 +425,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 +510,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 +546,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 +587,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,8 +14,9 @@ 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 gridded;
|
||||||
final bool noGrow;
|
final bool noGrow;
|
||||||
final bool isFlatted;
|
final BoxFit fit;
|
||||||
final double? maxHeight;
|
final double? maxHeight;
|
||||||
final EdgeInsets? listPadding;
|
final EdgeInsets? listPadding;
|
||||||
|
|
||||||
@ -23,8 +24,9 @@ class AttachmentList extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.data,
|
required this.data,
|
||||||
this.bordered = false,
|
this.bordered = false,
|
||||||
|
this.gridded = false,
|
||||||
this.noGrow = false,
|
this.noGrow = false,
|
||||||
this.isFlatted = false,
|
this.fit = BoxFit.cover,
|
||||||
this.maxHeight,
|
this.maxHeight,
|
||||||
this.listPadding,
|
this.listPadding,
|
||||||
});
|
});
|
||||||
@ -53,12 +55,11 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
final constraints = BoxConstraints(
|
final constraints = BoxConstraints(
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
maxHeight: widget.maxHeight ?? double.infinity,
|
maxHeight: widget.maxHeight ?? double.infinity,
|
||||||
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,
|
||||||
@ -66,54 +67,32 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
}
|
}
|
||||||
.toDouble();
|
.toDouble();
|
||||||
|
|
||||||
return Container(
|
return Padding(
|
||||||
constraints: ResponsiveBreakpoints.of(context).largerThan(MOBILE)
|
padding: widget.listPadding ?? EdgeInsets.zero,
|
||||||
? constraints.copyWith(
|
child: Container(
|
||||||
maxWidth: math.min(
|
constraints: constraints,
|
||||||
constraints.maxWidth,
|
width: double.infinity,
|
||||||
kAttachmentMaxWidth,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
child: AspectRatio(
|
|
||||||
aspectRatio: singleAspectRatio,
|
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: Builder(
|
child: AspectRatio(
|
||||||
builder: (context) {
|
aspectRatio: singleAspectRatio,
|
||||||
if (ResponsiveBreakpoints.of(context).largerThan(MOBILE) || widget.noGrow) {
|
child: Container(
|
||||||
return Padding(
|
decoration: BoxDecoration(
|
||||||
// Single child list-like displaying
|
color: backgroundColor,
|
||||||
padding: widget.listPadding ?? EdgeInsets.zero,
|
border: Border.fromBorderSide(borderSide),
|
||||||
child: Container(
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: backgroundColor,
|
child: ClipRRect(
|
||||||
border: Border(top: borderSide, bottom: borderSide),
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
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(
|
child: AttachmentItem(
|
||||||
data: widget.data[0],
|
data: widget.data[0],
|
||||||
heroTag: heroTags.first,
|
heroTag: heroTags[0],
|
||||||
|
fit: widget.fit,
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (widget.data.firstOrNull?.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(),
|
||||||
@ -129,39 +108,58 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.isFlatted) {
|
if (widget.gridded) {
|
||||||
return Wrap(
|
return Padding(
|
||||||
spacing: 4,
|
padding: widget.listPadding ?? EdgeInsets.zero,
|
||||||
runSpacing: 4,
|
child: Container(
|
||||||
children: widget.data
|
decoration: BoxDecoration(
|
||||||
.mapIndexed(
|
color: backgroundColor,
|
||||||
(idx, ele) => AspectRatio(
|
border: Border(
|
||||||
aspectRatio: (ele?.metadata['ratio'] ?? 1).toDouble(),
|
top: borderSide,
|
||||||
child: Container(
|
bottom: borderSide,
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: backgroundColor,
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
border: Border(
|
),
|
||||||
top: borderSide,
|
child: ClipRRect(
|
||||||
bottom: borderSide,
|
borderRadius: AttachmentList.kDefaultRadius,
|
||||||
|
child: StaggeredGrid.count(
|
||||||
|
crossAxisCount: math.min(widget.data.length, 2),
|
||||||
|
crossAxisSpacing: 4,
|
||||||
|
mainAxisSpacing: 4,
|
||||||
|
children: widget.data
|
||||||
|
.mapIndexed(
|
||||||
|
(idx, ele) => GestureDetector(
|
||||||
|
child: Container(
|
||||||
|
constraints: constraints,
|
||||||
|
child: AttachmentItem(
|
||||||
|
data: ele,
|
||||||
|
heroTag: heroTags[idx],
|
||||||
|
fit: widget.fit,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (widget.data[idx]!.mediaType != SnMediaType.image) return;
|
||||||
|
context.pushTransparentRoute(
|
||||||
|
AttachmentZoomView(
|
||||||
|
data: widget.data.where((ele) => ele != null).cast(),
|
||||||
|
initialIndex: idx,
|
||||||
|
heroTags: heroTags,
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.black.withOpacity(0.7),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
)
|
||||||
),
|
.toList(),
|
||||||
child: ClipRRect(
|
),
|
||||||
borderRadius: AttachmentList.kDefaultRadius,
|
),
|
||||||
child: AttachmentItem(
|
),
|
||||||
data: ele,
|
|
||||||
heroTag: heroTags[idx],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AspectRatio(
|
return AspectRatio(
|
||||||
aspectRatio: (widget.data.firstOrNull?.metadata['ratio'] ?? 1).toDouble(),
|
aspectRatio: (widget.data.firstOrNull?.data['ratio'] ?? 1).toDouble(),
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
constraints: BoxConstraints(maxHeight: constraints.maxHeight),
|
||||||
child: ScrollConfiguration(
|
child: ScrollConfiguration(
|
||||||
@ -173,12 +171,14 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
return Container(
|
return Container(
|
||||||
constraints: constraints,
|
constraints: constraints,
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: (widget.data[idx]?.metadata['ratio'] ?? 1).toDouble(),
|
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
|
||||||
child: GestureDetector(
|
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 && ele.mediaType == SnMediaType.image).cast(),
|
||||||
initialIndex: idx,
|
initialIndex: idx,
|
||||||
heroTags: heroTags,
|
heroTags: heroTags,
|
||||||
),
|
),
|
||||||
|
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';
|
||||||
@ -53,7 +54,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
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 ? (_) => 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}'])),
|
||||||
@ -158,6 +159,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
AttachmentList(
|
AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
bordered: true,
|
bordered: true,
|
||||||
|
gridded: true,
|
||||||
noGrow: true,
|
noGrow: true,
|
||||||
maxHeight: 520,
|
maxHeight: 520,
|
||||||
listPadding: const EdgeInsets.only(top: 8),
|
listPadding: const EdgeInsets.only(top: 8),
|
||||||
@ -173,7 +175,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
class _ChatMessageText extends StatelessWidget {
|
class _ChatMessageText extends StatelessWidget {
|
||||||
final SnChatMessage data;
|
final SnChatMessage data;
|
||||||
|
|
||||||
const _ChatMessageText({super.key, required this.data});
|
const _ChatMessageText({required this.data});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -213,7 +215,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,18 +1,14 @@
|
|||||||
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/markdown_content.dart';
|
||||||
@ -80,13 +76,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 +120,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() {
|
||||||
@ -294,63 +257,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,
|
||||||
|
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:responsive_framework/responsive_framework.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 isCollapseDrawer = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE);
|
||||||
|
if (!isCollapseDrawer) {
|
||||||
|
final isExpandDrawer = ResponsiveBreakpoints.of(context).largerThan(TABLET);
|
||||||
|
// Leave padding for side navigation
|
||||||
|
mousePosition = isExpandDrawer
|
||||||
|
? 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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ 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/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
@ -85,7 +84,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(
|
||||||
@ -253,7 +251,9 @@ class PostItem extends StatelessWidget {
|
|||||||
AttachmentList(
|
AttachmentList(
|
||||||
data: data.preload!.attachments!,
|
data: data.preload!.attachments!,
|
||||||
bordered: true,
|
bordered: true,
|
||||||
maxHeight: 560,
|
gridded: true,
|
||||||
|
maxHeight: showFullPost ? null : 480,
|
||||||
|
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
|
||||||
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
),
|
),
|
||||||
if (data.body['content'] != null)
|
if (data.body['content'] != null)
|
||||||
@ -334,17 +334,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(
|
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 +544,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,
|
||||||
});
|
});
|
||||||
@ -891,12 +885,9 @@ class _PostContentBody extends StatelessWidget {
|
|||||||
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 +929,14 @@ class _PostQuoteContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: AttachmentList(
|
child: AttachmentList(
|
||||||
data: child.preload!.attachments!,
|
data: child.preload!.attachments!,
|
||||||
isFlatted: isFlatted,
|
maxHeight: 360,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
gridded: true,
|
||||||
listPadding: const EdgeInsets.symmetric(horizontal: 12),
|
listPadding: 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 +955,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 +1028,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 +1053,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 +1095,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,27 @@ 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_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 +82,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 +130,61 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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: '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 +192,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 +200,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 +216,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 +227,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 +269,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 +292,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 +304,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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -282,20 +282,6 @@ class _PostCategoriesFieldState extends State<PostCategoriesField> {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
onChanged: (value) {
|
|
||||||
for (final divider in kTagsDividers) {
|
|
||||||
if (value.endsWith(divider)) {
|
|
||||||
final tagValue = value.substring(0, value.length - 1);
|
|
||||||
if (tagValue.isEmpty) return;
|
|
||||||
if (!_currentCategories.contains(tagValue)) {
|
|
||||||
setState(() => _currentCategories.add(tagValue));
|
|
||||||
}
|
|
||||||
controller.clear();
|
|
||||||
widget.onUpdate(_currentCategories);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
onSubmitted();
|
onSubmitted();
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
46
pubspec.lock
46
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:
|
||||||
@ -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: "3efe9828f19a07d29a51a726759ad0c70a840d231548a1c7d0332075a94db1df"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.4"
|
version: "0.12.5+hotfix.1"
|
||||||
freezed:
|
freezed:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -934,10 +934,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: 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:
|
||||||
@ -1086,10 +1086,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: livekit_client
|
name: livekit_client
|
||||||
sha256: "7802b5de1cae2ee3439db730d24d31c6dcbce173c5e6db2fc5774039a290bc2d"
|
sha256: "7cdeb3eaeec7fb70a4cf88d9caabccbef9e3bd5f0b23c086320bc5c9acb2770b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.4"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
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:
|
||||||
@ -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:
|
||||||
@ -2031,6 +2047,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:
|
||||||
|
@ -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+38
|
version: 2.2.1+41
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@ -80,7 +80,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 +113,9 @@ 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
|
||||||
|
|
||||||
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