♻️ Refactored attachment loading system

This commit is contained in:
LittleSheep 2024-12-26 22:19:01 +08:00
parent 619c90cdd9
commit 7656c08832
15 changed files with 341 additions and 276 deletions

View File

@ -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

View File

@ -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": "等待上傳"
}

View File

@ -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": "等待上傳"
}

View File

@ -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(

View File

@ -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<SnNetworkProvider>();
_home = context.read<HomeWidgetProvider>();
_config = context.read<ConfigProvider>();
}

View File

@ -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';

View File

@ -33,7 +33,6 @@ Future<ThemeData> createAppTheme(
brightness: brightness,
);
final hasBackground = prefs.getBool(kAppBackgroundStoreKey) ?? false;
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
return ThemeData(

View File

@ -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<String, dynamic> usermeta,
@Default({}) Map<String, dynamic> metadata,
}) = _SnAttachment;
factory SnAttachment.fromJson(Map<String, Object?> json) =>
_$SnAttachmentFromJson(json);
factory SnAttachment.fromJson(Map<String, Object?> 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
@ -51,6 +74,5 @@ class SnAttachmentPool with _$SnAttachmentPool {
required int? accountId,
}) = _SnAttachmentPool;
factory SnAttachmentPool.fromJson(Map<String, Object?> json) =>
_$SnAttachmentPoolFromJson(json);
factory SnAttachmentPool.fromJson(Map<String, Object?> json) => _$SnAttachmentPoolFromJson(json);
}

View File

@ -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<String, dynamic> get usermeta => throw _privateConstructorUsedError;
Map<String, dynamic> 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<String, dynamic> usermeta,
Map<String, dynamic> 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<String, dynamic>,
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<String, dynamic> usermeta,
Map<String, dynamic> 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<String, dynamic>,
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<String, dynamic> usermeta = const {},
final Map<String, dynamic> metadata = const {}})
: _metadata = metadata;
: _usermeta = usermeta,
_metadata = metadata,
super._();
factory _$SnAttachmentImpl.fromJson(Map<String, dynamic> 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<String, dynamic> _usermeta;
@override
@JsonKey()
Map<String, dynamic> get usermeta {
if (_usermeta is EqualUnmodifiableMapView) return _usermeta;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_usermeta);
}
final Map<String, dynamic> _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<String, dynamic> usermeta,
final Map<String, dynamic> metadata}) = _$SnAttachmentImpl;
const _SnAttachment._() : super._();
factory _SnAttachment.fromJson(Map<String, dynamic> 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<String, dynamic> get usermeta;
@override
Map<String, dynamic> get metadata;
/// Create a copy of SnAttachment

View File

@ -21,9 +21,10 @@ _$SnAttachmentImpl _$$SnAttachmentImplFromJson(Map<String, dynamic> 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<String, dynamic> json) =>
: SnAttachmentPool.fromJson(json['pool'] as Map<String, dynamic>),
poolId: (json['pool_id'] as num).toInt(),
accountId: (json['account_id'] as num).toInt(),
usermeta: json['usermeta'] as Map<String, dynamic>? ?? const {},
metadata: json['metadata'] as Map<String, dynamic>? ?? const {},
);
@ -52,9 +54,10 @@ Map<String, dynamic> _$$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<String, dynamic> _$$SnAttachmentImplToJson(_$SnAttachmentImpl instance) =>
'pool': instance.pool?.toJson(),
'pool_id': instance.poolId,
'account_id': instance.accountId,
'usermeta': instance.usermeta,
'metadata': instance.metadata,
};

View File

@ -99,10 +99,10 @@ class _AttachmentInputDialogState extends State<AttachmentInputDialog> {
),
actions: [
TextButton(
child: Text('dialogDismiss').tr(),
onPressed: _isBusy ? null : () {
Navigator.pop(context);
},
child: Text('dialogDismiss').tr(),
),
TextButton(
onPressed: _isBusy ? null : () => _finishUp(),

View File

@ -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();
},

View File

@ -58,7 +58,7 @@ class _AttachmentListState extends State<AttachmentList> {
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<AttachmentList> {
},
),
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<AttachmentList> {
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<AttachmentList> {
}
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<AttachmentList> {
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,
),

View File

@ -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<ChatMessageInput> {
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(

View File

@ -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<SnNetworkProvider>();
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,
),
]);
}