Compare commits
13 Commits
Author | SHA1 | Date | |
---|---|---|---|
7a3ab6fd7d | |||
3d15c0b9f9 | |||
67a29b4305 | |||
594f57e0d3 | |||
d1eb51c596 | |||
85d2eff7f8 | |||
2375c46852 | |||
fd2eb5cda6 | |||
1256f440bd | |||
5b05ca67b6 | |||
95af7140cd | |||
77e9994204 | |||
3f6c186c13 |
@ -57,7 +57,7 @@
|
|||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
"unset": "Unset",
|
"unset": "Unset",
|
||||||
"untitled": "Untitled",
|
"untitled": "Untitled",
|
||||||
"postDetail": "Post detail",
|
"postDetail": "Post Detail",
|
||||||
"postNoun": "Post",
|
"postNoun": "Post",
|
||||||
"postReadMore": "Read more",
|
"postReadMore": "Read more",
|
||||||
"postReadEstimate": "Est read time {}",
|
"postReadEstimate": "Est read time {}",
|
||||||
@ -139,6 +139,7 @@
|
|||||||
"fieldPostTitle": "Title",
|
"fieldPostTitle": "Title",
|
||||||
"fieldPostDescription": "Description",
|
"fieldPostDescription": "Description",
|
||||||
"fieldPostTags": "Tags",
|
"fieldPostTags": "Tags",
|
||||||
|
"fieldPostCategories": "Categories",
|
||||||
"fieldPostAlias": "Alias",
|
"fieldPostAlias": "Alias",
|
||||||
"fieldPostAliasHint": "Optional, used to represent the post in URL, should follow URL-Safe.",
|
"fieldPostAliasHint": "Optional, used to represent the post in URL, should follow URL-Safe.",
|
||||||
"postPublish": "Publish",
|
"postPublish": "Publish",
|
||||||
@ -178,12 +179,18 @@
|
|||||||
"other": "{} comments"
|
"other": "{} comments"
|
||||||
},
|
},
|
||||||
"settingsAppearance": "Appearance",
|
"settingsAppearance": "Appearance",
|
||||||
|
"settingsAppBarTransparent": "Transparent App Bar",
|
||||||
|
"settingsAppBarTransparentDescription": "Enable transparent effect for the app bar.",
|
||||||
"settingsBackgroundImage": "Background Image",
|
"settingsBackgroundImage": "Background Image",
|
||||||
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
|
"settingsBackgroundImageDescription": "Set the background image that will be applied globally.",
|
||||||
"settingsBackgroundImageClear": "Clear Existing Background Image",
|
"settingsBackgroundImageClear": "Clear Existing Background Image",
|
||||||
"settingsBackgroundImageClearDescription": "Reset the background image to blank.",
|
"settingsBackgroundImageClearDescription": "Reset the background image to blank.",
|
||||||
"settingsThemeMaterial3": "Use Material You Design",
|
"settingsThemeMaterial3": "Use Material You Design",
|
||||||
"settingsThemeMaterial3Description": "Set the application theme to Material 3 Design.",
|
"settingsThemeMaterial3Description": "Set the application theme to Material 3 Design.",
|
||||||
|
"settingsColorScheme": "Color Scheme",
|
||||||
|
"settingsColorSchemeDescription": "Set the application primary color.",
|
||||||
|
"settingsColorSeed": "Color Seed",
|
||||||
|
"settingsColorSeedDescription": "Select one of the present color schemes.",
|
||||||
"settingsNetwork": "Network",
|
"settingsNetwork": "Network",
|
||||||
"settingsNetworkServer": "HyperNet Server",
|
"settingsNetworkServer": "HyperNet Server",
|
||||||
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
|
"settingsNetworkServerDescription": "Set the HyperNet server address, choose ours or build your own.",
|
||||||
@ -450,7 +457,7 @@
|
|||||||
"publisherBlockHintDescription": "You are going to block this publisher's maintainer, this will also block publishers that run by the same user.",
|
"publisherBlockHintDescription": "You are going to block this publisher's maintainer, this will also block publishers that run by the same user.",
|
||||||
"userUnblocked": "{} has been unblocked.",
|
"userUnblocked": "{} has been unblocked.",
|
||||||
"userBlocked": "{} has been blocked.",
|
"userBlocked": "{} has been blocked.",
|
||||||
"postSharingViaPicture": "Capturing post as picture, please stand by...",
|
"postSharingViaPicture": "Capturing post as picture, please wait...",
|
||||||
"postImageShareReadMore": "Scan the QR code to read full post",
|
"postImageShareReadMore": "Scan the QR code to read full post",
|
||||||
"postImageShareAds": "Explore posts on the Solar Network",
|
"postImageShareAds": "Explore posts on the Solar Network",
|
||||||
"postShare": "Share",
|
"postShare": "Share",
|
||||||
@ -461,5 +468,25 @@
|
|||||||
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
"shareIntentDescription": "What do you want to do with the content you are sharing?",
|
||||||
"shareIntentPostStory": "Post a Story",
|
"shareIntentPostStory": "Post a Story",
|
||||||
"updateAvailable": "Update Available",
|
"updateAvailable": "Update Available",
|
||||||
"updateOngoing": "正在更新,请稍后..."
|
"updateOngoing": "Updating, please wait...",
|
||||||
|
"custom": "Custom",
|
||||||
|
"colorSchemeIndigo": "Indigo",
|
||||||
|
"colorSchemeBlue": "Blue",
|
||||||
|
"colorSchemeGreen": "Green",
|
||||||
|
"colorSchemeYellow": "Yellow",
|
||||||
|
"colorSchemeOrange": "Orange",
|
||||||
|
"colorSchemeRed": "Red",
|
||||||
|
"colorSchemeWhite": "White",
|
||||||
|
"colorSchemeBlack": "Black",
|
||||||
|
"colorSchemeApplied": "Color scheme has been applied, may need restart the app to take effect.",
|
||||||
|
"postCategoryTechnology": "Technology",
|
||||||
|
"postCategoryGaming": "Gaming",
|
||||||
|
"postCategoryLife": "Life",
|
||||||
|
"postCategoryArts": "Arts",
|
||||||
|
"postCategorySports": "Sports",
|
||||||
|
"postCategoryMusic": "Music",
|
||||||
|
"postCategoryNews": "News",
|
||||||
|
"postCategoryKnowledge": "Knowledge",
|
||||||
|
"postCategoryLiterature": "Literature",
|
||||||
|
"postCategoryUncategorized": "Uncategorized"
|
||||||
}
|
}
|
||||||
|
@ -123,6 +123,7 @@
|
|||||||
"fieldPostTitle": "标题",
|
"fieldPostTitle": "标题",
|
||||||
"fieldPostDescription": "描述",
|
"fieldPostDescription": "描述",
|
||||||
"fieldPostTags": "标签",
|
"fieldPostTags": "标签",
|
||||||
|
"fieldPostCategories": "分类",
|
||||||
"fieldPostAlias": "别名",
|
"fieldPostAlias": "别名",
|
||||||
"fieldPostAliasHint": "可选项,用于在 URL 中表示该帖子,应遵循 URL-Safe 的原则。",
|
"fieldPostAliasHint": "可选项,用于在 URL 中表示该帖子,应遵循 URL-Safe 的原则。",
|
||||||
"postPublish": "发布",
|
"postPublish": "发布",
|
||||||
@ -182,6 +183,12 @@
|
|||||||
"settingsBackgroundImageClearDescription": "将应用背景图重置为空白。",
|
"settingsBackgroundImageClearDescription": "将应用背景图重置为空白。",
|
||||||
"settingsThemeMaterial3": "使用 Material You 设计范式",
|
"settingsThemeMaterial3": "使用 Material You 设计范式",
|
||||||
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
|
"settingsThemeMaterial3Description": "将应用主题设置为 Material 3 设计范式的主题。",
|
||||||
|
"settingsAppBarTransparent": "透明顶栏",
|
||||||
|
"settingsAppBarTransparentDescription": "为顶栏启用透明效果。",
|
||||||
|
"settingsColorScheme": "主题色",
|
||||||
|
"settingsColorSchemeDescription": "设置应用主题色。",
|
||||||
|
"settingsColorSeed": "预设色彩主题",
|
||||||
|
"settingsColorSeedDescription": "选择一个预设色彩主题。",
|
||||||
"settingsNetwork": "网络",
|
"settingsNetwork": "网络",
|
||||||
"settingsNetworkServer": "HyperNet 服务器",
|
"settingsNetworkServer": "HyperNet 服务器",
|
||||||
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
|
"settingsNetworkServerDescription": "设置 HyperNet 服务器地址,选择我们提供的,或者自己搭建。",
|
||||||
@ -410,7 +417,7 @@
|
|||||||
"accountStatus": "状态",
|
"accountStatus": "状态",
|
||||||
"accountStatusOnline": "在线",
|
"accountStatusOnline": "在线",
|
||||||
"accountStatusOffline": "离线",
|
"accountStatusOffline": "离线",
|
||||||
"accountStatusLastSeen": "最后一次在 {} 上线",
|
"accountStatusLastSeen": "最后一次上线于 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
"articleWrittenAt": "发表于 {}",
|
"articleWrittenAt": "发表于 {}",
|
||||||
@ -459,5 +466,25 @@
|
|||||||
"shareIntentDescription": "您想对您分享的内容做些什么?",
|
"shareIntentDescription": "您想对您分享的内容做些什么?",
|
||||||
"shareIntentPostStory": "发布动态",
|
"shareIntentPostStory": "发布动态",
|
||||||
"updateAvailable": "检测到更新可用",
|
"updateAvailable": "检测到更新可用",
|
||||||
"updateOngoing": "正在更新,请稍后……"
|
"updateOngoing": "正在更新,请稍后……",
|
||||||
|
"custom": "自定义",
|
||||||
|
"colorSchemeIndigo": "靛蓝",
|
||||||
|
"colorSchemeBlue": "蓝色",
|
||||||
|
"colorSchemeGreen": "绿色",
|
||||||
|
"colorSchemeYellow": "黄色",
|
||||||
|
"colorSchemeOrange": "橙色",
|
||||||
|
"colorSchemeRed": "红色",
|
||||||
|
"colorSchemeWhite": "白色",
|
||||||
|
"colorSchemeBlack": "黑色",
|
||||||
|
"colorSchemeApplied": "主题色已应用,可能需要重启来生效。",
|
||||||
|
"postCategoryTechnology": "技术",
|
||||||
|
"postCategoryGaming": "游戏",
|
||||||
|
"postCategoryLife": "生活",
|
||||||
|
"postCategoryArts": "艺术",
|
||||||
|
"postCategorySports": "体育",
|
||||||
|
"postCategoryMusic": "音乐",
|
||||||
|
"postCategoryNews": "新闻",
|
||||||
|
"postCategoryKnowledge": "知识",
|
||||||
|
"postCategoryLiterature": "文学",
|
||||||
|
"postCategoryUncategorized": "未分类"
|
||||||
}
|
}
|
||||||
|
@ -123,6 +123,9 @@
|
|||||||
"fieldPostTitle": "標題",
|
"fieldPostTitle": "標題",
|
||||||
"fieldPostDescription": "描述",
|
"fieldPostDescription": "描述",
|
||||||
"fieldPostTags": "標籤",
|
"fieldPostTags": "標籤",
|
||||||
|
"fieldPostCategories": "分類",
|
||||||
|
"fieldPostAlias": "別名",
|
||||||
|
"fieldPostAliasHint": "可選項,用於在 URL 中表示該帖子,應遵循 URL-Safe 的原則。",
|
||||||
"postPublish": "發佈",
|
"postPublish": "發佈",
|
||||||
"postPublishedAt": "發佈於",
|
"postPublishedAt": "發佈於",
|
||||||
"postPublishedUntil": "取消發佈於",
|
"postPublishedUntil": "取消發佈於",
|
||||||
@ -180,6 +183,12 @@
|
|||||||
"settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
|
"settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
|
||||||
"settingsThemeMaterial3": "使用 Material You 設計範式",
|
"settingsThemeMaterial3": "使用 Material You 設計範式",
|
||||||
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
"settingsThemeMaterial3Description": "將應用主題設置為 Material 3 設計範式的主題。",
|
||||||
|
"settingsAppBarTransparent": "透明頂欄",
|
||||||
|
"settingsAppBarTransparentDescription": "為頂欄啓用透明效果。",
|
||||||
|
"settingsColorScheme": "主題色",
|
||||||
|
"settingsColorSchemeDescription": "設置應用主題色。",
|
||||||
|
"settingsColorSeed": "預設色彩主題",
|
||||||
|
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
||||||
"settingsNetwork": "網絡",
|
"settingsNetwork": "網絡",
|
||||||
"settingsNetworkServer": "HyperNet 服務器",
|
"settingsNetworkServer": "HyperNet 服務器",
|
||||||
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
"settingsNetworkServerDescription": "設置 HyperNet 服務器地址,選擇我們提供的,或者自己搭建。",
|
||||||
@ -368,6 +377,8 @@
|
|||||||
"dailyCheckNegativeHint6": "出門",
|
"dailyCheckNegativeHint6": "出門",
|
||||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||||
"happyBirthday": "生日快樂,{}!",
|
"happyBirthday": "生日快樂,{}!",
|
||||||
|
"celebrateMerryXmas": "聖誕快樂,{}!",
|
||||||
|
"celebrateNewYear": "新年快樂,{}!",
|
||||||
"friendNew": "添加好友",
|
"friendNew": "添加好友",
|
||||||
"friendRequests": "好友請求",
|
"friendRequests": "好友請求",
|
||||||
"friendRequestsDescription": {
|
"friendRequestsDescription": {
|
||||||
@ -406,7 +417,7 @@
|
|||||||
"accountStatus": "狀態",
|
"accountStatus": "狀態",
|
||||||
"accountStatusOnline": "在線",
|
"accountStatusOnline": "在線",
|
||||||
"accountStatusOffline": "離線",
|
"accountStatusOffline": "離線",
|
||||||
"accountStatusLastSeen": "最後一次在 {} 上線",
|
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
"articleWrittenAt": "發表於 {}",
|
"articleWrittenAt": "發表於 {}",
|
||||||
@ -453,5 +464,27 @@
|
|||||||
"poweredBy": "由 {} 提供支持",
|
"poweredBy": "由 {} 提供支持",
|
||||||
"shareIntent": "分享",
|
"shareIntent": "分享",
|
||||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||||
"shareIntentPostStory": "發佈動態"
|
"shareIntentPostStory": "發佈動態",
|
||||||
|
"updateAvailable": "檢測到更新可用",
|
||||||
|
"updateOngoing": "正在更新,請稍後……",
|
||||||
|
"custom": "自定義",
|
||||||
|
"colorSchemeIndigo": "靛藍",
|
||||||
|
"colorSchemeBlue": "藍色",
|
||||||
|
"colorSchemeGreen": "綠色",
|
||||||
|
"colorSchemeYellow": "黃色",
|
||||||
|
"colorSchemeOrange": "橙色",
|
||||||
|
"colorSchemeRed": "紅色",
|
||||||
|
"colorSchemeWhite": "白色",
|
||||||
|
"colorSchemeBlack": "黑色",
|
||||||
|
"colorSchemeApplied": "主題色已應用,可能需要重啓來生效。",
|
||||||
|
"postCategoryTechnology": "技術",
|
||||||
|
"postCategoryGaming": "遊戲",
|
||||||
|
"postCategoryLife": "生活",
|
||||||
|
"postCategoryArts": "藝術",
|
||||||
|
"postCategorySports": "體育",
|
||||||
|
"postCategoryMusic": "音樂",
|
||||||
|
"postCategoryNews": "新聞",
|
||||||
|
"postCategoryKnowledge": "知識",
|
||||||
|
"postCategoryLiterature": "文學",
|
||||||
|
"postCategoryUncategorized": "未分類"
|
||||||
}
|
}
|
||||||
|
@ -123,6 +123,9 @@
|
|||||||
"fieldPostTitle": "標題",
|
"fieldPostTitle": "標題",
|
||||||
"fieldPostDescription": "描述",
|
"fieldPostDescription": "描述",
|
||||||
"fieldPostTags": "標籤",
|
"fieldPostTags": "標籤",
|
||||||
|
"fieldPostCategories": "分類",
|
||||||
|
"fieldPostAlias": "別名",
|
||||||
|
"fieldPostAliasHint": "可選項,用於在 URL 中表示該帖子,應遵循 URL-Safe 的原則。",
|
||||||
"postPublish": "釋出",
|
"postPublish": "釋出",
|
||||||
"postPublishedAt": "釋出於",
|
"postPublishedAt": "釋出於",
|
||||||
"postPublishedUntil": "取消釋出於",
|
"postPublishedUntil": "取消釋出於",
|
||||||
@ -180,6 +183,12 @@
|
|||||||
"settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
|
"settingsBackgroundImageClearDescription": "將應用背景圖重置為空白。",
|
||||||
"settingsThemeMaterial3": "使用 Material You 設計正規化",
|
"settingsThemeMaterial3": "使用 Material You 設計正規化",
|
||||||
"settingsThemeMaterial3Description": "將應用主題設定為 Material 3 設計正規化的主題。",
|
"settingsThemeMaterial3Description": "將應用主題設定為 Material 3 設計正規化的主題。",
|
||||||
|
"settingsAppBarTransparent": "透明頂欄",
|
||||||
|
"settingsAppBarTransparentDescription": "為頂欄啟用透明效果。",
|
||||||
|
"settingsColorScheme": "主題色",
|
||||||
|
"settingsColorSchemeDescription": "設定應用主題色。",
|
||||||
|
"settingsColorSeed": "預設色彩主題",
|
||||||
|
"settingsColorSeedDescription": "選擇一個預設色彩主題。",
|
||||||
"settingsNetwork": "網路",
|
"settingsNetwork": "網路",
|
||||||
"settingsNetworkServer": "HyperNet 伺服器",
|
"settingsNetworkServer": "HyperNet 伺服器",
|
||||||
"settingsNetworkServerDescription": "設定 HyperNet 伺服器地址,選擇我們提供的,或者自己搭建。",
|
"settingsNetworkServerDescription": "設定 HyperNet 伺服器地址,選擇我們提供的,或者自己搭建。",
|
||||||
@ -368,6 +377,8 @@
|
|||||||
"dailyCheckNegativeHint6": "出門",
|
"dailyCheckNegativeHint6": "出門",
|
||||||
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
"dailyCheckNegativeHint6Description": "忘帶傘遇上大雨",
|
||||||
"happyBirthday": "生日快樂,{}!",
|
"happyBirthday": "生日快樂,{}!",
|
||||||
|
"celebrateMerryXmas": "聖誕快樂,{}!",
|
||||||
|
"celebrateNewYear": "新年快樂,{}!",
|
||||||
"friendNew": "新增好友",
|
"friendNew": "新增好友",
|
||||||
"friendRequests": "好友請求",
|
"friendRequests": "好友請求",
|
||||||
"friendRequestsDescription": {
|
"friendRequestsDescription": {
|
||||||
@ -406,7 +417,7 @@
|
|||||||
"accountStatus": "狀態",
|
"accountStatus": "狀態",
|
||||||
"accountStatusOnline": "線上",
|
"accountStatusOnline": "線上",
|
||||||
"accountStatusOffline": "離線",
|
"accountStatusOffline": "離線",
|
||||||
"accountStatusLastSeen": "最後一次在 {} 上線",
|
"accountStatusLastSeen": "最後一次上線於 {}",
|
||||||
"postArticle": "Solar Network 上的文章",
|
"postArticle": "Solar Network 上的文章",
|
||||||
"postStory": "Solar Network 上的故事",
|
"postStory": "Solar Network 上的故事",
|
||||||
"articleWrittenAt": "發表於 {}",
|
"articleWrittenAt": "發表於 {}",
|
||||||
@ -453,5 +464,27 @@
|
|||||||
"poweredBy": "由 {} 提供支援",
|
"poweredBy": "由 {} 提供支援",
|
||||||
"shareIntent": "分享",
|
"shareIntent": "分享",
|
||||||
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
"shareIntentDescription": "您想對您分享的內容做些什麼?",
|
||||||
"shareIntentPostStory": "釋出動態"
|
"shareIntentPostStory": "釋出動態",
|
||||||
|
"updateAvailable": "檢測到更新可用",
|
||||||
|
"updateOngoing": "正在更新,請稍後……",
|
||||||
|
"custom": "自定義",
|
||||||
|
"colorSchemeIndigo": "靛藍",
|
||||||
|
"colorSchemeBlue": "藍色",
|
||||||
|
"colorSchemeGreen": "綠色",
|
||||||
|
"colorSchemeYellow": "黃色",
|
||||||
|
"colorSchemeOrange": "橙色",
|
||||||
|
"colorSchemeRed": "紅色",
|
||||||
|
"colorSchemeWhite": "白色",
|
||||||
|
"colorSchemeBlack": "黑色",
|
||||||
|
"colorSchemeApplied": "主題色已應用,可能需要重啟來生效。",
|
||||||
|
"postCategoryTechnology": "技術",
|
||||||
|
"postCategoryGaming": "遊戲",
|
||||||
|
"postCategoryLife": "生活",
|
||||||
|
"postCategoryArts": "藝術",
|
||||||
|
"postCategorySports": "體育",
|
||||||
|
"postCategoryMusic": "音樂",
|
||||||
|
"postCategoryNews": "新聞",
|
||||||
|
"postCategoryKnowledge": "知識",
|
||||||
|
"postCategoryLiterature": "文學",
|
||||||
|
"postCategoryUncategorized": "未分類"
|
||||||
}
|
}
|
||||||
|
@ -178,6 +178,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
List<int> visibleUsers = List.empty();
|
List<int> visibleUsers = List.empty();
|
||||||
List<int> invisibleUsers = List.empty();
|
List<int> invisibleUsers = List.empty();
|
||||||
List<String> tags = List.empty();
|
List<String> tags = List.empty();
|
||||||
|
List<String> categories = List.empty();
|
||||||
PostWriteMedia? thumbnail;
|
PostWriteMedia? thumbnail;
|
||||||
List<PostWriteMedia> attachments = List.empty(growable: true);
|
List<PostWriteMedia> attachments = List.empty(growable: true);
|
||||||
DateTime? publishedAt, publishedUntil;
|
DateTime? publishedAt, publishedUntil;
|
||||||
@ -207,6 +208,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
invisibleUsers = List.from(post.invisibleUsersList ?? []);
|
invisibleUsers = List.from(post.invisibleUsersList ?? []);
|
||||||
visibility = post.visibility;
|
visibility = post.visibility;
|
||||||
tags = List.from(post.tags.map((ele) => ele.alias));
|
tags = List.from(post.tags.map((ele) => ele.alias));
|
||||||
|
categories = List.from(post.categories.map((ele) => ele.alias));
|
||||||
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
attachments.addAll(post.preload?.attachments?.map((ele) => PostWriteMedia(ele)) ?? []);
|
||||||
|
|
||||||
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
if (post.preload?.thumbnail != null && (post.preload?.thumbnail?.rid.isNotEmpty ?? false)) {
|
||||||
@ -345,6 +347,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid,
|
if (thumbnail != null && thumbnail!.attachment != null) 'thumbnail': thumbnail!.attachment!.rid,
|
||||||
'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
'attachments': attachments.where((e) => e.attachment != null).map((e) => e.attachment!.rid).toList(),
|
||||||
'tags': tags.map((ele) => {'alias': ele}).toList(),
|
'tags': tags.map((ele) => {'alias': ele}).toList(),
|
||||||
|
'categories': categories.map((ele) => {'alias': ele}).toList(),
|
||||||
'visibility': visibility,
|
'visibility': visibility,
|
||||||
'visible_users_list': visibleUsers,
|
'visible_users_list': visibleUsers,
|
||||||
'invisible_users_list': invisibleUsers,
|
'invisible_users_list': invisibleUsers,
|
||||||
@ -431,6 +434,11 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setCategories(List<String> value) {
|
||||||
|
categories = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void setVisibility(int value) {
|
void setVisibility(int value) {
|
||||||
visibility = value;
|
visibility = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@ -467,6 +475,9 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
titleController.clear();
|
titleController.clear();
|
||||||
descriptionController.clear();
|
descriptionController.clear();
|
||||||
contentController.clear();
|
contentController.clear();
|
||||||
|
aliasController.clear();
|
||||||
|
tags.clear();
|
||||||
|
categories.clear();
|
||||||
attachments.clear();
|
attachments.clear();
|
||||||
editingPost = null;
|
editingPost = null;
|
||||||
replyingPost = null;
|
replyingPost = null;
|
||||||
@ -480,6 +491,7 @@ class PostWriteController extends ChangeNotifier {
|
|||||||
contentController.dispose();
|
contentController.dispose();
|
||||||
titleController.dispose();
|
titleController.dispose();
|
||||||
descriptionController.dispose();
|
descriptionController.dispose();
|
||||||
|
aliasController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,10 @@ const kRtkStoreKey = 'nex_user_rtk';
|
|||||||
const kNetworkServerDefault = 'https://api.sn.solsynth.dev';
|
const kNetworkServerDefault = 'https://api.sn.solsynth.dev';
|
||||||
const kNetworkServerStoreKey = 'app_server_url';
|
const kNetworkServerStoreKey = 'app_server_url';
|
||||||
|
|
||||||
|
const kAppbarTransparentStoreKey = 'app_bar_transparent';
|
||||||
|
const kAppBackgroundStoreKey = 'app_has_background';
|
||||||
|
const kAppColorSchemeStoreKey = 'app_color_scheme';
|
||||||
|
|
||||||
const Map<String, FilterQuality> kImageQualityLevel = {
|
const Map<String, FilterQuality> kImageQualityLevel = {
|
||||||
'settingsImageQualityLowest': FilterQuality.none,
|
'settingsImageQualityLowest': FilterQuality.none,
|
||||||
'settingsImageQualityLow': FilterQuality.low,
|
'settingsImageQualityLow': FilterQuality.low,
|
||||||
|
41
lib/providers/experience.dart
Normal file
41
lib/providers/experience.dart
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
const List<int> kExperienceToLevelRequirements = [
|
||||||
|
0, // Level 0
|
||||||
|
1000, // Level 1
|
||||||
|
4000, // Level 2
|
||||||
|
9000, // Level 3
|
||||||
|
16000, // Level 4
|
||||||
|
25000, // Level 5
|
||||||
|
36000, // Level 6
|
||||||
|
49000, // Level 7
|
||||||
|
64000, // Level 8
|
||||||
|
81000, // Level 9
|
||||||
|
100000, // Level 10
|
||||||
|
121000, // Level 11
|
||||||
|
144000, // Level 12
|
||||||
|
368000 // Level 13
|
||||||
|
];
|
||||||
|
|
||||||
|
int getLevelFromExp(int experience) {
|
||||||
|
final exp = kExperienceToLevelRequirements.reversed.firstWhere((x) => x <= experience);
|
||||||
|
final idx = kExperienceToLevelRequirements.indexOf(exp);
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
double calcLevelUpProgress(int experience) {
|
||||||
|
final exp = kExperienceToLevelRequirements.reversed.firstWhere((x) => x <= experience);
|
||||||
|
final idx = kExperienceToLevelRequirements.indexOf(exp);
|
||||||
|
if (idx + 1 >= kExperienceToLevelRequirements.length) return 1;
|
||||||
|
final nextExp = kExperienceToLevelRequirements[idx + 1];
|
||||||
|
return (experience - exp).abs() / (exp - nextExp).abs();
|
||||||
|
}
|
||||||
|
|
||||||
|
String calcLevelUpProgressLevel(int experience) {
|
||||||
|
final exp = kExperienceToLevelRequirements.reversed.firstWhere((x) => x <= experience);
|
||||||
|
final idx = kExperienceToLevelRequirements.indexOf(exp);
|
||||||
|
if (idx + 1 >= kExperienceToLevelRequirements.length) return 'Infinity';
|
||||||
|
final nextExp = exp - kExperienceToLevelRequirements[idx + 1];
|
||||||
|
final formatter = NumberFormat.compactCurrency(symbol: '', decimalDigits: 1);
|
||||||
|
return '${formatter.format((exp - experience).abs())}/${formatter.format(nextExp.abs())}';
|
||||||
|
}
|
@ -83,12 +83,16 @@ class SnPostContentProvider {
|
|||||||
int offset = 0,
|
int offset = 0,
|
||||||
String? type,
|
String? type,
|
||||||
String? author,
|
String? author,
|
||||||
|
Iterable<String>? categories,
|
||||||
|
Iterable<String>? tags,
|
||||||
}) async {
|
}) async {
|
||||||
final resp = await _sn.client.get('/cgi/co/posts', queryParameters: {
|
final resp = await _sn.client.get('/cgi/co/posts', queryParameters: {
|
||||||
'take': take,
|
'take': take,
|
||||||
'offset': offset,
|
'offset': offset,
|
||||||
if (type != null) 'type': type,
|
if (type != null) 'type': type,
|
||||||
if (author != null) 'author': author,
|
if (author != null) 'author': author,
|
||||||
|
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
||||||
|
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
|
||||||
});
|
});
|
||||||
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
||||||
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
||||||
@ -118,12 +122,14 @@ class SnPostContentProvider {
|
|||||||
int take = 10,
|
int take = 10,
|
||||||
int offset = 0,
|
int offset = 0,
|
||||||
Iterable<String>? tags,
|
Iterable<String>? tags,
|
||||||
|
Iterable<String>? categories,
|
||||||
}) async {
|
}) async {
|
||||||
final resp = await _sn.client.get('/cgi/co/posts/search', queryParameters: {
|
final resp = await _sn.client.get('/cgi/co/posts/search', queryParameters: {
|
||||||
'take': take,
|
'take': take,
|
||||||
'offset': offset,
|
'offset': offset,
|
||||||
'probe': searchTerm,
|
'probe': searchTerm,
|
||||||
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
if (tags?.isNotEmpty ?? false) 'tags': tags!.join(','),
|
||||||
|
if (categories?.isNotEmpty ?? false) 'categories': categories!.join(','),
|
||||||
});
|
});
|
||||||
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
final List<SnPost> out = await _preloadRelatedDataInBatch(
|
||||||
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
List.from(resp.data['data']?.map((e) => SnPost.fromJson(e)) ?? []),
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:surface/theme.dart';
|
import 'package:surface/theme.dart';
|
||||||
|
|
||||||
@ -11,8 +13,8 @@ class ThemeProvider extends ChangeNotifier {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void reloadTheme({bool? useMaterial3}) {
|
void reloadTheme({Color? seedColorOverride, bool? useMaterial3}) {
|
||||||
createAppThemeSet().then((value) {
|
createAppThemeSet(seedColorOverride: seedColorOverride, useMaterial3: useMaterial3).then((value) {
|
||||||
theme = value;
|
theme = value;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:path/path.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:surface/providers/config.dart';
|
import 'package:surface/providers/config.dart';
|
||||||
|
@ -77,8 +77,11 @@ final _appRoutes = [
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/search',
|
path: '/search',
|
||||||
name: 'postSearch',
|
name: 'postSearch',
|
||||||
builder: (context, state) => const AppBackground(
|
builder: (context, state) => AppBackground(
|
||||||
child: PostSearchScreen(),
|
child: PostSearchScreen(
|
||||||
|
initialTags: state.uri.queryParameters['tags']?.split(','),
|
||||||
|
initialCategories: state.uri.queryParameters['categories']?.split(','),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@ -9,10 +10,12 @@ import 'package:material_symbols_icons/symbols.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:relative_time/relative_time.dart';
|
import 'package:relative_time/relative_time.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/experience.dart';
|
||||||
import 'package:surface/providers/relationship.dart';
|
import 'package:surface/providers/relationship.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/screens/abuse_report.dart';
|
import 'package:surface/screens/abuse_report.dart';
|
||||||
import 'package:surface/types/account.dart';
|
import 'package:surface/types/account.dart';
|
||||||
|
import 'package:surface/types/check_in.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/account/account_image.dart';
|
import 'package:surface/widgets/account/account_image.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
@ -61,6 +64,19 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<SnCheckInRecord>> _getCheckInRecords() async {
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
|
||||||
|
return List.from(
|
||||||
|
resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [],
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (mounted) context.showErrorDialog(err);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SnAccountStatusInfo? _status;
|
SnAccountStatusInfo? _status;
|
||||||
|
|
||||||
Future<void> _fetchStatus() async {
|
Future<void> _fetchStatus() async {
|
||||||
@ -228,7 +244,13 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppBar(
|
Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
appBarTheme: Theme.of(context).appBarTheme.copyWith(
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SliverAppBar(
|
||||||
expandedHeight: _appBarHeight,
|
expandedHeight: _appBarHeight,
|
||||||
title: _account == null
|
title: _account == null
|
||||||
? Text('loading').tr()
|
? Text('loading').tr()
|
||||||
@ -238,7 +260,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
TextSpan(
|
TextSpan(
|
||||||
text: _account!.nick,
|
text: _account!.nick,
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
color: Colors.white,
|
||||||
shadows: labelShadows,
|
shadows: labelShadows,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -246,7 +268,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
TextSpan(
|
TextSpan(
|
||||||
text: '@${_account!.name}',
|
text: '@${_account!.name}',
|
||||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
color: Colors.white,
|
||||||
shadows: labelShadows,
|
shadows: labelShadows,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -288,6 +310,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (_account != null)
|
if (_account != null)
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -430,6 +453,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.calendar_add_on),
|
const Icon(Symbols.calendar_add_on),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@ -437,6 +461,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.cake),
|
const Icon(Symbols.cake),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@ -450,6 +475,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Symbols.identity_platform),
|
const Icon(Symbols.identity_platform),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@ -459,6 +485,26 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
).opacity(0.8),
|
).opacity(0.8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.star),
|
||||||
|
const Gap(8),
|
||||||
|
Text('Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
|
||||||
|
const Gap(8),
|
||||||
|
Text(calcLevelUpProgressLevel(_account?.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
|
||||||
|
const Gap(8),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
constraints: const BoxConstraints(maxWidth: 160),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: calcLevelUpProgress(_account?.profile?.experience ?? 0),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
).alignment(Alignment.centerLeft),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 8),
|
).padding(horizontal: 8),
|
||||||
],
|
],
|
||||||
@ -466,6 +512,27 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
),
|
),
|
||||||
SliverToBoxAdapter(child: const Divider()),
|
SliverToBoxAdapter(child: const Divider()),
|
||||||
const SliverGap(12),
|
const SliverGap(12),
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: FutureBuilder<List<SnCheckInRecord>>(
|
||||||
|
future: _getCheckInRecords(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) return const SizedBox.shrink();
|
||||||
|
final records = snapshot.data!;
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 240,
|
||||||
|
child: CheckInRecordChart(records: records),
|
||||||
|
).padding(
|
||||||
|
right: 24,
|
||||||
|
left: 16,
|
||||||
|
top: 12,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverGap(12),
|
||||||
|
SliverToBoxAdapter(child: const Divider()),
|
||||||
|
const SliverGap(12),
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -534,3 +601,105 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CheckInRecordChart extends StatelessWidget {
|
||||||
|
const CheckInRecordChart({
|
||||||
|
super.key,
|
||||||
|
required this.records,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<SnCheckInRecord> records;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LineChart(
|
||||||
|
LineChartData(
|
||||||
|
lineBarsData: [
|
||||||
|
LineChartBarData(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
belowBarData: BarAreaData(
|
||||||
|
show: true,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: List.filled(
|
||||||
|
records.length,
|
||||||
|
Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
||||||
|
).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
spots: records
|
||||||
|
.map(
|
||||||
|
(x) => FlSpot(
|
||||||
|
x.createdAt
|
||||||
|
.copyWith(
|
||||||
|
hour: 0,
|
||||||
|
minute: 0,
|
||||||
|
second: 0,
|
||||||
|
millisecond: 0,
|
||||||
|
microsecond: 0,
|
||||||
|
)
|
||||||
|
.millisecondsSinceEpoch
|
||||||
|
.toDouble(),
|
||||||
|
x.resultTier.toDouble(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
lineTouchData: LineTouchData(
|
||||||
|
touchTooltipData: LineTouchTooltipData(
|
||||||
|
getTooltipItems: (spots) => spots
|
||||||
|
.map(
|
||||||
|
(spot) => LineTooltipItem(
|
||||||
|
'${kCheckInResultTierSymbols[spot.y.toInt()]}\n${DateFormat('MM/dd').format(DateTime.fromMillisecondsSinceEpoch(spot.x.toInt()))}',
|
||||||
|
TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
getTooltipColor: (_) => Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
titlesData: FlTitlesData(
|
||||||
|
topTitles: const AxisTitles(
|
||||||
|
sideTitles: SideTitles(showTitles: false),
|
||||||
|
),
|
||||||
|
rightTitles: const AxisTitles(
|
||||||
|
sideTitles: SideTitles(showTitles: false),
|
||||||
|
),
|
||||||
|
leftTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
reservedSize: 40,
|
||||||
|
interval: 1,
|
||||||
|
getTitlesWidget: (value, _) => Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text(
|
||||||
|
kCheckInResultTierSymbols[value.toInt()],
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
).padding(right: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
bottomTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
showTitles: true,
|
||||||
|
reservedSize: 28,
|
||||||
|
interval: 86400000,
|
||||||
|
getTitlesWidget: (value, _) => Text(
|
||||||
|
DateFormat('dd').format(
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
value.toInt(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
).padding(top: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
gridData: const FlGridData(show: false),
|
||||||
|
borderData: FlBorderData(show: false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,12 +5,27 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/post/post_item.dart';
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
|
const Map<String, IconData> kCategoryIcons = {
|
||||||
|
'technology': Symbols.tools_wrench,
|
||||||
|
'gaming': Symbols.gamepad,
|
||||||
|
'life': Symbols.nightlife,
|
||||||
|
'arts': Symbols.format_paint,
|
||||||
|
'sports': Symbols.sports_soccer,
|
||||||
|
'music': Symbols.music_note,
|
||||||
|
'news': Symbols.newspaper,
|
||||||
|
'knowledge': Symbols.book,
|
||||||
|
'literature': Symbols.book,
|
||||||
|
};
|
||||||
|
|
||||||
class ExploreScreen extends StatefulWidget {
|
class ExploreScreen extends StatefulWidget {
|
||||||
const ExploreScreen({super.key});
|
const ExploreScreen({super.key});
|
||||||
|
|
||||||
@ -24,15 +39,34 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
bool _isBusy = true;
|
bool _isBusy = true;
|
||||||
|
|
||||||
final List<SnPost> _posts = List.empty(growable: true);
|
final List<SnPost> _posts = List.empty(growable: true);
|
||||||
|
final List<SnPostCategory> _categories = List.empty(growable: true);
|
||||||
int? _postCount;
|
int? _postCount;
|
||||||
|
|
||||||
|
String? _selectedCategory;
|
||||||
|
|
||||||
|
Future<void> _fetchCategories() async {
|
||||||
|
_categories.clear();
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get('/cgi/co/categories?take=100');
|
||||||
|
_categories.addAll(resp.data.map((e) => SnPostCategory.fromJson(e)).cast<SnPostCategory>() ?? []);
|
||||||
|
} catch (err) {
|
||||||
|
if (!mounted) return;
|
||||||
|
context.showErrorDialog(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _fetchPosts() async {
|
Future<void> _fetchPosts() async {
|
||||||
if (_postCount != null && _posts.length >= _postCount!) return;
|
if (_postCount != null && _posts.length >= _postCount!) return;
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
final pt = context.read<SnPostContentProvider>();
|
final pt = context.read<SnPostContentProvider>();
|
||||||
final result = await pt.listPosts(take: 10, offset: _posts.length);
|
final result = await pt.listPosts(
|
||||||
|
take: 10,
|
||||||
|
offset: _posts.length,
|
||||||
|
categories: _selectedCategory != null ? [_selectedCategory!] : null,
|
||||||
|
);
|
||||||
final out = result.$1;
|
final out = result.$1;
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@ -43,10 +77,17 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
if (mounted) setState(() => _isBusy = false);
|
if (mounted) setState(() => _isBusy = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshPosts() {
|
||||||
|
_postCount = null;
|
||||||
|
_posts.clear();
|
||||||
|
return _fetchPosts();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_fetchPosts();
|
_fetchPosts();
|
||||||
|
_fetchCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -59,27 +100,20 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
type: ExpandableFabType.up,
|
type: ExpandableFabType.up,
|
||||||
childrenAnimation: ExpandableFabAnimation.none,
|
childrenAnimation: ExpandableFabAnimation.none,
|
||||||
overlayStyle: ExpandableFabOverlayStyle(
|
overlayStyle: ExpandableFabOverlayStyle(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context).colorScheme.surface.withAlpha((255 * 0.5).round()),
|
||||||
.colorScheme
|
|
||||||
.surface
|
|
||||||
.withAlpha((255 * 0.5).round()),
|
|
||||||
),
|
),
|
||||||
openButtonBuilder: RotateFloatingActionButtonBuilder(
|
openButtonBuilder: RotateFloatingActionButtonBuilder(
|
||||||
child: const Icon(Symbols.add, size: 28),
|
child: const Icon(Symbols.add, size: 28),
|
||||||
fabSize: ExpandableFabSize.regular,
|
fabSize: ExpandableFabSize.regular,
|
||||||
foregroundColor:
|
foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
|
||||||
shape: const CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
),
|
),
|
||||||
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
closeButtonBuilder: DefaultFloatingActionButtonBuilder(
|
||||||
child: const Icon(Symbols.close, size: 28),
|
child: const Icon(Symbols.close, size: 28),
|
||||||
fabSize: ExpandableFabSize.regular,
|
fabSize: ExpandableFabSize.regular,
|
||||||
foregroundColor:
|
foregroundColor: Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
||||||
Theme.of(context).floatingActionButtonTheme.foregroundColor,
|
backgroundColor: Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).floatingActionButtonTheme.backgroundColor,
|
|
||||||
shape: const CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
@ -95,8 +129,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
'mode': 'stories',
|
'mode': 'stories',
|
||||||
}).then((value) {
|
}).then((value) {
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
_posts.clear();
|
_refreshPosts();
|
||||||
_fetchPosts();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_fabKey.currentState!.toggle();
|
_fabKey.currentState!.toggle();
|
||||||
@ -117,8 +150,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
'mode': 'articles',
|
'mode': 'articles',
|
||||||
}).then((value) {
|
}).then((value) {
|
||||||
if (value == true) {
|
if (value == true) {
|
||||||
_posts.clear();
|
_refreshPosts();
|
||||||
_fetchPosts();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_fabKey.currentState!.toggle();
|
_fabKey.currentState!.toggle();
|
||||||
@ -131,10 +163,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
),
|
),
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
displacement: 40 + MediaQuery.of(context).padding.top,
|
displacement: 40 + MediaQuery.of(context).padding.top,
|
||||||
onRefresh: () {
|
onRefresh: () => _refreshPosts(),
|
||||||
_posts.clear();
|
|
||||||
return _fetchPosts();
|
|
||||||
},
|
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppBar(
|
SliverAppBar(
|
||||||
@ -151,6 +180,33 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(50),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 50,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 12),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: _categories.length,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final ele = _categories[idx];
|
||||||
|
return StyledWidget(ChoiceChip(
|
||||||
|
avatar: Icon(kCategoryIcons[ele.alias] ?? Symbols.question_mark),
|
||||||
|
label: Text(
|
||||||
|
'postCategory${ele.alias.capitalize()}'.trExists()
|
||||||
|
? 'postCategory${ele.alias.capitalize()}'.tr()
|
||||||
|
: ele.name,
|
||||||
|
),
|
||||||
|
selected: _selectedCategory == ele.alias,
|
||||||
|
onSelected: (value) {
|
||||||
|
_selectedCategory = value ? ele.alias : null;
|
||||||
|
_refreshPosts();
|
||||||
|
},
|
||||||
|
)).padding(horizontal: 4);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
SliverInfiniteList(
|
SliverInfiniteList(
|
||||||
itemCount: _posts.length,
|
itemCount: _posts.length,
|
||||||
@ -167,8 +223,7 @@ class _ExploreScreenState extends State<ExploreScreen> {
|
|||||||
setState(() => _posts[idx] = data);
|
setState(() => _posts[idx] = data);
|
||||||
},
|
},
|
||||||
onDeleted: () {
|
onDeleted: () {
|
||||||
_posts.clear();
|
_refreshPosts();
|
||||||
_fetchPosts();
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
@ -17,14 +17,13 @@ import 'package:surface/providers/config.dart';
|
|||||||
import 'package:surface/providers/post.dart';
|
import 'package:surface/providers/post.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
|
import 'package:surface/providers/widget.dart';
|
||||||
import 'package:surface/types/check_in.dart';
|
import 'package:surface/types/check_in.dart';
|
||||||
import 'package:surface/types/post.dart';
|
import 'package:surface/types/post.dart';
|
||||||
import 'package:surface/widgets/app_bar_leading.dart';
|
import 'package:surface/widgets/app_bar_leading.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
import 'package:surface/widgets/post/post_item.dart';
|
import 'package:surface/widgets/post/post_item.dart';
|
||||||
|
|
||||||
import '../providers/widget.dart';
|
|
||||||
|
|
||||||
class HomeScreenDashEntry {
|
class HomeScreenDashEntry {
|
||||||
final String name;
|
final String name;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@ -212,7 +211,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
final home = context.read<HomeWidgetProvider>();
|
final home = context.read<HomeWidgetProvider>();
|
||||||
final resp = await sn.client.get('/cgi/id/check-in/today');
|
final resp = await sn.client.get('/cgi/id/check-in/today');
|
||||||
_todayRecord = SnCheckInRecord.fromJson(resp.data);
|
_todayRecord = SnCheckInRecord.fromJson(resp.data);
|
||||||
home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
|
await home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isBusy = false);
|
setState(() => _isBusy = false);
|
||||||
}
|
}
|
||||||
@ -225,7 +224,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
|
|||||||
final home = context.read<HomeWidgetProvider>();
|
final home = context.read<HomeWidgetProvider>();
|
||||||
final resp = await sn.client.post('/cgi/id/check-in');
|
final resp = await sn.client.post('/cgi/id/check-in');
|
||||||
_todayRecord = SnCheckInRecord.fromJson(resp.data);
|
_todayRecord = SnCheckInRecord.fromJson(resp.data);
|
||||||
home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
|
await home.saveWidgetData('pas_check_in_record', _todayRecord!.toJson());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err);
|
context.showErrorDialog(err);
|
||||||
|
@ -13,7 +13,10 @@ import 'package:surface/widgets/post/post_tags_field.dart';
|
|||||||
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
||||||
|
|
||||||
class PostSearchScreen extends StatefulWidget {
|
class PostSearchScreen extends StatefulWidget {
|
||||||
const PostSearchScreen({super.key});
|
final Iterable<String>? initialTags;
|
||||||
|
final Iterable<String>? initialCategories;
|
||||||
|
|
||||||
|
const PostSearchScreen({super.key, this.initialTags, this.initialCategories});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PostSearchScreen> createState() => _PostSearchScreenState();
|
State<PostSearchScreen> createState() => _PostSearchScreenState();
|
||||||
@ -23,6 +26,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
|
|
||||||
List<String> _searchTags = List.empty(growable: true);
|
List<String> _searchTags = List.empty(growable: true);
|
||||||
|
List<String> _searchCategories = List.empty(growable: true);
|
||||||
|
|
||||||
final List<SnPost> _posts = List.empty(growable: true);
|
final List<SnPost> _posts = List.empty(growable: true);
|
||||||
int? _postCount;
|
int? _postCount;
|
||||||
@ -30,8 +34,18 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
String _searchTerm = '';
|
String _searchTerm = '';
|
||||||
Duration? _lastTook;
|
Duration? _lastTook;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_searchTags.addAll(widget.initialTags ?? []);
|
||||||
|
_searchCategories.addAll(widget.initialCategories ?? []);
|
||||||
|
if (_searchTags.isNotEmpty || _searchCategories.isNotEmpty) {
|
||||||
|
_fetchPosts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _fetchPosts() async {
|
Future<void> _fetchPosts() async {
|
||||||
if (_searchTerm.isEmpty && _searchTags.isEmpty) return;
|
if (_searchTerm.isEmpty && _searchCategories.isEmpty && _searchTags.isEmpty) return;
|
||||||
if (_postCount != null && _posts.length >= _postCount!) return;
|
if (_postCount != null && _posts.length >= _postCount!) return;
|
||||||
|
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
@ -45,6 +59,7 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
take: 10,
|
take: 10,
|
||||||
offset: _posts.length,
|
offset: _posts.length,
|
||||||
tags: _searchTags,
|
tags: _searchTags,
|
||||||
|
categories: _searchCategories,
|
||||||
);
|
);
|
||||||
final List<SnPost> out = result.$1;
|
final List<SnPost> out = result.$1;
|
||||||
_postCount = result.$2;
|
_postCount = result.$2;
|
||||||
@ -73,9 +88,20 @@ class _PostSearchScreenState extends State<PostSearchScreen> {
|
|||||||
setState(() => _searchTags = value);
|
setState(() => _searchTags = value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const Gap(4),
|
||||||
|
PostCategoriesField(
|
||||||
|
labelText: 'fieldPostCategories'.tr(),
|
||||||
|
initialCategories: _searchCategories,
|
||||||
|
onUpdate: (value) {
|
||||||
|
setState(() => _searchCategories = value);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 24, vertical: 16),
|
).padding(horizontal: 24, vertical: 16),
|
||||||
);
|
).then((_) {
|
||||||
|
_posts.clear();
|
||||||
|
_fetchPosts();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -45,17 +45,9 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
Future<void> _fetchPublisher() async {
|
Future<void> _fetchPublisher() async {
|
||||||
try {
|
try {
|
||||||
final sn = context.read<SnNetworkProvider>();
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final ud = context.read<UserDirectoryProvider>();
|
|
||||||
final rel = context.read<SnRelationshipProvider>();
|
|
||||||
final resp = await sn.client.get('/cgi/co/publishers/${widget.name}');
|
final resp = await sn.client.get('/cgi/co/publishers/${widget.name}');
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
_publisher = SnPublisher.fromJson(resp.data);
|
_publisher = SnPublisher.fromJson(resp.data);
|
||||||
_account = await ud.getAccount(_publisher?.accountId);
|
|
||||||
_accountRelationship = await rel.getRelationship(_account!.id);
|
|
||||||
if (_publisher?.realmId != null && _publisher!.realmId != 0) {
|
|
||||||
final resp = await sn.client.get('/cgi/id/realms/${_publisher!.realmId}');
|
|
||||||
_realm = SnRealm.fromJson(resp.data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
context.showErrorDialog(err).then((_) {
|
context.showErrorDialog(err).then((_) {
|
||||||
@ -65,6 +57,20 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
} finally {
|
} finally {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final ud = context.read<UserDirectoryProvider>();
|
||||||
|
final rel = context.read<SnRelationshipProvider>();
|
||||||
|
_account = await ud.getAccount(_publisher?.accountId);
|
||||||
|
_accountRelationship = await rel.getRelationship(_account!.id);
|
||||||
|
if (_publisher?.realmId != null && _publisher!.realmId != 0) {
|
||||||
|
final resp = await sn.client.get('/cgi/id/realms/${_publisher!.realmId}');
|
||||||
|
_realm = SnRealm.fromJson(resp.data);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isSubscribing = false;
|
bool _isSubscribing = false;
|
||||||
@ -277,7 +283,13 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
||||||
sliver: MultiSliver(
|
sliver: MultiSliver(
|
||||||
children: [
|
children: [
|
||||||
SliverAppBar(
|
Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
appBarTheme: Theme.of(context).appBarTheme.copyWith(
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SliverAppBar(
|
||||||
expandedHeight: _appBarHeight,
|
expandedHeight: _appBarHeight,
|
||||||
title: _publisher == null
|
title: _publisher == null
|
||||||
? Text('loading').tr()
|
? Text('loading').tr()
|
||||||
@ -287,7 +299,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
TextSpan(
|
TextSpan(
|
||||||
text: _publisher!.nick,
|
text: _publisher!.nick,
|
||||||
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
style: Theme.of(context).textTheme.titleLarge!.copyWith(
|
||||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
color: Colors.white,
|
||||||
shadows: labelShadows,
|
shadows: labelShadows,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -342,6 +354,7 @@ class _PostPublisherScreenState extends State<PostPublisherScreen> with SingleTi
|
|||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (_publisher != null)
|
if (_publisher != null)
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Container(
|
child: Container(
|
||||||
|
@ -5,6 +5,7 @@ import 'package:dropdown_button2/dropdown_button2.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
@ -18,6 +19,17 @@ import 'package:surface/providers/theme.dart';
|
|||||||
import 'package:surface/theme.dart';
|
import 'package:surface/theme.dart';
|
||||||
import 'package:surface/widgets/dialog.dart';
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
|
||||||
|
const Map<String, Color> kColorSchemes = {
|
||||||
|
'colorSchemeIndigo': Colors.indigo,
|
||||||
|
'colorSchemeBlue': Colors.blue,
|
||||||
|
'colorSchemeGreen': Colors.green,
|
||||||
|
'colorSchemeYellow': Colors.yellow,
|
||||||
|
'colorSchemeOrange': Colors.orange,
|
||||||
|
'colorSchemeRed': Colors.red,
|
||||||
|
'colorSchemeWhite': Colors.white,
|
||||||
|
'colorSchemeBlack': Colors.black,
|
||||||
|
};
|
||||||
|
|
||||||
class SettingsScreen extends StatefulWidget {
|
class SettingsScreen extends StatefulWidget {
|
||||||
const SettingsScreen({super.key});
|
const SettingsScreen({super.key});
|
||||||
|
|
||||||
@ -77,7 +89,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
if (image == null) return;
|
if (image == null) return;
|
||||||
|
|
||||||
await File(image.path).copy('$_docBasepath/app_background_image');
|
await File(image.path).copy('$_docBasepath/app_background_image');
|
||||||
_prefs.setBool('has_background_image', true);
|
_prefs.setBool(kAppBackgroundStoreKey, true);
|
||||||
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
@ -98,7 +110,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
trailing: const Icon(Symbols.chevron_right),
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
File('$_docBasepath/app_background_image').deleteSync();
|
File('$_docBasepath/app_background_image').deleteSync();
|
||||||
_prefs.remove('has_background_image');
|
_prefs.remove(kAppBackgroundStoreKey);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -116,10 +128,118 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
value ?? false,
|
value ?? false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
final th = context.watch<ThemeProvider>();
|
final th = context.read<ThemeProvider>();
|
||||||
th.reloadTheme(useMaterial3: value ?? false);
|
th.reloadTheme(useMaterial3: value ?? false);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.format_paint),
|
||||||
|
title: Text('settingsColorScheme').tr(),
|
||||||
|
subtitle: Text('settingsColorSchemeDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
trailing: const Icon(Symbols.chevron_right),
|
||||||
|
onTap: () async {
|
||||||
|
Color pickerColor = Color(_prefs.getInt(kAppColorSchemeStoreKey) ?? Colors.indigo.value);
|
||||||
|
final color = await showDialog<Color?>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: ColorPicker(
|
||||||
|
pickerColor: pickerColor,
|
||||||
|
onColorChanged: (color) => pickerColor = color,
|
||||||
|
enableAlpha: false,
|
||||||
|
hexInputBar: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
child: const Text('dialogDismiss').tr(),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: const Text('dialogConfirm').tr(),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(pickerColor);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (color == null || !context.mounted) return;
|
||||||
|
|
||||||
|
_prefs.setInt(kAppColorSchemeStoreKey, color.value);
|
||||||
|
final th = context.read<ThemeProvider>();
|
||||||
|
th.reloadTheme(seedColorOverride: color);
|
||||||
|
setState(() {});
|
||||||
|
|
||||||
|
context.showSnackbar('colorSchemeApplied'.tr());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.palette),
|
||||||
|
title: Text('settingsColorSeed').tr(),
|
||||||
|
subtitle: Text('settingsColorSeedDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
trailing: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton2<int?>(
|
||||||
|
isExpanded: true,
|
||||||
|
items: [
|
||||||
|
...kColorSchemes.entries.mapIndexed((idx, ele) {
|
||||||
|
return DropdownMenuItem<int>(
|
||||||
|
value: idx,
|
||||||
|
child: Text(ele.key).tr(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
DropdownMenuItem<int>(
|
||||||
|
value: -1,
|
||||||
|
child: Text('custom').tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
value: _prefs.getInt(kAppColorSchemeStoreKey) == null
|
||||||
|
? 1
|
||||||
|
: kColorSchemes.values
|
||||||
|
.toList()
|
||||||
|
.indexWhere((ele) => ele.value == _prefs.getInt(kAppColorSchemeStoreKey)),
|
||||||
|
onChanged: (int? value) {
|
||||||
|
if (value != null && value != -1) {
|
||||||
|
_prefs.setInt(kAppColorSchemeStoreKey, kColorSchemes.values.elementAt(value).value);
|
||||||
|
final th = context.read<ThemeProvider>();
|
||||||
|
th.reloadTheme(seedColorOverride: kColorSchemes.values.elementAt(value));
|
||||||
|
setState(() {});
|
||||||
|
|
||||||
|
context.showSnackbar('colorSchemeApplied'.tr());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buttonStyleData: const ButtonStyleData(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 5,
|
||||||
|
),
|
||||||
|
height: 40,
|
||||||
|
width: 160,
|
||||||
|
),
|
||||||
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
secondary: const Icon(Symbols.blur_on),
|
||||||
|
title: Text('settingsAppBarTransparent').tr(),
|
||||||
|
subtitle: Text('settingsAppBarTransparentDescription').tr(),
|
||||||
|
contentPadding: const EdgeInsets.only(left: 24, right: 17),
|
||||||
|
value: _prefs.getBool(kAppbarTransparentStoreKey) ?? false,
|
||||||
|
onChanged: (value) {
|
||||||
|
_prefs.setBool(kAppbarTransparentStoreKey, value ?? false);
|
||||||
|
final th = context.read<ThemeProvider>();
|
||||||
|
th.reloadTheme();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
@ -189,7 +309,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 5,
|
vertical: 5,
|
||||||
),
|
),
|
||||||
height: 40,
|
height: 56,
|
||||||
width: 160,
|
width: 160,
|
||||||
),
|
),
|
||||||
menuItemStyleData: const MenuItemStyleData(
|
menuItemStyleData: const MenuItemStyleData(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:surface/providers/config.dart';
|
||||||
|
|
||||||
const kMaterialYouToggleStoreKey = 'app_theme_material_you';
|
const kMaterialYouToggleStoreKey = 'app_theme_material_you';
|
||||||
|
|
||||||
@ -10,7 +11,7 @@ class ThemeSet {
|
|||||||
ThemeSet({required this.light, required this.dark});
|
ThemeSet({required this.light, required this.dark});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ThemeSet> createAppThemeSet({bool? useMaterial3}) async {
|
Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3}) async {
|
||||||
return ThemeSet(
|
return ThemeSet(
|
||||||
light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3),
|
light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3),
|
||||||
dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3),
|
dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3),
|
||||||
@ -19,16 +20,21 @@ Future<ThemeSet> createAppThemeSet({bool? useMaterial3}) async {
|
|||||||
|
|
||||||
Future<ThemeData> createAppTheme(
|
Future<ThemeData> createAppTheme(
|
||||||
Brightness brightness, {
|
Brightness brightness, {
|
||||||
|
Color? seedColorOverride,
|
||||||
bool? useMaterial3,
|
bool? useMaterial3,
|
||||||
}) async {
|
}) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
|
final seedColorString = prefs.getInt(kAppColorSchemeStoreKey);
|
||||||
|
final seedColor = seedColorString != null ? Color(seedColorString) : Colors.indigo;
|
||||||
|
|
||||||
final colorScheme = ColorScheme.fromSeed(
|
final colorScheme = ColorScheme.fromSeed(
|
||||||
seedColor: Colors.indigo,
|
seedColor: seedColorOverride ?? seedColor,
|
||||||
brightness: brightness,
|
brightness: brightness,
|
||||||
);
|
);
|
||||||
|
|
||||||
final hasBackground = prefs.getBool('has_background_image') ?? false;
|
final hasBackground = prefs.getBool(kAppBackgroundStoreKey) ?? false;
|
||||||
|
final hasAppBarBlurry = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false),
|
useMaterial3: useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? false),
|
||||||
@ -42,8 +48,9 @@ Future<ThemeData> createAppTheme(
|
|||||||
),
|
),
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
backgroundColor: hasBackground ? colorScheme.primary.withOpacity(0.75) : colorScheme.primary,
|
elevation: hasAppBarBlurry ? 0 : null,
|
||||||
foregroundColor: colorScheme.onPrimary,
|
backgroundColor: hasAppBarBlurry ? colorScheme.surfaceContainer.withAlpha(200) : colorScheme.primary,
|
||||||
|
foregroundColor: hasAppBarBlurry ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
scaffoldBackgroundColor: Colors.transparent,
|
scaffoldBackgroundColor: Colors.transparent,
|
||||||
);
|
);
|
||||||
|
@ -3,6 +3,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
part 'check_in.freezed.dart';
|
part 'check_in.freezed.dart';
|
||||||
part 'check_in.g.dart';
|
part 'check_in.g.dart';
|
||||||
|
|
||||||
|
const List<String> kCheckInResultTierSymbols = ['大凶', '凶', '中平', '吉', '大吉'];
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class SnCheckInRecord with _$SnCheckInRecord {
|
class SnCheckInRecord with _$SnCheckInRecord {
|
||||||
const SnCheckInRecord._();
|
const SnCheckInRecord._();
|
||||||
@ -21,11 +23,5 @@ class SnCheckInRecord with _$SnCheckInRecord {
|
|||||||
factory SnCheckInRecord.fromJson(Map<String, dynamic> json) =>
|
factory SnCheckInRecord.fromJson(Map<String, dynamic> json) =>
|
||||||
_$SnCheckInRecordFromJson(json);
|
_$SnCheckInRecordFromJson(json);
|
||||||
|
|
||||||
String get symbol => switch (resultTier) {
|
String get symbol => kCheckInResultTierSymbols[resultTier];
|
||||||
0 => '大凶',
|
|
||||||
1 => '凶',
|
|
||||||
2 => '中平',
|
|
||||||
3 => '吉',
|
|
||||||
_ => '大吉',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ class SnPost with _$SnPost {
|
|||||||
required String? alias,
|
required String? alias,
|
||||||
required String? aliasPrefix,
|
required String? aliasPrefix,
|
||||||
@Default([]) List<SnPostTag> tags,
|
@Default([]) List<SnPostTag> tags,
|
||||||
@Default([]) List<dynamic> categories,
|
@Default([]) List<SnPostCategory> categories,
|
||||||
required List<SnPost>? replies,
|
required List<SnPost>? replies,
|
||||||
required int? replyId,
|
required int? replyId,
|
||||||
required int? repostId,
|
required int? repostId,
|
||||||
@ -67,6 +67,23 @@ class SnPostTag with _$SnPostTag {
|
|||||||
_$SnPostTagFromJson(json);
|
_$SnPostTagFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class SnPostCategory with _$SnPostCategory {
|
||||||
|
const factory SnPostCategory({
|
||||||
|
required int id,
|
||||||
|
required DateTime createdAt,
|
||||||
|
required DateTime updatedAt,
|
||||||
|
required dynamic deletedAt,
|
||||||
|
required String alias,
|
||||||
|
required String name,
|
||||||
|
required String description,
|
||||||
|
required dynamic posts,
|
||||||
|
}) = _SnPostCategory;
|
||||||
|
|
||||||
|
factory SnPostCategory.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$SnPostCategoryFromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class SnPostPreload with _$SnPostPreload {
|
class SnPostPreload with _$SnPostPreload {
|
||||||
const factory SnPostPreload({
|
const factory SnPostPreload({
|
||||||
|
@ -30,7 +30,7 @@ mixin _$SnPost {
|
|||||||
String? get alias => throw _privateConstructorUsedError;
|
String? get alias => throw _privateConstructorUsedError;
|
||||||
String? get aliasPrefix => throw _privateConstructorUsedError;
|
String? get aliasPrefix => throw _privateConstructorUsedError;
|
||||||
List<SnPostTag> get tags => throw _privateConstructorUsedError;
|
List<SnPostTag> get tags => throw _privateConstructorUsedError;
|
||||||
List<dynamic> get categories => throw _privateConstructorUsedError;
|
List<SnPostCategory> get categories => throw _privateConstructorUsedError;
|
||||||
List<SnPost>? get replies => throw _privateConstructorUsedError;
|
List<SnPost>? get replies => throw _privateConstructorUsedError;
|
||||||
int? get replyId => throw _privateConstructorUsedError;
|
int? get replyId => throw _privateConstructorUsedError;
|
||||||
int? get repostId => throw _privateConstructorUsedError;
|
int? get repostId => throw _privateConstructorUsedError;
|
||||||
@ -77,7 +77,7 @@ abstract class $SnPostCopyWith<$Res> {
|
|||||||
String? alias,
|
String? alias,
|
||||||
String? aliasPrefix,
|
String? aliasPrefix,
|
||||||
List<SnPostTag> tags,
|
List<SnPostTag> tags,
|
||||||
List<dynamic> categories,
|
List<SnPostCategory> categories,
|
||||||
List<SnPost>? replies,
|
List<SnPost>? replies,
|
||||||
int? replyId,
|
int? replyId,
|
||||||
int? repostId,
|
int? repostId,
|
||||||
@ -197,7 +197,7 @@ class _$SnPostCopyWithImpl<$Res, $Val extends SnPost>
|
|||||||
categories: null == categories
|
categories: null == categories
|
||||||
? _value.categories
|
? _value.categories
|
||||||
: categories // ignore: cast_nullable_to_non_nullable
|
: categories // ignore: cast_nullable_to_non_nullable
|
||||||
as List<dynamic>,
|
as List<SnPostCategory>,
|
||||||
replies: freezed == replies
|
replies: freezed == replies
|
||||||
? _value.replies
|
? _value.replies
|
||||||
: replies // ignore: cast_nullable_to_non_nullable
|
: replies // ignore: cast_nullable_to_non_nullable
|
||||||
@ -362,7 +362,7 @@ abstract class _$$SnPostImplCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
|||||||
String? alias,
|
String? alias,
|
||||||
String? aliasPrefix,
|
String? aliasPrefix,
|
||||||
List<SnPostTag> tags,
|
List<SnPostTag> tags,
|
||||||
List<dynamic> categories,
|
List<SnPostCategory> categories,
|
||||||
List<SnPost>? replies,
|
List<SnPost>? replies,
|
||||||
int? replyId,
|
int? replyId,
|
||||||
int? repostId,
|
int? repostId,
|
||||||
@ -485,7 +485,7 @@ class __$$SnPostImplCopyWithImpl<$Res>
|
|||||||
categories: null == categories
|
categories: null == categories
|
||||||
? _value._categories
|
? _value._categories
|
||||||
: categories // ignore: cast_nullable_to_non_nullable
|
: categories // ignore: cast_nullable_to_non_nullable
|
||||||
as List<dynamic>,
|
as List<SnPostCategory>,
|
||||||
replies: freezed == replies
|
replies: freezed == replies
|
||||||
? _value._replies
|
? _value._replies
|
||||||
: replies // ignore: cast_nullable_to_non_nullable
|
: replies // ignore: cast_nullable_to_non_nullable
|
||||||
@ -584,7 +584,7 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
required this.alias,
|
required this.alias,
|
||||||
required this.aliasPrefix,
|
required this.aliasPrefix,
|
||||||
final List<SnPostTag> tags = const [],
|
final List<SnPostTag> tags = const [],
|
||||||
final List<dynamic> categories = const [],
|
final List<SnPostCategory> categories = const [],
|
||||||
required final List<SnPost>? replies,
|
required final List<SnPost>? replies,
|
||||||
required this.replyId,
|
required this.replyId,
|
||||||
required this.repostId,
|
required this.repostId,
|
||||||
@ -649,10 +649,10 @@ class _$SnPostImpl extends _SnPost {
|
|||||||
return EqualUnmodifiableListView(_tags);
|
return EqualUnmodifiableListView(_tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<dynamic> _categories;
|
final List<SnPostCategory> _categories;
|
||||||
@override
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
List<dynamic> get categories {
|
List<SnPostCategory> get categories {
|
||||||
if (_categories is EqualUnmodifiableListView) return _categories;
|
if (_categories is EqualUnmodifiableListView) return _categories;
|
||||||
// ignore: implicit_dynamic_type
|
// ignore: implicit_dynamic_type
|
||||||
return EqualUnmodifiableListView(_categories);
|
return EqualUnmodifiableListView(_categories);
|
||||||
@ -853,7 +853,7 @@ abstract class _SnPost extends SnPost {
|
|||||||
required final String? alias,
|
required final String? alias,
|
||||||
required final String? aliasPrefix,
|
required final String? aliasPrefix,
|
||||||
final List<SnPostTag> tags,
|
final List<SnPostTag> tags,
|
||||||
final List<dynamic> categories,
|
final List<SnPostCategory> categories,
|
||||||
required final List<SnPost>? replies,
|
required final List<SnPost>? replies,
|
||||||
required final int? replyId,
|
required final int? replyId,
|
||||||
required final int? repostId,
|
required final int? repostId,
|
||||||
@ -899,7 +899,7 @@ abstract class _SnPost extends SnPost {
|
|||||||
@override
|
@override
|
||||||
List<SnPostTag> get tags;
|
List<SnPostTag> get tags;
|
||||||
@override
|
@override
|
||||||
List<dynamic> get categories;
|
List<SnPostCategory> get categories;
|
||||||
@override
|
@override
|
||||||
List<SnPost>? get replies;
|
List<SnPost>? get replies;
|
||||||
@override
|
@override
|
||||||
@ -1253,6 +1253,312 @@ abstract class _SnPostTag implements SnPostTag {
|
|||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SnPostCategory _$SnPostCategoryFromJson(Map<String, dynamic> json) {
|
||||||
|
return _SnPostCategory.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$SnPostCategory {
|
||||||
|
int get id => throw _privateConstructorUsedError;
|
||||||
|
DateTime get createdAt => throw _privateConstructorUsedError;
|
||||||
|
DateTime get updatedAt => throw _privateConstructorUsedError;
|
||||||
|
dynamic get deletedAt => throw _privateConstructorUsedError;
|
||||||
|
String get alias => throw _privateConstructorUsedError;
|
||||||
|
String get name => throw _privateConstructorUsedError;
|
||||||
|
String get description => throw _privateConstructorUsedError;
|
||||||
|
dynamic get posts => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Serializes this SnPostCategory to a JSON map.
|
||||||
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
|
/// Create a copy of SnPostCategory
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
$SnPostCategoryCopyWith<SnPostCategory> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class $SnPostCategoryCopyWith<$Res> {
|
||||||
|
factory $SnPostCategoryCopyWith(
|
||||||
|
SnPostCategory value, $Res Function(SnPostCategory) then) =
|
||||||
|
_$SnPostCategoryCopyWithImpl<$Res, SnPostCategory>;
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int id,
|
||||||
|
DateTime createdAt,
|
||||||
|
DateTime updatedAt,
|
||||||
|
dynamic deletedAt,
|
||||||
|
String alias,
|
||||||
|
String name,
|
||||||
|
String description,
|
||||||
|
dynamic posts});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class _$SnPostCategoryCopyWithImpl<$Res, $Val extends SnPostCategory>
|
||||||
|
implements $SnPostCategoryCopyWith<$Res> {
|
||||||
|
_$SnPostCategoryCopyWithImpl(this._value, this._then);
|
||||||
|
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Val _value;
|
||||||
|
// ignore: unused_field
|
||||||
|
final $Res Function($Val) _then;
|
||||||
|
|
||||||
|
/// Create a copy of SnPostCategory
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? createdAt = null,
|
||||||
|
Object? updatedAt = null,
|
||||||
|
Object? deletedAt = freezed,
|
||||||
|
Object? alias = null,
|
||||||
|
Object? name = null,
|
||||||
|
Object? description = null,
|
||||||
|
Object? posts = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_value.copyWith(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
createdAt: null == createdAt
|
||||||
|
? _value.createdAt
|
||||||
|
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
updatedAt: null == updatedAt
|
||||||
|
? _value.updatedAt
|
||||||
|
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
deletedAt: freezed == deletedAt
|
||||||
|
? _value.deletedAt
|
||||||
|
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as dynamic,
|
||||||
|
alias: null == alias
|
||||||
|
? _value.alias
|
||||||
|
: alias // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
name: null == name
|
||||||
|
? _value.name
|
||||||
|
: name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
description: null == description
|
||||||
|
? _value.description
|
||||||
|
: description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
posts: freezed == posts
|
||||||
|
? _value.posts
|
||||||
|
: posts // ignore: cast_nullable_to_non_nullable
|
||||||
|
as dynamic,
|
||||||
|
) as $Val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract class _$$SnPostCategoryImplCopyWith<$Res>
|
||||||
|
implements $SnPostCategoryCopyWith<$Res> {
|
||||||
|
factory _$$SnPostCategoryImplCopyWith(_$SnPostCategoryImpl value,
|
||||||
|
$Res Function(_$SnPostCategoryImpl) then) =
|
||||||
|
__$$SnPostCategoryImplCopyWithImpl<$Res>;
|
||||||
|
@override
|
||||||
|
@useResult
|
||||||
|
$Res call(
|
||||||
|
{int id,
|
||||||
|
DateTime createdAt,
|
||||||
|
DateTime updatedAt,
|
||||||
|
dynamic deletedAt,
|
||||||
|
String alias,
|
||||||
|
String name,
|
||||||
|
String description,
|
||||||
|
dynamic posts});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
class __$$SnPostCategoryImplCopyWithImpl<$Res>
|
||||||
|
extends _$SnPostCategoryCopyWithImpl<$Res, _$SnPostCategoryImpl>
|
||||||
|
implements _$$SnPostCategoryImplCopyWith<$Res> {
|
||||||
|
__$$SnPostCategoryImplCopyWithImpl(
|
||||||
|
_$SnPostCategoryImpl _value, $Res Function(_$SnPostCategoryImpl) _then)
|
||||||
|
: super(_value, _then);
|
||||||
|
|
||||||
|
/// Create a copy of SnPostCategory
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
@override
|
||||||
|
$Res call({
|
||||||
|
Object? id = null,
|
||||||
|
Object? createdAt = null,
|
||||||
|
Object? updatedAt = null,
|
||||||
|
Object? deletedAt = freezed,
|
||||||
|
Object? alias = null,
|
||||||
|
Object? name = null,
|
||||||
|
Object? description = null,
|
||||||
|
Object? posts = freezed,
|
||||||
|
}) {
|
||||||
|
return _then(_$SnPostCategoryImpl(
|
||||||
|
id: null == id
|
||||||
|
? _value.id
|
||||||
|
: id // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
|
createdAt: null == createdAt
|
||||||
|
? _value.createdAt
|
||||||
|
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
updatedAt: null == updatedAt
|
||||||
|
? _value.updatedAt
|
||||||
|
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as DateTime,
|
||||||
|
deletedAt: freezed == deletedAt
|
||||||
|
? _value.deletedAt
|
||||||
|
: deletedAt // ignore: cast_nullable_to_non_nullable
|
||||||
|
as dynamic,
|
||||||
|
alias: null == alias
|
||||||
|
? _value.alias
|
||||||
|
: alias // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
name: null == name
|
||||||
|
? _value.name
|
||||||
|
: name // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
description: null == description
|
||||||
|
? _value.description
|
||||||
|
: description // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
posts: freezed == posts
|
||||||
|
? _value.posts
|
||||||
|
: posts // ignore: cast_nullable_to_non_nullable
|
||||||
|
as dynamic,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
@JsonSerializable()
|
||||||
|
class _$SnPostCategoryImpl implements _SnPostCategory {
|
||||||
|
const _$SnPostCategoryImpl(
|
||||||
|
{required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.deletedAt,
|
||||||
|
required this.alias,
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
required this.posts});
|
||||||
|
|
||||||
|
factory _$SnPostCategoryImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$$SnPostCategoryImplFromJson(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final int id;
|
||||||
|
@override
|
||||||
|
final DateTime createdAt;
|
||||||
|
@override
|
||||||
|
final DateTime updatedAt;
|
||||||
|
@override
|
||||||
|
final dynamic deletedAt;
|
||||||
|
@override
|
||||||
|
final String alias;
|
||||||
|
@override
|
||||||
|
final String name;
|
||||||
|
@override
|
||||||
|
final String description;
|
||||||
|
@override
|
||||||
|
final dynamic posts;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'SnPostCategory(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, alias: $alias, name: $name, description: $description, posts: $posts)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) ||
|
||||||
|
(other.runtimeType == runtimeType &&
|
||||||
|
other is _$SnPostCategoryImpl &&
|
||||||
|
(identical(other.id, id) || other.id == id) &&
|
||||||
|
(identical(other.createdAt, createdAt) ||
|
||||||
|
other.createdAt == createdAt) &&
|
||||||
|
(identical(other.updatedAt, updatedAt) ||
|
||||||
|
other.updatedAt == updatedAt) &&
|
||||||
|
const DeepCollectionEquality().equals(other.deletedAt, deletedAt) &&
|
||||||
|
(identical(other.alias, alias) || other.alias == alias) &&
|
||||||
|
(identical(other.name, name) || other.name == name) &&
|
||||||
|
(identical(other.description, description) ||
|
||||||
|
other.description == description) &&
|
||||||
|
const DeepCollectionEquality().equals(other.posts, posts));
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
runtimeType,
|
||||||
|
id,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
const DeepCollectionEquality().hash(deletedAt),
|
||||||
|
alias,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
const DeepCollectionEquality().hash(posts));
|
||||||
|
|
||||||
|
/// Create a copy of SnPostCategory
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$$SnPostCategoryImplCopyWith<_$SnPostCategoryImpl> get copyWith =>
|
||||||
|
__$$SnPostCategoryImplCopyWithImpl<_$SnPostCategoryImpl>(
|
||||||
|
this, _$identity);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return _$$SnPostCategoryImplToJson(
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _SnPostCategory implements SnPostCategory {
|
||||||
|
const factory _SnPostCategory(
|
||||||
|
{required final int id,
|
||||||
|
required final DateTime createdAt,
|
||||||
|
required final DateTime updatedAt,
|
||||||
|
required final dynamic deletedAt,
|
||||||
|
required final String alias,
|
||||||
|
required final String name,
|
||||||
|
required final String description,
|
||||||
|
required final dynamic posts}) = _$SnPostCategoryImpl;
|
||||||
|
|
||||||
|
factory _SnPostCategory.fromJson(Map<String, dynamic> json) =
|
||||||
|
_$SnPostCategoryImpl.fromJson;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get id;
|
||||||
|
@override
|
||||||
|
DateTime get createdAt;
|
||||||
|
@override
|
||||||
|
DateTime get updatedAt;
|
||||||
|
@override
|
||||||
|
dynamic get deletedAt;
|
||||||
|
@override
|
||||||
|
String get alias;
|
||||||
|
@override
|
||||||
|
String get name;
|
||||||
|
@override
|
||||||
|
String get description;
|
||||||
|
@override
|
||||||
|
dynamic get posts;
|
||||||
|
|
||||||
|
/// Create a copy of SnPostCategory
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
_$$SnPostCategoryImplCopyWith<_$SnPostCategoryImpl> get copyWith =>
|
||||||
|
throw _privateConstructorUsedError;
|
||||||
|
}
|
||||||
|
|
||||||
SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) {
|
SnPostPreload _$SnPostPreloadFromJson(Map<String, dynamic> json) {
|
||||||
return _SnPostPreload.fromJson(json);
|
return _SnPostPreload.fromJson(json);
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,10 @@ _$SnPostImpl _$$SnPostImplFromJson(Map<String, dynamic> json) => _$SnPostImpl(
|
|||||||
?.map((e) => SnPostTag.fromJson(e as Map<String, dynamic>))
|
?.map((e) => SnPostTag.fromJson(e as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
const [],
|
const [],
|
||||||
categories: json['categories'] as List<dynamic>? ?? const [],
|
categories: (json['categories'] as List<dynamic>?)
|
||||||
|
?.map((e) => SnPostCategory.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
const [],
|
||||||
replies: (json['replies'] as List<dynamic>?)
|
replies: (json['replies'] as List<dynamic>?)
|
||||||
?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
|
?.map((e) => SnPost.fromJson(e as Map<String, dynamic>))
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -80,7 +83,7 @@ Map<String, dynamic> _$$SnPostImplToJson(_$SnPostImpl instance) =>
|
|||||||
'alias': instance.alias,
|
'alias': instance.alias,
|
||||||
'alias_prefix': instance.aliasPrefix,
|
'alias_prefix': instance.aliasPrefix,
|
||||||
'tags': instance.tags.map((e) => e.toJson()).toList(),
|
'tags': instance.tags.map((e) => e.toJson()).toList(),
|
||||||
'categories': instance.categories,
|
'categories': instance.categories.map((e) => e.toJson()).toList(),
|
||||||
'replies': instance.replies?.map((e) => e.toJson()).toList(),
|
'replies': instance.replies?.map((e) => e.toJson()).toList(),
|
||||||
'reply_id': instance.replyId,
|
'reply_id': instance.replyId,
|
||||||
'repost_id': instance.repostId,
|
'repost_id': instance.repostId,
|
||||||
@ -127,6 +130,31 @@ Map<String, dynamic> _$$SnPostTagImplToJson(_$SnPostTagImpl instance) =>
|
|||||||
'posts': instance.posts,
|
'posts': instance.posts,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_$SnPostCategoryImpl _$$SnPostCategoryImplFromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SnPostCategoryImpl(
|
||||||
|
id: (json['id'] as num).toInt(),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
deletedAt: json['deleted_at'],
|
||||||
|
alias: json['alias'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
description: json['description'] as String,
|
||||||
|
posts: json['posts'],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$$SnPostCategoryImplToJson(
|
||||||
|
_$SnPostCategoryImpl instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'created_at': instance.createdAt.toIso8601String(),
|
||||||
|
'updated_at': instance.updatedAt.toIso8601String(),
|
||||||
|
'deleted_at': instance.deletedAt,
|
||||||
|
'alias': instance.alias,
|
||||||
|
'name': instance.name,
|
||||||
|
'description': instance.description,
|
||||||
|
'posts': instance.posts,
|
||||||
|
};
|
||||||
|
|
||||||
_$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
|
_$SnPostPreloadImpl _$$SnPostPreloadImplFromJson(Map<String, dynamic> json) =>
|
||||||
_$SnPostPreloadImpl(
|
_$SnPostPreloadImpl(
|
||||||
thumbnail: json['thumbnail'] == null
|
thumbnail: json['thumbnail'] == null
|
||||||
|
@ -142,7 +142,7 @@ class ChatMessage extends StatelessWidget {
|
|||||||
onEdit: onEdit,
|
onEdit: onEdit,
|
||||||
onDelete: onDelete,
|
onDelete: onDelete,
|
||||||
),
|
),
|
||||||
)).padding(bottom: 4, top: isMerged ? 4 : 2),
|
)).padding(bottom: 4, top: 4),
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
'messages.new' => _ChatMessageText(data: data),
|
'messages.new' => _ChatMessageText(data: data),
|
||||||
_ => _ChatMessageSystemNotify(data: data),
|
_ => _ChatMessageSystemNotify(data: data),
|
||||||
|
@ -179,6 +179,7 @@ class PostItem extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (data.visibility > 0) _PostVisibilityHint(data: data),
|
if (data.visibility > 0) _PostVisibilityHint(data: data),
|
||||||
_PostTruncatedHint(data: data),
|
_PostTruncatedHint(data: data),
|
||||||
|
if (data.tags.isNotEmpty) _PostTagsList(data: data),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 12),
|
).padding(horizontal: 12),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@ -186,7 +187,6 @@ class PostItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
|
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
|
||||||
if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, bottom: 6),
|
|
||||||
_PostBottomAction(
|
_PostBottomAction(
|
||||||
data: data,
|
data: data,
|
||||||
showComments: showComments,
|
showComments: showComments,
|
||||||
@ -245,7 +245,7 @@ class PostItem extends StatelessWidget {
|
|||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 4,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, bottom: 6),
|
if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, top: 4, bottom: 6),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -966,23 +966,69 @@ class _PostTagsList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Wrap(
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: data.categories
|
||||||
|
.map(
|
||||||
|
(ele) => InkWell(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.category, size: 20),
|
||||||
|
const Gap(4),
|
||||||
|
Text(
|
||||||
|
'postCategory${ele.alias.capitalize()}'.trExists()
|
||||||
|
? 'postCategory${ele.alias.capitalize()}'.tr()
|
||||||
|
: ele.alias,
|
||||||
|
style: GoogleFonts.robotoMono(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(
|
||||||
|
'postSearch',
|
||||||
|
queryParameters: {
|
||||||
|
'categories': ele.alias,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
).opacity(0.8),
|
||||||
|
Wrap(
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
children: data.tags
|
children: data.tags
|
||||||
.map(
|
.map(
|
||||||
(ele) => InkWell(
|
(ele) => InkWell(
|
||||||
child: Text(
|
child: Row(
|
||||||
'#${ele.alias}',
|
mainAxisSize: MainAxisSize.min,
|
||||||
style: TextStyle(
|
children: [
|
||||||
decoration: TextDecoration.underline,
|
const Icon(Symbols.label, size: 20),
|
||||||
|
const Gap(4),
|
||||||
|
Text(ele.alias, style: GoogleFonts.robotoMono()),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
).fontSize(13),
|
onTap: () {
|
||||||
onTap: () {},
|
GoRouter.of(context).pushNamed(
|
||||||
|
'postSearch',
|
||||||
|
queryParameters: {
|
||||||
|
'tags': ele.alias,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
).opacity(0.8);
|
).opacity(0.8),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1023,6 +1069,7 @@ class _PostTruncatedHint extends StatelessWidget {
|
|||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
if (data.body['content_length'] != null)
|
if (data.body['content_length'] != null)
|
||||||
Row(
|
Row(
|
||||||
@ -1035,7 +1082,7 @@ class _PostTruncatedHint extends StatelessWidget {
|
|||||||
).inSeconds}s',
|
).inSeconds}s',
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
).padding(right: 8),
|
),
|
||||||
if (data.body['content_length'] != null)
|
if (data.body['content_length'] != null)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
@ -83,7 +83,9 @@ class PostMetaEditor extends StatelessWidget {
|
|||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: controller,
|
listenable: controller,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return Column(
|
return SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 8),
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller.titleController,
|
controller: controller.titleController,
|
||||||
@ -115,6 +117,14 @@ class PostMetaEditor extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
).padding(horizontal: 24),
|
).padding(horizontal: 24),
|
||||||
const Gap(4),
|
const Gap(4),
|
||||||
|
PostCategoriesField(
|
||||||
|
initialCategories: controller.categories,
|
||||||
|
labelText: 'fieldPostCategories'.tr(),
|
||||||
|
onUpdate: (value) {
|
||||||
|
controller.setCategories(value);
|
||||||
|
},
|
||||||
|
).padding(horizontal: 24),
|
||||||
|
const Gap(4),
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller.aliasController,
|
controller: controller.aliasController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@ -243,7 +253,8 @@ class PostMetaEditor extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(vertical: 8);
|
).padding(vertical: 8),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:surface/providers/sn_network.dart';
|
import 'package:surface/providers/sn_network.dart';
|
||||||
|
import 'package:surface/widgets/dialog.dart';
|
||||||
|
|
||||||
class PostTagsField extends StatefulWidget {
|
class PostTagsField extends StatefulWidget {
|
||||||
final List<String>? initialTags;
|
final List<String>? initialTags;
|
||||||
@ -21,9 +23,9 @@ class PostTagsField extends StatefulWidget {
|
|||||||
State<PostTagsField> createState() => _PostTagsFieldState();
|
State<PostTagsField> createState() => _PostTagsFieldState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PostTagsFieldState extends State<PostTagsField> {
|
const List<String> kTagsDividers = [' ', ','];
|
||||||
static const List<String> kTagsDividers = [' ', ','];
|
|
||||||
|
|
||||||
|
class _PostTagsFieldState extends State<PostTagsField> {
|
||||||
late final _Debounceable<List<String>?, String> _debouncedSearch;
|
late final _Debounceable<List<String>?, String> _debouncedSearch;
|
||||||
|
|
||||||
final List<String> _currentTags = List.empty(growable: true);
|
final List<String> _currentTags = List.empty(growable: true);
|
||||||
@ -100,8 +102,7 @@ class _PostTagsFieldState extends State<PostTagsField> {
|
|||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
margin: const EdgeInsets.only(right: 8),
|
margin: const EdgeInsets.only(right: 8),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0),
|
||||||
horizontal: 10.0, vertical: 4.0),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@ -155,6 +156,155 @@ class _PostTagsFieldState extends State<PostTagsField> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PostCategoriesField extends StatefulWidget {
|
||||||
|
final List<String>? initialCategories;
|
||||||
|
final String labelText;
|
||||||
|
final Function(List<String>) onUpdate;
|
||||||
|
|
||||||
|
const PostCategoriesField({
|
||||||
|
super.key,
|
||||||
|
this.initialCategories,
|
||||||
|
required this.labelText,
|
||||||
|
required this.onUpdate,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PostCategoriesField> createState() => _PostCategoriesFieldState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostCategoriesFieldState extends State<PostCategoriesField> {
|
||||||
|
late final _Debounceable<List<String>?, String> _debouncedSearch;
|
||||||
|
|
||||||
|
final List<String> _currentCategories = List.empty(growable: true);
|
||||||
|
|
||||||
|
String? _currentSearchProbe;
|
||||||
|
List<String> _lastAutocompleteResult = List.empty();
|
||||||
|
TextEditingController? _textEditingController;
|
||||||
|
|
||||||
|
Future<List<String>?> _searchCategories(String probe) async {
|
||||||
|
_currentSearchProbe = probe;
|
||||||
|
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
final resp = await sn.client.get(
|
||||||
|
'/cgi/co/categories?take=10&probe=$_currentSearchProbe',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_currentSearchProbe != probe) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
_currentSearchProbe = null;
|
||||||
|
|
||||||
|
return resp.data.map((x) => x['alias']).toList().cast<String>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_debouncedSearch = _debounce<List<String>?, String>(_searchCategories);
|
||||||
|
if (widget.initialCategories != null) {
|
||||||
|
_currentCategories.addAll(widget.initialCategories!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Autocomplete<String>(
|
||||||
|
optionsBuilder: (TextEditingValue textEditingValue) async {
|
||||||
|
final result = await _debouncedSearch(textEditingValue.text);
|
||||||
|
if (result == null) {
|
||||||
|
return _lastAutocompleteResult;
|
||||||
|
}
|
||||||
|
_lastAutocompleteResult = result;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
onSelected: (String value) {
|
||||||
|
if (value.isEmpty) return;
|
||||||
|
if (!_currentCategories.contains(value)) {
|
||||||
|
setState(() => _currentCategories.add(value));
|
||||||
|
}
|
||||||
|
_textEditingController?.clear();
|
||||||
|
widget.onUpdate(_currentCategories);
|
||||||
|
},
|
||||||
|
fieldViewBuilder: (context, controller, focusNode, onSubmitted) {
|
||||||
|
_textEditingController = controller;
|
||||||
|
return TextField(
|
||||||
|
controller: controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
label: Text(widget.labelText),
|
||||||
|
border: const UnderlineInputBorder(),
|
||||||
|
prefixIconConstraints: BoxConstraints(
|
||||||
|
maxWidth: MediaQuery.of(context).size.width * 0.75,
|
||||||
|
),
|
||||||
|
prefixIcon: _currentCategories.isNotEmpty
|
||||||
|
? SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: _currentCategories.map((String category) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(20.0),
|
||||||
|
),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.only(right: 8),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
child: Text(
|
||||||
|
'postCategory${category.capitalize()}'.trExists()
|
||||||
|
? 'postCategory${category.capitalize()}'.tr()
|
||||||
|
: '#$category',
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
InkWell(
|
||||||
|
child: const Icon(
|
||||||
|
Icons.cancel,
|
||||||
|
size: 14.0,
|
||||||
|
color: Color.fromARGB(255, 233, 233, 233),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setState(() => _currentCategories.remove(category));
|
||||||
|
widget.onUpdate(_currentCategories);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
onChanged: (value) {
|
||||||
|
for (final divider in kTagsDividers) {
|
||||||
|
if (value.endsWith(divider)) {
|
||||||
|
final tagValue = value.substring(0, value.length - 1);
|
||||||
|
if (tagValue.isEmpty) return;
|
||||||
|
if (!_currentCategories.contains(tagValue)) {
|
||||||
|
setState(() => _currentCategories.add(tagValue));
|
||||||
|
}
|
||||||
|
controller.clear();
|
||||||
|
widget.onUpdate(_currentCategories);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubmitted: (_) {
|
||||||
|
onSubmitted();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
typedef _Debounceable<S, T> = Future<S?> Function(T parameter);
|
typedef _Debounceable<S, T> = Future<S?> Function(T parameter);
|
||||||
|
|
||||||
_Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) {
|
_Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) {
|
||||||
|
18
pubspec.lock
18
pubspec.lock
@ -614,6 +614,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
fl_chart:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: fl_chart
|
||||||
|
sha256: c724234b05e378383e958f3e82ca84a3e1e3c06a0898462044dd8a24b1ee9864
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.70.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -643,6 +651,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.1"
|
version: "3.4.1"
|
||||||
|
flutter_colorpicker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_colorpicker
|
||||||
|
sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
flutter_context_menu:
|
flutter_context_menu:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -2137,4 +2153,4 @@ packages:
|
|||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.6.0 <4.0.0"
|
dart: ">=3.6.0 <4.0.0"
|
||||||
flutter: ">=3.24.0"
|
flutter: ">=3.27.0"
|
||||||
|
@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 2.1.1+37
|
version: 2.1.1+38
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.4
|
sdk: ^3.5.4
|
||||||
@ -111,6 +111,8 @@ dependencies:
|
|||||||
flutter_app_update: ^3.2.2
|
flutter_app_update: ^3.2.2
|
||||||
in_app_review: ^2.0.10
|
in_app_review: ^2.0.10
|
||||||
version: ^3.0.2
|
version: ^3.0.2
|
||||||
|
flutter_colorpicker: ^1.1.0
|
||||||
|
fl_chart: ^0.70.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user