From 7656c08832b6e34dad9ac558a92d4dc0d0e4705b Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 26 Dec 2024 22:19:01 +0800 Subject: [PATCH] :recycle: Refactored attachment loading system --- analysis_options.yaml | 1 + assets/translations/zh-HK.json | 9 +- assets/translations/zh-TW.json | 291 +++++++++--------- lib/controllers/post_write_controller.dart | 29 +- lib/providers/userinfo.dart | 3 - lib/screens/post/post_editor.dart | 5 - lib/theme.dart | 1 - lib/types/attachment.dart | 32 +- lib/types/attachment.freezed.dart | 107 +++++-- lib/types/attachment.g.dart | 8 +- lib/widgets/attachment/attachment_input.dart | 2 +- lib/widgets/attachment/attachment_item.dart | 88 +++--- lib/widgets/attachment/attachment_list.dart | 13 +- lib/widgets/chat/chat_message_input.dart | 8 +- lib/widgets/post/post_media_pending_list.dart | 20 +- 15 files changed, 341 insertions(+), 276 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 1ff481e..09dd13a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -15,6 +15,7 @@ analyzer: - "**/*.freezed.dart" errors: 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: # The lint rules applied to this project can be customized in the diff --git a/assets/translations/zh-HK.json b/assets/translations/zh-HK.json index 6eb690f..b96e2b3 100644 --- a/assets/translations/zh-HK.json +++ b/assets/translations/zh-HK.json @@ -279,16 +279,22 @@ "one": "{} 個附件", "other": "{} 個附件" }, + "fieldAttachmentRandomId": "訪問 ID", "addAttachmentFromAlbum": "從相冊中添加附件", "addAttachmentFromClipboard": "粘貼附件", "addAttachmentFromCameraPhoto": "拍攝照片", "addAttachmentFromCameraVideo": "拍攝視頻", + "addAttachmentFromRandomId": "通過訪問 ID 鏈接", "attachmentPastedImage": "粘貼的圖片", "attachmentInsertLink": "插入連接", "attachmentSetAsPostThumbnail": "設置為帖子縮略圖", "attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖", "attachmentSetThumbnail": "設置縮略圖", + "attachmentCopyRandomId": "複製訪問 ID", "attachmentUpload": "上傳", + "attachmentInputDialog": "上傳附件", + "attachmentInputUseRandomId": "使用訪問 ID", + "attachmentInputNew": "新上傳附件", "notification": "通知", "notificationUnreadCount": { "zero": "無未讀通知", @@ -504,5 +510,6 @@ "postCategoryKnowledge": "知識", "postCategoryLiterature": "文學", "postCategoryFunny": "搞笑", - "postCategoryUncategorized": "未分類" + "postCategoryUncategorized": "未分類", + "waitingForUpload": "等待上傳" } diff --git a/assets/translations/zh-TW.json b/assets/translations/zh-TW.json index 5ca6d6e..8377725 100644 --- a/assets/translations/zh-TW.json +++ b/assets/translations/zh-TW.json @@ -7,15 +7,15 @@ "screenAuthLogin": "登陸", "screenAuthLoginSubtitle": "使用 Solarpass 登陸 Solar Network", "screenAuthLoginGreeting": "歡迎回來", - "screenAuthRegister": "建立賬號", - "screenAuthRegisterSubtitle": "建立一個 Solarpass 賬號", - "screenAccountPublishers": "釋出者", - "screenAccountPublisherNew": "新建釋出者", - "screenAccountPublisherEdit": "編輯釋出者", + "screenAuthRegister": "創建賬號", + "screenAuthRegisterSubtitle": "創建一個 Solarpass 賬號", + "screenAccountPublishers": "發佈者", + "screenAccountPublisherNew": "新建發佈者", + "screenAccountPublisherEdit": "編輯發佈者", "screenAccountProfileEdit": "編輯資料", "screenAbuseReport": "濫用檢舉", - "screenSettings": "設定", - "screenAlbum": "相簿", + "screenSettings": "設置", + "screenAlbum": "相冊", "screenChat": "聊天", "screenChatManage": "編輯聊天頻道", "screenChatNew": "新建聊天頻道", @@ -23,37 +23,37 @@ "screenRealmManage": "編輯領域", "screenRealmNew": "新建領域", "screenNotification": "通知", - "screenPostSearch": "搜尋帖子", + "screenPostSearch": "搜索帖子", "screenFriend": "好友", "dialogOkay": "好的", "dialogCancel": "取消", "dialogConfirm": "確認", "dialogDismiss": "忽略", "dialogError": "出了點問題", - "errorRequestBad": "伺服器拒絕了您的請求,請檢查您的輸入。", - "errorRequestUnauthorized": "未授權的請求,請登入或者嘗試重新登陸。", - "errorRequestForbidden": "被禁止的請求,您沒有足夠的許可權去做那件事。", - "errorRequestNotFound": "您正查詢的資源無法被找到。", - "errorRequestConnection": "網路連線錯誤,請檢查您的網路狀態或者檢查我們的服務狀態。", - "errorRequestUnknown": "未知請求錯誤,您可能想將此對話方塊截圖併發送給我們。", + "errorRequestBad": "服務器拒絕了您的請求,請檢查您的輸入。", + "errorRequestUnauthorized": "未授權的請求,請登錄或者嘗試重新登陸。", + "errorRequestForbidden": "被禁止的請求,您沒有足夠的權限去做那件事。", + "errorRequestNotFound": "您正查找的資源無法被找到。", + "errorRequestConnection": "網絡連接錯誤,請檢查您的網絡狀態或者檢查我們的服務狀態。", + "errorRequestUnknown": "未知請求錯誤,您可能想將此對話框截圖併發送給我們。", "unknown": "未知", - "loading": "載入中…", + "loading": "加載中…", "prev": "上一步", "next": "下一步", "edit": "編輯", "apply": "應用", "cancel": "取消", - "create": "建立", + "create": "創建", "preview": "預覽", "delete": "刪除", - "unlink": "解除連結", + "unlink": "解除鏈接", "crop": "裁剪", "compress": "壓縮", "report": "檢舉", "repost": "轉帖", "replyPost": "回貼", "reply": "回覆", - "unset": "未設定", + "unset": "未設置", "untitled": "無題", "postDetail": "帖子詳情", "postNoun": "帖子", @@ -64,20 +64,20 @@ "one": "總計 {} 字", "other": "總計 {} 字" }, - "fieldUsername": "使用者名稱", + "fieldUsername": "用戶名", "fieldNickname": "顯示名", "fieldEmail": "電子郵箱地址", "fieldPassword": "密碼", - "fieldUsernameAlphanumOnly": "使用者名稱只能包含英文大小寫字母和數字。", - "fieldUsernameLengthLimit": "使用者名稱必須在 {} 和 {} 之間。", - "fieldUsernameCannotEditHint": "使用者名稱在建立後無法修改", - "fieldUsernameLookupHint": "支援使用者名稱、電話號碼或郵箱地址", + "fieldUsernameAlphanumOnly": "用戶名只能包含英文大小寫字母和數字。", + "fieldUsernameLengthLimit": "用戶名必須在 {} 和 {} 之間。", + "fieldUsernameCannotEditHint": "用戶名在創建後無法修改", + "fieldUsernameLookupHint": "支持用戶名、電話號碼或郵箱地址", "fieldNicknameLengthLimit": "暱稱必須在 {} 和 {} 之間。", "fieldEmailAddressMustBeValid": "電子郵箱地址必須是一個電子郵箱地址。", "fieldFirstName": "名", "fieldLastName": "姓", "fieldBirthday": "生日", - "fieldImageHint": "你可以點選這些個人頭像來編輯它們。", + "fieldImageHint": "你可以點擊這些個人頭像來編輯它們。", "fieldDescription": "簡介", "forgotPassword": "忘記密碼", "loginPickFactor": "選擇方式驗證", @@ -85,24 +85,24 @@ "one": "{} 步驗證", "other": "{} 步驗證" }, - "loginEnterPassword": "驗證程式碼", - "loginSuccess": "登入為 {}", + "loginEnterPassword": "驗證代碼", + "loginSuccess": "登錄為 {}", "authFactorPassword": "密碼", "authFactorEmail": "電郵一次性驗證碼", "accountIntroTitle": "喜歡您來!", "accountIntroSubtitle": "登陸以探索更廣大的世界。", - "accountLogout": "退出登入", - "accountLogoutSubtitle": "登出當前賬戶的登陸狀態。", - "accountLogoutConfirmTitle": "您確定要退出登入嗎?", + "accountLogout": "退出登錄", + "accountLogoutSubtitle": "註銷當前賬戶的登陸狀態。", + "accountLogoutConfirmTitle": "您確定要退出登錄嗎?", "accountLogoutConfirm": "您需要重新輸入賬號密碼,甚至可能需要多步驗證來再次登陸。", - "accountPublishers": "你的釋出者", + "accountPublishers": "你的發佈者", "accountPublishersSubtitle": "管理你的公共形象。", "accountProfileEdit": "編輯資料", "accountProfileEditSubtitle": "使你的 Solarpass 賬戶更像你。", "accountProfileEditApplied": "個人資料修改已被應用。", "publishersNew": "新發布者", - "publisherNewSubtitle": "建立一個新的公共身份。", - "publisherSyncWithAccount": "同步賬戶資訊", + "publisherNewSubtitle": "創建一個新的公共身份。", + "publisherSyncWithAccount": "同步賬戶信息", "publisherTotalUpvote": "總頂數", "publisherTotalDownvote": "總踩數", "publisherSocialPoint": "社會信用點", @@ -115,10 +115,10 @@ "publisherAffiliatedBy": "隸屬於 {}", "publisherRunBy": "由 {} 管理", "fieldPublisherBelongToRealm": "所屬領域", - "fieldPublisherBelongToRealmUnset": "未設定釋出者所屬領域", + "fieldPublisherBelongToRealmUnset": "未設置發佈者所屬領域", "writePostTypeStory": "發動態", "writePostTypeArticle": "寫文章", - "fieldPostPublisher": "帖子釋出者", + "fieldPostPublisher": "帖子發佈者", "fieldPostContent": "發生什麼事了?!", "fieldPostTitle": "標題", "fieldPostDescription": "描述", @@ -126,26 +126,26 @@ "fieldPostCategories": "分類", "fieldPostAlias": "別名", "fieldPostAliasHint": "可選項,用於在 URL 中表示該帖子,應遵循 URL-Safe 的原則。", - "postPublish": "釋出", - "postPublishedAt": "釋出於", - "postPublishedUntil": "取消釋出於", + "postPublish": "發佈", + "postPublishedAt": "發佈於", + "postPublishedUntil": "取消發佈於", "postVisibility": "可見性", - "postVisibilityDescription": "帖子可見性決定了誰能檢視該篇帖子。", + "postVisibilityDescription": "帖子可見性決定了誰能查看該篇帖子。", "postVisibilityAll": "所有人可見", "postVisibilityFriends": "僅限好友可見", - "postVisibilitySelected": "選定的使用者可見", - "postVisibilityFiltered": "選定使用者不可見", + "postVisibilitySelected": "選定的用戶可見", + "postVisibilityFiltered": "選定用戶不可見", "postVisibilityNone": "僅自己可見", - "postVisibleUsers": "可見的使用者", - "postInvisibleUsers": "不可見的使用者", + "postVisibleUsers": "可見的用戶", + "postInvisibleUsers": "不可見的用戶", "postSelectedUsers": { - "zero": "未選擇使用者", - "one": "選擇了 {} 個使用者", - "other": "選擇了 {} 個使用者" + "zero": "未選擇用戶", + "one": "選擇了 {} 個用戶", + "other": "選擇了 {} 個用戶" }, - "postEditingNotice": "你正在修改由 {} 釋出的帖子。", - "postReplyingNotice": "你正在回覆由 {} 釋出的帖子。", - "postRepostingNotice": "你正在轉發由 {} 釋出的帖子。", + "postEditingNotice": "你正在修改由 {} 發佈的帖子。", + "postReplyingNotice": "你正在回覆由 {} 發佈的帖子。", + "postRepostingNotice": "你正在轉發由 {} 發佈的帖子。", "postReact": "反應", "postPosted": "帖子已經發表。", "postReactions": "帖子的反應", @@ -164,7 +164,7 @@ "one": "{} 點社會信用點變更", "other": "{} 點社會信用點變更" }, - "postReactCompleted": "反應已被新增。", + "postReactCompleted": "反應已被添加。", "postReactUncompleted": "反應已被移除。", "postComments": { "zero": "評論", @@ -178,76 +178,76 @@ }, "settingsAppearance": "外觀", "settingsBackgroundImage": "背景圖片", - "settingsBackgroundImageDescription": "設定應用全域性生效的的背景圖片。", + "settingsBackgroundImageDescription": "設置應用全局生效的的背景圖片。", "settingsBackgroundImageClear": "清除現存背景圖", "settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。", - "settingsThemeMaterial3": "使用 Material You 設計正規化", - "settingsThemeMaterial3Description": "將應用主題設定為 Material 3 設計正規化的主題。", + "settingsThemeMaterial3": "使用 Material You 設計範式", + "settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。", "settingsAppBarTransparent": "透明頂欄", "settingsAppBarTransparentDescription": "為頂欄啟用透明效果。", "settingsColorScheme": "主題色", - "settingsColorSchemeDescription": "設定應用主題色。", + "settingsColorSchemeDescription": "設置應用主題色。", "settingsColorSeed": "預設色彩主題", "settingsColorSeedDescription": "選擇一個預設色彩主題。", - "settingsNetwork": "網路", - "settingsNetworkServer": "HyperNet 伺服器", - "settingsNetworkServerDescription": "設定 HyperNet 伺服器地址,選擇我們提供的,或者自己搭建。", - "settingsNetworkServerReset": "重設為官方伺服器", - "settingsNetworkServerResetDescription": "重設為 Solar Network 的伺服器地址。", - "settingsNetworkServerPreset": "預設的 HyperNet 伺服器", - "settingsNetworkServerPresetDescription": "你可以在旁邊的列表中選擇我們提供的預設 HyperNet 伺服器地址。", - "settingsNetworkServerSaved": "伺服器地址已儲存。", - "settingsPerformance": "效能", + "settingsNetwork": "網絡", + "settingsNetworkServer": "HyperNet 服務器", + "settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。", + "settingsNetworkServerReset": "重設為官方服務器", + "settingsNetworkServerResetDescription": "重設為 Solar Network 的服務器地址。", + "settingsNetworkServerPreset": "預設的 HyperNet 服務器", + "settingsNetworkServerPresetDescription": "你可以在旁邊的列表中選擇我們提供的預設 HyperNet 服務器地址。", + "settingsNetworkServerSaved": "服務器地址已保存。", + "settingsPerformance": "性能", "settingsImageQuality": "圖片預覽質量", - "settingsImageQualityDescription": "設定圖片預覽質量,會影響圖片解碼速度。", + "settingsImageQualityDescription": "設置圖片預覽質量,會影響圖片解碼速度。", "settingsImageQualityLowest": "極低", "settingsImageQualityLow": "低", "settingsImageQualityMedium": "中", "settingsImageQualityHigh": "高", "settingsMisc": "雜項", "settingsMiscAbout": "關於", - "settingsMiscAboutDescription": "檢視 Solian 的版本資訊。", + "settingsMiscAboutDescription": "查看 Solian 的版本信息。", "sensitiveContent": "敏感內容", "sensitiveContentCollapsed": "敏感內容已摺疊。", - "sensitiveContentDescription": "此內容已被標記,可能不適合所有人檢視。", + "sensitiveContentDescription": "此內容已被標記,可能不適合所有人查看。", "sensitiveContentReveal": "顯示內容", - "serverConnecting": "正在連線伺服器…", - "serverDisconnected": "已與伺服器斷開連線", + "serverConnecting": "正在連接服務器…", + "serverDisconnected": "已與服務器斷開連接", "fieldChatAlias": "頻道別名", "fieldChatAliasHint": "全站範圍內唯一的頻道別名,用於在 URL 中表示該頻道,留空則自動生成。應遵循 URL-Safe 的原則。", "fieldChatName": "名稱", "fieldChatDescription": "描述", "fieldChatBelongToRealm": "所屬領域", - "fieldChatBelongToRealmUnset": "未設定頻道所屬領域", + "fieldChatBelongToRealmUnset": "未設置頻道所屬領域", "channelEditingNotice": "您正在編輯頻道 {}", "channelDeleted": "聊天頻道 {} 已被刪除", "channelDelete": "刪除聊天頻道 {}", - "channelDeleteDescription": "你確定要刪除這個聊天頻道嗎?該操作不可撤銷,其頻道內的所有訊息將被永久刪除。", + "channelDeleteDescription": "你確定要刪除這個聊天頻道嗎?該操作不可撤銷,其頻道內的所有消息將被永久刪除。", "channelDetailPersonalRegion": "個人區域", "channelDetailMemberRegion": "成員管理", "channelMemberManage": "管理成員", "channelMemberManageDescription": "管理頻道內現有成員。", - "channelMemberAdd": "新增成員", - "channelMemberAddDescription": "給當前頻道新增新成員。", - "channelMemberAdded": "頻道成員已新增。", + "channelMemberAdd": "添加成員", + "channelMemberAddDescription": "給當前頻道添加新成員。", + "channelMemberAdded": "頻道成員已添加。", "fieldMemberRelatedName": "成員名 / 賬戶 ID", "channelDetailAdminRegion": "管理區域", "channelEditProfile": "更改頻道身份", "channelEdit": "編輯頻道", - "channelEditDescription": "更改頻道基本資訊,元資料等。", + "channelEditDescription": "更改頻道基本信息,元數據等。", "channelProfileEdit": "編輯頻道身份", "channelActionDelete": "刪除頻道", - "channelActionDeleteDescription": "刪除整個頻道,並且刪除頻道里的所有資訊。", + "channelActionDeleteDescription": "刪除整個頻道,並且刪除頻道里的所有信息。", "channelLeave": "退出頻道 {}", - "channelLeaveDescription": "退出該頻道,但是你頻道內的資訊不會被移除。", + "channelLeaveDescription": "退出該頻道,但是你頻道內的信息不會被移除。", "channelActionLeave": "退出頻道", "channelActionLeaveDescription": "刪除你在這個頻道的身份。", "channelNotifyLevel": "通知級別", - "channelNotifyLevelDescription": "有您決定要接受多少來自這個頻道的訊息。", + "channelNotifyLevelDescription": "有您決定要接受多少來自這個頻道的消息。", "channelNotifyLevelAll": "全部通知", "channelNotifyLevelMentioned": "僅提及", "channelNotifyLevelNone": "全部靜音", - "channelNotifyLevelApplied": "已經儲存並應用頻道通知級別配置。", + "channelNotifyLevelApplied": "已經保存並應用頻道通知級別配置。", "fieldChannelProfileNick": "頻道內顯示名", "fieldChannelProfileNickHint": "在頻道內顯示的暱稱,留空則使用賬號顯示名。", "fieldRealmAlias": "領域別名", @@ -257,38 +257,44 @@ "realmEditingNotice": "您正在編輯領域 {}", "realmDeleted": "領域 {} 已被刪除", "realmDelete": "刪除領域 {}", - "realmDeleteDescription": "你確定要刪除這個領域嗎?該操作不可撤銷,其隸屬於該領域的所有資源(帖子、聊天頻道、釋出者、製品等)都將被永久刪除。三思而後行!", + "realmDeleteDescription": "你確定要刪除這個領域嗎?該操作不可撤銷,其隸屬於該領域的所有資源(帖子、聊天頻道、發佈者、製品等)都將被永久刪除。三思而後行!", "realmActionDelete": "刪除領域", "realmActionDeleteDescription": "刪除整個領域及其附屬的資源。", "realmEdit": "編輯領域", - "realmEditDescription": "更改領域基本資訊,元資料等。", - "realmMemberAdd": "新增成員", - "realmMemberAddDescription": "給當前領域新增新成員。", - "realmMemberAdded": "領域成員已新增。", - "fieldChatMessage": "在 {} 中發訊息", - "fieldChatMessageDirect": "給 {} 發訊息", - "eventResourceTag": "訊息 {}", - "messageDelete": "刪除訊息 {}", - "messageDeleteDescription": "你確定要刪除這個訊息嗎?該操作不可撤銷。同時您將留下一條刪除訊息的記錄。", - "messageDeleted": "訊息 {} 已被刪除", - "messageEdited": "訊息 {} 已被編輯", + "realmEditDescription": "更改領域基本信息,元數據等。", + "realmMemberAdd": "添加成員", + "realmMemberAddDescription": "給當前領域添加新成員。", + "realmMemberAdded": "領域成員已添加。", + "fieldChatMessage": "在 {} 中發消息", + "fieldChatMessageDirect": "給 {} 發消息", + "eventResourceTag": "消息 {}", + "messageDelete": "刪除消息 {}", + "messageDeleteDescription": "你確定要刪除這個消息嗎?該操作不可撤銷。同時您將留下一條刪除消息的記錄。", + "messageDeleted": "消息 {} 已被刪除", + "messageEdited": "消息 {} 已被編輯", "messageEditedHint": "已編輯", - "messageUnsupported": "不支援的訊息 {}", + "messageUnsupported": "不支持的消息 {}", "messageFileHint": { "zero": "沒有附件", "one": "{} 個附件", "other": "{} 個附件" }, - "addAttachmentFromAlbum": "從相簿中新增附件", - "addAttachmentFromClipboard": "貼上附件", + "fieldAttachmentRandomId": "訪問 ID", + "addAttachmentFromAlbum": "從相冊中添加附件", + "addAttachmentFromClipboard": "粘貼附件", "addAttachmentFromCameraPhoto": "拍攝照片", - "addAttachmentFromCameraVideo": "拍攝影片", - "attachmentPastedImage": "貼上的圖片", - "attachmentInsertLink": "插入連線", - "attachmentSetAsPostThumbnail": "設定為帖子縮圖", - "attachmentUnsetAsPostThumbnail": "取消設定為帖子縮圖", - "attachmentSetThumbnail": "設定縮圖", + "addAttachmentFromCameraVideo": "拍攝視頻", + "addAttachmentFromRandomId": "通過訪問 ID 鏈接", + "attachmentPastedImage": "粘貼的圖片", + "attachmentInsertLink": "插入連接", + "attachmentSetAsPostThumbnail": "設置為帖子縮略圖", + "attachmentUnsetAsPostThumbnail": "取消設置為帖子縮略圖", + "attachmentSetThumbnail": "設置縮略圖", + "attachmentCopyRandomId": "複製訪問 ID", "attachmentUpload": "上傳", + "attachmentInputDialog": "上傳附件", + "attachmentInputUseRandomId": "使用訪問 ID", + "attachmentInputNew": "新上傳附件", "notification": "通知", "notificationUnreadCount": { "zero": "無未讀通知", @@ -298,18 +304,18 @@ "notificationUnread": "未讀", "notificationRead": "已讀", "notificationMarkAllRead": "已讀所有通知", - "notificationMarkAllReadDescription": "您確定要將所有通知設定為已讀嗎?該操作不可撤銷。", + "notificationMarkAllReadDescription": "您確定要將所有通知設置為已讀嗎?該操作不可撤銷。", "notificationMarkAllReadPrompt": { "zero": "已將 0 個通知標記為已讀。", "one": "已將 {} 個通知標記為已讀。", "other": "已將 {} 個通知標記為已讀。" }, "notificationMarkOneReadPrompt": "已將通知 {} 標記為已讀。", - "search": "搜尋", + "search": "搜索", "postSearchResult": { - "zero": "沒有搜尋到結果", - "one": "搜尋到 {} 個結果", - "other": "搜尋到 {} 個結果" + "zero": "沒有搜索到結果", + "one": "搜索到 {} 個結果", + "other": "搜索到 {} 個結果" }, "postSearchTook": "耗時 {}", "postDelete": "刪除帖子 {}", @@ -321,26 +327,26 @@ "callResume": "恢復", "callMicrophone": "麥克風", "callCamera": "攝像頭", - "callMicrophoneDisabled": "麥克風已停用", + "callMicrophoneDisabled": "麥克風已禁用", "callMicrophoneSelect": "選擇麥克風", - "callCameraDisabled": "攝像頭已停用", + "callCameraDisabled": "攝像頭已禁用", "callCameraSelect": "選擇攝像頭", "callDisconnected": "通話已斷開", "callEnded": "通話已結束", - "callStatusConnected": "已連線", - "callStatusDisconnected": "未連線", - "callStatusConnecting": "正在連線", + "callStatusConnected": "已連接", + "callStatusDisconnected": "未連接", + "callStatusConnecting": "正在連接", "callStatusReconnecting": "正在重連", - "callDisconnect": "斷開連線", - "callDisconnectDescription": "您確定要與通話斷開連線嗎?", + "callDisconnect": "斷開連接", + "callDisconnectDescription": "您確定要與通話斷開連接嗎?", "callMicrophoneOff": "關閉麥克風", - "callMicrophoneOn": "開啟麥克風", + "callMicrophoneOn": "打開麥克風", "callCameraOff": "關閉攝像頭", - "callCameraOn": "開啟攝像頭", - "callVideoFlip": "映象畫面", + "callCameraOn": "打開攝像頭", + "callVideoFlip": "鏡像畫面", "callSpeakerphoneToggle": "切換揚聲器", - "callScreenOff": "關閉螢幕共享", - "callScreenOn": "開啟螢幕共享", + "callScreenOff": "關閉屏幕共享", + "callScreenOn": "開啟屏幕共享", "callMessageEnded": "通話持續了 {}", "callMessageStarted": "通話開始了", "dailyCheckIn": "每日簽到", @@ -396,27 +402,27 @@ "pendingFatherDay": "{} 過父親節", "pendingHalloween": "{} 過聖誕節", "pendingThanksgiving": "{} 過感恩節", - "friendNew": "新增好友", + "friendNew": "添加好友", "friendRequests": "好友請求", "friendRequestsDescription": { "zero": "你沒有好友請求", "one": "你有 {} 個好友請求", "other": "你有 {} 個好友請求" }, - "friendBlocklist": "遮蔽列表", + "friendBlocklist": "屏蔽列表", "friendBlocklistDescription": { - "zero": "你沒有遮蔽任何人", - "one": "你遮蔽了 {} 個使用者", - "other": "你遮蔽了 {} 個使用者" + "zero": "你沒有屏蔽任何人", + "one": "你屏蔽了 {} 個用戶", + "other": "你屏蔽了 {} 個用戶" }, "friendStatusPending": "待處理", "friendStatusWaiting": "等待中", "friendStatusActive": "正活躍", - "friendStatusBlocked": "已遮蔽", - "friendRequestSent": "好友請求已傳送。", + "friendStatusBlocked": "已屏蔽", + "friendRequestSent": "好友請求已發送。", "fieldFriendRelatedName": "好友名 / 賬戶 ID", - "friendBlock": "遮蔽", - "friendUnblock": "解除遮蔽", + "friendBlock": "屏蔽", + "friendUnblock": "解除屏蔽", "friendDeleteAction": "遺忘", "friendDelete": "遺忘跟 {} 的關係", "friendDeleteDescription": "你確定要遺忘跟 {} 的關係嗎?這個操作無法撤銷。", @@ -432,20 +438,20 @@ "badgeCompanyStaff": "索爾辛茨士大夫 · 員工", "badgeSiteMigration": "Solar Network 原住民", "accountStatus": "狀態", - "accountStatusOnline": "線上", + "accountStatusOnline": "在線", "accountStatusOffline": "離線", "accountStatusLastSeen": "最後一次上線於 {}", "postArticle": "Solar Network 上的文章", "postStory": "Solar Network 上的故事", "articleWrittenAt": "發表於 {}", "articleEditedAt": "編輯於 {}", - "attachmentSaved": "已儲存到相簿", - "attachmentSavedDesktop": "已儲存到下載目錄", - "openInAlbum": "在相簿中開啟", + "attachmentSaved": "已保存到相冊", + "attachmentSavedDesktop": "已保存到下載目錄", + "openInAlbum": "在相冊中打開", "postAbuseReport": "檢舉帖子", - "postAbuseReportDescription": "檢舉不符合我們使用者協議以及社群準則的帖子,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述該帖子如何違反我麼的相關規定。請勿填寫任何敏感資訊。我們將會在 24 小時內處理您的檢舉。", + "postAbuseReportDescription": "檢舉不符合我們用戶協議以及社區準則的帖子,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述該帖子如何違反我麼的相關規定。請勿填寫任何敏感信息。我們將會在 24 小時內處理您的檢舉。", "abuseReport": "檢舉", - "abuseReportDescription": "檢舉不符合我們使用者協議以及社群準則的任何資源,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述資源的位置(提供資源 ID 為佳)以及如何違反我麼的相關規定。請勿填寫任何敏感資訊。我們將會在 24 小時內處理您的檢舉。", + "abuseReportDescription": "檢舉不符合我們用戶協議以及社區準則的任何資源,來幫助我們更好的維護 Solar Network 上的內容。請在下面描述資源的位置(提供資源 ID 為佳)以及如何違反我麼的相關規定。請勿填寫任何敏感信息。我們將會在 24 小時內處理您的檢舉。", "abuseReportAction": "提交檢舉", "abuseReportActionDescription": "檢舉不合規行為。", "abuseReportResource": "資源位置 / ID", @@ -453,35 +459,35 @@ "abuseReportSubmitted": "檢舉已提交,感謝你的貢獻。", "submit": "提交", "accountDeletion": "刪除帳戶", - "accountDeletionDescription": "你確定要刪除這個帳戶嗎?該操作不可撤銷,其隸屬於該帳戶的所有資源(帖子、聊天頻道、釋出者、製品等)都將被永久刪除。三思而後行!", + "accountDeletionDescription": "你確定要刪除這個帳戶嗎?該操作不可撤銷,其隸屬於該帳戶的所有資源(帖子、聊天頻道、發佈者、製品等)都將被永久刪除。三思而後行!", "accountDeletionActionDescription": "刪除你的 Solarpass 帳戶。", "accountDeletionSubmitted": "帳戶刪除申請已發出,你可以檢查你的收件箱並根據郵件內的指示完成刪除操作。", "channelNewChannel": "新建頻道", "channelNewDirectMessage": "發起私信", "channelDirectMessageDescription": "與 {} 的私聊", - "fieldCannotBeEmpty": "此欄位不能為空。", + "fieldCannotBeEmpty": "此字段不能為空。", "termAcceptLink": "瀏覽條款", - "termAcceptNextWithAgree": "點選 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。", + "termAcceptNextWithAgree": "點擊 “下一步”,即表示你同意我們的各項條款,包括其之後的更新。", "unauthorized": "未登陸", "unauthorizedDescription": "登陸以探索整個 Solar Network。", "serviceStatus": "服務狀態", "termRelated": "相關條款", - "appDetails": "應用程式詳情", + "appDetails": "應用程序詳情", "postRecommendation": "推薦帖子", - "publisherBlockHint": "遮蔽 {}", - "publisherBlockHintDescription": "你正要遮蔽此釋出者的運營者,該操作也將遮蔽由同一使用者運營的釋出者。", - "userUnblocked": "已解除遮蔽使用者 {}", - "userBlocked": "已遮蔽使用者 {}", + "publisherBlockHint": "屏蔽 {}", + "publisherBlockHintDescription": "你正要屏蔽此發佈者的運營者,該操作也將屏蔽由同一用戶運營的發佈者。", + "userUnblocked": "已解除屏蔽用戶 {}", + "userBlocked": "已屏蔽用戶 {}", "postSharingViaPicture": "正在生成帖子截圖,請稍等片刻……", - "postImageShareReadMore": "掃描右側 QRCode 檢視全文", + "postImageShareReadMore": "掃描右側 QRCode 查看全文", "postImageShareAds": "來 Solar Network 探索更多有趣帖子", "postShare": "分享", "postShareImage": "分享帖圖", "appInitializing": "正在初始化", - "poweredBy": "由 {} 提供支援", + "poweredBy": "由 {} 提供支持", "shareIntent": "分享", "shareIntentDescription": "您想對您分享的內容做些什麼?", - "shareIntentPostStory": "釋出動態", + "shareIntentPostStory": "發佈動態", "updateAvailable": "檢測到更新可用", "updateOngoing": "正在更新,請稍後……", "custom": "自定義", @@ -504,5 +510,6 @@ "postCategoryKnowledge": "知識", "postCategoryLiterature": "文學", "postCategoryFunny": "搞笑", - "postCategoryUncategorized": "未分類" + "postCategoryUncategorized": "未分類", + "waitingForUpload": "等待上傳" } diff --git a/lib/controllers/post_write_controller.dart b/lib/controllers/post_write_controller.dart index 9c4913e..7a98ba7 100644 --- a/lib/controllers/post_write_controller.dart +++ b/lib/controllers/post_write_controller.dart @@ -15,16 +15,9 @@ import 'package:surface/types/post.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/universal_image.dart'; -enum PostWriteMediaType { - image, - video, - audio, - file, -} - class PostWriteMedia { late String name; - late PostWriteMediaType type; + late SnMediaType type; final SnAttachment? attachment; final XFile? file; final Uint8List? raw; @@ -36,16 +29,16 @@ class PostWriteMedia { switch (attachment?.mimetype.split('/').firstOrNull) { case 'image': - type = PostWriteMediaType.image; + type = SnMediaType.image; break; case 'video': - type = PostWriteMediaType.video; + type = SnMediaType.video; break; case 'audio': - type = PostWriteMediaType.audio; + type = SnMediaType.audio; break; default: - type = PostWriteMediaType.file; + type = SnMediaType.file; } } @@ -57,16 +50,16 @@ class PostWriteMedia { switch (mimetype?.split('/').firstOrNull) { case 'image': - type = PostWriteMediaType.image; + type = SnMediaType.image; break; case 'video': - type = PostWriteMediaType.video; + type = SnMediaType.video; break; case 'audio': - type = PostWriteMediaType.audio; + type = SnMediaType.audio; break; default: - type = PostWriteMediaType.file; + type = SnMediaType.file; } } @@ -244,7 +237,7 @@ class PostWriteController extends ChangeNotifier { media.name, 'interactive', 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( @@ -301,7 +294,7 @@ class PostWriteController extends ChangeNotifier { media.name, 'interactive', 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( diff --git a/lib/providers/userinfo.dart b/lib/providers/userinfo.dart index 255a14f..03ae8b5 100644 --- a/lib/providers/userinfo.dart +++ b/lib/providers/userinfo.dart @@ -5,7 +5,6 @@ import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:surface/providers/config.dart'; import 'package:surface/providers/sn_network.dart'; -import 'package:surface/providers/widget.dart'; import 'package:surface/types/account.dart'; class UserProvider extends ChangeNotifier { @@ -13,12 +12,10 @@ class UserProvider extends ChangeNotifier { SnAccount? user; late final SnNetworkProvider _sn; - late final HomeWidgetProvider _home; late final ConfigProvider _config; UserProvider(BuildContext context) { _sn = context.read(); - _home = context.read(); _config = context.read(); } diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index 61081a1..39dd462 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -1,16 +1,11 @@ -import 'dart:io'; - import 'package:collection/collection.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:material_symbols_icons/symbols.dart'; -import 'package:pasteboard/pasteboard.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/controllers/post_write_controller.dart'; import 'package:surface/providers/config.dart'; diff --git a/lib/theme.dart b/lib/theme.dart index 0559822..c2123f3 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -33,7 +33,6 @@ Future createAppTheme( brightness: brightness, ); - final hasBackground = prefs.getBool(kAppBackgroundStoreKey) ?? false; final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false; return ThemeData( diff --git a/lib/types/attachment.dart b/lib/types/attachment.dart index 8fbb263..2bd38af 100644 --- a/lib/types/attachment.dart +++ b/lib/types/attachment.dart @@ -1,10 +1,20 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'attachment.freezed.dart'; + part 'attachment.g.dart'; +enum SnMediaType { + image, + video, + audio, + file, +} + @freezed class SnAttachment with _$SnAttachment { + const SnAttachment._(); + const factory SnAttachment({ required int id, required DateTime createdAt, @@ -19,9 +29,10 @@ class SnAttachment with _$SnAttachment { required String hash, required int destination, required int refCount, + @Default(0) int contentRating, + @Default(0) int qualityRating, required dynamic fileChunks, required dynamic cleanedAt, - required bool isMature, required bool isAnalyzed, required bool isUploaded, required bool isSelfRef, @@ -30,11 +41,23 @@ class SnAttachment with _$SnAttachment { required SnAttachmentPool? pool, required int poolId, required int accountId, + @Default({}) Map usermeta, @Default({}) Map metadata, }) = _SnAttachment; - factory SnAttachment.fromJson(Map json) => - _$SnAttachmentFromJson(json); + factory SnAttachment.fromJson(Map json) => _$SnAttachmentFromJson(json); + + Map get data => { + ...metadata, + ...usermeta, + }; + + SnMediaType get mediaType => switch (mimetype.split('/').firstOrNull) { + 'image' => SnMediaType.image, + 'video' => SnMediaType.video, + 'audio' => SnMediaType.audio, + _ => SnMediaType.file, + }; } @freezed @@ -51,6 +74,5 @@ class SnAttachmentPool with _$SnAttachmentPool { required int? accountId, }) = _SnAttachmentPool; - factory SnAttachmentPool.fromJson(Map json) => - _$SnAttachmentPoolFromJson(json); + factory SnAttachmentPool.fromJson(Map json) => _$SnAttachmentPoolFromJson(json); } diff --git a/lib/types/attachment.freezed.dart b/lib/types/attachment.freezed.dart index 84ef1ec..ab4ba29 100644 --- a/lib/types/attachment.freezed.dart +++ b/lib/types/attachment.freezed.dart @@ -33,9 +33,10 @@ mixin _$SnAttachment { String get hash => throw _privateConstructorUsedError; int get destination => throw _privateConstructorUsedError; int get refCount => throw _privateConstructorUsedError; + int get contentRating => throw _privateConstructorUsedError; + int get qualityRating => throw _privateConstructorUsedError; dynamic get fileChunks => throw _privateConstructorUsedError; dynamic get cleanedAt => throw _privateConstructorUsedError; - bool get isMature => throw _privateConstructorUsedError; bool get isAnalyzed => throw _privateConstructorUsedError; bool get isUploaded => throw _privateConstructorUsedError; bool get isSelfRef => throw _privateConstructorUsedError; @@ -44,6 +45,7 @@ mixin _$SnAttachment { SnAttachmentPool? get pool => throw _privateConstructorUsedError; int get poolId => throw _privateConstructorUsedError; int get accountId => throw _privateConstructorUsedError; + Map get usermeta => throw _privateConstructorUsedError; Map get metadata => throw _privateConstructorUsedError; /// Serializes this SnAttachment to a JSON map. @@ -76,9 +78,10 @@ abstract class $SnAttachmentCopyWith<$Res> { String hash, int destination, int refCount, + int contentRating, + int qualityRating, dynamic fileChunks, dynamic cleanedAt, - bool isMature, bool isAnalyzed, bool isUploaded, bool isSelfRef, @@ -87,6 +90,7 @@ abstract class $SnAttachmentCopyWith<$Res> { SnAttachmentPool? pool, int poolId, int accountId, + Map usermeta, Map metadata}); $SnAttachmentPoolCopyWith<$Res>? get pool; @@ -120,9 +124,10 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment> Object? hash = null, Object? destination = null, Object? refCount = null, + Object? contentRating = null, + Object? qualityRating = null, Object? fileChunks = freezed, Object? cleanedAt = freezed, - Object? isMature = null, Object? isAnalyzed = null, Object? isUploaded = null, Object? isSelfRef = null, @@ -131,6 +136,7 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment> Object? pool = freezed, Object? poolId = null, Object? accountId = null, + Object? usermeta = null, Object? metadata = null, }) { return _then(_value.copyWith( @@ -186,6 +192,14 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment> ? _value.refCount : refCount // ignore: cast_nullable_to_non_nullable as int, + contentRating: null == contentRating + ? _value.contentRating + : contentRating // ignore: cast_nullable_to_non_nullable + as int, + qualityRating: null == qualityRating + ? _value.qualityRating + : qualityRating // ignore: cast_nullable_to_non_nullable + as int, fileChunks: freezed == fileChunks ? _value.fileChunks : fileChunks // ignore: cast_nullable_to_non_nullable @@ -194,10 +208,6 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment> ? _value.cleanedAt : cleanedAt // ignore: cast_nullable_to_non_nullable as dynamic, - isMature: null == isMature - ? _value.isMature - : isMature // ignore: cast_nullable_to_non_nullable - as bool, isAnalyzed: null == isAnalyzed ? _value.isAnalyzed : isAnalyzed // ignore: cast_nullable_to_non_nullable @@ -230,6 +240,10 @@ class _$SnAttachmentCopyWithImpl<$Res, $Val extends SnAttachment> ? _value.accountId : accountId // ignore: cast_nullable_to_non_nullable as int, + usermeta: null == usermeta + ? _value.usermeta + : usermeta // ignore: cast_nullable_to_non_nullable + as Map, metadata: null == metadata ? _value.metadata : metadata // ignore: cast_nullable_to_non_nullable @@ -274,9 +288,10 @@ abstract class _$$SnAttachmentImplCopyWith<$Res> String hash, int destination, int refCount, + int contentRating, + int qualityRating, dynamic fileChunks, dynamic cleanedAt, - bool isMature, bool isAnalyzed, bool isUploaded, bool isSelfRef, @@ -285,6 +300,7 @@ abstract class _$$SnAttachmentImplCopyWith<$Res> SnAttachmentPool? pool, int poolId, int accountId, + Map usermeta, Map metadata}); @override @@ -317,9 +333,10 @@ class __$$SnAttachmentImplCopyWithImpl<$Res> Object? hash = null, Object? destination = null, Object? refCount = null, + Object? contentRating = null, + Object? qualityRating = null, Object? fileChunks = freezed, Object? cleanedAt = freezed, - Object? isMature = null, Object? isAnalyzed = null, Object? isUploaded = null, Object? isSelfRef = null, @@ -328,6 +345,7 @@ class __$$SnAttachmentImplCopyWithImpl<$Res> Object? pool = freezed, Object? poolId = null, Object? accountId = null, + Object? usermeta = null, Object? metadata = null, }) { return _then(_$SnAttachmentImpl( @@ -383,6 +401,14 @@ class __$$SnAttachmentImplCopyWithImpl<$Res> ? _value.refCount : refCount // ignore: cast_nullable_to_non_nullable as int, + contentRating: null == contentRating + ? _value.contentRating + : contentRating // ignore: cast_nullable_to_non_nullable + as int, + qualityRating: null == qualityRating + ? _value.qualityRating + : qualityRating // ignore: cast_nullable_to_non_nullable + as int, fileChunks: freezed == fileChunks ? _value.fileChunks : fileChunks // ignore: cast_nullable_to_non_nullable @@ -391,10 +417,6 @@ class __$$SnAttachmentImplCopyWithImpl<$Res> ? _value.cleanedAt : cleanedAt // ignore: cast_nullable_to_non_nullable as dynamic, - isMature: null == isMature - ? _value.isMature - : isMature // ignore: cast_nullable_to_non_nullable - as bool, isAnalyzed: null == isAnalyzed ? _value.isAnalyzed : isAnalyzed // ignore: cast_nullable_to_non_nullable @@ -427,6 +449,10 @@ class __$$SnAttachmentImplCopyWithImpl<$Res> ? _value.accountId : accountId // ignore: cast_nullable_to_non_nullable as int, + usermeta: null == usermeta + ? _value._usermeta + : usermeta // ignore: cast_nullable_to_non_nullable + as Map, metadata: null == metadata ? _value._metadata : metadata // ignore: cast_nullable_to_non_nullable @@ -437,7 +463,7 @@ class __$$SnAttachmentImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$SnAttachmentImpl implements _SnAttachment { +class _$SnAttachmentImpl extends _SnAttachment { const _$SnAttachmentImpl( {required this.id, required this.createdAt, @@ -452,9 +478,10 @@ class _$SnAttachmentImpl implements _SnAttachment { required this.hash, required this.destination, required this.refCount, + this.contentRating = 0, + this.qualityRating = 0, required this.fileChunks, required this.cleanedAt, - required this.isMature, required this.isAnalyzed, required this.isUploaded, required this.isSelfRef, @@ -463,8 +490,11 @@ class _$SnAttachmentImpl implements _SnAttachment { required this.pool, required this.poolId, required this.accountId, + final Map usermeta = const {}, final Map metadata = const {}}) - : _metadata = metadata; + : _usermeta = usermeta, + _metadata = metadata, + super._(); factory _$SnAttachmentImpl.fromJson(Map json) => _$$SnAttachmentImplFromJson(json); @@ -496,12 +526,16 @@ class _$SnAttachmentImpl implements _SnAttachment { @override final int refCount; @override + @JsonKey() + final int contentRating; + @override + @JsonKey() + final int qualityRating; + @override final dynamic fileChunks; @override final dynamic cleanedAt; @override - final bool isMature; - @override final bool isAnalyzed; @override final bool isUploaded; @@ -517,6 +551,15 @@ class _$SnAttachmentImpl implements _SnAttachment { final int poolId; @override final int accountId; + final Map _usermeta; + @override + @JsonKey() + Map get usermeta { + if (_usermeta is EqualUnmodifiableMapView) return _usermeta; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_usermeta); + } + final Map _metadata; @override @JsonKey() @@ -528,7 +571,7 @@ class _$SnAttachmentImpl implements _SnAttachment { @override String toString() { - return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, fileChunks: $fileChunks, cleanedAt: $cleanedAt, isMature: $isMature, isAnalyzed: $isAnalyzed, isUploaded: $isUploaded, isSelfRef: $isSelfRef, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, metadata: $metadata)'; + return 'SnAttachment(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, rid: $rid, uuid: $uuid, size: $size, name: $name, alt: $alt, mimetype: $mimetype, hash: $hash, destination: $destination, refCount: $refCount, contentRating: $contentRating, qualityRating: $qualityRating, fileChunks: $fileChunks, cleanedAt: $cleanedAt, isAnalyzed: $isAnalyzed, isUploaded: $isUploaded, isSelfRef: $isSelfRef, ref: $ref, refId: $refId, pool: $pool, poolId: $poolId, accountId: $accountId, usermeta: $usermeta, metadata: $metadata)'; } @override @@ -554,11 +597,13 @@ class _$SnAttachmentImpl implements _SnAttachment { other.destination == destination) && (identical(other.refCount, refCount) || other.refCount == refCount) && + (identical(other.contentRating, contentRating) || + other.contentRating == contentRating) && + (identical(other.qualityRating, qualityRating) || + other.qualityRating == qualityRating) && const DeepCollectionEquality() .equals(other.fileChunks, fileChunks) && const DeepCollectionEquality().equals(other.cleanedAt, cleanedAt) && - (identical(other.isMature, isMature) || - other.isMature == isMature) && (identical(other.isAnalyzed, isAnalyzed) || other.isAnalyzed == isAnalyzed) && (identical(other.isUploaded, isUploaded) || @@ -571,6 +616,7 @@ class _$SnAttachmentImpl implements _SnAttachment { (identical(other.poolId, poolId) || other.poolId == poolId) && (identical(other.accountId, accountId) || other.accountId == accountId) && + const DeepCollectionEquality().equals(other._usermeta, _usermeta) && const DeepCollectionEquality().equals(other._metadata, _metadata)); } @@ -591,9 +637,10 @@ class _$SnAttachmentImpl implements _SnAttachment { hash, destination, refCount, + contentRating, + qualityRating, const DeepCollectionEquality().hash(fileChunks), const DeepCollectionEquality().hash(cleanedAt), - isMature, isAnalyzed, isUploaded, isSelfRef, @@ -602,6 +649,7 @@ class _$SnAttachmentImpl implements _SnAttachment { pool, poolId, accountId, + const DeepCollectionEquality().hash(_usermeta), const DeepCollectionEquality().hash(_metadata) ]); @@ -621,7 +669,7 @@ class _$SnAttachmentImpl implements _SnAttachment { } } -abstract class _SnAttachment implements SnAttachment { +abstract class _SnAttachment extends SnAttachment { const factory _SnAttachment( {required final int id, required final DateTime createdAt, @@ -636,9 +684,10 @@ abstract class _SnAttachment implements SnAttachment { required final String hash, required final int destination, required final int refCount, + final int contentRating, + final int qualityRating, required final dynamic fileChunks, required final dynamic cleanedAt, - required final bool isMature, required final bool isAnalyzed, required final bool isUploaded, required final bool isSelfRef, @@ -647,7 +696,9 @@ abstract class _SnAttachment implements SnAttachment { required final SnAttachmentPool? pool, required final int poolId, required final int accountId, + final Map usermeta, final Map metadata}) = _$SnAttachmentImpl; + const _SnAttachment._() : super._(); factory _SnAttachment.fromJson(Map json) = _$SnAttachmentImpl.fromJson; @@ -679,12 +730,14 @@ abstract class _SnAttachment implements SnAttachment { @override int get refCount; @override + int get contentRating; + @override + int get qualityRating; + @override dynamic get fileChunks; @override dynamic get cleanedAt; @override - bool get isMature; - @override bool get isAnalyzed; @override bool get isUploaded; @@ -701,6 +754,8 @@ abstract class _SnAttachment implements SnAttachment { @override int get accountId; @override + Map get usermeta; + @override Map get metadata; /// Create a copy of SnAttachment diff --git a/lib/types/attachment.g.dart b/lib/types/attachment.g.dart index fa2cb92..0e00d48 100644 --- a/lib/types/attachment.g.dart +++ b/lib/types/attachment.g.dart @@ -21,9 +21,10 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map json) => hash: json['hash'] as String, destination: (json['destination'] as num).toInt(), refCount: (json['ref_count'] as num).toInt(), + contentRating: (json['content_rating'] as num?)?.toInt() ?? 0, + qualityRating: (json['quality_rating'] as num?)?.toInt() ?? 0, fileChunks: json['file_chunks'], cleanedAt: json['cleaned_at'], - isMature: json['is_mature'] as bool, isAnalyzed: json['is_analyzed'] as bool, isUploaded: json['is_uploaded'] as bool, isSelfRef: json['is_self_ref'] as bool, @@ -34,6 +35,7 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map json) => : SnAttachmentPool.fromJson(json['pool'] as Map), poolId: (json['pool_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(), + usermeta: json['usermeta'] as Map? ?? const {}, metadata: json['metadata'] as Map? ?? const {}, ); @@ -52,9 +54,10 @@ Map _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) => 'hash': instance.hash, 'destination': instance.destination, 'ref_count': instance.refCount, + 'content_rating': instance.contentRating, + 'quality_rating': instance.qualityRating, 'file_chunks': instance.fileChunks, 'cleaned_at': instance.cleanedAt, - 'is_mature': instance.isMature, 'is_analyzed': instance.isAnalyzed, 'is_uploaded': instance.isUploaded, 'is_self_ref': instance.isSelfRef, @@ -63,6 +66,7 @@ Map _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) => 'pool': instance.pool?.toJson(), 'pool_id': instance.poolId, 'account_id': instance.accountId, + 'usermeta': instance.usermeta, 'metadata': instance.metadata, }; diff --git a/lib/widgets/attachment/attachment_input.dart b/lib/widgets/attachment/attachment_input.dart index 4334026..56f5c83 100644 --- a/lib/widgets/attachment/attachment_input.dart +++ b/lib/widgets/attachment/attachment_input.dart @@ -99,10 +99,10 @@ class _AttachmentInputDialogState extends State { ), actions: [ TextButton( - child: Text('dialogDismiss').tr(), onPressed: _isBusy ? null : () { Navigator.pop(context); }, + child: Text('dialogDismiss').tr(), ), TextButton( onPressed: _isBusy ? null : () => _finishUp(), diff --git a/lib/widgets/attachment/attachment_item.dart b/lib/widgets/attachment/attachment_item.dart index 5c7657d..28194b6 100644 --- a/lib/widgets/attachment/attachment_item.dart +++ b/lib/widgets/attachment/attachment_item.dart @@ -18,6 +18,7 @@ import 'package:uuid/uuid.dart'; class AttachmentItem extends StatelessWidget { final SnAttachment? data; final String? heroTag; + const AttachmentItem({ super.key, required this.data, @@ -60,9 +61,14 @@ class AttachmentItem extends StatelessWidget { @override Widget build(BuildContext context) { - if (data!.isMature) { - return _AttachmentItemSensitiveBlur( - child: _buildContent(context), + if (data!.contentRating > 0) { + return LayoutBuilder( + builder: (context, constraints) { + return _AttachmentItemSensitiveBlur( + isCompact: constraints.maxHeight < 360, + child: _buildContent(context), + ); + } ); } @@ -72,15 +78,15 @@ class AttachmentItem extends StatelessWidget { class _AttachmentItemSensitiveBlur extends StatefulWidget { final Widget child; - const _AttachmentItemSensitiveBlur({super.key, required this.child}); + final bool isCompact; + + const _AttachmentItemSensitiveBlur({super.key, required this.child, this.isCompact = false}); @override - State<_AttachmentItemSensitiveBlur> createState() => - _AttachmentItemSensitiveBlurState(); + State<_AttachmentItemSensitiveBlur> createState() => _AttachmentItemSensitiveBlurState(); } -class _AttachmentItemSensitiveBlurState - extends State<_AttachmentItemSensitiveBlur> { +class _AttachmentItemSensitiveBlurState extends State<_AttachmentItemSensitiveBlur> { bool _doesShow = false; @override @@ -104,24 +110,21 @@ class _AttachmentItemSensitiveBlurState color: Colors.white, size: 32, ), - const Gap(8), - Text('sensitiveContent', textAlign: TextAlign.center) - .tr() - .fontSize(20) - .textColor(Colors.white) - .bold(), - Text( - 'sensitiveContentDescription', - textAlign: TextAlign.center, - ) - .tr() - .fontSize(14) - .textColor(Colors.white.withOpacity(0.8)), - const Gap(16), - InkWell( - child: Text('sensitiveContentReveal') + if (!widget.isCompact) const Gap(8), + if (!widget.isCompact) + Text('sensitiveContent', textAlign: TextAlign.center) .tr() - .textColor(Colors.white), + .fontSize(20) + .textColor(Colors.white) + .bold(), + if (!widget.isCompact) + Text( + 'sensitiveContentDescription', + textAlign: TextAlign.center, + ).tr().fontSize(14).textColor(Colors.white.withOpacity(0.8)), + if (!widget.isCompact) const Gap(16), + InkWell( + child: Text('sensitiveContentReveal').tr().textColor(Colors.white), onTap: () { setState(() => _doesShow = !_doesShow); }, @@ -131,9 +134,7 @@ class _AttachmentItemSensitiveBlurState ).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) Positioned( top: 0, @@ -163,6 +164,7 @@ class _AttachmentItemSensitiveBlurState class _AttachmentItemContentVideo extends StatefulWidget { final SnAttachment data; final bool isAutoload; + const _AttachmentItemContentVideo({ super.key, required this.data, @@ -170,12 +172,10 @@ class _AttachmentItemContentVideo extends StatefulWidget { }); @override - State<_AttachmentItemContentVideo> createState() => - _AttachmentItemContentVideoState(); + State<_AttachmentItemContentVideo> createState() => _AttachmentItemContentVideoState(); } -class _AttachmentItemContentVideoState - extends State<_AttachmentItemContentVideo> { +class _AttachmentItemContentVideoState extends State<_AttachmentItemContentVideo> { bool _showContent = false; Player? _videoPlayer; @@ -266,10 +266,7 @@ class _AttachmentItemContentVideoState ), Text( Duration( - milliseconds: - (widget.data.metadata['duration'] ?? 0) - .toInt() * - 1000, + milliseconds: (widget.data.metadata['duration'] ?? 0).toInt() * 1000, ).toString(), style: GoogleFonts.robotoMono( fontSize: 12, @@ -317,6 +314,7 @@ class _AttachmentItemContentVideoState class _AttachmentItemContentAudio extends StatefulWidget { final SnAttachment data; final bool isAutoload; + const _AttachmentItemContentAudio({ super.key, required this.data, @@ -324,12 +322,10 @@ class _AttachmentItemContentAudio extends StatefulWidget { }); @override - State<_AttachmentItemContentAudio> createState() => - _AttachmentItemContentAudioState(); + State<_AttachmentItemContentAudio> createState() => _AttachmentItemContentAudioState(); } -class _AttachmentItemContentAudioState - extends State<_AttachmentItemContentAudio> { +class _AttachmentItemContentAudioState extends State<_AttachmentItemContentAudio> { bool _showContent = false; double? _draggingValue; @@ -499,12 +495,8 @@ class _AttachmentItemContentAudioState overlayShape: SliderComponentShape.noOverlay, ), child: Slider( - secondaryTrackValue: _bufferedPosition - .inMilliseconds - .abs() - .toDouble(), - value: _draggingValue?.abs() ?? - _position.inMilliseconds.toDouble().abs(), + secondaryTrackValue: _bufferedPosition.inMilliseconds.abs().toDouble(), + value: _draggingValue?.abs() ?? _position.inMilliseconds.toDouble().abs(), min: 0, max: math .max( @@ -544,9 +536,7 @@ class _AttachmentItemContentAudioState ), const Gap(16), IconButton.filled( - icon: _isPlaying - ? const Icon(Symbols.pause) - : const Icon(Symbols.play_arrow), + icon: _isPlaying ? const Icon(Symbols.pause) : const Icon(Symbols.play_arrow), onPressed: () { _audioPlayer!.playOrPause(); }, diff --git a/lib/widgets/attachment/attachment_list.dart b/lib/widgets/attachment/attachment_list.dart index db0d6ca..445c310 100644 --- a/lib/widgets/attachment/attachment_list.dart +++ b/lib/widgets/attachment/attachment_list.dart @@ -58,7 +58,7 @@ class _AttachmentListState extends State { if (widget.data.isEmpty) return const SizedBox.shrink(); 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) { 'audio' => 16 / 9, 'video' => 16 / 9, @@ -114,6 +114,7 @@ class _AttachmentListState extends State { }, ), onTap: () { + if (widget.data.firstOrNull?.mediaType != SnMediaType.image) return; context.pushTransparentRoute( AttachmentZoomView( data: widget.data.where((ele) => ele != null).cast(), @@ -136,7 +137,7 @@ class _AttachmentListState extends State { children: widget.data .mapIndexed( (idx, ele) => AspectRatio( - aspectRatio: (ele?.metadata['ratio'] ?? 1).toDouble(), + aspectRatio: (ele?.data['ratio'] ?? 1).toDouble(), child: Container( decoration: BoxDecoration( color: backgroundColor, @@ -161,7 +162,7 @@ class _AttachmentListState extends State { } return AspectRatio( - aspectRatio: (widget.data.firstOrNull?.metadata['ratio'] ?? 1).toDouble(), + aspectRatio: (widget.data.firstOrNull?.data['ratio'] ?? 1).toDouble(), child: Container( constraints: BoxConstraints(maxHeight: constraints.maxHeight), child: ScrollConfiguration( @@ -173,12 +174,14 @@ class _AttachmentListState extends State { return Container( constraints: constraints, child: AspectRatio( - aspectRatio: (widget.data[idx]?.metadata['ratio'] ?? 1).toDouble(), + aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(), child: GestureDetector( onTap: () { + if (widget.data[idx]?.mediaType != SnMediaType.image) return; context.pushTransparentRoute( AttachmentZoomView( - data: widget.data.where((ele) => ele != null).cast(), + data: + widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(), initialIndex: idx, heroTags: heroTags, ), diff --git a/lib/widgets/chat/chat_message_input.dart b/lib/widgets/chat/chat_message_input.dart index b98f6dc..460b827 100644 --- a/lib/widgets/chat/chat_message_input.dart +++ b/lib/widgets/chat/chat_message_input.dart @@ -1,18 +1,14 @@ -import 'dart:io'; - import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.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:pasteboard/pasteboard.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/controllers/chat_message_controller.dart'; import 'package:surface/controllers/post_write_controller.dart'; import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/user_directory.dart'; +import 'package:surface/types/attachment.dart'; import 'package:surface/types/chat.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/markdown_content.dart'; @@ -80,7 +76,7 @@ class ChatMessageInputState extends State { media.name, 'messaging', 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( diff --git a/lib/widgets/post/post_media_pending_list.dart b/lib/widgets/post/post_media_pending_list.dart index e8645e1..51e985b 100644 --- a/lib/widgets/post/post_media_pending_list.dart +++ b/lib/widgets/post/post_media_pending_list.dart @@ -124,7 +124,7 @@ class PostMediaPendingList extends StatelessWidget { ContextMenu _createContextMenu(BuildContext context, int idx, PostWriteMedia media) { return ContextMenu( entries: [ - if (media.attachment != null && media.type == PostWriteMediaType.video) + if (media.attachment != null && media.type == SnMediaType.video) MenuItem( label: 'attachmentSetThumbnail'.tr(), icon: Symbols.image, @@ -140,7 +140,7 @@ class PostMediaPendingList extends StatelessWidget { onUpload!(idx); }), if (media.attachment != null && - media.type == PostWriteMediaType.image && + media.type == SnMediaType.image && onPostSetThumbnail != null && idx != -1) MenuItem( @@ -150,7 +150,7 @@ class PostMediaPendingList extends StatelessWidget { onPostSetThumbnail!(idx); }, ) - else if (media.attachment != null && media.type == PostWriteMediaType.image && onPostSetThumbnail != null) + else if (media.attachment != null && media.type == SnMediaType.image && onPostSetThumbnail != null) MenuItem( label: 'attachmentUnsetAsPostThumbnail'.tr(), icon: Symbols.cancel, @@ -166,7 +166,7 @@ class PostMediaPendingList extends StatelessWidget { onInsertLink!(idx); }, ), - if (media.type == PostWriteMediaType.image && media.attachment != null) + if (media.type == SnMediaType.image && media.attachment != null) MenuItem( label: 'preview'.tr(), icon: Symbols.preview, @@ -177,7 +177,7 @@ class PostMediaPendingList extends StatelessWidget { ); }, ), - if (media.type == PostWriteMediaType.image && media.attachment == null) + if (media.type == SnMediaType.image && media.attachment == null) MenuItem( label: 'crop'.tr(), icon: Symbols.crop, @@ -219,10 +219,6 @@ class PostMediaPendingList extends StatelessWidget { @override Widget build(BuildContext context) { - final devicePixelRatio = MediaQuery.of(context).devicePixelRatio; - - final sn = context.read(); - return Container( constraints: const BoxConstraints(maxHeight: 120), child: Row( @@ -285,7 +281,7 @@ class _PostMediaPendingItem extends StatelessWidget { child: AspectRatio( aspectRatio: 1, child: switch (media.type) { - PostWriteMediaType.image => Container( + SnMediaType.image => Container( color: Theme.of(context).colorScheme.surfaceContainer, child: LayoutBuilder(builder: (context, constraints) { return Image( @@ -298,7 +294,7 @@ class _PostMediaPendingItem extends StatelessWidget { ); }), ), - PostWriteMediaType.video => Container( + SnMediaType.video => Container( color: Theme.of(context).colorScheme.surfaceContainer, child: media.attachment?.metadata['thumbnail'] != null ? AutoResizeUniversalImage(sn.getAttachmentUrl(media.attachment?.metadata['thumbnail'])) @@ -345,7 +341,7 @@ class AddPostMediaButton extends StatelessWidget { PostWriteMedia.fromBytes( imageBytes, 'attachmentPastedImage'.tr(), - PostWriteMediaType.image, + SnMediaType.image, ), ]); }