Compare commits
11 Commits
2.1.1+39
...
2c7dc8c2ea
Author | SHA1 | Date | |
---|---|---|---|
|
2c7dc8c2ea | ||
|
cf0df91d8c | ||
|
91c85e8a58 | ||
|
2851780dda | ||
|
00fd58fb97 | ||
|
ee7d0ddd25 | ||
|
7656c08832 | ||
|
619c90cdd9 | ||
|
168d51c9fe | ||
|
d4b831f98e | ||
|
4d96a15c31 |
@@ -15,6 +15,7 @@ analyzer:
|
|||||||
- "**/*.freezed.dart"
|
- "**/*.freezed.dart"
|
||||||
errors:
|
errors:
|
||||||
invalid_annotation_target: ignore # Due to freezed + json_serializable issue, ref https://github.com/rrousselGit/freezed/issues/488#issuecomment-894358980
|
invalid_annotation_target: ignore # Due to freezed + json_serializable issue, ref https://github.com/rrousselGit/freezed/issues/488#issuecomment-894358980
|
||||||
|
deprecated_member_use: ignore
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
# The lint rules applied to this project can be customized in the
|
# The lint rules applied to this project can be customized in the
|
||||||
|
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,23 @@
|
|||||||
"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",
|
||||||
"notification": "Notification",
|
"notification": "Notification",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "All notifications read",
|
"zero": "All notifications read",
|
||||||
@@ -506,5 +513,12 @@
|
|||||||
"postCategoryKnowledge": "Knowledge",
|
"postCategoryKnowledge": "Knowledge",
|
||||||
"postCategoryLiterature": "Literature",
|
"postCategoryLiterature": "Literature",
|
||||||
"postCategoryFunny": "Funny",
|
"postCategoryFunny": "Funny",
|
||||||
"postCategoryUncategorized": "Uncategorized"
|
"postCategoryUncategorized": "Uncategorized",
|
||||||
|
"waitingForUpload": "Waiting for upload",
|
||||||
|
"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."
|
||||||
}
|
}
|
||||||
|
@@ -279,16 +279,23 @@
|
|||||||
"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": "新上传附件",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "无未读通知",
|
"zero": "无未读通知",
|
||||||
@@ -504,5 +511,12 @@
|
|||||||
"postCategoryKnowledge": "知识",
|
"postCategoryKnowledge": "知识",
|
||||||
"postCategoryLiterature": "文学",
|
"postCategoryLiterature": "文学",
|
||||||
"postCategoryFunny": "搞笑",
|
"postCategoryFunny": "搞笑",
|
||||||
"postCategoryUncategorized": "未分类"
|
"postCategoryUncategorized": "未分类",
|
||||||
|
"waitingForUpload": "等待上传",
|
||||||
|
"attachmentCompressQuality": "压缩质量",
|
||||||
|
"attachmentCompressQualityHighest": "最高",
|
||||||
|
"attachmentCompressQualityDefault": "默认",
|
||||||
|
"attachmentCompressQualityMedium": "中等",
|
||||||
|
"attachmentCompressQualityLow": "低",
|
||||||
|
"attachmentCompressQualityHint": "Solar Network 并没有阻止你上传大文件、高分辨率、高码率的视频,但是为了你的网络情况观众考虑,我们建议你选择一个合适的压缩质量。"
|
||||||
}
|
}
|
||||||
|
@@ -279,16 +279,23 @@
|
|||||||
"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": "新上傳附件",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "無未讀通知",
|
"zero": "無未讀通知",
|
||||||
@@ -504,5 +511,12 @@
|
|||||||
"postCategoryKnowledge": "知識",
|
"postCategoryKnowledge": "知識",
|
||||||
"postCategoryLiterature": "文學",
|
"postCategoryLiterature": "文學",
|
||||||
"postCategoryFunny": "搞笑",
|
"postCategoryFunny": "搞笑",
|
||||||
"postCategoryUncategorized": "未分類"
|
"postCategoryUncategorized": "未分類",
|
||||||
|
"waitingForUpload": "等待上傳",
|
||||||
|
"attachmentCompressQuality": "壓縮質量",
|
||||||
|
"attachmentCompressQualityHighest": "最高",
|
||||||
|
"attachmentCompressQualityDefault": "默認",
|
||||||
|
"attachmentCompressQualityMedium": "中等",
|
||||||
|
"attachmentCompressQualityLow": "低",
|
||||||
|
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。"
|
||||||
}
|
}
|
||||||
|
@@ -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,45 @@
|
|||||||
"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": "新上傳附件",
|
||||||
"notification": "通知",
|
"notification": "通知",
|
||||||
"notificationUnreadCount": {
|
"notificationUnreadCount": {
|
||||||
"zero": "無未讀通知",
|
"zero": "無未讀通知",
|
||||||
@@ -298,18 +305,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 +328,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": "每日簽到",
|
||||||
@@ -396,27 +403,27 @@
|
|||||||
"pendingFatherDay": "{} 過父親節",
|
"pendingFatherDay": "{} 過父親節",
|
||||||
"pendingHalloween": "{} 過聖誕節",
|
"pendingHalloween": "{} 過聖誕節",
|
||||||
"pendingThanksgiving": "{} 過感恩節",
|
"pendingThanksgiving": "{} 過感恩節",
|
||||||
"friendNew": "新增好友",
|
"friendNew": "添加好友",
|
||||||
"friendRequests": "好友請求",
|
"friendRequests": "好友請求",
|
||||||
"friendRequestsDescription": {
|
"friendRequestsDescription": {
|
||||||
"zero": "你沒有好友請求",
|
"zero": "你沒有好友請求",
|
||||||
"one": "你有 {} 個好友請求",
|
"one": "你有 {} 個好友請求",
|
||||||
"other": "你有 {} 個好友請求"
|
"other": "你有 {} 個好友請求"
|
||||||
},
|
},
|
||||||
"friendBlocklist": "遮蔽列表",
|
"friendBlocklist": "屏蔽列表",
|
||||||
"friendBlocklistDescription": {
|
"friendBlocklistDescription": {
|
||||||
"zero": "你沒有遮蔽任何人",
|
"zero": "你沒有屏蔽任何人",
|
||||||
"one": "你遮蔽了 {} 個使用者",
|
"one": "你屏蔽了 {} 個用戶",
|
||||||
"other": "你遮蔽了 {} 個使用者"
|
"other": "你屏蔽了 {} 個用戶"
|
||||||
},
|
},
|
||||||
"friendStatusPending": "待處理",
|
"friendStatusPending": "待處理",
|
||||||
"friendStatusWaiting": "等待中",
|
"friendStatusWaiting": "等待中",
|
||||||
"friendStatusActive": "正活躍",
|
"friendStatusActive": "正活躍",
|
||||||
"friendStatusBlocked": "已遮蔽",
|
"friendStatusBlocked": "已屏蔽",
|
||||||
"friendRequestSent": "好友請求已傳送。",
|
"friendRequestSent": "好友請求已發送。",
|
||||||
"fieldFriendRelatedName": "好友名 / 賬戶 ID",
|
"fieldFriendRelatedName": "好友名 / 賬戶 ID",
|
||||||
"friendBlock": "遮蔽",
|
"friendBlock": "屏蔽",
|
||||||
"friendUnblock": "解除遮蔽",
|
"friendUnblock": "解除屏蔽",
|
||||||
"friendDeleteAction": "遺忘",
|
"friendDeleteAction": "遺忘",
|
||||||
"friendDelete": "遺忘跟 {} 的關係",
|
"friendDelete": "遺忘跟 {} 的關係",
|
||||||
"friendDeleteDescription": "你確定要遺忘跟 {} 的關係嗎?這個操作無法撤銷。",
|
"friendDeleteDescription": "你確定要遺忘跟 {} 的關係嗎?這個操作無法撤銷。",
|
||||||
@@ -432,20 +439,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",
|
||||||
@@ -453,35 +460,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": "自定義",
|
||||||
@@ -504,5 +511,12 @@
|
|||||||
"postCategoryKnowledge": "知識",
|
"postCategoryKnowledge": "知識",
|
||||||
"postCategoryLiterature": "文學",
|
"postCategoryLiterature": "文學",
|
||||||
"postCategoryFunny": "搞笑",
|
"postCategoryFunny": "搞笑",
|
||||||
"postCategoryUncategorized": "未分類"
|
"postCategoryUncategorized": "未分類",
|
||||||
|
"waitingForUpload": "等待上傳",
|
||||||
|
"attachmentCompressQuality": "壓縮質量",
|
||||||
|
"attachmentCompressQualityHighest": "最高",
|
||||||
|
"attachmentCompressQualityDefault": "默認",
|
||||||
|
"attachmentCompressQualityMedium": "中等",
|
||||||
|
"attachmentCompressQualityLow": "低",
|
||||||
|
"attachmentCompressQualityHint": "Solar Network 並沒有阻止你上傳大文件、高分辨率、高碼率的視頻,但是為了你的網絡情況觀眾考慮,我們建議你選擇一個合適的壓縮質量。"
|
||||||
}
|
}
|
||||||
|
@@ -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.3):
|
||||||
- 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: 02cf2cc4357a655af12ccee70ff5596ae4e6feef
|
||||||
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
|
||||||
|
@@ -15,16 +15,9 @@ 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';
|
||||||
|
|
||||||
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 +29,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 +50,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +237,7 @@ 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(
|
final item = await attach.chunkedUploadParts(
|
||||||
@@ -301,7 +294,7 @@ 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(
|
final item = await attach.chunkedUploadParts(
|
||||||
|
@@ -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;
|
||||||
@@ -117,7 +118,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 +135,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,17 +144,17 @@ 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,
|
||||||
}) 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,
|
||||||
options: Options(headers: {'Content-Type': 'application/octet-stream'}),
|
options: Options(headers: {'Content-Type': 'application/octet-stream'}),
|
||||||
onSendProgress: (count, total) {
|
onSendProgress: (count, total) {
|
||||||
@@ -163,21 +164,27 @@ 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,
|
||||||
}) async {
|
}) async {
|
||||||
final Map<String, dynamic> chunks = place.fileChunks ?? {};
|
final Map<String, dynamic> chunks = place.fileChunks;
|
||||||
var currentTask = 0;
|
var currentTask = 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 +194,25 @@ 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,
|
||||||
|
onProgress: (progress) {
|
||||||
|
final overallProgress = (currentTask + progress) / chunks.length;
|
||||||
|
onProgress?.call(overallProgress);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
currentTask++;
|
||||||
final overallProgress = currentTask / chunks.length;
|
final overallProgress = currentTask / chunks.length;
|
||||||
onProgress?.call(overallProgress);
|
onProgress?.call(overallProgress);
|
||||||
|
|
||||||
currentTask++;
|
if (result is SnAttachmentFragment) {
|
||||||
|
place = result;
|
||||||
|
} else {
|
||||||
|
out = result as SnAttachment;
|
||||||
|
}
|
||||||
}());
|
}());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +229,24 @@ class SnAttachmentProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return place;
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SnAttachment> updateOne(
|
||||||
|
int id, {
|
||||||
|
String? alt,
|
||||||
|
int? thumbnailId,
|
||||||
|
int? compressedId,
|
||||||
|
Map<String, dynamic>? metadata,
|
||||||
|
bool? isIndexable,
|
||||||
|
}) async {
|
||||||
|
final resp = await _sn.client.put('/cgi/uc/attachments/$id', data: {
|
||||||
|
'alt': alt,
|
||||||
|
'thumbnail': thumbnailId,
|
||||||
|
'compressed': compressedId,
|
||||||
|
'metadata': metadata,
|
||||||
|
'is_indexable': isIndexable,
|
||||||
|
});
|
||||||
|
return SnAttachment.fromJson(resp.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -129,7 +129,7 @@ class SpecialDayProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
double getSpecialDayProgress(DateTime last, DateTime next) {
|
double getSpecialDayProgress(DateTime last, DateTime next) {
|
||||||
final totalDuration = next.difference(last).inSeconds.toDouble();
|
final totalDuration = next.add(-const Duration(days: 1)).difference(last).inSeconds.toDouble();
|
||||||
final elapsedDuration = DateTime.now().difference(last).inSeconds.toDouble();
|
final elapsedDuration = DateTime.now().difference(last).inSeconds.toDouble();
|
||||||
return (elapsedDuration / totalDuration).clamp(0.0, 1.0);
|
return (elapsedDuration / totalDuration).clamp(0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/widget.dart';
|
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
|
|
||||||
class UserProvider extends ChangeNotifier {
|
class UserProvider extends ChangeNotifier {
|
||||||
@@ -13,12 +12,10 @@ class UserProvider extends ChangeNotifier {
|
|||||||
SnAccount? user;
|
SnAccount? user;
|
||||||
|
|
||||||
late final SnNetworkProvider _sn;
|
late final SnNetworkProvider _sn;
|
||||||
late final HomeWidgetProvider _home;
|
|
||||||
late final ConfigProvider _config;
|
late final ConfigProvider _config;
|
||||||
|
|
||||||
UserProvider(BuildContext context) {
|
UserProvider(BuildContext context) {
|
||||||
_sn = context.read<SnNetworkProvider>();
|
_sn = context.read<SnNetworkProvider>();
|
||||||
_home = context.read<HomeWidgetProvider>();
|
|
||||||
_config = context.read<ConfigProvider>();
|
_config = context.read<ConfigProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,9 +28,10 @@ class UserProvider extends ChangeNotifier {
|
|||||||
final value = _config.prefs.getString(kAtkStoreKey);
|
final value = _config.prefs.getString(kAtkStoreKey);
|
||||||
isAuthorized = value != null;
|
isAuthorized = value != null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
refreshUser().then((value) {
|
refreshUser().then((value) async {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
log('Logged in as @${value.name}');
|
log('Logged in as @${value.name}');
|
||||||
|
log('Atk: ${await atk}');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
||||||
|
@@ -289,7 +289,7 @@ class _FriendScreenState extends State<FriendScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _NewFriendWidget extends StatefulWidget {
|
class _NewFriendWidget extends StatefulWidget {
|
||||||
const _NewFriendWidget({super.key});
|
const _NewFriendWidget();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_NewFriendWidget> createState() => _NewFriendWidgetState();
|
State<_NewFriendWidget> createState() => _NewFriendWidgetState();
|
||||||
@@ -365,7 +365,7 @@ class _NewFriendWidgetState extends State<_NewFriendWidget> {
|
|||||||
|
|
||||||
class _FriendshipListWidget extends StatefulWidget {
|
class _FriendshipListWidget extends StatefulWidget {
|
||||||
final List<SnRelationship> relations;
|
final List<SnRelationship> relations;
|
||||||
const _FriendshipListWidget({super.key, required this.relations});
|
const _FriendshipListWidget({required this.relations});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_FriendshipListWidget> createState() => _FriendshipListWidgetState();
|
State<_FriendshipListWidget> createState() => _FriendshipListWidgetState();
|
||||||
|
@@ -12,6 +12,7 @@ import 'package:google_fonts/google_fonts.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
|
import 'package:slide_countdown/slide_countdown.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
@@ -109,7 +110,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
class _HomeDashUpdateWidget extends StatelessWidget {
|
class _HomeDashUpdateWidget extends StatelessWidget {
|
||||||
final EdgeInsets? padding;
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
const _HomeDashUpdateWidget({super.key, this.padding});
|
const _HomeDashUpdateWidget({this.padding});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -153,7 +154,7 @@ 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) {
|
||||||
@@ -188,7 +189,7 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
var (name, date) = nextOne;
|
var (name, date) = nextOne;
|
||||||
date = date.add(Duration(days: 1));
|
date = date.add(Duration(days: 1));
|
||||||
final progress = dayz.getSpecialDayProgress(lastOne.$2, date);
|
final progress = dayz.getSpecialDayProgress(lastOne.$2, date);
|
||||||
final diff = nextOne.$2.add(-const Duration(days: 1)).difference(lastOne.$2);
|
final diff = nextOne.$2.difference(DateTime.now());
|
||||||
return Card(
|
return Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
leading: Text(kSpecialDaysSymbol[name] ?? '🎉').fontSize(24),
|
||||||
@@ -196,8 +197,15 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
subtitle: Row(
|
subtitle: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text('${diff.inDays}d · ${(progress * 100).toStringAsFixed(2)}%'),
|
SlideCountdown(
|
||||||
const Gap(8),
|
duration: diff,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
separatorStyle: GoogleFonts.robotoMono(fontSize: 13),
|
||||||
|
separatorType: SeparatorType.symbol,
|
||||||
|
decoration: BoxDecoration(),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: progress,
|
value: progress,
|
||||||
@@ -215,7 +223,7 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HomeDashCheckInWidget extends StatefulWidget {
|
class _HomeDashCheckInWidget extends StatefulWidget {
|
||||||
const _HomeDashCheckInWidget({super.key});
|
const _HomeDashCheckInWidget();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_HomeDashCheckInWidget> createState() => _HomeDashCheckInWidgetState();
|
State<_HomeDashCheckInWidget> createState() => _HomeDashCheckInWidgetState();
|
||||||
@@ -433,7 +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();
|
||||||
@@ -504,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();
|
||||||
|
@@ -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,11 +255,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
if (_writeController.replyingPost != null)
|
if (_writeController.replyingPost != null)
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Theme(
|
ExpansionTile(
|
||||||
data: Theme.of(context).copyWith(
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
),
|
|
||||||
child: ExpansionTile(
|
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
leading: const Icon(Symbols.reply).padding(left: 4),
|
leading: const Icon(Symbols.reply).padding(left: 4),
|
||||||
title: Text('postReplyingNotice')
|
title: Text('postReplyingNotice')
|
||||||
@@ -304,7 +263,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
.tr(args: ['@${_writeController.replyingPost!.publisher.name}']),
|
.tr(args: ['@${_writeController.replyingPost!.publisher.name}']),
|
||||||
children: <Widget>[PostItem(data: _writeController.replyingPost!)],
|
children: <Widget>[PostItem(data: _writeController.replyingPost!)],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -312,11 +270,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
if (_writeController.repostingPost != null)
|
if (_writeController.repostingPost != null)
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Theme(
|
ExpansionTile(
|
||||||
data: Theme.of(context).copyWith(
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
),
|
|
||||||
child: ExpansionTile(
|
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
leading: const Icon(Symbols.forward).padding(left: 4),
|
leading: const Icon(Symbols.forward).padding(left: 4),
|
||||||
title: Text('postRepostingNotice')
|
title: Text('postRepostingNotice')
|
||||||
@@ -328,7 +282,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -336,11 +289,7 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
if (_writeController.editingPost != null)
|
if (_writeController.editingPost != null)
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Theme(
|
ExpansionTile(
|
||||||
data: Theme.of(context).copyWith(
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
),
|
|
||||||
child: ExpansionTile(
|
|
||||||
minTileHeight: 48,
|
minTileHeight: 48,
|
||||||
leading: const Icon(Symbols.edit_note).padding(left: 4),
|
leading: const Icon(Symbols.edit_note).padding(left: 4),
|
||||||
title: Text('postEditingNotice')
|
title: Text('postEditingNotice')
|
||||||
@@ -348,7 +297,6 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
.tr(args: ['@${_writeController.editingPost!.publisher.name}']),
|
.tr(args: ['@${_writeController.editingPost!.publisher.name}']),
|
||||||
children: <Widget>[PostItem(data: _writeController.editingPost!)],
|
children: <Widget>[PostItem(data: _writeController.editingPost!)],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -435,64 +383,13 @@ class _PostEditorScreenState extends State<PostEditorScreen> {
|
|||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
PopupMenuButton(
|
AddPostMediaButton(
|
||||||
icon: Icon(
|
onAdd: (items) {
|
||||||
Symbols.add_photo_alternate,
|
setState(() {
|
||||||
color: Theme.of(context).colorScheme.primary,
|
_writeController.addAttachments(items);
|
||||||
),
|
});
|
||||||
itemBuilder: (context) => [
|
|
||||||
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.photo_camera),
|
|
||||||
const Gap(16),
|
|
||||||
Text('addAttachmentFromCameraPhoto').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_takeMedia(false);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (!kIsWeb && !Platform.isLinux && !Platform.isMacOS && !Platform.isWindows)
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.videocam),
|
|
||||||
const Gap(16),
|
|
||||||
Text('addAttachmentFromCameraVideo').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_takeMedia(true);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.photo_library),
|
|
||||||
const Gap(16),
|
|
||||||
Text('addAttachmentFromAlbum').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_selectMedia();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.content_paste),
|
|
||||||
const Gap(16),
|
|
||||||
Text('addAttachmentFromClipboard').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_pasteMedia();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@@ -580,7 +580,6 @@ class _PublisherPostList extends StatelessWidget {
|
|||||||
final void Function() onDeleted;
|
final void Function() onDeleted;
|
||||||
|
|
||||||
const _PublisherPostList({
|
const _PublisherPostList({
|
||||||
super.key,
|
|
||||||
required this.isBusy,
|
required this.isBusy,
|
||||||
required this.postCount,
|
required this.postCount,
|
||||||
required this.posts,
|
required this.posts,
|
||||||
|
@@ -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,68 @@ 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 SnAttachment? ref,
|
||||||
required dynamic refId,
|
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({}) 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 +107,5 @@ 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);
|
|
||||||
}
|
}
|
||||||
|
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,31 @@ _$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'],
|
ref: json['ref'] == null
|
||||||
refId: json['ref_id'],
|
? 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>),
|
||||||
|
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 +56,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 +66,70 @@ 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,
|
'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(),
|
||||||
|
'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(
|
||||||
|
@@ -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;
|
||||||
|
114
lib/widgets/attachment/attachment_input.dart
Normal file
114
lib/widgets/attachment/attachment_input.dart
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
const AttachmentInputDialog({super.key, required this.title});
|
||||||
|
|
||||||
|
@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,
|
||||||
|
);
|
||||||
|
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 OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -18,6 +18,7 @@ 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;
|
||||||
|
|
||||||
const AttachmentItem({
|
const AttachmentItem({
|
||||||
super.key,
|
super.key,
|
||||||
required this.data,
|
required this.data,
|
||||||
@@ -60,11 +61,16 @@ class AttachmentItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (data!.isMature) {
|
if (data!.contentRating > 0) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
return _AttachmentItemSensitiveBlur(
|
return _AttachmentItemSensitiveBlur(
|
||||||
|
isCompact: constraints.maxHeight < 360,
|
||||||
child: _buildContent(context),
|
child: _buildContent(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return _buildContent(context);
|
return _buildContent(context);
|
||||||
}
|
}
|
||||||
@@ -72,15 +78,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 +110,21 @@ class _AttachmentItemSensitiveBlurState
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 32,
|
size: 32,
|
||||||
),
|
),
|
||||||
const Gap(8),
|
if (!widget.isCompact) const Gap(8),
|
||||||
|
if (!widget.isCompact)
|
||||||
Text('sensitiveContent', textAlign: TextAlign.center)
|
Text('sensitiveContent', textAlign: TextAlign.center)
|
||||||
.tr()
|
.tr()
|
||||||
.fontSize(20)
|
.fontSize(20)
|
||||||
.textColor(Colors.white)
|
.textColor(Colors.white)
|
||||||
.bold(),
|
.bold(),
|
||||||
|
if (!widget.isCompact)
|
||||||
Text(
|
Text(
|
||||||
'sensitiveContentDescription',
|
'sensitiveContentDescription',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
)
|
).tr().fontSize(14).textColor(Colors.white.withOpacity(0.8)),
|
||||||
.tr()
|
if (!widget.isCompact) const Gap(16),
|
||||||
.fontSize(14)
|
|
||||||
.textColor(Colors.white.withOpacity(0.8)),
|
|
||||||
const Gap(16),
|
|
||||||
InkWell(
|
InkWell(
|
||||||
child: Text('sensitiveContentReveal')
|
child: Text('sensitiveContentReveal').tr().textColor(Colors.white),
|
||||||
.tr()
|
|
||||||
.textColor(Colors.white),
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() => _doesShow = !_doesShow);
|
setState(() => _doesShow = !_doesShow);
|
||||||
},
|
},
|
||||||
@@ -131,9 +134,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,19 +164,17 @@ 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;
|
||||||
|
|
||||||
Player? _videoPlayer;
|
Player? _videoPlayer;
|
||||||
@@ -207,7 +206,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 +215,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 +265,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,
|
||||||
@@ -317,19 +313,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 +372,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 +457,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 +493,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 +534,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();
|
||||||
},
|
},
|
||||||
|
@@ -58,7 +58,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
|
|
||||||
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,
|
||||||
@@ -114,6 +114,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
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(),
|
||||||
@@ -136,7 +137,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
children: widget.data
|
children: widget.data
|
||||||
.mapIndexed(
|
.mapIndexed(
|
||||||
(idx, ele) => AspectRatio(
|
(idx, ele) => AspectRatio(
|
||||||
aspectRatio: (ele?.metadata['ratio'] ?? 1).toDouble(),
|
aspectRatio: (ele?.data['ratio'] ?? 1).toDouble(),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
@@ -161,7 +162,7 @@ class _AttachmentListState extends State<AttachmentList> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 +174,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,
|
||||||
),
|
),
|
||||||
|
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}'])),
|
||||||
@@ -173,7 +174,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 +214,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,7 +76,7 @@ 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(
|
||||||
@@ -123,40 +119,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,64 +256,13 @@ 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,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
|
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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -549,7 +549,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,
|
||||||
});
|
});
|
||||||
@@ -894,7 +893,6 @@ class _PostQuoteContent extends StatelessWidget {
|
|||||||
final bool isFlatted;
|
final bool isFlatted;
|
||||||
|
|
||||||
const _PostQuoteContent({
|
const _PostQuoteContent({
|
||||||
super.key,
|
|
||||||
this.isRelativeDate = true,
|
this.isRelativeDate = true,
|
||||||
this.isFlatted = false,
|
this.isFlatted = false,
|
||||||
required this.child,
|
required this.child,
|
||||||
@@ -962,7 +960,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 +1033,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 +1058,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 +1100,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,25 @@ 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: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/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 +80,32 @@ 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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (thumbnail == null) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
final attach = context.read<SnAttachmentProvider>();
|
||||||
|
final newAttach = await attach.updateOne(
|
||||||
|
attachments[idx].attachment!.id,
|
||||||
|
thumbnailId: thumbnail.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
onUpdate!(idx, PostWriteMedia(newAttach));
|
||||||
|
}
|
||||||
|
|
||||||
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 +123,36 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ContextMenu _buildContextMenu(BuildContext context, int idx, PostWriteMedia media) {
|
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 && 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 +160,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 +168,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 +184,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 +195,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 +237,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,9 +260,33 @@ 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),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Theme.of(context).dividerColor,
|
color: Theme.of(context).dividerColor,
|
||||||
@@ -239,7 +299,7 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
child: switch (media.type) {
|
child: switch (media.type) {
|
||||||
PostWriteMediaType.image => Container(
|
SnMediaType.image => Container(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
child: LayoutBuilder(builder: (context, constraints) {
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
return Image(
|
return Image(
|
||||||
@@ -252,6 +312,40 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
SnMediaType.video => Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: 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 => Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: 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(
|
_ => Container(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
child: const Icon(Symbols.docs).center(),
|
child: const Icon(Symbols.docs).center(),
|
||||||
@@ -259,13 +353,165 @@ class PostMediaPendingList extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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.3):
|
||||||
- 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: 8b1b90a6f2445d127a018ce93cc8cf6d8ab62982
|
||||||
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: a3ff529fe6745ee40cdedcd021d81c4a6ad946dd495e782596f2856eeeabc739
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.3"
|
||||||
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+39
|
version: 2.2.1+40
|
||||||
|
|
||||||
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:
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
<!DOCTYPE html><html><head>
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" oncontextmenu="event.preventDefault();">
|
||||||
|
<head>
|
||||||
<!--
|
<!--
|
||||||
If you are serving your web app in a path other than the root, change the
|
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.
|
href value below to reflect the base path you are serving from.
|
||||||
@@ -31,9 +33,8 @@
|
|||||||
<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">
|
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
name="viewport">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style id="splash-screen-style">
|
<style id="splash-screen-style">
|
||||||
@@ -109,22 +110,24 @@
|
|||||||
</script>
|
</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)">
|
||||||
|
<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)">
|
||||||
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
|
<img class="center" aria-hidden="true" src="splash/img/light-1x.png" alt="">
|
||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="flutter_bootstrap.js" async=""></script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
<script src="flutter_bootstrap.js" async=""></script>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
</body></html>
|
|
Reference in New Issue
Block a user