Compare commits
131 Commits
3.2.0+134
...
b6896ded23
| Author | SHA1 | Date | |
|---|---|---|---|
|
b6896ded23
|
|||
|
f28a73ff9c
|
|||
|
a014b64235
|
|||
|
7e0e7c20d7
|
|||
|
389fa515ba
|
|||
|
681ead02eb
|
|||
|
8d1c145b0b
|
|||
|
51b4754182
|
|||
|
8a2b321701
|
|||
|
f685a7a249
|
|||
|
76009147e9
|
|||
|
ce12f28e56
|
|||
|
3604373a1e
|
|||
|
9704a4c2c7
|
|||
|
67def56ad1
|
|||
|
1be33916af
|
|||
|
e8ff1bfd22
|
|||
|
3ae56f3d89
|
|||
|
707143e998
|
|||
|
1fd34eb2a3
|
|||
|
d7ca41e946
|
|||
|
ad9fb0719a
|
|||
|
e2d315afd4
|
|||
|
6124dbfd79
|
|||
|
5327f04ec0
|
|||
|
41c56a2319
|
|||
|
f9d033542e
|
|||
|
91784e65e6
|
|||
|
9d39c6a825
|
|||
|
537e49f1a4
|
|||
|
75bbd4df71
|
|||
|
6ef4580d93
|
|||
|
6ffd498761
|
|||
|
27157e7cc1
|
|||
|
bbb07d574a
|
|||
|
c660a419e2
|
|||
|
c3f61467c8
|
|||
|
9bc47df452
|
|||
|
9ef8ca4d45
|
|||
|
b55cbd08d1
|
|||
|
8c6bd0feaa
|
|||
|
7dd4b20628
|
|||
|
fec0cb7640
|
|||
|
75deb04a2b
|
|||
|
7c7ed21a96
|
|||
|
a201f20793
|
|||
|
598c51bc1a
|
|||
|
e1ea61c5f1
|
|||
|
ac424bde36
|
|||
|
b43b70df3f
|
|||
|
4321aa621a
|
|||
|
d5d275fb43
|
|||
|
6bb3307144
|
|||
|
391604d4a2
|
|||
|
1d9361c12f
|
|||
|
a129b9cdd0
|
|||
|
3bf815ac61
|
|||
|
77bae4d6fd
|
|||
|
0a301c4c9b
|
|||
|
27b390a51c
|
|||
|
018386d14e
|
|||
|
3825d7c6c7
|
|||
|
bf930291e4
|
|||
|
a8c4988790
|
|||
|
28dd204b1a
|
|||
|
3cbc1a59a7
|
|||
|
277e9ae3d1
|
|||
|
27b3ca25b7
|
|||
|
f871cd3b62
|
|||
|
a8a59ee30c
|
|||
|
2cd1416a13
|
|||
|
6be7dfbc61
|
|||
|
1abbd85614
|
|||
|
31ac5ad07c
|
|||
|
ae2ba495e9
|
|||
|
637aa44548
|
|||
|
44dbfc36d9
|
|||
|
5dbe7371cb
|
|||
|
6c91093198
|
|||
|
3f640b7898
|
|||
|
7db164fda6
|
|||
|
6df1d96cc9
|
|||
|
122a796f8c
|
|||
|
fbc7812a16
|
|||
|
0b1a23e81a
|
|||
|
c87e6cfe07
|
|||
|
53d51b8a0e
|
|||
|
337ae39e08
|
|||
|
8fe3a664a6
|
|||
|
3bfc0b8181
|
|||
| ac2951479b | |||
| 2bfd13d843 | |||
| 28db6f9f01 | |||
|
a4f7b8415d
|
|||
|
2255d3d591
|
|||
|
97792ae734
|
|||
|
a5d13250cc
|
|||
|
de9e235d0c
|
|||
|
56fb5451cd
|
|||
|
870de961f5
|
|||
|
22bf6d1c33
|
|||
|
5b62f89531
|
|||
|
b1326d8f04
|
|||
|
fffca4a78c
|
|||
|
42bd7f97cb
|
|||
| 6377856ae0 | |||
| 0f1c52b9e3 | |||
| 6ed6f60fbc | |||
| e65a414065 | |||
| 214d5c4a53 | |||
| fe33931304 | |||
| 113309257e | |||
| b95a8b2ed2 | |||
|
|
e922971a5e | ||
| 9d5b71bead | |||
| 890efa2efb | |||
| 674097e425 | |||
|
3379dcb7f3
|
|||
| eb5a849e1f | |||
|
4981a23e8e
|
|||
|
c64d4bacb6
|
|||
|
838d18013b
|
|||
|
3f7902e463
|
|||
|
54560ad5d8
|
|||
|
0c729db639
|
|||
|
1fbaac8d88
|
|||
|
b9dc724f0b
|
|||
|
a2cc55696f
|
|||
|
e79f857feb
|
|||
|
affba29c04
|
|||
|
756746b144
|
@@ -14,13 +14,13 @@ The backend of the Solar Network is written in Go and is a microservices app. Th
|
||||
|
||||
## Commit Messages
|
||||
|
||||
We're using the gitmoji to clarify the reason and changes of the commit. To learn more about gitmoji, visit https://gitmoji.dev
|
||||
We're using the gitmoji to clarify the reason and changes of the commit. To learn more about gitmoji, visit <https://gitmoji.dev>
|
||||
|
||||
All the commit message should follow `:[gitmoji]: <commit message>` syntax
|
||||
|
||||
## Translations & Localization
|
||||
|
||||
We're not accepting translation and localization improvements, or fixes on the GitHub or Solsynth Git Repository. If you want to contribute to those, please head to our Crowdin project: https://crowdin.com/project/solian
|
||||
We're not accepting translation and localization improvements, or fixes on the GitHub or Solsynth Git Repository. If you want to contribute to those, please head to our Crowdin project: <https://crowdin.com/project/solian>
|
||||
|
||||
## New Features
|
||||
|
||||
@@ -28,9 +28,14 @@ To contribute new features, please create an issue or mention the feature you wa
|
||||
|
||||
## Bug Reports / Ask for help
|
||||
|
||||
Read the error message, check for the update (including pre-releases), and wiki before creating an issue. At the same time, be respectful and don't argue with our developers and contributors in the development chat or GitHub issue. Otherwise your issue may got deleted and your Solar Network Account may got a strike.
|
||||
Read the error message, check for the update (including pre-releases), and wiki before creating an issue. At the same time, be respectful and don't argue with our developers and contributors in the development chat or GitHub issue. Otherwise your issue may got deleted and your Solar Network Account may got a strike.
|
||||
|
||||
## Styles of Code
|
||||
|
||||
Before you create a Pull Request, make sure your code has pass the `flutter analyze` check, if there is any notes, fix as much as possible, if there is no way to fix, do ignore.
|
||||
|
||||
When the code contains comments, use English. We do not any other language of comments existing in the codebase. It might confuse future contributors, cause the code hard to understand and maintaiance.
|
||||
|
||||
-----------
|
||||
|
||||
We appreciate every single commit you contributed. Let's work together and create a better Solar Network!
|
||||
|
||||
|
||||
@@ -51,6 +51,12 @@
|
||||
<data android:scheme="http" android:host="solian.app" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="solian" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Share Intent Filters -->
|
||||
<intent-filter>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1079
assets/i18n/es-ES.json
Normal file
1079
assets/i18n/es-ES.json
Normal file
File diff suppressed because it is too large
Load Diff
1079
assets/i18n/ja-JP.json
Normal file
1079
assets/i18n/ja-JP.json
Normal file
File diff suppressed because it is too large
Load Diff
1079
assets/i18n/ko-KR.json
Normal file
1079
assets/i18n/ko-KR.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1079
assets/i18n/zh-OG.json
Normal file
1079
assets/i18n/zh-OG.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -116,16 +116,12 @@
|
||||
},
|
||||
"postHasAttachments": {
|
||||
"one": "{} 個附件",
|
||||
"other": "{}個附件"
|
||||
"other": "{} 個附件"
|
||||
},
|
||||
"edited": "已編輯",
|
||||
"addVideo": "添加視頻",
|
||||
"addPhoto": "添加照片",
|
||||
"addFile": "添加文件",
|
||||
"uploadFile": "上傳文件",
|
||||
"settingsDefaultPool": "選擇文件池",
|
||||
"settingsDefaultPoolHelper": "爲文件上傳選擇一個默認池",
|
||||
|
||||
"createDirectMessage": "創建新私人消息",
|
||||
"gotoDirectMessage": "前往私信",
|
||||
"react": "反應",
|
||||
@@ -307,8 +303,7 @@
|
||||
"notifications": "通知",
|
||||
"posts": "帖子",
|
||||
"settingsBackgroundImage": "背景圖片",
|
||||
"settingsBackgroundImageEnable": "顯示背景圖片",
|
||||
"settingsBackgroundImageClear": "清除背景圖片",
|
||||
"settingsBackgroundImageClear": "清除背景圖片",
|
||||
"settingsBackgroundGenerateColor": "從背景圖像生成主題色",
|
||||
"messageNone": "沒有內容可顯示",
|
||||
"unreadMessages": {
|
||||
@@ -319,8 +314,6 @@
|
||||
"settingsRealmCompactView": "緊湊領域視圖",
|
||||
"settingsMixedFeed": "混合動態",
|
||||
"settingsAutoTranslate": "自動翻譯",
|
||||
"settingsDataSavingMode": "低數據模式",
|
||||
"dataSavingHint": "低數據模式",
|
||||
"settingsHideBottomNav": "隱藏底部導航",
|
||||
"settingsSoundEffects": "音效",
|
||||
"settingsAprilFoolFeatures": "愚人節功能",
|
||||
@@ -675,7 +668,6 @@
|
||||
"publisherFeatureDevelopDescription": "為你的開發者解鎖包括應用套件,API 及更多開發功能。",
|
||||
"publisherFeatureDevelopHint": "目前該功能還在開發中,你需要邀請才可解鎖。",
|
||||
"learnMore": "瞭解更多",
|
||||
"discoverWebArticles": "來自站外的文章",
|
||||
"webArticlesStand": "文章亭",
|
||||
"about": "關於",
|
||||
"somethingWentWrong": "發生了一些錯誤",
|
||||
@@ -698,8 +690,6 @@
|
||||
"sharePostPhoto": "通過圖片分享帖子",
|
||||
"wouldYouLikeToNavigateToChat": "你想要前往聊天頁面嗎?",
|
||||
"abuseReports": "舉報",
|
||||
"discoverRealms": "發現領域",
|
||||
"discoverPublishers": "發現發佈者",
|
||||
"membershipCancel": "取消會員訂閱",
|
||||
"membershipCancelConfirm": "你確定要取消會員訂閱嗎?",
|
||||
"membershipCancelHint": "你確定要取消會員訂閱嗎?你將不會再次被扣費。你的會員資格將在當前計費週期結束前保持有效。並且你將無法重新訂閱,直到當前訂閱結束。",
|
||||
@@ -819,6 +809,159 @@
|
||||
"one": "+{} 個文件被摺疊",
|
||||
"other": "+{} 個文件被摺疊"
|
||||
},
|
||||
"pollQuestions": "問題",
|
||||
"pollAnswerSubmitted": "投票答案已提交。",
|
||||
"modifyAnswers": "修改答案",
|
||||
"back": "返回",
|
||||
"submit": "提交",
|
||||
"pollOptionDefaultLabel": "選項1",
|
||||
"pollUpdated": "投票已更新。",
|
||||
"pollCreated": "投票已創建。",
|
||||
"pollCreate": "創建投票",
|
||||
"pollEdit": "編輯投票",
|
||||
"pollPreviewJsonDebug": "調試預覽",
|
||||
"pollTitleRequired": "標題不可為空",
|
||||
"pollEndDateOptional": "結束日期和時間 (可選)",
|
||||
"notSet": "未設定",
|
||||
"pick": "選擇",
|
||||
"clear": "清除",
|
||||
"questions": "問題",
|
||||
"pollAddQuestion": "添加問題",
|
||||
"pollQuestionTypeSingleChoice": "單選框",
|
||||
"pollQuestionTypeMultipleChoice": "多選框",
|
||||
"pollQuestionTypeFreeText": "自由文本",
|
||||
"pollQuestionTypeYesNo": "是 / 不是",
|
||||
"pollQuestionTypeRating": "評分",
|
||||
"pollNoQuestionsYet": "尚未有問題",
|
||||
"pollNoQuestionsHint": "使用「添加問題」開始建立您的投票。",
|
||||
"pollDebugPreview": "調試預覽",
|
||||
"pollUntitledQuestion": "無標題問題",
|
||||
"moveUp": "往上移動",
|
||||
"moveDown": "往下移動",
|
||||
"required": "必需的",
|
||||
"pollQuestionTitle": "問題標題",
|
||||
"pollQuestionTitleRequired": "問題標題是必需的",
|
||||
"pollQuestionDescriptionOptional": "問題描述(選填)",
|
||||
"options": "選項",
|
||||
"pollAddOption": "添加選項",
|
||||
"pollOptionLabel": "選項標籤",
|
||||
"pollLongTextAnswerPreview": "長文本答案 (預覽)",
|
||||
"pollShortTextAnswerPreview": "短文本答案 (預覽)",
|
||||
"award": "讚賞",
|
||||
"awardPost": "讚賞帖子",
|
||||
"awardMessage": "消息",
|
||||
"awardMessageHint": "輸入您的讚賞消息...",
|
||||
"awardAttitude": "態度",
|
||||
"awardAttitudePositive": "積極",
|
||||
"awardAttitudeNegative": "消极",
|
||||
"awardAmount": "金額",
|
||||
"awardAmountHint": "輸入金額……",
|
||||
"awardAmountRequired": "「金額」為必填字段",
|
||||
"awardAmountInvalid": "請輸入有效金額",
|
||||
"awardMessageTooLong": "消息太長(最多4096個字符)",
|
||||
"awardSuccess": "獎勵已成功發送!",
|
||||
"awardSubmit": "讚賞",
|
||||
"awardPostPreview": "帖子預覽",
|
||||
"awardNoContent": "暫無內容",
|
||||
"awardByPublisher": "由 {} 發表",
|
||||
"awardBenefits": "讚賞福利",
|
||||
"awardBenefitsDescription": "為該帖子授予獎勵可以提升其價值和曝光度。價值更高的帖子更有可能在社區中被推薦和突出顯示。",
|
||||
"checkInResultLevel5": "生日快樂 🥳",
|
||||
"region": "區域",
|
||||
"accountRegionHint": "這個區域將用於內容傳遞和本地化。",
|
||||
"settingsCustomFontsHelper": "使用逗號分隔。",
|
||||
"settingsBackgroundImageEnable": "顯示背景圖片",
|
||||
"settingsDataSavingMode": "低數據模式",
|
||||
"dataSavingHint": "低數據模式",
|
||||
"postTypePost": "帖子",
|
||||
"searchDrafts": "搜尋草稿……",
|
||||
"noSearchResults": "無搜尋結果",
|
||||
"contactMethodMakePublic": "設為公開",
|
||||
"contactMethodMakePrivate": "設定為僅自己可見",
|
||||
"contactMethodPublic": "公開",
|
||||
"contactMethodPrivate": "私密",
|
||||
"discoverRealms": "發現領域",
|
||||
"discoverPublishers": "發現發佈者",
|
||||
"discoverShuffledPost": "隨機帖子",
|
||||
"projects": "項目",
|
||||
"noProjects": "未找到項目。",
|
||||
"deleteProject": "刪除項目",
|
||||
"deleteProjectHint": "確定要刪除此項目嗎?此操作無法撤銷。",
|
||||
"createProject": "新建專案",
|
||||
"editProject": "編輯項目",
|
||||
"projectDetails": "專案描述",
|
||||
"createBot": "創建機器人",
|
||||
"bots": "機器人",
|
||||
"noBots": "還沒有機器人。",
|
||||
"deleteBotHint": "您確定要刪除這個機器人嗎?此操作無法撤銷。",
|
||||
"deleteBot": "刪除機器人",
|
||||
"discoverWebArticles": "來自站外的文章",
|
||||
"messageJumpNotLoaded": "引用的訊息未加載,無法跳轉到該訊息。",
|
||||
"postUnlinkRealm": "未連結到領域",
|
||||
"postSlug": "別名",
|
||||
"postSlugHint": "這個別名可以用於在網頁通過 URL 瀏覽到你的帖子,它應該在同一發布者中是唯一。",
|
||||
"attachmentOnDevice": "離線",
|
||||
"attachmentOnCloud": "在線",
|
||||
"attachments": "附件",
|
||||
"publisherCollabInvitation": "協作邀請",
|
||||
"publisherCollabInvitationCount": {
|
||||
"zero": "無邀請",
|
||||
"one": "{} 個可用邀請",
|
||||
"other": "{} 個可用邀請"
|
||||
},
|
||||
"failedToLoadUserInfo": "無法加載用戶資訊",
|
||||
"failedToLoadUserInfoNetwork": "看起來是網絡問題,您可以點擊下面的按鈕再試一次。",
|
||||
"failedToLoadUserInfoUnauthorized": "看起來您的會話已經登出或不再可用,如果您想的話,您仍然可以嘗試再次獲取用戶資訊。",
|
||||
"okay": "好的",
|
||||
"postDetail": "帖子詳情",
|
||||
"postCount": {
|
||||
"zero": "沒有帖子",
|
||||
"one": "{} 帖子",
|
||||
"other": "{} 帖子"
|
||||
},
|
||||
"mimeType": "類型",
|
||||
"fileSize": "文件大小",
|
||||
"fileHash": "文件哈希",
|
||||
"exifData": "EXIF 數據",
|
||||
"postShuffle": "隨機帖子",
|
||||
"leveling": "等級",
|
||||
"levelingHistory": "經驗記錄",
|
||||
"stellarProgram": "恆星計畫",
|
||||
"socialCredits": "社會信用點",
|
||||
"credits": "信用",
|
||||
"creditsStatus": "積分狀態",
|
||||
"socialCreditsDescription": "社會信用是 Solar Network 評價用戶的一種方式。它基於用戶的行為和互動來計算。以 100 分為基準,分數越高表示用戶在社區中的信譽越好。分數會隨著時間的推移而變化,反映用戶的最新行為。信用等級高的用戶可以享受到更多的福利,反之的用戶部分功能可能受到限制。",
|
||||
"socialCreditsLevelPoor": "糟糕",
|
||||
"socialCreditsLevelNormal": "正常",
|
||||
"socialCreditsLevelGood": "良好",
|
||||
"socialCreditsLevelExcellent": "優秀",
|
||||
"orderByPopularity": "按熱度排序",
|
||||
"orderByReleaseDate": "按發佈日期排序",
|
||||
"editBot": "編輯機器人",
|
||||
"botAutomatedBy": "由 {} 自動化",
|
||||
"botDetails": "機器人描述",
|
||||
"overview": "概述",
|
||||
"keys": "密鑰",
|
||||
"botNotFound": "機器人未找到。",
|
||||
"newBotKey": "新建密鑰",
|
||||
"newBotKeyHint": "輸入新密鑰的名稱,密鑰只會顯示一次。",
|
||||
"revokeBotKey": "撤銷密鑰",
|
||||
"revokeBotKeyHint": "你確定要撤銷這個密鑰?這個操作無法撤回,所有使用該密鑰的應用程式會停止工作。",
|
||||
"noBotKeys": "機器人未找到。",
|
||||
"revoke": "撤銷",
|
||||
"keyName": "密鑰名稱",
|
||||
"newKeyGenerated": "新密鑰已生成",
|
||||
"copyKeyHint": "請安全地保存該密鑰,你不會再次看到它。",
|
||||
"rotateKey": "旋轉密鑰",
|
||||
"rotateBotKey": "旋轉密鑰",
|
||||
"rotateBotKeyHint": "你確認要旋轉這個密鑰?久的密鑰會立即失效,該操作無法撤銷。",
|
||||
"webFeedArticleCount": {
|
||||
"zero": "無文章",
|
||||
"one": "{} 文章",
|
||||
"other": "{} 文章"
|
||||
},
|
||||
"webFeedSubscribed": "你已經訂閱了這個來源",
|
||||
"webFeedUnsubscribed": "你已經取消訂閱這個來源",
|
||||
"appDetails": "應用程式詳情",
|
||||
"secrets": "密鑰",
|
||||
"appNotFound": "找不到應用程式。",
|
||||
@@ -830,5 +973,109 @@
|
||||
"newSecretGenerated": "已產生新密鑰",
|
||||
"copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。",
|
||||
"expiresIn": "過期時間(秒)",
|
||||
"isOidc": "OIDC 相容"
|
||||
"isOidc": "OIDC 相容",
|
||||
"pinPost": "置頂帖子",
|
||||
"unpinPost": "取消置頂",
|
||||
"pinnedPost": "已置顶",
|
||||
"publisherPage": "發布者頁面",
|
||||
"realmPage": "領域頁面",
|
||||
"replyPage": "回覆頁面",
|
||||
"pinPostPublisherHint": "將這篇文章置顶到您的發佈者頁面",
|
||||
"pinPostRealmHint": "將這篇文章置顶到領域頁面",
|
||||
"pinPostRealmDisabledHint": "這個帖子不屬於任何領域",
|
||||
"pinPostReplyHint": "將這篇文章置顶到回覆頁面",
|
||||
"pinPostReplyDisabledHint": "這篇帖子不是回覆",
|
||||
"pin": "置顶",
|
||||
"unpinPostHint": "你確定要取消置顶這篇帖子嗎?",
|
||||
"all": "所有",
|
||||
"statusPresent": "至今",
|
||||
"accountAutomated": "機器人",
|
||||
"chatBreakClearButton": "清除",
|
||||
"chatBreak5m": "5 分鐘",
|
||||
"chatBreak10m": "10 分鐘",
|
||||
"chatBreak15m": "15 分鐘",
|
||||
"chatBreak30m": "30 分鐘",
|
||||
"chatBreakCustomMinutes": "自訂(分鐘)",
|
||||
"errorGeneric": "錯誤:{}",
|
||||
"searchMessages": "搜尋消息",
|
||||
"messagesCount": "{} 消息",
|
||||
"dotSeparator": ".",
|
||||
"roleValidationHint": "成員角色必須設置在0到100之間",
|
||||
"searchMessagesHint": "搜尋消息…",
|
||||
"searchLinks": "連結",
|
||||
"searchAttachments": "附件",
|
||||
"noMessagesFound": "未找到消息",
|
||||
"openInBrowser": "在瀏覽器打開",
|
||||
"highlightPost": "精選帖子",
|
||||
"filters": "過濾器",
|
||||
"apply": "應用",
|
||||
"pubName": "題目名稱",
|
||||
"realm": "領域",
|
||||
"shuffle": "隨機",
|
||||
"pinned": "已置顶",
|
||||
"noResultsFound": "未找到結果",
|
||||
"toggleFilters": "切換篩檢器",
|
||||
"notableDayNext": "距離 {} 還有",
|
||||
"expandPoll": "展開投票",
|
||||
"collapsePoll": "摺叠投票",
|
||||
"embedView": "嵌入視圖",
|
||||
"embedUri": "嵌入URL",
|
||||
"aspectRatio": "縱橫比",
|
||||
"renderer": "渲染器",
|
||||
"addEmbed": "添加嵌入",
|
||||
"editEmbed": "編輯嵌入",
|
||||
"deleteEmbed": "刪除嵌入",
|
||||
"deleteEmbedConfirm": "您確定要刪除這個嵌入嗎?",
|
||||
"currentEmbed": "當前嵌入",
|
||||
"noEmbed": "尚未嵌入",
|
||||
"save": "保存",
|
||||
"webView": "網頁視圖",
|
||||
"settingsDefaultPool": "預設檔案池",
|
||||
"settingsDefaultPoolHelper": "選擇文件上傳的默認儲存池",
|
||||
"uploadFile": "上傳檔案",
|
||||
"authDeviceChallenges": "設備活動",
|
||||
"authDeviceHint": "向左滑動以編輯標籤,向右滑動以登出設備。",
|
||||
"settingsMessageDisplayStyle": "訊息顯示樣式",
|
||||
"auto": "自動",
|
||||
"manual": "手動",
|
||||
"iframeCode": "Iframe 代碼",
|
||||
"iframeCodeHint": "<iframe src=\"...\" width=\"...\" height=\"...\">",
|
||||
"parseIframe": "解析 Iframe",
|
||||
"messageActions": "消息選項",
|
||||
"viewEmbedLoadHint": "點擊以載入",
|
||||
"levelingStage1": "新手",
|
||||
"levelingStage2": "學徒",
|
||||
"levelingStage3": "學徒工",
|
||||
"levelingStage4": "熟練",
|
||||
"levelingStage5": "專家",
|
||||
"levelingStage6": "大師",
|
||||
"levelingStage7": "宗師",
|
||||
"levelingStage8": "傳說",
|
||||
"levelingStage9": "神話",
|
||||
"levelingStage10": "不朽",
|
||||
"levelingStage11": "神聖",
|
||||
"levelingStage12": "超凡",
|
||||
"uploadAttachment": "上傳附件",
|
||||
"attachmentPreview": "附件預覽",
|
||||
"selectPool": "選擇檔案池",
|
||||
"choosePool": "選擇一個檔案池",
|
||||
"errorLoadingPools": "加載池時出錯",
|
||||
"quotaCostInfo": "這次上傳將消耗 {} 配額點",
|
||||
"uploadConstraints": "上傳限制",
|
||||
"fileSizeExceeded": "檔案大小超過了 {} 的最大限制",
|
||||
"fileTypeNotAccepted": "該文件類型不被此池接受",
|
||||
"files": "附件",
|
||||
"confirmDeleteFile": "你確定要刪除這個文件嗎?",
|
||||
"deleteFile": "刪除文件",
|
||||
"failedToDeleteFile": "刪除文件失敗",
|
||||
"drive": "雲盤",
|
||||
"allPools": "全部的池",
|
||||
"includeRecycled": "包含已回收文件",
|
||||
"confirmDeleteRecycledFiles": "您確定要刪除所有回收的檔案嗎?",
|
||||
"deleteRecycledFiles": "刪除已回收檔案",
|
||||
"recycledFilesDeleted": "已回收檔案刪除成功",
|
||||
"failedToDeleteRecycledFiles": "已回收檔案刪除失敗",
|
||||
"upload": "上傳",
|
||||
"postCompose": "撰寫帖子",
|
||||
"postPublish": "發佈帖子"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
PODS:
|
||||
- Alamofire (5.10.2)
|
||||
- app_links (6.4.1):
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- croppy (0.0.1):
|
||||
@@ -215,7 +217,7 @@ PODS:
|
||||
- Flutter
|
||||
- irondash_engine_context (0.0.1):
|
||||
- Flutter
|
||||
- Kingfisher (8.5.0)
|
||||
- Kingfisher (8.6.0)
|
||||
- livekit_client (2.5.0):
|
||||
- Flutter
|
||||
- flutter_webrtc
|
||||
@@ -252,9 +254,9 @@ PODS:
|
||||
- record_ios (1.1.0):
|
||||
- Flutter
|
||||
- SAMKeychain (1.5.3)
|
||||
- SDWebImage (5.21.2):
|
||||
- SDWebImage/Core (= 5.21.2)
|
||||
- SDWebImage/Core (5.21.2)
|
||||
- SDWebImage (5.21.3):
|
||||
- SDWebImage/Core (= 5.21.3)
|
||||
- SDWebImage/Core (5.21.3)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@@ -293,6 +295,8 @@ PODS:
|
||||
- super_native_extensions (0.0.1):
|
||||
- Flutter
|
||||
- SwiftyGif (5.4.5)
|
||||
- syncfusion_flutter_pdfviewer (0.0.1):
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
@@ -303,6 +307,7 @@ PODS:
|
||||
|
||||
DEPENDENCIES:
|
||||
- Alamofire
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- croppy (from `.symlinks/plugins/croppy/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
@@ -344,6 +349,7 @@ DEPENDENCIES:
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
@@ -379,6 +385,8 @@ SPEC REPOS:
|
||||
- WebRTC-SDK
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_links:
|
||||
:path: ".symlinks/plugins/app_links/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
croppy:
|
||||
@@ -459,6 +467,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||
super_native_extensions:
|
||||
:path: ".symlinks/plugins/super_native_extensions/ios"
|
||||
syncfusion_flutter_pdfviewer:
|
||||
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
volume_controller:
|
||||
@@ -468,6 +478,7 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||
app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
@@ -507,7 +518,7 @@ SPEC CHECKSUMS:
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
|
||||
Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c
|
||||
Kingfisher: 64278f126a815d0e2d391cdf71311b85882c4de0
|
||||
livekit_client: a6f5fa86ac28ccd7ded53626a5379961db311ab4
|
||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
|
||||
@@ -524,7 +535,7 @@ SPEC CHECKSUMS:
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
|
||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
||||
@@ -533,6 +544,7 @@ SPEC CHECKSUMS:
|
||||
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
|
||||
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
|
||||
@@ -12,7 +12,7 @@ import UIKit
|
||||
UNUserNotificationCenter.current().delegate = notifyDelegate
|
||||
|
||||
let replyableMessageCategory = UNNotificationCategory(
|
||||
identifier: "REPLYABLE_MESSAGE",
|
||||
identifier: "CHAT_MESSAGE",
|
||||
actions: [
|
||||
UNTextInputNotificationAction(
|
||||
identifier: "reply_action",
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>solian</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
|
||||
@@ -47,7 +47,6 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
private func processNotification(request: UNNotificationRequest, content: UNMutableNotificationContent) throws {
|
||||
switch content.userInfo["type"] as? String {
|
||||
case "messages.new":
|
||||
content.categoryIdentifier = "REPLYABLE_MESSAGE"
|
||||
try handleMessagingNotification(request: request, content: content)
|
||||
default:
|
||||
try handleDefaultNotification(content: content)
|
||||
@@ -89,7 +88,12 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body)
|
||||
self.donateInteraction(for: intent)
|
||||
let updatedContent = try? request.content.updating(from: intent)
|
||||
self.contentHandler?(updatedContent ?? content)
|
||||
content.categoryIdentifier = "CHAT_MESSAGE"
|
||||
if let updatedContent = updatedContent {
|
||||
self.contentHandler?(updatedContent)
|
||||
} else {
|
||||
self.contentHandler?(content)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/wasm.dart';
|
||||
import 'package:island/database/drift_db.dart';
|
||||
import 'package:island/talker.dart';
|
||||
|
||||
AppDatabase constructDb() {
|
||||
return AppDatabase(connectOnWeb());
|
||||
@@ -9,12 +10,17 @@ AppDatabase constructDb() {
|
||||
DatabaseConnection connectOnWeb() {
|
||||
return DatabaseConnection.delayed(
|
||||
Future(() async {
|
||||
final result = await WasmDatabase.open(
|
||||
databaseName: 'solar_network_data',
|
||||
sqlite3Uri: Uri.parse('sqlite3.wasm'),
|
||||
driftWorkerUri: Uri.parse('drift_worker.dart.js'),
|
||||
);
|
||||
return result.resolvedExecutor;
|
||||
try {
|
||||
final result = await WasmDatabase.open(
|
||||
databaseName: 'solar_network_data',
|
||||
sqlite3Uri: Uri.parse('sqlite3.wasm'),
|
||||
driftWorkerUri: Uri.parse('drift_worker.dart.js'),
|
||||
);
|
||||
return result.resolvedExecutor;
|
||||
} catch (e, stackTrace) {
|
||||
talker.error('Failed to open WASM database...', e, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
132
lib/main.dart
132
lib/main.dart
@@ -1,6 +1,4 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:croppy/croppy.dart';
|
||||
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
@@ -8,16 +6,15 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker_android/image_picker_android.dart';
|
||||
import 'package:island/talker.dart';
|
||||
import 'package:island/firebase_options.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/theme.dart';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/route.dart';
|
||||
@@ -29,18 +26,21 @@ import 'package:relative_time/relative_time.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:talker_flutter/talker_flutter.dart';
|
||||
import 'package:talker_riverpod_logger/talker_riverpod_logger.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||
log('Handling a background message: ${message.messageId}');
|
||||
talker.info('Handling a background message: ${message.messageId}');
|
||||
}
|
||||
|
||||
void main() async {
|
||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||
log(
|
||||
talker.info(
|
||||
"[SplashScreen] Keeping the flash screen to loading other resources...",
|
||||
);
|
||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||
@@ -73,48 +73,59 @@ void main() async {
|
||||
}
|
||||
}
|
||||
|
||||
log("[SplashScreen] Firebase is ready!");
|
||||
talker.info("[SplashScreen] Firebase is ready!");
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
|
||||
try {
|
||||
log("[SplashScreen] Loading timezone database...");
|
||||
talker.info("[SplashScreen] Loading timezone database...");
|
||||
await initializeTzdb();
|
||||
log("[SplashScreen] Time zone database was loaded!");
|
||||
talker.info("[SplashScreen] Time zone database was loaded!");
|
||||
} catch (err) {
|
||||
log("[SplashScreen] Failed to load timezone database... $err");
|
||||
talker.error("[SplashScreen] Failed to load timezone database... $err");
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
|
||||
doWhenWindowReady(() {
|
||||
const defaultSize = Size(360, 640);
|
||||
await windowManager.ensureInitialized();
|
||||
|
||||
// Get saved window size from preferences
|
||||
final savedSizeString = prefs.getString(kAppWindowSize);
|
||||
Size initialSize = defaultSize;
|
||||
const defaultSize = Size(360, 640);
|
||||
|
||||
if (savedSizeString != null) {
|
||||
try {
|
||||
final parts = savedSizeString.split(',');
|
||||
if (parts.length == 2) {
|
||||
final width = double.parse(parts[0]);
|
||||
final height = double.parse(parts[1]);
|
||||
initialSize = Size(width, height);
|
||||
}
|
||||
} catch (e) {
|
||||
log("[SplashScreen] Failed to parse saved window size: $e");
|
||||
initialSize = defaultSize;
|
||||
// Get saved window size from preferences
|
||||
final savedSizeString = prefs.getString(kAppWindowSize);
|
||||
Size initialSize = defaultSize;
|
||||
|
||||
if (savedSizeString != null) {
|
||||
try {
|
||||
final parts = savedSizeString.split(',');
|
||||
if (parts.length == 2) {
|
||||
final width = double.parse(parts[0]);
|
||||
final height = double.parse(parts[1]);
|
||||
initialSize = Size(width, height);
|
||||
}
|
||||
} catch (e) {
|
||||
talker.error("[SplashScreen] Failed to parse saved window size: $e");
|
||||
initialSize = defaultSize;
|
||||
}
|
||||
}
|
||||
|
||||
appWindow.minSize = defaultSize;
|
||||
appWindow.size = initialSize;
|
||||
appWindow.alignment = Alignment.center;
|
||||
appWindow.show();
|
||||
log(
|
||||
WindowOptions windowOptions = WindowOptions(
|
||||
size: initialSize,
|
||||
center: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
skipTaskbar: false,
|
||||
titleBarStyle: TitleBarStyle.hidden,
|
||||
windowButtonVisibility: true,
|
||||
);
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.setMinimumSize(defaultSize);
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
final opacity = prefs.getDouble(kAppWindowOpacity) ?? 1.0;
|
||||
await windowManager.setOpacity(opacity);
|
||||
talker.info(
|
||||
"[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}",
|
||||
);
|
||||
});
|
||||
@@ -126,16 +137,27 @@ void main() async {
|
||||
if (imagePickerImplementation is ImagePickerAndroid) {
|
||||
imagePickerImplementation.useAndroidPhotoPicker = true;
|
||||
}
|
||||
log("[SplashScreen] Android image picker is ready!");
|
||||
talker.info("[SplashScreen] Android image picker is ready!");
|
||||
}
|
||||
|
||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||
FlutterNativeSplash.remove();
|
||||
log("[SplashScreen] Now hiding the splash screen...");
|
||||
talker.info("[SplashScreen] Now hiding the splash screen...");
|
||||
}
|
||||
|
||||
runApp(
|
||||
ProviderScope(
|
||||
observers: [
|
||||
TalkerRiverpodObserver(
|
||||
talker: talker,
|
||||
settings: TalkerRiverpodLoggerSettings(
|
||||
printProviderAdded: false,
|
||||
printProviderDisposed: false,
|
||||
printProviderUpdated: false,
|
||||
printStateFullData: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
@@ -144,6 +166,10 @@ void main() async {
|
||||
Locale('en', 'US'),
|
||||
Locale('zh', 'CN'),
|
||||
Locale('zh', 'TW'),
|
||||
Locale('zh', 'OG'),
|
||||
Locale('ja', 'JP'),
|
||||
Locale('ko', 'KR'),
|
||||
Locale('es', 'ES'),
|
||||
],
|
||||
path: 'assets/i18n',
|
||||
fallbackLocale: Locale('en', 'US'),
|
||||
@@ -165,6 +191,21 @@ class IslandApp extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = ref.watch(themeProvider);
|
||||
final settings = ref.watch(appSettingsNotifierProvider);
|
||||
|
||||
// Convert string theme mode to ThemeMode enum
|
||||
ThemeMode getThemeMode() {
|
||||
final themeMode = settings.themeMode ?? 'system';
|
||||
switch (themeMode) {
|
||||
case 'light':
|
||||
return ThemeMode.light;
|
||||
case 'dark':
|
||||
return ThemeMode.dark;
|
||||
case 'system':
|
||||
default:
|
||||
return ThemeMode.system;
|
||||
}
|
||||
}
|
||||
|
||||
void handleMessage(RemoteMessage notification) {
|
||||
if (notification.data['meta']?['action_uri'] != null) {
|
||||
@@ -201,7 +242,9 @@ class IslandApp extends HookConsumerWidget {
|
||||
final onMessageSubscription = FirebaseMessaging.onMessage.listen((
|
||||
message,
|
||||
) {
|
||||
log('Foreground message received: ${message.messageId}');
|
||||
talker.info(
|
||||
'[Notification] foreground message received: ${message.messageId}',
|
||||
);
|
||||
handleMessage(message);
|
||||
});
|
||||
|
||||
@@ -215,7 +258,7 @@ class IslandApp extends HookConsumerWidget {
|
||||
// Load userinfo
|
||||
final userNotifier = ref.read(userInfoProvider.notifier);
|
||||
ref.listen(websocketStateProvider, (_, state) {
|
||||
log('[WebSocket] $state');
|
||||
talker.info('[WebSocket] $state');
|
||||
});
|
||||
Future(() {
|
||||
userNotifier.fetchUser().then((_) {
|
||||
@@ -235,9 +278,10 @@ class IslandApp extends HookConsumerWidget {
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
theme: theme?.light,
|
||||
darkTheme: theme?.dark,
|
||||
themeMode: ThemeMode.system,
|
||||
color: Colors.transparent,
|
||||
theme: theme.light,
|
||||
darkTheme: theme.dark,
|
||||
themeMode: getThemeMode(),
|
||||
routerConfig: router,
|
||||
supportedLocales: context.supportedLocales,
|
||||
scrollBehavior: AppScrollBehavior(),
|
||||
@@ -252,9 +296,15 @@ class IslandApp extends HookConsumerWidget {
|
||||
key: globalOverlay,
|
||||
initialEntries: [
|
||||
OverlayEntry(
|
||||
builder:
|
||||
(_) =>
|
||||
WindowScaffold(child: child ?? const SizedBox.shrink()),
|
||||
builder: (_) {
|
||||
return TalkerWrapper(
|
||||
talker: talker,
|
||||
options: const TalkerWrapperOptions(enableErrorAlerts: true),
|
||||
child: WindowScaffold(
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -55,6 +55,19 @@ class ProfileLinkConverter
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class UsernameColor with _$UsernameColor {
|
||||
const factory UsernameColor({
|
||||
@Default('plain') String type,
|
||||
String? value,
|
||||
String? direction,
|
||||
List<String>? colors,
|
||||
}) = _UsernameColor;
|
||||
|
||||
factory UsernameColor.fromJson(Map<String, dynamic> json) =>
|
||||
_$UsernameColorFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class SnAccountProfile with _$SnAccountProfile {
|
||||
const factory SnAccountProfile({
|
||||
@@ -79,6 +92,7 @@ sealed class SnAccountProfile with _$SnAccountProfile {
|
||||
required SnCloudFile? picture,
|
||||
required SnCloudFile? background,
|
||||
required SnVerificationMark? verification,
|
||||
UsernameColor? usernameColor,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
|
||||
@@ -622,10 +622,284 @@ as String,
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$UsernameColor {
|
||||
|
||||
String get type; String? get value; String? get direction; List<String>? get colors;
|
||||
/// Create a copy of UsernameColor
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$UsernameColorCopyWith<UsernameColor> get copyWith => _$UsernameColorCopyWithImpl<UsernameColor>(this as UsernameColor, _$identity);
|
||||
|
||||
/// Serializes this UsernameColor to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is UsernameColor&&(identical(other.type, type) || other.type == type)&&(identical(other.value, value) || other.value == value)&&(identical(other.direction, direction) || other.direction == direction)&&const DeepCollectionEquality().equals(other.colors, colors));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,value,direction,const DeepCollectionEquality().hash(colors));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UsernameColor(type: $type, value: $value, direction: $direction, colors: $colors)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $UsernameColorCopyWith<$Res> {
|
||||
factory $UsernameColorCopyWith(UsernameColor value, $Res Function(UsernameColor) _then) = _$UsernameColorCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String type, String? value, String? direction, List<String>? colors
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$UsernameColorCopyWithImpl<$Res>
|
||||
implements $UsernameColorCopyWith<$Res> {
|
||||
_$UsernameColorCopyWithImpl(this._self, this._then);
|
||||
|
||||
final UsernameColor _self;
|
||||
final $Res Function(UsernameColor) _then;
|
||||
|
||||
/// Create a copy of UsernameColor
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? value = freezed,Object? direction = freezed,Object? colors = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable
|
||||
as String?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable
|
||||
as String?,colors: freezed == colors ? _self.colors : colors // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [UsernameColor].
|
||||
extension UsernameColorPatterns on UsernameColor {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _UsernameColor value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _UsernameColor() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _UsernameColor value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _UsernameColor():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _UsernameColor value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _UsernameColor() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type, String? value, String? direction, List<String>? colors)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _UsernameColor() when $default != null:
|
||||
return $default(_that.type,_that.value,_that.direction,_that.colors);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type, String? value, String? direction, List<String>? colors) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _UsernameColor():
|
||||
return $default(_that.type,_that.value,_that.direction,_that.colors);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type, String? value, String? direction, List<String>? colors)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _UsernameColor() when $default != null:
|
||||
return $default(_that.type,_that.value,_that.direction,_that.colors);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _UsernameColor implements UsernameColor {
|
||||
const _UsernameColor({this.type = 'plain', this.value, this.direction, final List<String>? colors}): _colors = colors;
|
||||
factory _UsernameColor.fromJson(Map<String, dynamic> json) => _$UsernameColorFromJson(json);
|
||||
|
||||
@override@JsonKey() final String type;
|
||||
@override final String? value;
|
||||
@override final String? direction;
|
||||
final List<String>? _colors;
|
||||
@override List<String>? get colors {
|
||||
final value = _colors;
|
||||
if (value == null) return null;
|
||||
if (_colors is EqualUnmodifiableListView) return _colors;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of UsernameColor
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$UsernameColorCopyWith<_UsernameColor> get copyWith => __$UsernameColorCopyWithImpl<_UsernameColor>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$UsernameColorToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UsernameColor&&(identical(other.type, type) || other.type == type)&&(identical(other.value, value) || other.value == value)&&(identical(other.direction, direction) || other.direction == direction)&&const DeepCollectionEquality().equals(other._colors, _colors));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,value,direction,const DeepCollectionEquality().hash(_colors));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UsernameColor(type: $type, value: $value, direction: $direction, colors: $colors)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$UsernameColorCopyWith<$Res> implements $UsernameColorCopyWith<$Res> {
|
||||
factory _$UsernameColorCopyWith(_UsernameColor value, $Res Function(_UsernameColor) _then) = __$UsernameColorCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String type, String? value, String? direction, List<String>? colors
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$UsernameColorCopyWithImpl<$Res>
|
||||
implements _$UsernameColorCopyWith<$Res> {
|
||||
__$UsernameColorCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _UsernameColor _self;
|
||||
final $Res Function(_UsernameColor) _then;
|
||||
|
||||
/// Create a copy of UsernameColor
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? value = freezed,Object? direction = freezed,Object? colors = freezed,}) {
|
||||
return _then(_UsernameColor(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,value: freezed == value ? _self.value : value // ignore: cast_nullable_to_non_nullable
|
||||
as String?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable
|
||||
as String?,colors: freezed == colors ? _self._colors : colors // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnAccountProfile {
|
||||
|
||||
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get socialCredits; int get socialCreditsLevel; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get socialCredits; int get socialCreditsLevel; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; UsernameColor? get usernameColor; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnAccountProfile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -638,16 +912,16 @@ $SnAccountProfileCopyWith<SnAccountProfile> get copyWith => _$SnAccountProfileCo
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other.links, links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.usernameColor, usernameColor) || other.usernameColor == usernameColor)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
|
||||
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,usernameColor,createdAt,updatedAt,deletedAt]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, usernameColor: $usernameColor, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -658,11 +932,11 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> {
|
||||
factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
$SnAccountBadgeCopyWith<$Res>? get activeBadge;$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification;
|
||||
$SnAccountBadgeCopyWith<$Res>? get activeBadge;$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification;$UsernameColorCopyWith<$Res>? get usernameColor;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -675,7 +949,7 @@ class _$SnAccountProfileCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAccountProfile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? usernameColor = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
|
||||
@@ -698,7 +972,8 @@ as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : lev
|
||||
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
|
||||
as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as SnVerificationMark?,usernameColor: freezed == usernameColor ? _self.usernameColor : usernameColor // ignore: cast_nullable_to_non_nullable
|
||||
as UsernameColor?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
@@ -752,6 +1027,18 @@ $SnVerificationMarkCopyWith<$Res>? get verification {
|
||||
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
|
||||
return _then(_self.copyWith(verification: value));
|
||||
});
|
||||
}/// Create a copy of SnAccountProfile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$UsernameColorCopyWith<$Res>? get usernameColor {
|
||||
if (_self.usernameColor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $UsernameColorCopyWith<$Res>(_self.usernameColor!, (value) {
|
||||
return _then(_self.copyWith(usernameColor: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -831,10 +1118,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountProfile() when $default != null:
|
||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -852,10 +1139,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountProfile():
|
||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -869,10 +1156,10 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, @ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnAccountProfile() when $default != null:
|
||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.socialCredits,_that.socialCreditsLevel,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.usernameColor,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -884,7 +1171,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnAccountProfile implements SnAccountProfile {
|
||||
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, this.socialCredits = 100, this.socialCreditsLevel = 0, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
|
||||
const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, this.socialCredits = 100, this.socialCreditsLevel = 0, required this.levelingProgress, required this.picture, required this.background, required this.verification, this.usernameColor, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links;
|
||||
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@@ -914,6 +1201,7 @@ class _SnAccountProfile implements SnAccountProfile {
|
||||
@override final SnCloudFile? picture;
|
||||
@override final SnCloudFile? background;
|
||||
@override final SnVerificationMark? verification;
|
||||
@override final UsernameColor? usernameColor;
|
||||
@override final DateTime createdAt;
|
||||
@override final DateTime updatedAt;
|
||||
@override final DateTime? deletedAt;
|
||||
@@ -931,16 +1219,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountProfile&&(identical(other.id, id) || other.id == id)&&(identical(other.firstName, firstName) || other.firstName == firstName)&&(identical(other.middleName, middleName) || other.middleName == middleName)&&(identical(other.lastName, lastName) || other.lastName == lastName)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.pronouns, pronouns) || other.pronouns == pronouns)&&(identical(other.location, location) || other.location == location)&&(identical(other.timeZone, timeZone) || other.timeZone == timeZone)&&(identical(other.birthday, birthday) || other.birthday == birthday)&&const DeepCollectionEquality().equals(other._links, _links)&&(identical(other.lastSeenAt, lastSeenAt) || other.lastSeenAt == lastSeenAt)&&(identical(other.activeBadge, activeBadge) || other.activeBadge == activeBadge)&&(identical(other.experience, experience) || other.experience == experience)&&(identical(other.level, level) || other.level == level)&&(identical(other.socialCredits, socialCredits) || other.socialCredits == socialCredits)&&(identical(other.socialCreditsLevel, socialCreditsLevel) || other.socialCreditsLevel == socialCreditsLevel)&&(identical(other.levelingProgress, levelingProgress) || other.levelingProgress == levelingProgress)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.usernameColor, usernameColor) || other.usernameColor == usernameColor)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,createdAt,updatedAt,deletedAt]);
|
||||
int get hashCode => Object.hashAll([runtimeType,id,firstName,middleName,lastName,bio,gender,pronouns,location,timeZone,birthday,const DeepCollectionEquality().hash(_links),lastSeenAt,activeBadge,experience,level,socialCredits,socialCreditsLevel,levelingProgress,picture,background,verification,usernameColor,createdAt,updatedAt,deletedAt]);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
return 'SnAccountProfile(id: $id, firstName: $firstName, middleName: $middleName, lastName: $lastName, bio: $bio, gender: $gender, pronouns: $pronouns, location: $location, timeZone: $timeZone, birthday: $birthday, links: $links, lastSeenAt: $lastSeenAt, activeBadge: $activeBadge, experience: $experience, level: $level, socialCredits: $socialCredits, socialCreditsLevel: $socialCreditsLevel, levelingProgress: $levelingProgress, picture: $picture, background: $background, verification: $verification, usernameColor: $usernameColor, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
@@ -951,11 +1239,11 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi
|
||||
factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double socialCredits, int socialCreditsLevel, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, UsernameColor? usernameColor, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@override $SnAccountBadgeCopyWith<$Res>? get activeBadge;@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification;
|
||||
@override $SnAccountBadgeCopyWith<$Res>? get activeBadge;@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification;@override $UsernameColorCopyWith<$Res>? get usernameColor;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -968,7 +1256,7 @@ class __$SnAccountProfileCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnAccountProfile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? firstName = null,Object? middleName = null,Object? lastName = null,Object? bio = null,Object? gender = null,Object? pronouns = null,Object? location = null,Object? timeZone = null,Object? birthday = freezed,Object? links = null,Object? lastSeenAt = freezed,Object? activeBadge = freezed,Object? experience = null,Object? level = null,Object? socialCredits = null,Object? socialCreditsLevel = null,Object? levelingProgress = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? usernameColor = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnAccountProfile(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,firstName: null == firstName ? _self.firstName : firstName // ignore: cast_nullable_to_non_nullable
|
||||
@@ -991,7 +1279,8 @@ as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : lev
|
||||
as double,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
|
||||
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
|
||||
as SnVerificationMark?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as SnVerificationMark?,usernameColor: freezed == usernameColor ? _self.usernameColor : usernameColor // ignore: cast_nullable_to_non_nullable
|
||||
as UsernameColor?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
@@ -1046,6 +1335,18 @@ $SnVerificationMarkCopyWith<$Res>? get verification {
|
||||
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
|
||||
return _then(_self.copyWith(verification: value));
|
||||
});
|
||||
}/// Create a copy of SnAccountProfile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$UsernameColorCopyWith<$Res>? get usernameColor {
|
||||
if (_self.usernameColor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $UsernameColorCopyWith<$Res>(_self.usernameColor!, (value) {
|
||||
return _then(_self.copyWith(usernameColor: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,23 @@ _ProfileLink _$ProfileLinkFromJson(Map<String, dynamic> json) =>
|
||||
Map<String, dynamic> _$ProfileLinkToJson(_ProfileLink instance) =>
|
||||
<String, dynamic>{'name': instance.name, 'url': instance.url};
|
||||
|
||||
_UsernameColor _$UsernameColorFromJson(Map<String, dynamic> json) =>
|
||||
_UsernameColor(
|
||||
type: json['type'] as String? ?? 'plain',
|
||||
value: json['value'] as String?,
|
||||
direction: json['direction'] as String?,
|
||||
colors:
|
||||
(json['colors'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UsernameColorToJson(_UsernameColor instance) =>
|
||||
<String, dynamic>{
|
||||
'type': instance.type,
|
||||
'value': instance.value,
|
||||
'direction': instance.direction,
|
||||
'colors': instance.colors,
|
||||
};
|
||||
|
||||
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
|
||||
_SnAccountProfile(
|
||||
id: json['id'] as String,
|
||||
@@ -113,6 +130,12 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
|
||||
: SnVerificationMark.fromJson(
|
||||
json['verification'] as Map<String, dynamic>,
|
||||
),
|
||||
usernameColor:
|
||||
json['username_color'] == null
|
||||
? null
|
||||
: UsernameColor.fromJson(
|
||||
json['username_color'] as Map<String, dynamic>,
|
||||
),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt:
|
||||
@@ -144,6 +167,7 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
|
||||
'picture': instance.picture?.toJson(),
|
||||
'background': instance.background?.toJson(),
|
||||
'verification': instance.verification?.toJson(),
|
||||
'username_color': instance.usernameColor?.toJson(),
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
|
||||
16
lib/models/autocomplete_response.dart
Normal file
16
lib/models/autocomplete_response.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'autocomplete_response.freezed.dart';
|
||||
part 'autocomplete_response.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class AutocompleteSuggestion with _$AutocompleteSuggestion {
|
||||
const factory AutocompleteSuggestion({
|
||||
required String type,
|
||||
required String keyword,
|
||||
required dynamic data,
|
||||
}) = _AutocompleteSuggestion;
|
||||
|
||||
factory AutocompleteSuggestion.fromJson(Map<String, dynamic> json) =>
|
||||
_$AutocompleteSuggestionFromJson(json);
|
||||
}
|
||||
277
lib/models/autocomplete_response.freezed.dart
Normal file
277
lib/models/autocomplete_response.freezed.dart
Normal file
@@ -0,0 +1,277 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'autocomplete_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$AutocompleteSuggestion {
|
||||
|
||||
String get type; String get keyword; dynamic get data;
|
||||
/// Create a copy of AutocompleteSuggestion
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$AutocompleteSuggestionCopyWith<AutocompleteSuggestion> get copyWith => _$AutocompleteSuggestionCopyWithImpl<AutocompleteSuggestion>(this as AutocompleteSuggestion, _$identity);
|
||||
|
||||
/// Serializes this AutocompleteSuggestion to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AutocompleteSuggestion&&(identical(other.type, type) || other.type == type)&&(identical(other.keyword, keyword) || other.keyword == keyword)&&const DeepCollectionEquality().equals(other.data, data));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,keyword,const DeepCollectionEquality().hash(data));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AutocompleteSuggestion(type: $type, keyword: $keyword, data: $data)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $AutocompleteSuggestionCopyWith<$Res> {
|
||||
factory $AutocompleteSuggestionCopyWith(AutocompleteSuggestion value, $Res Function(AutocompleteSuggestion) _then) = _$AutocompleteSuggestionCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String type, String keyword, dynamic data
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$AutocompleteSuggestionCopyWithImpl<$Res>
|
||||
implements $AutocompleteSuggestionCopyWith<$Res> {
|
||||
_$AutocompleteSuggestionCopyWithImpl(this._self, this._then);
|
||||
|
||||
final AutocompleteSuggestion _self;
|
||||
final $Res Function(AutocompleteSuggestion) _then;
|
||||
|
||||
/// Create a copy of AutocompleteSuggestion
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? keyword = null,Object? data = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,keyword: null == keyword ? _self.keyword : keyword // ignore: cast_nullable_to_non_nullable
|
||||
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [AutocompleteSuggestion].
|
||||
extension AutocompleteSuggestionPatterns on AutocompleteSuggestion {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _AutocompleteSuggestion value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AutocompleteSuggestion() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _AutocompleteSuggestion value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AutocompleteSuggestion():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _AutocompleteSuggestion value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AutocompleteSuggestion() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type, String keyword, dynamic data)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AutocompleteSuggestion() when $default != null:
|
||||
return $default(_that.type,_that.keyword,_that.data);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type, String keyword, dynamic data) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AutocompleteSuggestion():
|
||||
return $default(_that.type,_that.keyword,_that.data);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type, String keyword, dynamic data)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AutocompleteSuggestion() when $default != null:
|
||||
return $default(_that.type,_that.keyword,_that.data);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _AutocompleteSuggestion implements AutocompleteSuggestion {
|
||||
const _AutocompleteSuggestion({required this.type, required this.keyword, required this.data});
|
||||
factory _AutocompleteSuggestion.fromJson(Map<String, dynamic> json) => _$AutocompleteSuggestionFromJson(json);
|
||||
|
||||
@override final String type;
|
||||
@override final String keyword;
|
||||
@override final dynamic data;
|
||||
|
||||
/// Create a copy of AutocompleteSuggestion
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$AutocompleteSuggestionCopyWith<_AutocompleteSuggestion> get copyWith => __$AutocompleteSuggestionCopyWithImpl<_AutocompleteSuggestion>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$AutocompleteSuggestionToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AutocompleteSuggestion&&(identical(other.type, type) || other.type == type)&&(identical(other.keyword, keyword) || other.keyword == keyword)&&const DeepCollectionEquality().equals(other.data, data));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,keyword,const DeepCollectionEquality().hash(data));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AutocompleteSuggestion(type: $type, keyword: $keyword, data: $data)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$AutocompleteSuggestionCopyWith<$Res> implements $AutocompleteSuggestionCopyWith<$Res> {
|
||||
factory _$AutocompleteSuggestionCopyWith(_AutocompleteSuggestion value, $Res Function(_AutocompleteSuggestion) _then) = __$AutocompleteSuggestionCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String type, String keyword, dynamic data
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$AutocompleteSuggestionCopyWithImpl<$Res>
|
||||
implements _$AutocompleteSuggestionCopyWith<$Res> {
|
||||
__$AutocompleteSuggestionCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _AutocompleteSuggestion _self;
|
||||
final $Res Function(_AutocompleteSuggestion) _then;
|
||||
|
||||
/// Create a copy of AutocompleteSuggestion
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? keyword = null,Object? data = freezed,}) {
|
||||
return _then(_AutocompleteSuggestion(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,keyword: null == keyword ? _self.keyword : keyword // ignore: cast_nullable_to_non_nullable
|
||||
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
23
lib/models/autocomplete_response.g.dart
Normal file
23
lib/models/autocomplete_response.g.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'autocomplete_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_AutocompleteSuggestion _$AutocompleteSuggestionFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _AutocompleteSuggestion(
|
||||
type: json['type'] as String,
|
||||
keyword: json['keyword'] as String,
|
||||
data: json['data'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AutocompleteSuggestionToJson(
|
||||
_AutocompleteSuggestion instance,
|
||||
) => <String, dynamic>{
|
||||
'type': instance.type,
|
||||
'keyword': instance.keyword,
|
||||
'data': instance.data,
|
||||
};
|
||||
@@ -14,6 +14,7 @@ sealed class UniversalFile with _$UniversalFile {
|
||||
required dynamic data,
|
||||
required UniversalFileType type,
|
||||
@Default(false) bool isLink,
|
||||
String? displayName,
|
||||
}) = _UniversalFile;
|
||||
|
||||
factory UniversalFile.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -31,6 +32,7 @@ sealed class UniversalFile with _$UniversalFile {
|
||||
'video' => UniversalFileType.video,
|
||||
_ => UniversalFileType.file,
|
||||
},
|
||||
displayName: attachment.name,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$UniversalFile {
|
||||
|
||||
dynamic get data; UniversalFileType get type; bool get isLink;
|
||||
dynamic get data; UniversalFileType get type; bool get isLink; String? get displayName;
|
||||
/// Create a copy of UniversalFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -28,16 +28,16 @@ $UniversalFileCopyWith<UniversalFile> get copyWith => _$UniversalFileCopyWithImp
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is UniversalFile&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.type, type) || other.type == type)&&(identical(other.isLink, isLink) || other.isLink == isLink));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is UniversalFile&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.type, type) || other.type == type)&&(identical(other.isLink, isLink) || other.isLink == isLink)&&(identical(other.displayName, displayName) || other.displayName == displayName));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data),type,isLink);
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data),type,isLink,displayName);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UniversalFile(data: $data, type: $type, isLink: $isLink)';
|
||||
return 'UniversalFile(data: $data, type: $type, isLink: $isLink, displayName: $displayName)';
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ abstract mixin class $UniversalFileCopyWith<$Res> {
|
||||
factory $UniversalFileCopyWith(UniversalFile value, $Res Function(UniversalFile) _then) = _$UniversalFileCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
dynamic data, UniversalFileType type, bool isLink
|
||||
dynamic data, UniversalFileType type, bool isLink, String? displayName
|
||||
});
|
||||
|
||||
|
||||
@@ -65,12 +65,13 @@ class _$UniversalFileCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of UniversalFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? data = freezed,Object? type = null,Object? isLink = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? data = freezed,Object? type = null,Object? isLink = null,Object? displayName = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as UniversalFileType,isLink: null == isLink ? _self.isLink : isLink // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
as bool,displayName: freezed == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -152,10 +153,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( dynamic data, UniversalFileType type, bool isLink)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( dynamic data, UniversalFileType type, bool isLink, String? displayName)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _UniversalFile() when $default != null:
|
||||
return $default(_that.data,_that.type,_that.isLink);case _:
|
||||
return $default(_that.data,_that.type,_that.isLink,_that.displayName);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -173,10 +174,10 @@ return $default(_that.data,_that.type,_that.isLink);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( dynamic data, UniversalFileType type, bool isLink) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( dynamic data, UniversalFileType type, bool isLink, String? displayName) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _UniversalFile():
|
||||
return $default(_that.data,_that.type,_that.isLink);}
|
||||
return $default(_that.data,_that.type,_that.isLink,_that.displayName);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -190,10 +191,10 @@ return $default(_that.data,_that.type,_that.isLink);}
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( dynamic data, UniversalFileType type, bool isLink)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( dynamic data, UniversalFileType type, bool isLink, String? displayName)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _UniversalFile() when $default != null:
|
||||
return $default(_that.data,_that.type,_that.isLink);case _:
|
||||
return $default(_that.data,_that.type,_that.isLink,_that.displayName);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -205,12 +206,13 @@ return $default(_that.data,_that.type,_that.isLink);case _:
|
||||
@JsonSerializable()
|
||||
|
||||
class _UniversalFile extends UniversalFile {
|
||||
const _UniversalFile({required this.data, required this.type, this.isLink = false}): super._();
|
||||
const _UniversalFile({required this.data, required this.type, this.isLink = false, this.displayName}): super._();
|
||||
factory _UniversalFile.fromJson(Map<String, dynamic> json) => _$UniversalFileFromJson(json);
|
||||
|
||||
@override final dynamic data;
|
||||
@override final UniversalFileType type;
|
||||
@override@JsonKey() final bool isLink;
|
||||
@override final String? displayName;
|
||||
|
||||
/// Create a copy of UniversalFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -225,16 +227,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UniversalFile&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.type, type) || other.type == type)&&(identical(other.isLink, isLink) || other.isLink == isLink));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _UniversalFile&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.type, type) || other.type == type)&&(identical(other.isLink, isLink) || other.isLink == isLink)&&(identical(other.displayName, displayName) || other.displayName == displayName));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data),type,isLink);
|
||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data),type,isLink,displayName);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UniversalFile(data: $data, type: $type, isLink: $isLink)';
|
||||
return 'UniversalFile(data: $data, type: $type, isLink: $isLink, displayName: $displayName)';
|
||||
}
|
||||
|
||||
|
||||
@@ -245,7 +247,7 @@ abstract mixin class _$UniversalFileCopyWith<$Res> implements $UniversalFileCopy
|
||||
factory _$UniversalFileCopyWith(_UniversalFile value, $Res Function(_UniversalFile) _then) = __$UniversalFileCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
dynamic data, UniversalFileType type, bool isLink
|
||||
dynamic data, UniversalFileType type, bool isLink, String? displayName
|
||||
});
|
||||
|
||||
|
||||
@@ -262,12 +264,13 @@ class __$UniversalFileCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of UniversalFile
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? data = freezed,Object? type = null,Object? isLink = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? data = freezed,Object? type = null,Object? isLink = null,Object? displayName = freezed,}) {
|
||||
return _then(_UniversalFile(
|
||||
data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as UniversalFileType,isLink: null == isLink ? _self.isLink : isLink // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
as bool,displayName: freezed == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ _UniversalFile _$UniversalFileFromJson(Map<String, dynamic> json) =>
|
||||
data: json['data'],
|
||||
type: $enumDecode(_$UniversalFileTypeEnumMap, json['type']),
|
||||
isLink: json['is_link'] as bool? ?? false,
|
||||
displayName: json['display_name'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$UniversalFileToJson(_UniversalFile instance) =>
|
||||
@@ -18,6 +19,7 @@ Map<String, dynamic> _$UniversalFileToJson(_UniversalFile instance) =>
|
||||
'data': instance.data,
|
||||
'type': _$UniversalFileTypeEnumMap[instance.type]!,
|
||||
'is_link': instance.isLink,
|
||||
'display_name': instance.displayName,
|
||||
};
|
||||
|
||||
const _$UniversalFileTypeEnumMap = {
|
||||
|
||||
26
lib/models/heatmap.dart
Normal file
26
lib/models/heatmap.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'heatmap.freezed.dart';
|
||||
part 'heatmap.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class SnHeatmap with _$SnHeatmap {
|
||||
const factory SnHeatmap({
|
||||
required String unit,
|
||||
@JsonKey(name: 'period_start') required DateTime periodStart,
|
||||
@JsonKey(name: 'period_end') required DateTime periodEnd,
|
||||
required List<SnHeatmapItem> items,
|
||||
}) = _SnHeatmap;
|
||||
|
||||
factory SnHeatmap.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnHeatmapFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class SnHeatmapItem with _$SnHeatmapItem {
|
||||
const factory SnHeatmapItem({required DateTime date, required int count}) =
|
||||
_SnHeatmapItem;
|
||||
|
||||
factory SnHeatmapItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnHeatmapItemFromJson(json);
|
||||
}
|
||||
546
lib/models/heatmap.freezed.dart
Normal file
546
lib/models/heatmap.freezed.dart
Normal file
@@ -0,0 +1,546 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'heatmap.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnHeatmap {
|
||||
|
||||
String get unit;@JsonKey(name: 'period_start') DateTime get periodStart;@JsonKey(name: 'period_end') DateTime get periodEnd; List<SnHeatmapItem> get items;
|
||||
/// Create a copy of SnHeatmap
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnHeatmapCopyWith<SnHeatmap> get copyWith => _$SnHeatmapCopyWithImpl<SnHeatmap>(this as SnHeatmap, _$identity);
|
||||
|
||||
/// Serializes this SnHeatmap to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnHeatmap&&(identical(other.unit, unit) || other.unit == unit)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&const DeepCollectionEquality().equals(other.items, items));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,unit,periodStart,periodEnd,const DeepCollectionEquality().hash(items));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnHeatmap(unit: $unit, periodStart: $periodStart, periodEnd: $periodEnd, items: $items)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnHeatmapCopyWith<$Res> {
|
||||
factory $SnHeatmapCopyWith(SnHeatmap value, $Res Function(SnHeatmap) _then) = _$SnHeatmapCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String unit,@JsonKey(name: 'period_start') DateTime periodStart,@JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnHeatmapCopyWithImpl<$Res>
|
||||
implements $SnHeatmapCopyWith<$Res> {
|
||||
_$SnHeatmapCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnHeatmap _self;
|
||||
final $Res Function(SnHeatmap) _then;
|
||||
|
||||
/// Create a copy of SnHeatmap
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? unit = null,Object? periodStart = null,Object? periodEnd = null,Object? items = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
unit: null == unit ? _self.unit : unit // ignore: cast_nullable_to_non_nullable
|
||||
as String,periodStart: null == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,periodEnd: null == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,items: null == items ? _self.items : items // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnHeatmapItem>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnHeatmap].
|
||||
extension SnHeatmapPatterns on SnHeatmap {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnHeatmap value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnHeatmap() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnHeatmap value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnHeatmap():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnHeatmap value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnHeatmap() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String unit, @JsonKey(name: 'period_start') DateTime periodStart, @JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnHeatmap() when $default != null:
|
||||
return $default(_that.unit,_that.periodStart,_that.periodEnd,_that.items);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String unit, @JsonKey(name: 'period_start') DateTime periodStart, @JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnHeatmap():
|
||||
return $default(_that.unit,_that.periodStart,_that.periodEnd,_that.items);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String unit, @JsonKey(name: 'period_start') DateTime periodStart, @JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnHeatmap() when $default != null:
|
||||
return $default(_that.unit,_that.periodStart,_that.periodEnd,_that.items);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnHeatmap implements SnHeatmap {
|
||||
const _SnHeatmap({required this.unit, @JsonKey(name: 'period_start') required this.periodStart, @JsonKey(name: 'period_end') required this.periodEnd, required final List<SnHeatmapItem> items}): _items = items;
|
||||
factory _SnHeatmap.fromJson(Map<String, dynamic> json) => _$SnHeatmapFromJson(json);
|
||||
|
||||
@override final String unit;
|
||||
@override@JsonKey(name: 'period_start') final DateTime periodStart;
|
||||
@override@JsonKey(name: 'period_end') final DateTime periodEnd;
|
||||
final List<SnHeatmapItem> _items;
|
||||
@override List<SnHeatmapItem> get items {
|
||||
if (_items is EqualUnmodifiableListView) return _items;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_items);
|
||||
}
|
||||
|
||||
|
||||
/// Create a copy of SnHeatmap
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnHeatmapCopyWith<_SnHeatmap> get copyWith => __$SnHeatmapCopyWithImpl<_SnHeatmap>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnHeatmapToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnHeatmap&&(identical(other.unit, unit) || other.unit == unit)&&(identical(other.periodStart, periodStart) || other.periodStart == periodStart)&&(identical(other.periodEnd, periodEnd) || other.periodEnd == periodEnd)&&const DeepCollectionEquality().equals(other._items, _items));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,unit,periodStart,periodEnd,const DeepCollectionEquality().hash(_items));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnHeatmap(unit: $unit, periodStart: $periodStart, periodEnd: $periodEnd, items: $items)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnHeatmapCopyWith<$Res> implements $SnHeatmapCopyWith<$Res> {
|
||||
factory _$SnHeatmapCopyWith(_SnHeatmap value, $Res Function(_SnHeatmap) _then) = __$SnHeatmapCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String unit,@JsonKey(name: 'period_start') DateTime periodStart,@JsonKey(name: 'period_end') DateTime periodEnd, List<SnHeatmapItem> items
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnHeatmapCopyWithImpl<$Res>
|
||||
implements _$SnHeatmapCopyWith<$Res> {
|
||||
__$SnHeatmapCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnHeatmap _self;
|
||||
final $Res Function(_SnHeatmap) _then;
|
||||
|
||||
/// Create a copy of SnHeatmap
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? unit = null,Object? periodStart = null,Object? periodEnd = null,Object? items = null,}) {
|
||||
return _then(_SnHeatmap(
|
||||
unit: null == unit ? _self.unit : unit // ignore: cast_nullable_to_non_nullable
|
||||
as String,periodStart: null == periodStart ? _self.periodStart : periodStart // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,periodEnd: null == periodEnd ? _self.periodEnd : periodEnd // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnHeatmapItem>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnHeatmapItem {
|
||||
|
||||
DateTime get date; int get count;
|
||||
/// Create a copy of SnHeatmapItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnHeatmapItemCopyWith<SnHeatmapItem> get copyWith => _$SnHeatmapItemCopyWithImpl<SnHeatmapItem>(this as SnHeatmapItem, _$identity);
|
||||
|
||||
/// Serializes this SnHeatmapItem to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnHeatmapItem&&(identical(other.date, date) || other.date == date)&&(identical(other.count, count) || other.count == count));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,date,count);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnHeatmapItem(date: $date, count: $count)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnHeatmapItemCopyWith<$Res> {
|
||||
factory $SnHeatmapItemCopyWith(SnHeatmapItem value, $Res Function(SnHeatmapItem) _then) = _$SnHeatmapItemCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
DateTime date, int count
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnHeatmapItemCopyWithImpl<$Res>
|
||||
implements $SnHeatmapItemCopyWith<$Res> {
|
||||
_$SnHeatmapItemCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnHeatmapItem _self;
|
||||
final $Res Function(SnHeatmapItem) _then;
|
||||
|
||||
/// Create a copy of SnHeatmapItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? date = null,Object? count = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,count: null == count ? _self.count : count // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnHeatmapItem].
|
||||
extension SnHeatmapItemPatterns on SnHeatmapItem {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnHeatmapItem value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnHeatmapItem() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnHeatmapItem value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnHeatmapItem():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnHeatmapItem value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnHeatmapItem() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime date, int count)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnHeatmapItem() when $default != null:
|
||||
return $default(_that.date,_that.count);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime date, int count) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnHeatmapItem():
|
||||
return $default(_that.date,_that.count);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime date, int count)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnHeatmapItem() when $default != null:
|
||||
return $default(_that.date,_that.count);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnHeatmapItem implements SnHeatmapItem {
|
||||
const _SnHeatmapItem({required this.date, required this.count});
|
||||
factory _SnHeatmapItem.fromJson(Map<String, dynamic> json) => _$SnHeatmapItemFromJson(json);
|
||||
|
||||
@override final DateTime date;
|
||||
@override final int count;
|
||||
|
||||
/// Create a copy of SnHeatmapItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnHeatmapItemCopyWith<_SnHeatmapItem> get copyWith => __$SnHeatmapItemCopyWithImpl<_SnHeatmapItem>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnHeatmapItemToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnHeatmapItem&&(identical(other.date, date) || other.date == date)&&(identical(other.count, count) || other.count == count));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,date,count);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnHeatmapItem(date: $date, count: $count)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnHeatmapItemCopyWith<$Res> implements $SnHeatmapItemCopyWith<$Res> {
|
||||
factory _$SnHeatmapItemCopyWith(_SnHeatmapItem value, $Res Function(_SnHeatmapItem) _then) = __$SnHeatmapItemCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
DateTime date, int count
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnHeatmapItemCopyWithImpl<$Res>
|
||||
implements _$SnHeatmapItemCopyWith<$Res> {
|
||||
__$SnHeatmapItemCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnHeatmapItem _self;
|
||||
final $Res Function(_SnHeatmapItem) _then;
|
||||
|
||||
/// Create a copy of SnHeatmapItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? date = null,Object? count = null,}) {
|
||||
return _then(_SnHeatmapItem(
|
||||
date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,count: null == count ? _self.count : count // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
37
lib/models/heatmap.g.dart
Normal file
37
lib/models/heatmap.g.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'heatmap.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_SnHeatmap _$SnHeatmapFromJson(Map<String, dynamic> json) => _SnHeatmap(
|
||||
unit: json['unit'] as String,
|
||||
periodStart: DateTime.parse(json['period_start'] as String),
|
||||
periodEnd: DateTime.parse(json['period_end'] as String),
|
||||
items:
|
||||
(json['items'] as List<dynamic>)
|
||||
.map((e) => SnHeatmapItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnHeatmapToJson(_SnHeatmap instance) =>
|
||||
<String, dynamic>{
|
||||
'unit': instance.unit,
|
||||
'period_start': instance.periodStart.toIso8601String(),
|
||||
'period_end': instance.periodEnd.toIso8601String(),
|
||||
'items': instance.items.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
_SnHeatmapItem _$SnHeatmapItemFromJson(Map<String, dynamic> json) =>
|
||||
_SnHeatmapItem(
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
count: (json['count'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnHeatmapItemToJson(_SnHeatmapItem instance) =>
|
||||
<String, dynamic>{
|
||||
'date': instance.date.toIso8601String(),
|
||||
'count': instance.count,
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/post_category.dart';
|
||||
import 'package:island/models/post_tag.dart';
|
||||
@@ -142,3 +143,21 @@ sealed class SnPostAward with _$SnPostAward {
|
||||
factory SnPostAward.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnPostAwardFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class SnPostReaction with _$SnPostReaction {
|
||||
const factory SnPostReaction({
|
||||
required String id,
|
||||
required String symbol,
|
||||
required int attitude,
|
||||
required String postId,
|
||||
required String accountId,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
@Default(null) SnAccount? account,
|
||||
DateTime? deletedAt,
|
||||
}) = _SnPostReaction;
|
||||
|
||||
factory SnPostReaction.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnPostReactionFromJson(json);
|
||||
}
|
||||
|
||||
@@ -1900,4 +1900,309 @@ as DateTime?,
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnPostReaction {
|
||||
|
||||
String get id; String get symbol; int get attitude; String get postId; String get accountId; DateTime get createdAt; DateTime get updatedAt; SnAccount? get account; DateTime? get deletedAt;
|
||||
/// Create a copy of SnPostReaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnPostReactionCopyWith<SnPostReaction> get copyWith => _$SnPostReactionCopyWithImpl<SnPostReaction>(this as SnPostReaction, _$identity);
|
||||
|
||||
/// Serializes this SnPostReaction to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPostReaction&&(identical(other.id, id) || other.id == id)&&(identical(other.symbol, symbol) || other.symbol == symbol)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.postId, postId) || other.postId == postId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.account, account) || other.account == account)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,symbol,attitude,postId,accountId,createdAt,updatedAt,account,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPostReaction(id: $id, symbol: $symbol, attitude: $attitude, postId: $postId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, account: $account, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnPostReactionCopyWith<$Res> {
|
||||
factory $SnPostReactionCopyWith(SnPostReaction value, $Res Function(SnPostReaction) _then) = _$SnPostReactionCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
$SnAccountCopyWith<$Res>? get account;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnPostReactionCopyWithImpl<$Res>
|
||||
implements $SnPostReactionCopyWith<$Res> {
|
||||
_$SnPostReactionCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnPostReaction _self;
|
||||
final $Res Function(SnPostReaction) _then;
|
||||
|
||||
/// Create a copy of SnPostReaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? symbol = null,Object? attitude = null,Object? postId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? account = freezed,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,symbol: null == symbol ? _self.symbol : symbol // ignore: cast_nullable_to_non_nullable
|
||||
as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable
|
||||
as int,postId: null == postId ? _self.postId : postId // ignore: cast_nullable_to_non_nullable
|
||||
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccount?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of SnPostReaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountCopyWith<$Res>? get account {
|
||||
if (_self.account == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||
return _then(_self.copyWith(account: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnPostReaction].
|
||||
extension SnPostReactionPatterns on SnPostReaction {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnPostReaction value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnPostReaction() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnPostReaction value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnPostReaction():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnPostReaction value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnPostReaction() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnPostReaction() when $default != null:
|
||||
return $default(_that.id,_that.symbol,_that.attitude,_that.postId,_that.accountId,_that.createdAt,_that.updatedAt,_that.account,_that.deletedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnPostReaction():
|
||||
return $default(_that.id,_that.symbol,_that.attitude,_that.postId,_that.accountId,_that.createdAt,_that.updatedAt,_that.account,_that.deletedAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnPostReaction() when $default != null:
|
||||
return $default(_that.id,_that.symbol,_that.attitude,_that.postId,_that.accountId,_that.createdAt,_that.updatedAt,_that.account,_that.deletedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnPostReaction implements SnPostReaction {
|
||||
const _SnPostReaction({required this.id, required this.symbol, required this.attitude, required this.postId, required this.accountId, required this.createdAt, required this.updatedAt, this.account = null, this.deletedAt});
|
||||
factory _SnPostReaction.fromJson(Map<String, dynamic> json) => _$SnPostReactionFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String symbol;
|
||||
@override final int attitude;
|
||||
@override final String postId;
|
||||
@override final String accountId;
|
||||
@override final DateTime createdAt;
|
||||
@override final DateTime updatedAt;
|
||||
@override@JsonKey() final SnAccount? account;
|
||||
@override final DateTime? deletedAt;
|
||||
|
||||
/// Create a copy of SnPostReaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnPostReactionCopyWith<_SnPostReaction> get copyWith => __$SnPostReactionCopyWithImpl<_SnPostReaction>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnPostReactionToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPostReaction&&(identical(other.id, id) || other.id == id)&&(identical(other.symbol, symbol) || other.symbol == symbol)&&(identical(other.attitude, attitude) || other.attitude == attitude)&&(identical(other.postId, postId) || other.postId == postId)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.account, account) || other.account == account)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,symbol,attitude,postId,accountId,createdAt,updatedAt,account,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnPostReaction(id: $id, symbol: $symbol, attitude: $attitude, postId: $postId, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, account: $account, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnPostReactionCopyWith<$Res> implements $SnPostReactionCopyWith<$Res> {
|
||||
factory _$SnPostReactionCopyWith(_SnPostReaction value, $Res Function(_SnPostReaction) _then) = __$SnPostReactionCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String symbol, int attitude, String postId, String accountId, DateTime createdAt, DateTime updatedAt, SnAccount? account, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@override $SnAccountCopyWith<$Res>? get account;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnPostReactionCopyWithImpl<$Res>
|
||||
implements _$SnPostReactionCopyWith<$Res> {
|
||||
__$SnPostReactionCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnPostReaction _self;
|
||||
final $Res Function(_SnPostReaction) _then;
|
||||
|
||||
/// Create a copy of SnPostReaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? symbol = null,Object? attitude = null,Object? postId = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? account = freezed,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnPostReaction(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,symbol: null == symbol ? _self.symbol : symbol // ignore: cast_nullable_to_non_nullable
|
||||
as String,attitude: null == attitude ? _self.attitude : attitude // ignore: cast_nullable_to_non_nullable
|
||||
as int,postId: null == postId ? _self.postId : postId // ignore: cast_nullable_to_non_nullable
|
||||
as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
|
||||
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
|
||||
as SnAccount?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnPostReaction
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnAccountCopyWith<$Res>? get account {
|
||||
if (_self.account == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
|
||||
return _then(_self.copyWith(account: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
||||
@@ -230,3 +230,35 @@ Map<String, dynamic> _$SnPostAwardToJson(_SnPostAward instance) =>
|
||||
'updated_at': instance.updatedAt?.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
_SnPostReaction _$SnPostReactionFromJson(Map<String, dynamic> json) =>
|
||||
_SnPostReaction(
|
||||
id: json['id'] as String,
|
||||
symbol: json['symbol'] as String,
|
||||
attitude: (json['attitude'] as num).toInt(),
|
||||
postId: json['post_id'] as String,
|
||||
accountId: json['account_id'] as String,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
account:
|
||||
json['account'] == null
|
||||
? null
|
||||
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||
deletedAt:
|
||||
json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnPostReactionToJson(_SnPostReaction instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'symbol': instance.symbol,
|
||||
'attitude': instance.attitude,
|
||||
'post_id': instance.postId,
|
||||
'account_id': instance.accountId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'account': instance.account?.toJson(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
@@ -20,6 +20,24 @@ sealed class SnWallet with _$SnWallet {
|
||||
_$SnWalletFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class SnWalletStats with _$SnWalletStats {
|
||||
const factory SnWalletStats({
|
||||
required DateTime periodBegin,
|
||||
required DateTime periodEnd,
|
||||
required int totalTransactions,
|
||||
required int totalOrders,
|
||||
required double totalIncome,
|
||||
required double totalOutgoing,
|
||||
required double sum,
|
||||
@Default({}) Map<String, double> incomeCategories,
|
||||
@Default({}) Map<String, double> outgoingCategories,
|
||||
}) = _SnWalletStats;
|
||||
|
||||
factory SnWalletStats.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnWalletStatsFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class SnWalletPocket with _$SnWalletPocket {
|
||||
const factory SnWalletPocket({
|
||||
@@ -124,3 +142,72 @@ sealed class SnWalletOrder with _$SnWalletOrder {
|
||||
factory SnWalletOrder.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnWalletOrderFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class SnWalletGift with _$SnWalletGift {
|
||||
const factory SnWalletGift({
|
||||
required String id,
|
||||
required String giftCode,
|
||||
required String subscriptionIdentifier,
|
||||
required String? recipientId,
|
||||
required SnAccount? recipient,
|
||||
required String gifterId,
|
||||
required SnAccount? gifter,
|
||||
required String? redeemerId,
|
||||
required SnAccount? redeemer,
|
||||
required String? message,
|
||||
required int status,
|
||||
required DateTime? redeemedAt,
|
||||
required DateTime? expiredAt,
|
||||
required String? subscriptionId,
|
||||
required SnWalletSubscription? subscription,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
}) = _SnWalletGift;
|
||||
|
||||
factory SnWalletGift.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnWalletGiftFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class SnWalletFund with _$SnWalletFund {
|
||||
const factory SnWalletFund({
|
||||
required String id,
|
||||
required String currency,
|
||||
required double totalAmount,
|
||||
required int splitType, // 0: even, 1: random
|
||||
required int
|
||||
status, // 0: created, 1: partially claimed, 2: fully claimed, 3: expired
|
||||
required String? message,
|
||||
required String creatorAccountId,
|
||||
required SnAccount? creatorAccount,
|
||||
required DateTime expiredAt,
|
||||
required List<SnWalletFundRecipient> recipients,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
}) = _SnWalletFund;
|
||||
|
||||
factory SnWalletFund.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnWalletFundFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class SnWalletFundRecipient with _$SnWalletFundRecipient {
|
||||
const factory SnWalletFundRecipient({
|
||||
required String id,
|
||||
required String fundId,
|
||||
required String recipientAccountId,
|
||||
required SnAccount? recipientAccount,
|
||||
required double amount,
|
||||
required bool isReceived,
|
||||
required DateTime? receivedAt,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
}) = _SnWalletFundRecipient;
|
||||
|
||||
factory SnWalletFundRecipient.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnWalletFundRecipientFromJson(json);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,40 @@ Map<String, dynamic> _$SnWalletToJson(_SnWallet instance) => <String, dynamic>{
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
_SnWalletStats _$SnWalletStatsFromJson(Map<String, dynamic> json) =>
|
||||
_SnWalletStats(
|
||||
periodBegin: DateTime.parse(json['period_begin'] as String),
|
||||
periodEnd: DateTime.parse(json['period_end'] as String),
|
||||
totalTransactions: (json['total_transactions'] as num).toInt(),
|
||||
totalOrders: (json['total_orders'] as num).toInt(),
|
||||
totalIncome: (json['total_income'] as num).toDouble(),
|
||||
totalOutgoing: (json['total_outgoing'] as num).toDouble(),
|
||||
sum: (json['sum'] as num).toDouble(),
|
||||
incomeCategories:
|
||||
(json['income_categories'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, (e as num).toDouble()),
|
||||
) ??
|
||||
const {},
|
||||
outgoingCategories:
|
||||
(json['outgoing_categories'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, (e as num).toDouble()),
|
||||
) ??
|
||||
const {},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnWalletStatsToJson(_SnWalletStats instance) =>
|
||||
<String, dynamic>{
|
||||
'period_begin': instance.periodBegin.toIso8601String(),
|
||||
'period_end': instance.periodEnd.toIso8601String(),
|
||||
'total_transactions': instance.totalTransactions,
|
||||
'total_orders': instance.totalOrders,
|
||||
'total_income': instance.totalIncome,
|
||||
'total_outgoing': instance.totalOutgoing,
|
||||
'sum': instance.sum,
|
||||
'income_categories': instance.incomeCategories,
|
||||
'outgoing_categories': instance.outgoingCategories,
|
||||
};
|
||||
|
||||
_SnWalletPocket _$SnWalletPocketFromJson(Map<String, dynamic> json) =>
|
||||
_SnWalletPocket(
|
||||
id: json['id'] as String,
|
||||
@@ -228,3 +262,155 @@ Map<String, dynamic> _$SnWalletOrderToJson(_SnWalletOrder instance) =>
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
_SnWalletGift _$SnWalletGiftFromJson(Map<String, dynamic> json) =>
|
||||
_SnWalletGift(
|
||||
id: json['id'] as String,
|
||||
giftCode: json['gift_code'] as String,
|
||||
subscriptionIdentifier: json['subscription_identifier'] as String,
|
||||
recipientId: json['recipient_id'] as String?,
|
||||
recipient:
|
||||
json['recipient'] == null
|
||||
? null
|
||||
: SnAccount.fromJson(json['recipient'] as Map<String, dynamic>),
|
||||
gifterId: json['gifter_id'] as String,
|
||||
gifter:
|
||||
json['gifter'] == null
|
||||
? null
|
||||
: SnAccount.fromJson(json['gifter'] as Map<String, dynamic>),
|
||||
redeemerId: json['redeemer_id'] as String?,
|
||||
redeemer:
|
||||
json['redeemer'] == null
|
||||
? null
|
||||
: SnAccount.fromJson(json['redeemer'] as Map<String, dynamic>),
|
||||
message: json['message'] as String?,
|
||||
status: (json['status'] as num).toInt(),
|
||||
redeemedAt:
|
||||
json['redeemed_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['redeemed_at'] as String),
|
||||
expiredAt:
|
||||
json['expired_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expired_at'] as String),
|
||||
subscriptionId: json['subscription_id'] as String?,
|
||||
subscription:
|
||||
json['subscription'] == null
|
||||
? null
|
||||
: SnWalletSubscription.fromJson(
|
||||
json['subscription'] as Map<String, dynamic>,
|
||||
),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt:
|
||||
json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnWalletGiftToJson(_SnWalletGift instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'gift_code': instance.giftCode,
|
||||
'subscription_identifier': instance.subscriptionIdentifier,
|
||||
'recipient_id': instance.recipientId,
|
||||
'recipient': instance.recipient?.toJson(),
|
||||
'gifter_id': instance.gifterId,
|
||||
'gifter': instance.gifter?.toJson(),
|
||||
'redeemer_id': instance.redeemerId,
|
||||
'redeemer': instance.redeemer?.toJson(),
|
||||
'message': instance.message,
|
||||
'status': instance.status,
|
||||
'redeemed_at': instance.redeemedAt?.toIso8601String(),
|
||||
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||
'subscription_id': instance.subscriptionId,
|
||||
'subscription': instance.subscription?.toJson(),
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
_SnWalletFund _$SnWalletFundFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _SnWalletFund(
|
||||
id: json['id'] as String,
|
||||
currency: json['currency'] as String,
|
||||
totalAmount: (json['total_amount'] as num).toDouble(),
|
||||
splitType: (json['split_type'] as num).toInt(),
|
||||
status: (json['status'] as num).toInt(),
|
||||
message: json['message'] as String?,
|
||||
creatorAccountId: json['creator_account_id'] as String,
|
||||
creatorAccount:
|
||||
json['creator_account'] == null
|
||||
? null
|
||||
: SnAccount.fromJson(json['creator_account'] as Map<String, dynamic>),
|
||||
expiredAt: DateTime.parse(json['expired_at'] as String),
|
||||
recipients:
|
||||
(json['recipients'] as List<dynamic>)
|
||||
.map((e) => SnWalletFundRecipient.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt:
|
||||
json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnWalletFundToJson(_SnWalletFund instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'currency': instance.currency,
|
||||
'total_amount': instance.totalAmount,
|
||||
'split_type': instance.splitType,
|
||||
'status': instance.status,
|
||||
'message': instance.message,
|
||||
'creator_account_id': instance.creatorAccountId,
|
||||
'creator_account': instance.creatorAccount?.toJson(),
|
||||
'expired_at': instance.expiredAt.toIso8601String(),
|
||||
'recipients': instance.recipients.map((e) => e.toJson()).toList(),
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
_SnWalletFundRecipient _$SnWalletFundRecipientFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _SnWalletFundRecipient(
|
||||
id: json['id'] as String,
|
||||
fundId: json['fund_id'] as String,
|
||||
recipientAccountId: json['recipient_account_id'] as String,
|
||||
recipientAccount:
|
||||
json['recipient_account'] == null
|
||||
? null
|
||||
: SnAccount.fromJson(
|
||||
json['recipient_account'] as Map<String, dynamic>,
|
||||
),
|
||||
amount: (json['amount'] as num).toDouble(),
|
||||
isReceived: json['is_received'] as bool,
|
||||
receivedAt:
|
||||
json['received_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['received_at'] as String),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt:
|
||||
json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnWalletFundRecipientToJson(
|
||||
_SnWalletFundRecipient instance,
|
||||
) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'fund_id': instance.fundId,
|
||||
'recipient_account_id': instance.recipientAccountId,
|
||||
'recipient_account': instance.recipientAccount?.toJson(),
|
||||
'amount': instance.amount,
|
||||
'is_received': instance.isReceived,
|
||||
'received_at': instance.receivedAt?.toIso8601String(),
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/talker.dart';
|
||||
import 'package:island/widgets/account/status.dart';
|
||||
import 'package:shelf/shelf.dart';
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
@@ -69,13 +69,13 @@ class ActivityRpcServer {
|
||||
|
||||
// Start WebSocket server
|
||||
while (port <= portRange[1]) {
|
||||
developer.log('Trying port $port', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] Trying port $port');
|
||||
try {
|
||||
_httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
|
||||
developer.log('Listening on $port', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] Listening on $port');
|
||||
|
||||
shelf_io.serveRequests(_httpServer!, (Request request) async {
|
||||
developer.log('New request', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] New request');
|
||||
if (request.headers['upgrade']?.toLowerCase() == 'websocket') {
|
||||
final handler = webSocketHandler((WebSocketChannel channel, _) {
|
||||
_wsSockets.add(channel);
|
||||
@@ -83,19 +83,16 @@ class ActivityRpcServer {
|
||||
});
|
||||
return handler(request);
|
||||
}
|
||||
developer.log(
|
||||
'New request disposed due to not websocket',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
talker.log('New request disposed due to not websocket');
|
||||
return Response.notFound('Not a WebSocket request');
|
||||
});
|
||||
wsSuccess = true;
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e is SocketException && e.osError?.errorCode == 98) {
|
||||
developer.log('$port in use!', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] $port in use!');
|
||||
} else {
|
||||
developer.log('HTTP error: $e', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] HTTP error: $e');
|
||||
}
|
||||
port++;
|
||||
await Future.delayed(Duration(milliseconds: 100)); // Add delay
|
||||
@@ -121,13 +118,10 @@ class ActivityRpcServer {
|
||||
|
||||
await _ipcServer!.start();
|
||||
} catch (e) {
|
||||
developer.log('IPC server error: $e', name: kRpcIpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] IPC server error: $e');
|
||||
}
|
||||
} else {
|
||||
developer.log(
|
||||
'IPC server disabled on macOS or web in production mode',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
talker.log('IPC server disabled on macOS or web in production mode');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +132,7 @@ class ActivityRpcServer {
|
||||
try {
|
||||
await socket.sink.close();
|
||||
} catch (e) {
|
||||
developer.log('Error closing WebSocket: $e', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] Error closing WebSocket: $e');
|
||||
}
|
||||
}
|
||||
_wsSockets.clear();
|
||||
@@ -147,7 +141,7 @@ class ActivityRpcServer {
|
||||
// Stop IPC server
|
||||
await _ipcServer?.stop();
|
||||
|
||||
developer.log('Servers stopped', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] Servers stopped');
|
||||
}
|
||||
|
||||
// Handle new WebSocket connection
|
||||
@@ -159,10 +153,7 @@ class ActivityRpcServer {
|
||||
final clientId = params['client_id'] ?? '';
|
||||
final origin = request.headers['origin'] ?? '';
|
||||
|
||||
developer.log(
|
||||
'New WS connection! origin: $origin, params: $params',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
talker.log('New WS connection! origin: $origin, params: $params');
|
||||
|
||||
if (origin.isNotEmpty &&
|
||||
![
|
||||
@@ -170,22 +161,19 @@ class ActivityRpcServer {
|
||||
'https://ptb.discord.com',
|
||||
'https://canary.discord.com',
|
||||
].contains(origin)) {
|
||||
developer.log('Disallowed origin: $origin', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] Disallowed origin: $origin');
|
||||
socket.sink.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (encoding != 'json') {
|
||||
developer.log(
|
||||
'Unsupported encoding requested: $encoding',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
talker.log('Unsupported encoding requested: $encoding');
|
||||
socket.sink.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ver != 1) {
|
||||
developer.log('Unsupported version requested: $ver', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] Unsupported version requested: $ver');
|
||||
socket.sink.close();
|
||||
return;
|
||||
}
|
||||
@@ -195,10 +183,10 @@ class ActivityRpcServer {
|
||||
socket.stream.listen(
|
||||
(data) => _onWsMessage(socketWithMeta, data),
|
||||
onError: (e) {
|
||||
developer.log('WS socket error: $e', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] WS socket error: $e');
|
||||
},
|
||||
onDone: () {
|
||||
developer.log('WS socket closed', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] WS socket closed');
|
||||
handlers['close']?.call(socketWithMeta);
|
||||
_wsSockets.remove(socket);
|
||||
},
|
||||
@@ -210,25 +198,19 @@ class ActivityRpcServer {
|
||||
// Handle incoming WebSocket message
|
||||
Future<void> _onWsMessage(_WsSocketWrapper socket, dynamic data) async {
|
||||
if (data is! String) {
|
||||
developer.log(
|
||||
'Invalid WebSocket message: not a string',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
talker.log('Invalid WebSocket message: not a string');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final jsonData = await compute(jsonDecode, data);
|
||||
if (jsonData is! Map<String, dynamic>) {
|
||||
developer.log(
|
||||
'Invalid WebSocket message: not a JSON object',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
talker.log('Invalid WebSocket message: not a JSON object');
|
||||
return;
|
||||
}
|
||||
developer.log('WS message: $jsonData', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] WS message: $jsonData');
|
||||
handlers['message']?.call(socket, jsonData);
|
||||
} catch (e) {
|
||||
developer.log('WS message parse error: $e', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] WS message parse error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,12 +218,12 @@ class ActivityRpcServer {
|
||||
void _handleIpcPacket(IpcSocketWrapper socket, IpcPacket packet) {
|
||||
switch (packet.type) {
|
||||
case IpcTypes.ping:
|
||||
developer.log('IPC ping received', name: kRpcIpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] IPC ping received');
|
||||
socket.sendPong(packet.data);
|
||||
break;
|
||||
|
||||
case IpcTypes.pong:
|
||||
developer.log('IPC pong received', name: kRpcIpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] IPC pong received');
|
||||
break;
|
||||
|
||||
case IpcTypes.handshake:
|
||||
@@ -256,7 +238,7 @@ class ActivityRpcServer {
|
||||
if (!socket.handshook) {
|
||||
throw Exception('Need to handshake first');
|
||||
}
|
||||
developer.log('IPC frame: ${packet.data}', name: kRpcIpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] IPC frame: ${packet.data}');
|
||||
handlers['message']?.call(socket, packet.data);
|
||||
break;
|
||||
|
||||
@@ -271,22 +253,19 @@ class ActivityRpcServer {
|
||||
|
||||
// Handle IPC handshake
|
||||
void _onIpcHandshake(IpcSocketWrapper socket, Map<String, dynamic> params) {
|
||||
developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] IPC handshake: $params');
|
||||
|
||||
final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
|
||||
final clientId = params['client_id']?.toString() ?? '';
|
||||
|
||||
if (ver != 1) {
|
||||
developer.log(
|
||||
'IPC unsupported version requested: $ver',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
talker.log('IPC unsupported version requested: $ver');
|
||||
socket.closeWithCode(IpcErrorCodes.invalidVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
if (clientId.isEmpty) {
|
||||
developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] IPC client ID required');
|
||||
socket.closeWithCode(IpcErrorCodes.invalidClientId);
|
||||
return;
|
||||
}
|
||||
@@ -306,7 +285,7 @@ class _WsSocketWrapper {
|
||||
_WsSocketWrapper(this.channel, this.clientId, this.encoding);
|
||||
|
||||
void send(Map<String, dynamic> msg) {
|
||||
developer.log('WS sending: $msg', name: kRpcLogPrefix);
|
||||
talker.log('[$kRpcLogPrefix] WS sending: $msg');
|
||||
channel.sink.add(jsonEncode(msg));
|
||||
}
|
||||
}
|
||||
@@ -333,7 +312,7 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
|
||||
: super(ServerState(status: 'Server not started'));
|
||||
|
||||
Future<void> start() async {
|
||||
if (!Platform.isAndroid && !Platform.isIOS && !kIsWeb) {
|
||||
if (!kIsWeb && !Platform.isAndroid && !Platform.isIOS) {
|
||||
try {
|
||||
await server.start();
|
||||
state = state.copyWith(status: 'Server running');
|
||||
@@ -398,12 +377,7 @@ final rpcServerStateProvider =
|
||||
final appId = socket.clientId;
|
||||
final meta = data['args']['activity'];
|
||||
try {
|
||||
await setRemoteActivityStatus(
|
||||
ref,
|
||||
label,
|
||||
appId,
|
||||
meta,
|
||||
);
|
||||
await setRemoteActivityStatus(ref, label, appId, meta);
|
||||
final now = DateTime.now();
|
||||
final status = SnAccountStatus(
|
||||
id: 'local_$appId',
|
||||
@@ -422,10 +396,7 @@ final rpcServerStateProvider =
|
||||
);
|
||||
ref.read(currentAccountStatusProvider.notifier).setStatus(status);
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'Failed to set remote activity status: $e',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
talker.log('Failed to set remote activity status: $e');
|
||||
}
|
||||
socket.send({
|
||||
'cmd': 'SET_ACTIVITY',
|
||||
@@ -442,10 +413,7 @@ final rpcServerStateProvider =
|
||||
await unsetRemoteActivityStatus(ref, appId);
|
||||
ref.read(currentAccountStatusProvider.notifier).clearStatus();
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'Failed to unset remote activity status: $e',
|
||||
name: kRpcLogPrefix,
|
||||
);
|
||||
talker.log('Failed to unset remote activity status: $e');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:dart_ipc/dart_ipc.dart';
|
||||
import 'package:island/talker.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
const String kRpcIpcLogPrefix = 'arRPC.ipc';
|
||||
@@ -128,23 +128,18 @@ class MultiPlatformIpcServer extends IpcServer {
|
||||
@override
|
||||
Future<void> start() async {
|
||||
try {
|
||||
final ipcPath = Platform.isWindows
|
||||
? r'\\.\pipe\discord-ipc-0'
|
||||
: await _findAvailableUnixIpcPath();
|
||||
final ipcPath =
|
||||
Platform.isWindows
|
||||
? r'\\.\pipe\discord-ipc-0'
|
||||
: await _findAvailableUnixIpcPath();
|
||||
|
||||
final serverSocket = await bind(ipcPath);
|
||||
developer.log(
|
||||
'IPC listening at $ipcPath',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
talker.log('IPC listening at $ipcPath');
|
||||
|
||||
_serverSubscription = serverSocket.listen((socket) {
|
||||
final socketWrapper = MultiPlatformIpcSocketWrapper(socket);
|
||||
addSocket(socketWrapper);
|
||||
developer.log(
|
||||
'New IPC connection!',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
talker.log('New IPC connection!');
|
||||
_handleIpcData(socketWrapper);
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -158,7 +153,7 @@ class MultiPlatformIpcServer extends IpcServer {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (e) {
|
||||
developer.log('Error closing IPC socket: $e', name: kRpcIpcLogPrefix);
|
||||
talker.log('Error closing IPC socket: $e');
|
||||
}
|
||||
}
|
||||
sockets.clear();
|
||||
@@ -168,31 +163,30 @@ class MultiPlatformIpcServer extends IpcServer {
|
||||
// Handle incoming IPC data
|
||||
void _handleIpcData(MultiPlatformIpcSocketWrapper socket) {
|
||||
final startTime = DateTime.now();
|
||||
socket.socket.listen((data) {
|
||||
final readStart = DateTime.now();
|
||||
socket.addData(data);
|
||||
final readDuration = DateTime.now().difference(readStart).inMicroseconds;
|
||||
developer.log(
|
||||
'Read data took $readDuration microseconds',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
socket.socket.listen(
|
||||
(data) {
|
||||
final readStart = DateTime.now();
|
||||
socket.addData(data);
|
||||
final readDuration =
|
||||
DateTime.now().difference(readStart).inMicroseconds;
|
||||
talker.log('Read data took $readDuration microseconds');
|
||||
|
||||
final packets = socket.readPackets();
|
||||
for (final packet in packets) {
|
||||
handlePacket?.call(socket, packet, {});
|
||||
}
|
||||
}, onDone: () {
|
||||
developer.log('IPC connection closed', name: kRpcIpcLogPrefix);
|
||||
socket.close();
|
||||
}, onError: (e) {
|
||||
developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
|
||||
socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
|
||||
});
|
||||
final totalDuration = DateTime.now().difference(startTime).inMicroseconds;
|
||||
developer.log(
|
||||
'_handleIpcData took $totalDuration microseconds',
|
||||
name: kRpcIpcLogPrefix,
|
||||
final packets = socket.readPackets();
|
||||
for (final packet in packets) {
|
||||
handlePacket?.call(socket, packet, {});
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
talker.log('IPC connection closed');
|
||||
socket.close();
|
||||
},
|
||||
onError: (e) {
|
||||
talker.log('IPC data error: $e');
|
||||
socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
|
||||
},
|
||||
);
|
||||
final totalDuration = DateTime.now().difference(startTime).inMicroseconds;
|
||||
talker.log('_handleIpcData took $totalDuration microseconds');
|
||||
}
|
||||
|
||||
Future<String> _getMacOsSystemTmpDir() async {
|
||||
@@ -212,10 +206,7 @@ class MultiPlatformIpcServer extends IpcServer {
|
||||
baseDirs.add(macTempDir);
|
||||
}
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'Failed to get macOS system temp dir: $e',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
talker.log('Failed to get macOS system temp dir: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,17 +232,11 @@ class MultiPlatformIpcServer extends IpcServer {
|
||||
try {
|
||||
await File(socketPath).delete();
|
||||
} catch (_) {}
|
||||
developer.log(
|
||||
'IPC socket will be created at: $socketPath',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
talker.log('IPC socket will be created at: $socketPath');
|
||||
return socketPath;
|
||||
} catch (e) {
|
||||
if (i == 0) {
|
||||
developer.log(
|
||||
'IPC path $socketPath not available: $e',
|
||||
name: kRpcIpcLogPrefix,
|
||||
);
|
||||
talker.log('IPC path $socketPath not available: $e');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -271,7 +256,7 @@ class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {
|
||||
|
||||
@override
|
||||
void send(Map<String, dynamic> msg) {
|
||||
developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
|
||||
talker.log('IPC sending: $msg');
|
||||
final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg);
|
||||
socket.add(packet);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/chat/call_button.dart';
|
||||
import 'package:livekit_client/livekit_client.dart';
|
||||
import 'package:livekit_client/livekit_client.dart' as lk;
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:island/talker.dart';
|
||||
|
||||
part 'call.g.dart';
|
||||
part 'call.freezed.dart';
|
||||
@@ -41,7 +43,7 @@ sealed class CallParticipantLive with _$CallParticipantLive {
|
||||
|
||||
const factory CallParticipantLive({
|
||||
required CallParticipant participant,
|
||||
required Participant remoteParticipant,
|
||||
required lk.Participant remoteParticipant,
|
||||
}) = _CallParticipantLive;
|
||||
|
||||
bool get isSpeaking => remoteParticipant.isSpeaking;
|
||||
@@ -57,21 +59,21 @@ sealed class CallParticipantLive with _$CallParticipantLive {
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class CallNotifier extends _$CallNotifier {
|
||||
Room? _room;
|
||||
LocalParticipant? _localParticipant;
|
||||
lk.Room? _room;
|
||||
lk.LocalParticipant? _localParticipant;
|
||||
List<CallParticipantLive> _participants = [];
|
||||
final Map<String, CallParticipant> _participantInfoByIdentity = {};
|
||||
EventsListener? _roomListener;
|
||||
lk.EventsListener? _roomListener;
|
||||
|
||||
List<CallParticipantLive> get participants =>
|
||||
List.unmodifiable(_participants);
|
||||
LocalParticipant? get localParticipant => _localParticipant;
|
||||
lk.LocalParticipant? get localParticipant => _localParticipant;
|
||||
|
||||
Map<String, double> participantsVolumes = {};
|
||||
|
||||
Timer? _durationTimer;
|
||||
|
||||
Room? get room => _room;
|
||||
lk.Room? get room => _room;
|
||||
|
||||
@override
|
||||
CallState build() {
|
||||
@@ -91,10 +93,10 @@ class CallNotifier extends _$CallNotifier {
|
||||
_roomListener = _room!.createListener();
|
||||
_room!.addListener(_onRoomChange);
|
||||
_roomListener!
|
||||
..on<ParticipantConnectedEvent>((e) {
|
||||
..on<lk.ParticipantConnectedEvent>((e) {
|
||||
_refreshLiveParticipants();
|
||||
})
|
||||
..on<RoomDisconnectedEvent>((e) {
|
||||
..on<lk.RoomDisconnectedEvent>((e) {
|
||||
_participants = [];
|
||||
state = state.copyWith();
|
||||
});
|
||||
@@ -188,7 +190,7 @@ class CallNotifier extends _$CallNotifier {
|
||||
// Add remote participants
|
||||
_participants.addAll(
|
||||
participants.map((p) {
|
||||
RemoteParticipant? remote;
|
||||
lk.RemoteParticipant? remote;
|
||||
for (final r in remotes) {
|
||||
if (r.identity == p.identity) {
|
||||
remote = r;
|
||||
@@ -212,11 +214,11 @@ class CallNotifier extends _$CallNotifier {
|
||||
|
||||
Future<void> joinRoom(String roomId) async {
|
||||
if (_roomId == roomId && _room != null) {
|
||||
log('[Call] Call skipped. Already has data');
|
||||
talker.info('[Call] Call skipped. Already has data');
|
||||
return;
|
||||
} else if (_room != null) {
|
||||
if (!_room!.isDisposed &&
|
||||
_room!.connectionState != ConnectionState.disconnected) {
|
||||
_room!.connectionState != lk.ConnectionState.disconnected) {
|
||||
throw Exception('Call already connected');
|
||||
}
|
||||
}
|
||||
@@ -256,15 +258,15 @@ class CallNotifier extends _$CallNotifier {
|
||||
});
|
||||
|
||||
// Connect to LiveKit
|
||||
_room = Room();
|
||||
_room = lk.Room();
|
||||
|
||||
await _room!.connect(
|
||||
endpoint,
|
||||
token,
|
||||
connectOptions: ConnectOptions(autoSubscribe: true),
|
||||
roomOptions: RoomOptions(adaptiveStream: true, dynacast: true),
|
||||
fastConnectOptions: FastConnectOptions(
|
||||
microphone: TrackOption(enabled: true),
|
||||
connectOptions: lk.ConnectOptions(autoSubscribe: true),
|
||||
roomOptions: lk.RoomOptions(adaptiveStream: true, dynacast: true),
|
||||
fastConnectOptions: lk.FastConnectOptions(
|
||||
microphone: lk.TrackOption(enabled: true),
|
||||
),
|
||||
);
|
||||
_localParticipant = _room!.localParticipant;
|
||||
@@ -273,14 +275,14 @@ class CallNotifier extends _$CallNotifier {
|
||||
_updateLiveParticipants(participants);
|
||||
|
||||
if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
|
||||
Hardware.instance.setSpeakerphoneOn(true);
|
||||
lk.Hardware.instance.setSpeakerphoneOn(true);
|
||||
}
|
||||
|
||||
// Listen for connection updates
|
||||
_room!.addListener(() {
|
||||
final wasConnected = state.isConnected;
|
||||
final isNowConnected =
|
||||
_room!.connectionState == ConnectionState.connected;
|
||||
_room!.connectionState == lk.ConnectionState.connected;
|
||||
state = state.copyWith(
|
||||
isConnected: isNowConnected,
|
||||
isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(),
|
||||
@@ -334,18 +336,43 @@ class CallNotifier extends _$CallNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleScreenShare() async {
|
||||
Future<void> toggleScreenShare(BuildContext context) async {
|
||||
if (_localParticipant != null) {
|
||||
final target = !_localParticipant!.isScreenShareEnabled();
|
||||
state = state.copyWith(isScreenSharing: target);
|
||||
await _localParticipant!.setScreenShareEnabled(target);
|
||||
|
||||
if (target && lk.lkPlatformIsDesktop()) {
|
||||
try {
|
||||
final source = await showDialog<DesktopCapturerSource>(
|
||||
context: context,
|
||||
builder: (context) => lk.ScreenSelectDialog(),
|
||||
);
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
var track = await lk.LocalVideoTrack.createScreenShareTrack(
|
||||
lk.ScreenShareCaptureOptions(
|
||||
sourceId: source.id,
|
||||
maxFrameRate: 30.0,
|
||||
captureScreenAudio: true,
|
||||
),
|
||||
);
|
||||
await _localParticipant!.publishVideoTrack(track);
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
await _localParticipant!.setScreenShareEnabled(target);
|
||||
}
|
||||
|
||||
state = state.copyWith();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleSpeakerphone() async {
|
||||
state = state.copyWith(isSpeakerphone: !state.isSpeakerphone);
|
||||
await Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone);
|
||||
await lk.Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone);
|
||||
state = state.copyWith();
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ as String?,
|
||||
/// @nodoc
|
||||
mixin _$CallParticipantLive implements DiagnosticableTreeMixin {
|
||||
|
||||
CallParticipant get participant; Participant get remoteParticipant;
|
||||
CallParticipant get participant; lk.Participant get remoteParticipant;
|
||||
/// Create a copy of CallParticipantLive
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -332,7 +332,7 @@ abstract mixin class $CallParticipantLiveCopyWith<$Res> {
|
||||
factory $CallParticipantLiveCopyWith(CallParticipantLive value, $Res Function(CallParticipantLive) _then) = _$CallParticipantLiveCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
CallParticipant participant, Participant remoteParticipant
|
||||
CallParticipant participant, lk.Participant remoteParticipant
|
||||
});
|
||||
|
||||
|
||||
@@ -353,7 +353,7 @@ class _$CallParticipantLiveCopyWithImpl<$Res>
|
||||
return _then(_self.copyWith(
|
||||
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
|
||||
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
|
||||
as Participant,
|
||||
as lk.Participant,
|
||||
));
|
||||
}
|
||||
/// Create a copy of CallParticipantLive
|
||||
@@ -444,7 +444,7 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( CallParticipant participant, Participant remoteParticipant)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( CallParticipant participant, lk.Participant remoteParticipant)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _CallParticipantLive() when $default != null:
|
||||
return $default(_that.participant,_that.remoteParticipant);case _:
|
||||
@@ -465,7 +465,7 @@ return $default(_that.participant,_that.remoteParticipant);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( CallParticipant participant, Participant remoteParticipant) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( CallParticipant participant, lk.Participant remoteParticipant) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _CallParticipantLive():
|
||||
return $default(_that.participant,_that.remoteParticipant);}
|
||||
@@ -482,7 +482,7 @@ return $default(_that.participant,_that.remoteParticipant);}
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( CallParticipant participant, Participant remoteParticipant)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( CallParticipant participant, lk.Participant remoteParticipant)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _CallParticipantLive() when $default != null:
|
||||
return $default(_that.participant,_that.remoteParticipant);case _:
|
||||
@@ -501,7 +501,7 @@ class _CallParticipantLive extends CallParticipantLive with DiagnosticableTreeMi
|
||||
|
||||
|
||||
@override final CallParticipant participant;
|
||||
@override final Participant remoteParticipant;
|
||||
@override final lk.Participant remoteParticipant;
|
||||
|
||||
/// Create a copy of CallParticipantLive
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -539,7 +539,7 @@ abstract mixin class _$CallParticipantLiveCopyWith<$Res> implements $CallPartici
|
||||
factory _$CallParticipantLiveCopyWith(_CallParticipantLive value, $Res Function(_CallParticipantLive) _then) = __$CallParticipantLiveCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
CallParticipant participant, Participant remoteParticipant
|
||||
CallParticipant participant, lk.Participant remoteParticipant
|
||||
});
|
||||
|
||||
|
||||
@@ -560,7 +560,7 @@ class __$CallParticipantLiveCopyWithImpl<$Res>
|
||||
return _then(_CallParticipantLive(
|
||||
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
|
||||
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
|
||||
as Participant,
|
||||
as lk.Participant,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'call.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$callNotifierHash() => r'eb9bd41b97e9b5e9d54007c8327edb6567458846';
|
||||
String _$callNotifierHash() => r'a8ca3f625c0db3ad9992033ae70864ce15efc281';
|
||||
|
||||
/// See also [CallNotifier].
|
||||
@ProviderFor(CallNotifier)
|
||||
50
lib/pods/chat/chat_online_count.dart
Normal file
50
lib/pods/chat/chat_online_count.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'dart:async';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
|
||||
part 'chat_online_count.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ChatOnlineCountNotifier extends _$ChatOnlineCountNotifier {
|
||||
@override
|
||||
Future<int> build(String chatroomId) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final ws = ref.watch(websocketProvider);
|
||||
|
||||
// Fetch initial online count
|
||||
final response = await apiClient.get(
|
||||
'/sphere/chat/$chatroomId/members/online',
|
||||
);
|
||||
final initialCount = response.data as int;
|
||||
|
||||
// Listen for websocket status updates
|
||||
final subscription = ws.dataStream.listen((WebSocketPacket packet) {
|
||||
if (packet.type == 'accounts.status.update') {
|
||||
final data = packet.data;
|
||||
if (data != null && data['chat_room_id'] == chatroomId) {
|
||||
final status = SnAccountStatus.fromJson(data['status']);
|
||||
var delta = status.isOnline ? 1 : -1;
|
||||
if (status.clearedAt != null &&
|
||||
status.clearedAt!.isBefore(DateTime.now())) {
|
||||
if (status.isInvisible) delta = 1;
|
||||
}
|
||||
// Update count based on online status
|
||||
state.whenData((currentCount) {
|
||||
final newCount = currentCount + delta;
|
||||
state = AsyncData(
|
||||
newCount.clamp(0, double.infinity).toInt(),
|
||||
); // Ensure non-negative
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
subscription.cancel();
|
||||
});
|
||||
|
||||
return initialCount;
|
||||
}
|
||||
}
|
||||
168
lib/pods/chat/chat_online_count.g.dart
Normal file
168
lib/pods/chat/chat_online_count.g.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'chat_online_count.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatOnlineCountNotifierHash() =>
|
||||
r'19af8fd0e9f62c65e12a68215406776085235fa3';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$ChatOnlineCountNotifier
|
||||
extends BuildlessAutoDisposeAsyncNotifier<int> {
|
||||
late final String chatroomId;
|
||||
|
||||
FutureOr<int> build(String chatroomId);
|
||||
}
|
||||
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
@ProviderFor(ChatOnlineCountNotifier)
|
||||
const chatOnlineCountNotifierProvider = ChatOnlineCountNotifierFamily();
|
||||
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
class ChatOnlineCountNotifierFamily extends Family<AsyncValue<int>> {
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
const ChatOnlineCountNotifierFamily();
|
||||
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
ChatOnlineCountNotifierProvider call(String chatroomId) {
|
||||
return ChatOnlineCountNotifierProvider(chatroomId);
|
||||
}
|
||||
|
||||
@override
|
||||
ChatOnlineCountNotifierProvider getProviderOverride(
|
||||
covariant ChatOnlineCountNotifierProvider provider,
|
||||
) {
|
||||
return call(provider.chatroomId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'chatOnlineCountNotifierProvider';
|
||||
}
|
||||
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
class ChatOnlineCountNotifierProvider
|
||||
extends AutoDisposeAsyncNotifierProviderImpl<ChatOnlineCountNotifier, int> {
|
||||
/// See also [ChatOnlineCountNotifier].
|
||||
ChatOnlineCountNotifierProvider(String chatroomId)
|
||||
: this._internal(
|
||||
() => ChatOnlineCountNotifier()..chatroomId = chatroomId,
|
||||
from: chatOnlineCountNotifierProvider,
|
||||
name: r'chatOnlineCountNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$chatOnlineCountNotifierHash,
|
||||
dependencies: ChatOnlineCountNotifierFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
ChatOnlineCountNotifierFamily._allTransitiveDependencies,
|
||||
chatroomId: chatroomId,
|
||||
);
|
||||
|
||||
ChatOnlineCountNotifierProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.chatroomId,
|
||||
}) : super.internal();
|
||||
|
||||
final String chatroomId;
|
||||
|
||||
@override
|
||||
FutureOr<int> runNotifierBuild(covariant ChatOnlineCountNotifier notifier) {
|
||||
return notifier.build(chatroomId);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(ChatOnlineCountNotifier Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: ChatOnlineCountNotifierProvider._internal(
|
||||
() => create()..chatroomId = chatroomId,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
chatroomId: chatroomId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeAsyncNotifierProviderElement<ChatOnlineCountNotifier, int>
|
||||
createElement() {
|
||||
return _ChatOnlineCountNotifierProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ChatOnlineCountNotifierProvider &&
|
||||
other.chatroomId == chatroomId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, chatroomId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin ChatOnlineCountNotifierRef on AutoDisposeAsyncNotifierProviderRef<int> {
|
||||
/// The parameter `chatroomId` of this provider.
|
||||
String get chatroomId;
|
||||
}
|
||||
|
||||
class _ChatOnlineCountNotifierProviderElement
|
||||
extends
|
||||
AutoDisposeAsyncNotifierProviderElement<ChatOnlineCountNotifier, int>
|
||||
with ChatOnlineCountNotifierRef {
|
||||
_ChatOnlineCountNotifierProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get chatroomId =>
|
||||
(origin as ChatOnlineCountNotifierProvider).chatroomId;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
5
lib/pods/chat/chat_rooms.dart
Normal file
5
lib/pods/chat/chat_rooms.dart
Normal file
@@ -0,0 +1,5 @@
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
|
||||
final isSyncingProvider = StateProvider.autoDispose<bool>((ref) => false);
|
||||
|
||||
final flashingMessagesProvider = StateProvider<Set<String>>((ref) => {});
|
||||
229
lib/pods/chat/chat_subscribe.dart
Normal file
229
lib/pods/chat/chat_subscribe.dart
Normal file
@@ -0,0 +1,229 @@
|
||||
import "dart:async";
|
||||
import "dart:convert";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/pods/lifecycle.dart";
|
||||
import "package:island/pods/chat/messages_notifier.dart";
|
||||
import "package:island/pods/websocket.dart";
|
||||
import "package:island/screens/chat/chat.dart";
|
||||
import "package:island/widgets/chat/call_button.dart";
|
||||
import "package:riverpod_annotation/riverpod_annotation.dart";
|
||||
|
||||
part 'chat_subscribe.g.dart';
|
||||
|
||||
final currentSubscribedChatIdProvider = StateProvider<String?>((ref) => null);
|
||||
|
||||
@riverpod
|
||||
class ChatSubscribeNotifier extends _$ChatSubscribeNotifier {
|
||||
late final String _roomId;
|
||||
late final SnChatRoom _chatRoom;
|
||||
late final SnChatMember _chatIdentity;
|
||||
late final MessagesNotifier _messagesNotifier;
|
||||
|
||||
final List<SnChatMember> _typingStatuses = [];
|
||||
Timer? _typingCleanupTimer;
|
||||
Timer? _typingCooldownTimer;
|
||||
Timer? _periodicSubscribeTimer;
|
||||
StreamSubscription? _wsSubscription;
|
||||
|
||||
@override
|
||||
List<SnChatMember> build(String roomId) {
|
||||
_roomId = roomId;
|
||||
final ws = ref.watch(websocketProvider);
|
||||
final chatRoomAsync = ref.watch(chatroomProvider(roomId));
|
||||
final chatIdentityAsync = ref.watch(chatroomIdentityProvider(roomId));
|
||||
_messagesNotifier = ref.watch(messagesNotifierProvider(roomId).notifier);
|
||||
|
||||
if (chatRoomAsync.isLoading || chatIdentityAsync.isLoading) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (chatRoomAsync.value == null || chatIdentityAsync.value == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
_chatRoom = chatRoomAsync.value!;
|
||||
_chatIdentity = chatIdentityAsync.value!;
|
||||
|
||||
// Subscribe to messages
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.subscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Future.microtask(
|
||||
() => ref.read(currentSubscribedChatIdProvider.notifier).state = roomId,
|
||||
);
|
||||
|
||||
// Send initial read receipt
|
||||
sendReadReceipt();
|
||||
|
||||
// Set up WebSocket listener
|
||||
_wsSubscription = ws.dataStream.listen(onMessage);
|
||||
|
||||
// Set up typing status cleanup timer
|
||||
_typingCleanupTimer = Timer.periodic(const Duration(seconds: 5), (_) {
|
||||
if (_typingStatuses.isNotEmpty) {
|
||||
// Remove typing statuses older than 5 seconds
|
||||
final now = DateTime.now();
|
||||
_typingStatuses.removeWhere((member) {
|
||||
final lastTyped =
|
||||
member.lastTyped ??
|
||||
DateTime.now().subtract(const Duration(milliseconds: 1350));
|
||||
return now.difference(lastTyped).inSeconds > 5;
|
||||
});
|
||||
state = List.of(_typingStatuses);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up periodic subscribe timer (every 5 minutes)
|
||||
_periodicSubscribeTimer = Timer.periodic(const Duration(minutes: 5), (_) {
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.subscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Listen to app lifecycle changes
|
||||
ref.listen(appLifecycleStateProvider, (previous, next) {
|
||||
final lifecycleState = next.value;
|
||||
if (lifecycleState == AppLifecycleState.paused ||
|
||||
lifecycleState == AppLifecycleState.inactive) {
|
||||
// Unsubscribe when app goes to background
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.unsubscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (lifecycleState == AppLifecycleState.resumed) {
|
||||
// Resubscribe when app comes back to foreground
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.subscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on dispose
|
||||
ref.onDispose(() {
|
||||
ref.read(currentSubscribedChatIdProvider.notifier).state = null;
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.unsubscribe',
|
||||
data: {'chat_room_id': roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
_wsSubscription?.cancel();
|
||||
_typingCleanupTimer?.cancel();
|
||||
_typingCooldownTimer?.cancel();
|
||||
_periodicSubscribeTimer?.cancel();
|
||||
});
|
||||
|
||||
return _typingStatuses;
|
||||
}
|
||||
|
||||
void onMessage(WebSocketPacket pkt) {
|
||||
if (!pkt.type.startsWith('messages')) return;
|
||||
if (['messages.read'].contains(pkt.type)) return;
|
||||
|
||||
if (pkt.type == 'messages.typing' && pkt.data?['sender'] != null) {
|
||||
if (pkt.data?['room_id'] != _chatRoom.id) return;
|
||||
if (pkt.data?['sender_id'] == _chatIdentity.id) return;
|
||||
|
||||
final sender = SnChatMember.fromJson(
|
||||
pkt.data?['sender'],
|
||||
).copyWith(lastTyped: DateTime.now());
|
||||
|
||||
// Check if the sender is already in the typing list
|
||||
final existingIndex = _typingStatuses.indexWhere(
|
||||
(member) => member.id == sender.id,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
// Update the existing entry with new timestamp
|
||||
_typingStatuses[existingIndex] = sender;
|
||||
} else {
|
||||
// Add new typing status
|
||||
_typingStatuses.add(sender);
|
||||
}
|
||||
state = List.of(_typingStatuses);
|
||||
return;
|
||||
}
|
||||
|
||||
final message = SnChatMessage.fromJson(pkt.data!);
|
||||
if (message.chatRoomId != _chatRoom.id) return;
|
||||
switch (pkt.type) {
|
||||
case 'messages.new':
|
||||
case 'messages.update':
|
||||
case 'messages.delete':
|
||||
if (message.type.startsWith('call')) {
|
||||
// Handle the ongoing call.
|
||||
ref.invalidate(ongoingCallProvider(message.chatRoomId));
|
||||
}
|
||||
_messagesNotifier.receiveMessage(message);
|
||||
// Send read receipt for new message
|
||||
sendReadReceipt();
|
||||
}
|
||||
}
|
||||
|
||||
void sendReadReceipt() {
|
||||
// Send websocket packet
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.read',
|
||||
data: {'chat_room_id': _roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void sendTypingStatus() {
|
||||
// Don't send if we're already in a cooldown period
|
||||
if (_typingCooldownTimer != null) return;
|
||||
|
||||
// Send typing status immediately
|
||||
final wsState = ref.read(websocketStateProvider.notifier);
|
||||
wsState.sendMessage(
|
||||
jsonEncode(
|
||||
WebSocketPacket(
|
||||
type: 'messages.typing',
|
||||
data: {'chat_room_id': _roomId},
|
||||
endpoint: 'sphere',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
_typingCooldownTimer = Timer(const Duration(milliseconds: 850), () {
|
||||
_typingCooldownTimer = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
176
lib/pods/chat/chat_subscribe.g.dart
Normal file
176
lib/pods/chat/chat_subscribe.g.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'chat_subscribe.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatSubscribeNotifierHash() =>
|
||||
r'c605e0c9c45df64e5ba7b65f8de9b47bde8e2b3b';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$ChatSubscribeNotifier
|
||||
extends BuildlessAutoDisposeNotifier<List<SnChatMember>> {
|
||||
late final String roomId;
|
||||
|
||||
List<SnChatMember> build(String roomId);
|
||||
}
|
||||
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
@ProviderFor(ChatSubscribeNotifier)
|
||||
const chatSubscribeNotifierProvider = ChatSubscribeNotifierFamily();
|
||||
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
class ChatSubscribeNotifierFamily extends Family<List<SnChatMember>> {
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
const ChatSubscribeNotifierFamily();
|
||||
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
ChatSubscribeNotifierProvider call(String roomId) {
|
||||
return ChatSubscribeNotifierProvider(roomId);
|
||||
}
|
||||
|
||||
@override
|
||||
ChatSubscribeNotifierProvider getProviderOverride(
|
||||
covariant ChatSubscribeNotifierProvider provider,
|
||||
) {
|
||||
return call(provider.roomId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'chatSubscribeNotifierProvider';
|
||||
}
|
||||
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
class ChatSubscribeNotifierProvider
|
||||
extends
|
||||
AutoDisposeNotifierProviderImpl<
|
||||
ChatSubscribeNotifier,
|
||||
List<SnChatMember>
|
||||
> {
|
||||
/// See also [ChatSubscribeNotifier].
|
||||
ChatSubscribeNotifierProvider(String roomId)
|
||||
: this._internal(
|
||||
() => ChatSubscribeNotifier()..roomId = roomId,
|
||||
from: chatSubscribeNotifierProvider,
|
||||
name: r'chatSubscribeNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$chatSubscribeNotifierHash,
|
||||
dependencies: ChatSubscribeNotifierFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
ChatSubscribeNotifierFamily._allTransitiveDependencies,
|
||||
roomId: roomId,
|
||||
);
|
||||
|
||||
ChatSubscribeNotifierProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.roomId,
|
||||
}) : super.internal();
|
||||
|
||||
final String roomId;
|
||||
|
||||
@override
|
||||
List<SnChatMember> runNotifierBuild(
|
||||
covariant ChatSubscribeNotifier notifier,
|
||||
) {
|
||||
return notifier.build(roomId);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(ChatSubscribeNotifier Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: ChatSubscribeNotifierProvider._internal(
|
||||
() => create()..roomId = roomId,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
roomId: roomId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeNotifierProviderElement<ChatSubscribeNotifier, List<SnChatMember>>
|
||||
createElement() {
|
||||
return _ChatSubscribeNotifierProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ChatSubscribeNotifierProvider && other.roomId == roomId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, roomId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin ChatSubscribeNotifierRef
|
||||
on AutoDisposeNotifierProviderRef<List<SnChatMember>> {
|
||||
/// The parameter `roomId` of this provider.
|
||||
String get roomId;
|
||||
}
|
||||
|
||||
class _ChatSubscribeNotifierProviderElement
|
||||
extends
|
||||
AutoDisposeNotifierProviderElement<
|
||||
ChatSubscribeNotifier,
|
||||
List<SnChatMember>
|
||||
>
|
||||
with ChatSubscribeNotifierRef {
|
||||
_ChatSubscribeNotifierProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get roomId => (origin as ChatSubscribeNotifierProvider).roomId;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/pods/chat/chat_subscribe.dart';
|
||||
|
||||
part 'chat_summary.g.dart';
|
||||
|
||||
@@ -12,9 +14,27 @@ class ChatSummary extends _$ChatSummary {
|
||||
final resp = await client.get('/sphere/chat/summary');
|
||||
|
||||
final Map<String, dynamic> data = resp.data;
|
||||
return data.map(
|
||||
final summaries = data.map(
|
||||
(key, value) => MapEntry(key, SnChatSummary.fromJson(value)),
|
||||
);
|
||||
|
||||
final ws = ref.watch(websocketProvider);
|
||||
final subscription = ws.dataStream.listen((WebSocketPacket pkt) {
|
||||
if (!pkt.type.startsWith('messages')) return;
|
||||
if (pkt.type == 'messages.new') {
|
||||
final message = SnChatMessage.fromJson(pkt.data!);
|
||||
updateLastMessage(message.chatRoomId, message);
|
||||
} else if (pkt.type == 'messages.update') {
|
||||
final message = SnChatMessage.fromJson(pkt.data!);
|
||||
updateMessageContent(message.chatRoomId, message);
|
||||
}
|
||||
});
|
||||
|
||||
ref.onDispose(() {
|
||||
subscription.cancel();
|
||||
});
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
Future<void> clearUnreadCount(String chatId) async {
|
||||
@@ -36,10 +56,12 @@ class ChatSummary extends _$ChatSummary {
|
||||
state.whenData((summaries) {
|
||||
final summary = summaries[chatId];
|
||||
if (summary != null) {
|
||||
final currentSubscribed = ref.read(currentSubscribedChatIdProvider);
|
||||
final increment = (chatId != currentSubscribed) ? 1 : 0;
|
||||
state = AsyncData({
|
||||
...summaries,
|
||||
chatId: SnChatSummary(
|
||||
unreadCount: summary.unreadCount + 1,
|
||||
unreadCount: summary.unreadCount + increment,
|
||||
lastMessage: message,
|
||||
),
|
||||
});
|
||||
@@ -61,4 +83,19 @@ class ChatSummary extends _$ChatSummary {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void updateMessageContent(String chatId, SnChatMessage message) {
|
||||
state.whenData((summaries) {
|
||||
final summary = summaries[chatId];
|
||||
if (summary != null && summary.lastMessage?.id == message.id) {
|
||||
state = AsyncData({
|
||||
...summaries,
|
||||
chatId: SnChatSummary(
|
||||
unreadCount: summary.unreadCount,
|
||||
lastMessage: message,
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ part of 'chat_summary.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatSummaryHash() => r'87a10e4cefa37dc5fa8eadb175ef1b2bed6070bf';
|
||||
String _$chatSummaryHash() => r'33815a3bd81d20902b7063e8194fe336930df9b4';
|
||||
|
||||
/// See also [ChatSummary].
|
||||
@ProviderFor(ChatSummary)
|
||||
@@ -1,5 +1,4 @@
|
||||
import "dart:async";
|
||||
import "dart:developer" as developer;
|
||||
import "package:dio/dio.dart";
|
||||
import "package:drift/drift.dart" show Variable;
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
@@ -8,15 +7,16 @@ import "package:island/database/drift_db.dart";
|
||||
import "package:island/database/message.dart";
|
||||
import "package:island/models/chat.dart";
|
||||
import "package:island/models/file.dart";
|
||||
import "package:island/pods/config.dart";
|
||||
import "package:island/pods/database.dart";
|
||||
import "package:island/pods/lifecycle.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
import "package:island/services/file.dart";
|
||||
import "package:island/services/file_uploader.dart";
|
||||
import "package:island/talker.dart";
|
||||
import "package:island/widgets/alert.dart";
|
||||
import "package:riverpod_annotation/riverpod_annotation.dart";
|
||||
import "package:uuid/uuid.dart";
|
||||
import "package:island/screens/chat/chat.dart";
|
||||
import "package:island/pods/chat_rooms.dart";
|
||||
import "package:island/pods/chat/chat_rooms.dart";
|
||||
|
||||
part 'messages_notifier.g.dart';
|
||||
|
||||
@@ -39,6 +39,8 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
bool _hasMore = true;
|
||||
bool _isSyncing = false;
|
||||
bool _isJumping = false;
|
||||
bool _isUpdatingState = false;
|
||||
DateTime? _lastPauseTime;
|
||||
|
||||
@override
|
||||
FutureOr<List<LocalChatMessage>> build(String roomId) async {
|
||||
@@ -58,21 +60,27 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
_identity = identity;
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'MessagesNotifier built for room $roomId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
talker.log('MessagesNotifier built for room $roomId');
|
||||
|
||||
// Only setup sync and lifecycle listeners if user is a member
|
||||
if (identity != null) {
|
||||
ref.listen(appLifecycleStateProvider, (_, next) {
|
||||
if (next.hasValue && next.value == AppLifecycleState.resumed) {
|
||||
developer.log(
|
||||
'App resumed, syncing messages',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
syncMessages();
|
||||
}
|
||||
next.whenData((state) {
|
||||
if (state == AppLifecycleState.paused) {
|
||||
_lastPauseTime = DateTime.now();
|
||||
talker.log('App paused, recording time');
|
||||
} else if (state == AppLifecycleState.resumed) {
|
||||
if (_lastPauseTime != null) {
|
||||
final diff = DateTime.now().difference(_lastPauseTime!);
|
||||
if (diff > const Duration(minutes: 1)) {
|
||||
talker.log('App resumed after >1 min, syncing messages');
|
||||
syncMessages();
|
||||
} else {
|
||||
talker.log('App resumed within 1 min, skipping sync');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,20 +93,43 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
return messages;
|
||||
}
|
||||
|
||||
Future<void> _updateStateSafely(List<LocalChatMessage> messages) async {
|
||||
if (_isUpdatingState) {
|
||||
talker.log('State update already in progress, skipping');
|
||||
return;
|
||||
}
|
||||
_isUpdatingState = true;
|
||||
try {
|
||||
// Ensure messages are properly sorted and deduplicated
|
||||
final sortedMessages = _sortMessages(messages);
|
||||
final uniqueMessages = <LocalChatMessage>[];
|
||||
final seenIds = <String>{};
|
||||
for (final message in sortedMessages) {
|
||||
if (seenIds.add(message.id)) {
|
||||
uniqueMessages.add(message);
|
||||
}
|
||||
}
|
||||
state = AsyncValue.data(uniqueMessages);
|
||||
} finally {
|
||||
_isUpdatingState = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<LocalChatMessage>> _getCachedMessages({
|
||||
int offset = 0,
|
||||
int take = 20,
|
||||
String? searchQuery,
|
||||
bool? withLinks,
|
||||
bool? withAttachments,
|
||||
}) async {
|
||||
developer.log(
|
||||
'Getting cached messages from offset $offset, take $take',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
talker.log('Getting cached messages from offset $offset, take $take');
|
||||
final List<LocalChatMessage> dbMessages;
|
||||
if (_searchQuery != null && _searchQuery!.isNotEmpty) {
|
||||
|
||||
if (searchQuery != null && searchQuery.isNotEmpty) {
|
||||
dbMessages = await _database.searchMessages(
|
||||
_roomId,
|
||||
_searchQuery ?? '',
|
||||
withAttachments: _withAttachments,
|
||||
searchQuery,
|
||||
withAttachments: withAttachments,
|
||||
);
|
||||
} else {
|
||||
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
||||
@@ -112,11 +143,18 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
|
||||
List<LocalChatMessage> filteredMessages = dbMessages;
|
||||
|
||||
if (_withLinks == true) {
|
||||
if (withLinks == true) {
|
||||
filteredMessages =
|
||||
filteredMessages.where((msg) => _hasLink(msg)).toList();
|
||||
}
|
||||
|
||||
if (withAttachments == true) {
|
||||
filteredMessages =
|
||||
filteredMessages
|
||||
.where((msg) => msg.toRemoteMessage().attachments.isNotEmpty)
|
||||
.toList();
|
||||
}
|
||||
|
||||
final dbLocalMessages = filteredMessages;
|
||||
|
||||
// Always ensure unique messages to prevent duplicate keys
|
||||
@@ -150,14 +188,56 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
return uniqueMessages;
|
||||
}
|
||||
|
||||
/// Get all messages without search filters for jump operations
|
||||
Future<List<LocalChatMessage>> _getAllMessagesForJump({
|
||||
int offset = 0,
|
||||
int take = 20,
|
||||
}) async {
|
||||
talker.log('Getting all messages for jump from offset $offset, take $take');
|
||||
final chatMessagesFromDb = await _database.getMessagesForRoom(
|
||||
_roomId,
|
||||
offset: offset,
|
||||
limit: take,
|
||||
);
|
||||
final dbMessages =
|
||||
chatMessagesFromDb.map(_database.companionToMessage).toList();
|
||||
|
||||
// Always ensure unique messages to prevent duplicate keys
|
||||
final uniqueMessages = <LocalChatMessage>[];
|
||||
final seenIds = <String>{};
|
||||
for (final message in dbMessages) {
|
||||
if (seenIds.add(message.id)) {
|
||||
uniqueMessages.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
if (offset == 0) {
|
||||
final pendingForRoom =
|
||||
_pendingMessages.values
|
||||
.where((msg) => msg.roomId == _roomId)
|
||||
.toList();
|
||||
|
||||
final allMessages = [...pendingForRoom, ...uniqueMessages];
|
||||
_sortMessages(allMessages);
|
||||
|
||||
final finalUniqueMessages = <LocalChatMessage>[];
|
||||
final finalSeenIds = <String>{};
|
||||
for (final message in allMessages) {
|
||||
if (finalSeenIds.add(message.id)) {
|
||||
finalUniqueMessages.add(message);
|
||||
}
|
||||
}
|
||||
return finalUniqueMessages;
|
||||
}
|
||||
|
||||
return uniqueMessages;
|
||||
}
|
||||
|
||||
Future<List<LocalChatMessage>> _fetchAndCacheMessages({
|
||||
int offset = 0,
|
||||
int take = 20,
|
||||
}) async {
|
||||
developer.log(
|
||||
'Fetching messages from API, offset $offset, take $take',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
talker.log('Fetching messages from API, offset $offset, take $take');
|
||||
if (_totalCount == null) {
|
||||
final response = await _apiClient.get(
|
||||
'/sphere/chat/$_roomId/messages',
|
||||
@@ -201,15 +281,12 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
|
||||
Future<void> syncMessages() async {
|
||||
if (_isSyncing) {
|
||||
developer.log(
|
||||
'Sync already in progress, skipping.',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
talker.log('Sync already in progress, skipping.');
|
||||
return;
|
||||
}
|
||||
_isSyncing = true;
|
||||
|
||||
developer.log('Starting message sync', name: 'MessagesNotifier');
|
||||
talker.log('Starting message sync');
|
||||
Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true);
|
||||
try {
|
||||
final dbMessages = await _database.getMessagesForRoom(
|
||||
@@ -223,10 +300,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
: _database.companionToMessage(dbMessages.first);
|
||||
|
||||
if (lastMessage == null) {
|
||||
developer.log(
|
||||
'No local messages, fetching from network',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
talker.log('No local messages, fetching from network');
|
||||
final newMessages = await _fetchAndCacheMessages(
|
||||
offset: 0,
|
||||
take: _pageSize,
|
||||
@@ -244,33 +318,19 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
);
|
||||
|
||||
final response = MessageSyncResponse.fromJson(resp.data);
|
||||
developer.log(
|
||||
'Sync response: ${response.messages.length} changes',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
talker.log('Sync response: ${response.messages.length} changes');
|
||||
for (final message in response.messages) {
|
||||
switch (message.type) {
|
||||
case "messages.update":
|
||||
case "messages.update.links":
|
||||
await receiveMessageUpdate(message);
|
||||
break;
|
||||
case "messages.delete":
|
||||
await receiveMessageDeletion(message.id.toString());
|
||||
break;
|
||||
}
|
||||
// Still need receive the message to show the history actions
|
||||
await receiveMessage(message);
|
||||
}
|
||||
} catch (err, stackTrace) {
|
||||
developer.log(
|
||||
talker.log(
|
||||
'Error syncing messages',
|
||||
name: 'MessagesNotifier',
|
||||
error: err,
|
||||
exception: err,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
developer.log('Finished message sync', name: 'MessagesNotifier');
|
||||
talker.log('Finished message sync');
|
||||
Future.microtask(
|
||||
() => ref.read(isSyncingProvider.notifier).state = false,
|
||||
);
|
||||
@@ -295,6 +355,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
final localMessages = await _getCachedMessages(
|
||||
offset: offset,
|
||||
take: take,
|
||||
searchQuery: _searchQuery,
|
||||
withLinks: _withLinks,
|
||||
withAttachments: _withAttachments,
|
||||
);
|
||||
|
||||
if (localMessages.isNotEmpty) {
|
||||
@@ -310,6 +373,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
final localMessages = await _getCachedMessages(
|
||||
offset: offset,
|
||||
take: take,
|
||||
searchQuery: _searchQuery,
|
||||
withLinks: _withLinks,
|
||||
withAttachments: _withAttachments,
|
||||
);
|
||||
|
||||
if (localMessages.isNotEmpty) {
|
||||
@@ -320,12 +386,18 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
}
|
||||
|
||||
Future<void> loadInitial() async {
|
||||
developer.log('Loading initial messages', name: 'MessagesNotifier');
|
||||
talker.log('Loading initial messages');
|
||||
if (_searchQuery == null || _searchQuery!.isEmpty) {
|
||||
syncMessages();
|
||||
}
|
||||
|
||||
final messages = await _getCachedMessages(offset: 0, take: _pageSize);
|
||||
final messages = await _getCachedMessages(
|
||||
offset: 0,
|
||||
take: _pageSize,
|
||||
searchQuery: _searchQuery,
|
||||
withLinks: _withLinks,
|
||||
withAttachments: _withAttachments,
|
||||
);
|
||||
|
||||
_hasMore = messages.length == _pageSize;
|
||||
|
||||
@@ -334,7 +406,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
|
||||
Future<void> loadMore() async {
|
||||
if (!_hasMore || state is AsyncLoading) return;
|
||||
developer.log('Loading more messages', name: 'MessagesNotifier');
|
||||
talker.log('Loading more messages');
|
||||
|
||||
try {
|
||||
final currentMessages = state.value ?? [];
|
||||
@@ -350,10 +422,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
_sortMessages([...currentMessages, ...newMessages]),
|
||||
);
|
||||
} catch (err, stackTrace) {
|
||||
developer.log(
|
||||
talker.log(
|
||||
'Error loading more messages',
|
||||
name: 'MessagesNotifier',
|
||||
error: err,
|
||||
|
||||
exception: err,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
showErrorAlert(err);
|
||||
@@ -369,13 +441,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
Function(String, Map<int, double>)? onProgress,
|
||||
}) async {
|
||||
final nonce = const Uuid().v4();
|
||||
developer.log(
|
||||
'Sending message with nonce $nonce',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
final baseUrl = ref.read(serverUrlProvider);
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Access token is null');
|
||||
talker.log('Sending message with nonce $nonce');
|
||||
|
||||
final mockMessage = SnChatMessage(
|
||||
id: 'pending_$nonce',
|
||||
@@ -404,19 +470,9 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
var cloudAttachments = List.empty(growable: true);
|
||||
for (var idx = 0; idx < attachments.length; idx++) {
|
||||
final cloudFile =
|
||||
await putFileToCloud(
|
||||
await FileUploader.createCloudFile(
|
||||
fileData: attachments[idx],
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: attachments[idx].data.name ?? 'Post media',
|
||||
mimetype:
|
||||
attachments[idx].data.mimeType ??
|
||||
switch (attachments[idx].type) {
|
||||
UniversalFileType.image => 'image/unknown',
|
||||
UniversalFileType.video => 'video/unknown',
|
||||
UniversalFileType.audio => 'audio/unknown',
|
||||
UniversalFileType.file => 'application/octet-stream',
|
||||
},
|
||||
client: ref.read(apiClientProvider),
|
||||
onProgress: (progress, _) {
|
||||
_fileUploadProgress[localMessage.id]?[idx] = progress;
|
||||
onProgress?.call(
|
||||
@@ -476,15 +532,12 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
}).toList();
|
||||
state = AsyncValue.data(newMessages);
|
||||
}
|
||||
developer.log(
|
||||
'Message with nonce $nonce sent successfully',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
talker.log('Message with nonce $nonce sent successfully');
|
||||
} catch (e, stackTrace) {
|
||||
developer.log(
|
||||
talker.log(
|
||||
'Failed to send message with nonce $nonce',
|
||||
name: 'MessagesNotifier',
|
||||
error: e,
|
||||
|
||||
exception: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
localMessage.status = MessageStatus.failed;
|
||||
@@ -506,10 +559,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
}
|
||||
|
||||
Future<void> retryMessage(String pendingMessageId) async {
|
||||
developer.log(
|
||||
'Retrying message $pendingMessageId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
talker.log('Retrying message $pendingMessageId');
|
||||
final message = await fetchMessageById(pendingMessageId);
|
||||
if (message == null) {
|
||||
throw Exception('Message not found');
|
||||
@@ -553,10 +603,10 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
}).toList();
|
||||
state = AsyncValue.data(newMessages);
|
||||
} catch (e, stackTrace) {
|
||||
developer.log(
|
||||
talker.log(
|
||||
'Failed to retry message $pendingMessageId',
|
||||
name: 'MessagesNotifier',
|
||||
error: e,
|
||||
|
||||
exception: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
message.status = MessageStatus.failed;
|
||||
@@ -579,10 +629,14 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
|
||||
Future<void> receiveMessage(SnChatMessage remoteMessage) async {
|
||||
if (remoteMessage.chatRoomId != _roomId) return;
|
||||
developer.log(
|
||||
'Received new message ${remoteMessage.id}',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
// Block message receiving during jumps to prevent list resets
|
||||
if (_isJumping) {
|
||||
talker.log('Blocking message receive during jump operation');
|
||||
return;
|
||||
}
|
||||
|
||||
talker.log('Received new message ${remoteMessage.id}');
|
||||
|
||||
final localMessage = LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
@@ -613,17 +667,37 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
_sortMessages([localMessage, ...currentMessages]),
|
||||
);
|
||||
}
|
||||
|
||||
switch (remoteMessage.type) {
|
||||
case "messages.delete":
|
||||
await receiveMessageDeletion(
|
||||
remoteMessage.meta['message_id'] ?? remoteMessage.id,
|
||||
);
|
||||
case "messages.update":
|
||||
case "messages.update.links":
|
||||
await receiveMessageUpdate(remoteMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async {
|
||||
if (remoteMessage.chatRoomId != _roomId) return;
|
||||
developer.log(
|
||||
'Received message update ${remoteMessage.id}',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
// Block message updates during jumps to prevent list resets
|
||||
if (_isJumping) {
|
||||
talker.log('Blocking message update during jump operation');
|
||||
return;
|
||||
}
|
||||
|
||||
talker.log('Received message update ${remoteMessage.id}');
|
||||
|
||||
final targetId = remoteMessage.meta['message_id'] ?? remoteMessage.id;
|
||||
final updatedMessage = LocalChatMessage.fromRemoteMessage(
|
||||
remoteMessage,
|
||||
remoteMessage.copyWith(
|
||||
id: targetId,
|
||||
meta: Map.of(remoteMessage.meta)..remove('message_id'),
|
||||
type: 'text',
|
||||
editedAt: remoteMessage.createdAt,
|
||||
),
|
||||
MessageStatus.sent,
|
||||
);
|
||||
await _database.updateMessage(_database.messageToCompanion(updatedMessage));
|
||||
@@ -639,10 +713,13 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
}
|
||||
|
||||
Future<void> receiveMessageDeletion(String messageId) async {
|
||||
developer.log(
|
||||
'Received message deletion $messageId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
// Block message deletions during jumps to prevent list resets
|
||||
if (_isJumping) {
|
||||
talker.log('Blocking message deletion during jump operation');
|
||||
return;
|
||||
}
|
||||
|
||||
talker.log('Received message deletion $messageId');
|
||||
_pendingMessages.remove(messageId);
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
@@ -679,26 +756,114 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
}
|
||||
|
||||
Future<void> deleteMessage(String messageId) async {
|
||||
developer.log('Deleting message $messageId', name: 'MessagesNotifier');
|
||||
talker.log('Deleting message $messageId');
|
||||
|
||||
// Fetch message to check its status before attempting server delete
|
||||
final message = await fetchMessageById(messageId);
|
||||
if (message == null) {
|
||||
talker.log('Message $messageId not found for deletion');
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip server delete for failed messages (never successfully sent)
|
||||
if (message.status == MessageStatus.failed) {
|
||||
talker.log('Skipping server delete for failed message $messageId');
|
||||
// For failed messages, remove them completely from the active list
|
||||
_pendingMessages.remove(messageId);
|
||||
await _database.deleteMessage(messageId);
|
||||
|
||||
final currentMessages = state.value ?? [];
|
||||
final newMessages =
|
||||
currentMessages.where((m) => m.id != messageId).toList();
|
||||
state = AsyncValue.data(newMessages);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await _apiClient.delete('/sphere/chat/$_roomId/messages/$messageId');
|
||||
await receiveMessageDeletion(messageId);
|
||||
} catch (err, stackTrace) {
|
||||
developer.log(
|
||||
talker.log(
|
||||
'Error deleting message $messageId',
|
||||
name: 'MessagesNotifier',
|
||||
error: err,
|
||||
exception: err,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
void searchMessages(String query, {bool? withLinks, bool? withAttachments}) {
|
||||
/// Get search results without updating shared state
|
||||
Future<List<LocalChatMessage>> getSearchResults(
|
||||
String query, {
|
||||
bool? withLinks,
|
||||
bool? withAttachments,
|
||||
}) async {
|
||||
final trimmedQuery = query.trim();
|
||||
final hasFilters = [withLinks, withAttachments].any((e) => e == true);
|
||||
|
||||
if (trimmedQuery.isEmpty && !hasFilters) {
|
||||
return [];
|
||||
}
|
||||
|
||||
talker.log(
|
||||
'Getting search results for query: $trimmedQuery, filters: links=$withLinks, attachments=$withAttachments',
|
||||
);
|
||||
|
||||
try {
|
||||
// When filtering without query, get more messages to ensure we find all matches
|
||||
final take = (trimmedQuery.isEmpty && hasFilters) ? 1000 : 50;
|
||||
final messages = await _getCachedMessages(
|
||||
offset: 0,
|
||||
take: take,
|
||||
searchQuery: trimmedQuery.isNotEmpty ? trimmedQuery : null,
|
||||
withLinks: withLinks,
|
||||
withAttachments: withAttachments,
|
||||
); // Limit initial search results
|
||||
return messages;
|
||||
} catch (e, stackTrace) {
|
||||
talker.log(
|
||||
'Error getting search results',
|
||||
exception: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> searchMessages(
|
||||
String query, {
|
||||
bool? withLinks,
|
||||
bool? withAttachments,
|
||||
}) async {
|
||||
_searchQuery = query.trim();
|
||||
_withLinks = withLinks;
|
||||
_withAttachments = withAttachments;
|
||||
loadInitial();
|
||||
|
||||
if (_searchQuery!.isEmpty) {
|
||||
state = AsyncValue.data([]);
|
||||
return;
|
||||
}
|
||||
|
||||
talker.log('Searching messages with query: $_searchQuery');
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
try {
|
||||
final messages = await _getCachedMessages(
|
||||
offset: 0,
|
||||
take: 50,
|
||||
searchQuery: _searchQuery,
|
||||
withLinks: _withLinks,
|
||||
withAttachments: _withAttachments,
|
||||
); // Limit initial search results
|
||||
state = AsyncValue.data(messages);
|
||||
} catch (e, stackTrace) {
|
||||
talker.log(
|
||||
'Error searching messages',
|
||||
exception: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
state = AsyncValue.error(e, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
void clearSearch() {
|
||||
@@ -709,10 +874,7 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
}
|
||||
|
||||
Future<LocalChatMessage?> fetchMessageById(String messageId) async {
|
||||
developer.log(
|
||||
'Fetching message by id $messageId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
talker.log('Fetching message by id $messageId');
|
||||
try {
|
||||
final localMessage =
|
||||
await (_database.select(_database.chatMessages)
|
||||
@@ -739,24 +901,21 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
}
|
||||
|
||||
Future<int> jumpToMessage(String messageId) async {
|
||||
developer.log(
|
||||
'Starting jump to message $messageId',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
talker.log('Starting jump to message $messageId');
|
||||
if (_isJumping) {
|
||||
developer.log(
|
||||
'Jump already in progress, skipping',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
talker.log('Jump already in progress, skipping');
|
||||
return -1;
|
||||
}
|
||||
_isJumping = true;
|
||||
|
||||
// Clear flashing messages when starting a new jump
|
||||
ref.read(flashingMessagesProvider.notifier).state = {};
|
||||
|
||||
try {
|
||||
developer.log('Fetching message $messageId', name: 'MessagesNotifier');
|
||||
talker.log('Fetching message $messageId');
|
||||
final message = await fetchMessageById(messageId);
|
||||
if (message == null) {
|
||||
developer.log('Message $messageId not found', name: 'MessagesNotifier');
|
||||
talker.log('Message $messageId not found');
|
||||
showSnackBar('messageNotFound'.tr());
|
||||
return -1;
|
||||
}
|
||||
@@ -767,19 +926,18 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
(m) => m.id == messageId,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
developer.log(
|
||||
talker.log(
|
||||
'Message $messageId already in current state at index $existingIndex, jumping directly',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
return existingIndex;
|
||||
}
|
||||
|
||||
developer.log(
|
||||
'Message $messageId not in current state, loading messages around it',
|
||||
name: 'MessagesNotifier',
|
||||
talker.log(
|
||||
'Message $messageId not in current state, calculating position and loading messages around it',
|
||||
);
|
||||
|
||||
// Count messages newer than this one
|
||||
// Count messages newer than the target message to calculate optimal offset
|
||||
// Use full message list (not filtered by search) for accurate position calculation
|
||||
final query = _database.customSelect(
|
||||
'SELECT COUNT(*) as count FROM chat_messages WHERE room_id = ? AND created_at > ?',
|
||||
variables: [
|
||||
@@ -791,29 +949,29 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
final result = await query.getSingle();
|
||||
final newerCount = result.read<int>('count');
|
||||
|
||||
// Load messages around this position
|
||||
// Calculate offset to position target message in the middle of the loaded chunk
|
||||
const chunkSize = 100; // Load 100 messages around the target
|
||||
final offset =
|
||||
(newerCount - _pageSize ~/ 2).clamp(0, double.infinity).toInt();
|
||||
developer.log(
|
||||
'Loading messages with offset $offset, take $_pageSize',
|
||||
name: 'MessagesNotifier',
|
||||
(newerCount - chunkSize ~/ 2).clamp(0, double.infinity).toInt();
|
||||
talker.log(
|
||||
'Calculated offset $offset for target message (newer: $newerCount, chunk: $chunkSize)',
|
||||
);
|
||||
final loadedMessages = await _getCachedMessages(
|
||||
// Use full message list (not filtered by search) for jump operations
|
||||
final loadedMessages = await _getAllMessagesForJump(
|
||||
offset: offset,
|
||||
take: _pageSize,
|
||||
take: chunkSize,
|
||||
);
|
||||
|
||||
// Check if loaded messages are already in current state
|
||||
final currentIds = currentMessages.map((m) => m.id).toSet();
|
||||
final newMessages =
|
||||
loadedMessages.where((m) => !currentIds.contains(m.id)).toList();
|
||||
developer.log(
|
||||
talker.log(
|
||||
'Loaded ${loadedMessages.length} messages, ${newMessages.length} are new',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
|
||||
if (newMessages.isNotEmpty) {
|
||||
// Merge with current messages
|
||||
// Merge with current messages more safely
|
||||
final allMessages = [...currentMessages, ...newMessages];
|
||||
final uniqueMessages = <LocalChatMessage>[];
|
||||
final seenIds = <String>{};
|
||||
@@ -822,21 +980,37 @@ class MessagesNotifier extends _$MessagesNotifier {
|
||||
uniqueMessages.add(message);
|
||||
}
|
||||
}
|
||||
_sortMessages(uniqueMessages);
|
||||
state = AsyncValue.data(uniqueMessages);
|
||||
developer.log(
|
||||
await _updateStateSafely(uniqueMessages);
|
||||
talker.log(
|
||||
'Updated state with ${uniqueMessages.length} total messages',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
}
|
||||
|
||||
// Wait a bit for the UI to rebuild with new messages
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
final finalIndex = (state.value ?? []).indexWhere(
|
||||
(m) => m.id == messageId,
|
||||
);
|
||||
developer.log(
|
||||
'Final index for message $messageId is $finalIndex',
|
||||
name: 'MessagesNotifier',
|
||||
);
|
||||
talker.log('Final index for message $messageId is $finalIndex');
|
||||
|
||||
// Verify the message is actually in the list before returning
|
||||
if (finalIndex == -1) {
|
||||
talker.log(
|
||||
'Message $messageId still not found after loading, trying direct fetch',
|
||||
);
|
||||
// Try to fetch and add the specific message if it's still not found
|
||||
final directMessage = await fetchMessageById(messageId);
|
||||
if (directMessage != null) {
|
||||
final currentList = state.value ?? [];
|
||||
final updatedList = [...currentList, directMessage];
|
||||
await _updateStateSafely(updatedList);
|
||||
final newIndex = updatedList.indexWhere((m) => m.id == messageId);
|
||||
talker.log('Added message directly, new index: $newIndex');
|
||||
return newIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return finalIndex;
|
||||
} finally {
|
||||
_isJumping = false;
|
||||
@@ -6,7 +6,7 @@ part of 'messages_notifier.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$messagesNotifierHash() => r'37bab723531a5c5248471ef810b74cf57b3dc237';
|
||||
String _$messagesNotifierHash() => r'6adefd9152cdd686c2a863964993f24c42d405b5';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/pods/theme.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
part 'config.freezed.dart';
|
||||
part 'config.g.dart';
|
||||
@@ -17,6 +19,7 @@ const kAppbarTransparentStoreKey = 'app_bar_transparent';
|
||||
const kAppBackgroundStoreKey = 'app_has_background';
|
||||
const kAppShowBackgroundImage = 'app_show_background_image';
|
||||
const kAppColorSchemeStoreKey = 'app_color_scheme';
|
||||
const kAppCustomColorsStoreKey = 'app_custom_colors';
|
||||
const kAppNotifyWithHaptic = 'app_notify_with_haptic';
|
||||
const kAppCustomFonts = 'app_custom_fonts';
|
||||
const kAppAutoTranslate = 'app_auto_translate';
|
||||
@@ -24,9 +27,14 @@ const kAppDataSavingMode = 'app_data_saving_mode';
|
||||
const kAppSoundEffects = 'app_sound_effects';
|
||||
const kAppAprilFoolFeatures = 'app_april_fool_features';
|
||||
const kAppWindowSize = 'app_window_size';
|
||||
const kAppWindowOpacity = 'app_window_opacity';
|
||||
const kAppCardTransparent = 'app_card_transparent';
|
||||
const kAppEnterToSend = 'app_enter_to_send';
|
||||
const kAppDefaultPoolId = 'app_default_pool_id';
|
||||
const kAppMessageDisplayStyle = 'app_message_display_style';
|
||||
const kAppThemeMode = 'app_theme_mode';
|
||||
const kMaterialYouToggleStoreKey = 'app_theme_material_you';
|
||||
const kAppDisableAnimation = 'app_disable_animation';
|
||||
const kFeaturedPostsCollapsedId =
|
||||
'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post
|
||||
|
||||
@@ -54,6 +62,21 @@ final serverUrlProvider = Provider<String>((ref) {
|
||||
return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
|
||||
});
|
||||
|
||||
@freezed
|
||||
sealed class ThemeColors with _$ThemeColors {
|
||||
factory ThemeColors({
|
||||
int? primary,
|
||||
int? secondary,
|
||||
int? tertiary,
|
||||
int? surface,
|
||||
int? background,
|
||||
int? error,
|
||||
}) = _ThemeColors;
|
||||
|
||||
factory ThemeColors.fromJson(Map<String, dynamic> json) =>
|
||||
_$ThemeColorsFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class AppSettings with _$AppSettings {
|
||||
const factory AppSettings({
|
||||
@@ -66,9 +89,15 @@ sealed class AppSettings with _$AppSettings {
|
||||
required bool showBackgroundImage,
|
||||
required String? customFonts,
|
||||
required int? appColorScheme, // The color stored via the int type
|
||||
required ThemeColors? customColors,
|
||||
required Size? windowSize, // The window size for desktop platforms
|
||||
required double windowOpacity, // The window opacity for desktop platforms
|
||||
required double cardTransparency, // The card background opacity
|
||||
required String? defaultPoolId,
|
||||
required String messageDisplayStyle,
|
||||
required String? themeMode,
|
||||
required bool useMaterial3,
|
||||
required bool disableAnimation,
|
||||
}) = _AppSettings;
|
||||
}
|
||||
|
||||
@@ -87,9 +116,15 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
||||
showBackgroundImage: prefs.getBool(kAppShowBackgroundImage) ?? true,
|
||||
customFonts: prefs.getString(kAppCustomFonts),
|
||||
appColorScheme: prefs.getInt(kAppColorSchemeStoreKey),
|
||||
customColors: _getThemeColorsFromPrefs(prefs),
|
||||
windowSize: _getWindowSizeFromPrefs(prefs),
|
||||
windowOpacity: prefs.getDouble(kAppWindowOpacity) ?? 1.0,
|
||||
cardTransparency: prefs.getDouble(kAppCardTransparent) ?? 1.0,
|
||||
defaultPoolId: prefs.getString(kAppDefaultPoolId),
|
||||
messageDisplayStyle: prefs.getString(kAppMessageDisplayStyle) ?? 'bubble',
|
||||
themeMode: prefs.getString(kAppThemeMode) ?? 'system',
|
||||
useMaterial3: prefs.getBool(kMaterialYouToggleStoreKey) ?? true,
|
||||
disableAnimation: prefs.getBool(kAppDisableAnimation) ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,6 +145,18 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
||||
return null;
|
||||
}
|
||||
|
||||
ThemeColors? _getThemeColorsFromPrefs(SharedPreferences prefs) {
|
||||
final jsonString = prefs.getString(kAppCustomColorsStoreKey);
|
||||
if (jsonString == null) return null;
|
||||
|
||||
try {
|
||||
final json = jsonDecode(jsonString);
|
||||
return ThemeColors.fromJson(json);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void setDefaultPoolId(String? value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
if (value != null) {
|
||||
@@ -154,7 +201,6 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setBool(kAppbarTransparentStoreKey, value);
|
||||
state = state.copyWith(appBarTransparent: value);
|
||||
ref.read(themeProvider.notifier).reloadTheme();
|
||||
}
|
||||
|
||||
void setShowBackgroundImage(bool value) {
|
||||
@@ -167,14 +213,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setString(kAppCustomFonts, value ?? '');
|
||||
state = state.copyWith(customFonts: value);
|
||||
ref.read(themeProvider.notifier).reloadTheme();
|
||||
}
|
||||
|
||||
void setAppColorScheme(int? value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setInt(kAppColorSchemeStoreKey, value ?? 0);
|
||||
state = state.copyWith(appColorScheme: value);
|
||||
ref.read(themeProvider.notifier).reloadTheme();
|
||||
}
|
||||
|
||||
void setWindowSize(Size? size) {
|
||||
@@ -196,6 +240,48 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
||||
prefs.setString(kAppMessageDisplayStyle, value);
|
||||
state = state.copyWith(messageDisplayStyle: value);
|
||||
}
|
||||
|
||||
void setWindowOpacity(double value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setDouble(kAppWindowOpacity, value);
|
||||
state = state.copyWith(windowOpacity: value);
|
||||
Future(() => windowManager.setOpacity(value));
|
||||
}
|
||||
|
||||
void setThemeMode(String value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setString(kAppThemeMode, value);
|
||||
state = state.copyWith(themeMode: value);
|
||||
}
|
||||
|
||||
void setAppTransparentBackground(double value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setDouble(kAppCardTransparent, value);
|
||||
state = state.copyWith(cardTransparency: value);
|
||||
}
|
||||
|
||||
void setUseMaterial3(bool value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setBool(kMaterialYouToggleStoreKey, value);
|
||||
state = state.copyWith(useMaterial3: value);
|
||||
}
|
||||
|
||||
void setCustomColors(ThemeColors? value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
if (value != null) {
|
||||
final json = jsonEncode(value.toJson());
|
||||
prefs.setString(kAppCustomColorsStoreKey, json);
|
||||
} else {
|
||||
prefs.remove(kAppCustomColorsStoreKey);
|
||||
}
|
||||
state = state.copyWith(customColors: value);
|
||||
}
|
||||
|
||||
void setDisableAnimation(bool value) {
|
||||
final prefs = ref.read(sharedPreferencesProvider);
|
||||
prefs.setBool(kAppDisableAnimation, value);
|
||||
state = state.copyWith(disableAnimation: value);
|
||||
}
|
||||
}
|
||||
|
||||
final updateInfoProvider =
|
||||
|
||||
@@ -11,12 +11,286 @@ part of 'config.dart';
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$ThemeColors {
|
||||
|
||||
int? get primary; int? get secondary; int? get tertiary; int? get surface; int? get background; int? get error;
|
||||
/// Create a copy of ThemeColors
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ThemeColorsCopyWith<ThemeColors> get copyWith => _$ThemeColorsCopyWithImpl<ThemeColors>(this as ThemeColors, _$identity);
|
||||
|
||||
/// Serializes this ThemeColors to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ThemeColors&&(identical(other.primary, primary) || other.primary == primary)&&(identical(other.secondary, secondary) || other.secondary == secondary)&&(identical(other.tertiary, tertiary) || other.tertiary == tertiary)&&(identical(other.surface, surface) || other.surface == surface)&&(identical(other.background, background) || other.background == background)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,primary,secondary,tertiary,surface,background,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ThemeColors(primary: $primary, secondary: $secondary, tertiary: $tertiary, surface: $surface, background: $background, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ThemeColorsCopyWith<$Res> {
|
||||
factory $ThemeColorsCopyWith(ThemeColors value, $Res Function(ThemeColors) _then) = _$ThemeColorsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ThemeColorsCopyWithImpl<$Res>
|
||||
implements $ThemeColorsCopyWith<$Res> {
|
||||
_$ThemeColorsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ThemeColors _self;
|
||||
final $Res Function(ThemeColors) _then;
|
||||
|
||||
/// Create a copy of ThemeColors
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? primary = freezed,Object? secondary = freezed,Object? tertiary = freezed,Object? surface = freezed,Object? background = freezed,Object? error = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
primary: freezed == primary ? _self.primary : primary // ignore: cast_nullable_to_non_nullable
|
||||
as int?,secondary: freezed == secondary ? _self.secondary : secondary // ignore: cast_nullable_to_non_nullable
|
||||
as int?,tertiary: freezed == tertiary ? _self.tertiary : tertiary // ignore: cast_nullable_to_non_nullable
|
||||
as int?,surface: freezed == surface ? _self.surface : surface // ignore: cast_nullable_to_non_nullable
|
||||
as int?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
|
||||
as int?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ThemeColors].
|
||||
extension ThemeColorsPatterns on ThemeColors {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ThemeColors value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ThemeColors() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ThemeColors value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ThemeColors():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ThemeColors value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ThemeColors() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ThemeColors() when $default != null:
|
||||
return $default(_that.primary,_that.secondary,_that.tertiary,_that.surface,_that.background,_that.error);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ThemeColors():
|
||||
return $default(_that.primary,_that.secondary,_that.tertiary,_that.surface,_that.background,_that.error);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ThemeColors() when $default != null:
|
||||
return $default(_that.primary,_that.secondary,_that.tertiary,_that.surface,_that.background,_that.error);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _ThemeColors implements ThemeColors {
|
||||
_ThemeColors({this.primary, this.secondary, this.tertiary, this.surface, this.background, this.error});
|
||||
factory _ThemeColors.fromJson(Map<String, dynamic> json) => _$ThemeColorsFromJson(json);
|
||||
|
||||
@override final int? primary;
|
||||
@override final int? secondary;
|
||||
@override final int? tertiary;
|
||||
@override final int? surface;
|
||||
@override final int? background;
|
||||
@override final int? error;
|
||||
|
||||
/// Create a copy of ThemeColors
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ThemeColorsCopyWith<_ThemeColors> get copyWith => __$ThemeColorsCopyWithImpl<_ThemeColors>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$ThemeColorsToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ThemeColors&&(identical(other.primary, primary) || other.primary == primary)&&(identical(other.secondary, secondary) || other.secondary == secondary)&&(identical(other.tertiary, tertiary) || other.tertiary == tertiary)&&(identical(other.surface, surface) || other.surface == surface)&&(identical(other.background, background) || other.background == background)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,primary,secondary,tertiary,surface,background,error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ThemeColors(primary: $primary, secondary: $secondary, tertiary: $tertiary, surface: $surface, background: $background, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ThemeColorsCopyWith<$Res> implements $ThemeColorsCopyWith<$Res> {
|
||||
factory _$ThemeColorsCopyWith(_ThemeColors value, $Res Function(_ThemeColors) _then) = __$ThemeColorsCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
int? primary, int? secondary, int? tertiary, int? surface, int? background, int? error
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ThemeColorsCopyWithImpl<$Res>
|
||||
implements _$ThemeColorsCopyWith<$Res> {
|
||||
__$ThemeColorsCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ThemeColors _self;
|
||||
final $Res Function(_ThemeColors) _then;
|
||||
|
||||
/// Create a copy of ThemeColors
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? primary = freezed,Object? secondary = freezed,Object? tertiary = freezed,Object? surface = freezed,Object? background = freezed,Object? error = freezed,}) {
|
||||
return _then(_ThemeColors(
|
||||
primary: freezed == primary ? _self.primary : primary // ignore: cast_nullable_to_non_nullable
|
||||
as int?,secondary: freezed == secondary ? _self.secondary : secondary // ignore: cast_nullable_to_non_nullable
|
||||
as int?,tertiary: freezed == tertiary ? _self.tertiary : tertiary // ignore: cast_nullable_to_non_nullable
|
||||
as int?,surface: freezed == surface ? _self.surface : surface // ignore: cast_nullable_to_non_nullable
|
||||
as int?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
|
||||
as int?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$AppSettings {
|
||||
|
||||
bool get autoTranslate; bool get dataSavingMode; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; bool get showBackgroundImage; String? get customFonts; int? get appColorScheme;// The color stored via the int type
|
||||
Size? get windowSize;// The window size for desktop platforms
|
||||
String? get defaultPoolId; String get messageDisplayStyle;
|
||||
ThemeColors? get customColors; Size? get windowSize;// The window size for desktop platforms
|
||||
double get windowOpacity;// The window opacity for desktop platforms
|
||||
double get cardTransparency;// The card background opacity
|
||||
String? get defaultPoolId; String get messageDisplayStyle; String? get themeMode; bool get useMaterial3; bool get disableAnimation;
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -27,16 +301,16 @@ $AppSettingsCopyWith<AppSettings> get copyWith => _$AppSettingsCopyWithImpl<AppS
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.customColors, customColors) || other.customColors == customColors)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.useMaterial3, useMaterial3) || other.useMaterial3 == useMaterial3)&&(identical(other.disableAnimation, disableAnimation) || other.disableAnimation == disableAnimation));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId,messageDisplayStyle);
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3,disableAnimation);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)';
|
||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3, disableAnimation: $disableAnimation)';
|
||||
}
|
||||
|
||||
|
||||
@@ -47,11 +321,11 @@ abstract mixin class $AppSettingsCopyWith<$Res> {
|
||||
factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle
|
||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation
|
||||
});
|
||||
|
||||
|
||||
|
||||
$ThemeColorsCopyWith<$Res>? get customColors;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -64,7 +338,7 @@ class _$AppSettingsCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,Object? disableAnimation = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
|
||||
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
|
||||
@@ -75,13 +349,31 @@ as bool,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent :
|
||||
as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundImage : showBackgroundImage // ignore: cast_nullable_to_non_nullable
|
||||
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
|
||||
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
|
||||
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
|
||||
as Size?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,customColors: freezed == customColors ? _self.customColors : customColors // ignore: cast_nullable_to_non_nullable
|
||||
as ThemeColors?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
|
||||
as Size?,windowOpacity: null == windowOpacity ? _self.windowOpacity : windowOpacity // ignore: cast_nullable_to_non_nullable
|
||||
as double,cardTransparency: null == cardTransparency ? _self.cardTransparency : cardTransparency // ignore: cast_nullable_to_non_nullable
|
||||
as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as String,themeMode: freezed == themeMode ? _self.themeMode : themeMode // ignore: cast_nullable_to_non_nullable
|
||||
as String?,useMaterial3: null == useMaterial3 ? _self.useMaterial3 : useMaterial3 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,disableAnimation: null == disableAnimation ? _self.disableAnimation : disableAnimation // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ThemeColorsCopyWith<$Res>? get customColors {
|
||||
if (_self.customColors == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $ThemeColorsCopyWith<$Res>(_self.customColors!, (value) {
|
||||
return _then(_self.copyWith(customColors: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -160,10 +452,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppSettings() when $default != null:
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);case _:
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3,_that.disableAnimation);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -181,10 +473,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppSettings():
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);}
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3,_that.disableAnimation);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -198,10 +490,10 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppSettings() when $default != null:
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize,_that.defaultPoolId,_that.messageDisplayStyle);case _:
|
||||
return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.customColors,_that.windowSize,_that.windowOpacity,_that.cardTransparency,_that.defaultPoolId,_that.messageDisplayStyle,_that.themeMode,_that.useMaterial3,_that.disableAnimation);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -213,7 +505,7 @@ return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_tha
|
||||
|
||||
|
||||
class _AppSettings implements AppSettings {
|
||||
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.windowSize, required this.defaultPoolId, required this.messageDisplayStyle});
|
||||
const _AppSettings({required this.autoTranslate, required this.dataSavingMode, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.showBackgroundImage, required this.customFonts, required this.appColorScheme, required this.customColors, required this.windowSize, required this.windowOpacity, required this.cardTransparency, required this.defaultPoolId, required this.messageDisplayStyle, required this.themeMode, required this.useMaterial3, required this.disableAnimation});
|
||||
|
||||
|
||||
@override final bool autoTranslate;
|
||||
@@ -226,10 +518,18 @@ class _AppSettings implements AppSettings {
|
||||
@override final String? customFonts;
|
||||
@override final int? appColorScheme;
|
||||
// The color stored via the int type
|
||||
@override final ThemeColors? customColors;
|
||||
@override final Size? windowSize;
|
||||
// The window size for desktop platforms
|
||||
@override final double windowOpacity;
|
||||
// The window opacity for desktop platforms
|
||||
@override final double cardTransparency;
|
||||
// The card background opacity
|
||||
@override final String? defaultPoolId;
|
||||
@override final String messageDisplayStyle;
|
||||
@override final String? themeMode;
|
||||
@override final bool useMaterial3;
|
||||
@override final bool disableAnimation;
|
||||
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -241,16 +541,16 @@ _$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.autoTranslate, autoTranslate) || other.autoTranslate == autoTranslate)&&(identical(other.dataSavingMode, dataSavingMode) || other.dataSavingMode == dataSavingMode)&&(identical(other.soundEffects, soundEffects) || other.soundEffects == soundEffects)&&(identical(other.aprilFoolFeatures, aprilFoolFeatures) || other.aprilFoolFeatures == aprilFoolFeatures)&&(identical(other.enterToSend, enterToSend) || other.enterToSend == enterToSend)&&(identical(other.appBarTransparent, appBarTransparent) || other.appBarTransparent == appBarTransparent)&&(identical(other.showBackgroundImage, showBackgroundImage) || other.showBackgroundImage == showBackgroundImage)&&(identical(other.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.customColors, customColors) || other.customColors == customColors)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)&&(identical(other.windowOpacity, windowOpacity) || other.windowOpacity == windowOpacity)&&(identical(other.cardTransparency, cardTransparency) || other.cardTransparency == cardTransparency)&&(identical(other.defaultPoolId, defaultPoolId) || other.defaultPoolId == defaultPoolId)&&(identical(other.messageDisplayStyle, messageDisplayStyle) || other.messageDisplayStyle == messageDisplayStyle)&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.useMaterial3, useMaterial3) || other.useMaterial3 == useMaterial3)&&(identical(other.disableAnimation, disableAnimation) || other.disableAnimation == disableAnimation));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize,defaultPoolId,messageDisplayStyle);
|
||||
int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,customColors,windowSize,windowOpacity,cardTransparency,defaultPoolId,messageDisplayStyle,themeMode,useMaterial3,disableAnimation);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle)';
|
||||
return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, customColors: $customColors, windowSize: $windowSize, windowOpacity: $windowOpacity, cardTransparency: $cardTransparency, defaultPoolId: $defaultPoolId, messageDisplayStyle: $messageDisplayStyle, themeMode: $themeMode, useMaterial3: $useMaterial3, disableAnimation: $disableAnimation)';
|
||||
}
|
||||
|
||||
|
||||
@@ -261,11 +561,11 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith
|
||||
factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize, String? defaultPoolId, String messageDisplayStyle
|
||||
bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, ThemeColors? customColors, Size? windowSize, double windowOpacity, double cardTransparency, String? defaultPoolId, String messageDisplayStyle, String? themeMode, bool useMaterial3, bool disableAnimation
|
||||
});
|
||||
|
||||
|
||||
|
||||
@override $ThemeColorsCopyWith<$Res>? get customColors;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
@@ -278,7 +578,7 @@ class __$AppSettingsCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? autoTranslate = null,Object? dataSavingMode = null,Object? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? showBackgroundImage = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? customColors = freezed,Object? windowSize = freezed,Object? windowOpacity = null,Object? cardTransparency = null,Object? defaultPoolId = freezed,Object? messageDisplayStyle = null,Object? themeMode = freezed,Object? useMaterial3 = null,Object? disableAnimation = null,}) {
|
||||
return _then(_AppSettings(
|
||||
autoTranslate: null == autoTranslate ? _self.autoTranslate : autoTranslate // ignore: cast_nullable_to_non_nullable
|
||||
as bool,dataSavingMode: null == dataSavingMode ? _self.dataSavingMode : dataSavingMode // ignore: cast_nullable_to_non_nullable
|
||||
@@ -289,14 +589,32 @@ as bool,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent :
|
||||
as bool,showBackgroundImage: null == showBackgroundImage ? _self.showBackgroundImage : showBackgroundImage // ignore: cast_nullable_to_non_nullable
|
||||
as bool,customFonts: freezed == customFonts ? _self.customFonts : customFonts // ignore: cast_nullable_to_non_nullable
|
||||
as String?,appColorScheme: freezed == appColorScheme ? _self.appColorScheme : appColorScheme // ignore: cast_nullable_to_non_nullable
|
||||
as int?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
|
||||
as Size?,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
||||
as int?,customColors: freezed == customColors ? _self.customColors : customColors // ignore: cast_nullable_to_non_nullable
|
||||
as ThemeColors?,windowSize: freezed == windowSize ? _self.windowSize : windowSize // ignore: cast_nullable_to_non_nullable
|
||||
as Size?,windowOpacity: null == windowOpacity ? _self.windowOpacity : windowOpacity // ignore: cast_nullable_to_non_nullable
|
||||
as double,cardTransparency: null == cardTransparency ? _self.cardTransparency : cardTransparency // ignore: cast_nullable_to_non_nullable
|
||||
as double,defaultPoolId: freezed == defaultPoolId ? _self.defaultPoolId : defaultPoolId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,messageDisplayStyle: null == messageDisplayStyle ? _self.messageDisplayStyle : messageDisplayStyle // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
as String,themeMode: freezed == themeMode ? _self.themeMode : themeMode // ignore: cast_nullable_to_non_nullable
|
||||
as String?,useMaterial3: null == useMaterial3 ? _self.useMaterial3 : useMaterial3 // ignore: cast_nullable_to_non_nullable
|
||||
as bool,disableAnimation: null == disableAnimation ? _self.disableAnimation : disableAnimation // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of AppSettings
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ThemeColorsCopyWith<$Res>? get customColors {
|
||||
if (_self.customColors == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $ThemeColorsCopyWith<$Res>(_self.customColors!, (value) {
|
||||
return _then(_self.copyWith(customColors: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
||||
@@ -2,12 +2,35 @@
|
||||
|
||||
part of 'config.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_ThemeColors _$ThemeColorsFromJson(Map<String, dynamic> json) => _ThemeColors(
|
||||
primary: (json['primary'] as num?)?.toInt(),
|
||||
secondary: (json['secondary'] as num?)?.toInt(),
|
||||
tertiary: (json['tertiary'] as num?)?.toInt(),
|
||||
surface: (json['surface'] as num?)?.toInt(),
|
||||
background: (json['background'] as num?)?.toInt(),
|
||||
error: (json['error'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ThemeColorsToJson(_ThemeColors instance) =>
|
||||
<String, dynamic>{
|
||||
'primary': instance.primary,
|
||||
'secondary': instance.secondary,
|
||||
'tertiary': instance.tertiary,
|
||||
'surface': instance.surface,
|
||||
'background': instance.background,
|
||||
'error': instance.error,
|
||||
};
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$appSettingsNotifierHash() =>
|
||||
r'9f0979f18b107e61185391e7c39bd81ac4b8ca50';
|
||||
r'10ebd893c39d24ae588a4c0bf4144ee4656465a4';
|
||||
|
||||
/// See also [AppSettingsNotifier].
|
||||
@ProviderFor(AppSettingsNotifier)
|
||||
|
||||
@@ -2,10 +2,6 @@ import "dart:async";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
|
||||
final isSyncingProvider = StateProvider.autoDispose<bool>((ref) => false);
|
||||
|
||||
final flashingMessagesProvider = StateProvider<Set<String>>((ref) => {});
|
||||
|
||||
final appLifecycleStateProvider = StreamProvider<AppLifecycleState>((ref) {
|
||||
final controller = StreamController<AppLifecycleState>();
|
||||
|
||||
@@ -10,6 +10,8 @@ import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:island/models/auth.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:talker_dio_logger/talker_dio_logger.dart';
|
||||
import 'package:island/talker.dart';
|
||||
|
||||
import 'config.dart';
|
||||
|
||||
@@ -75,7 +77,7 @@ final apiClientProvider = Provider<Dio>((ref) {
|
||||
),
|
||||
);
|
||||
|
||||
dio.interceptors.add(
|
||||
dio.interceptors.addAll([
|
||||
InterceptorsWrapper(
|
||||
onRequest: (
|
||||
RequestOptions options,
|
||||
@@ -97,7 +99,17 @@ final apiClientProvider = Provider<Dio>((ref) {
|
||||
return handler.next(options);
|
||||
},
|
||||
),
|
||||
);
|
||||
TalkerDioLogger(
|
||||
talker: talker,
|
||||
settings: const TalkerDioLoggerSettings(
|
||||
printRequestHeaders: false,
|
||||
printResponseHeaders: false,
|
||||
printResponseMessage: false,
|
||||
printRequestData: false,
|
||||
printResponseData: false,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
return dio;
|
||||
});
|
||||
|
||||
@@ -1,38 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
final themeProvider = StateNotifierProvider<ThemeNotifier, ThemeSet?>((ref) {
|
||||
return ThemeNotifier();
|
||||
});
|
||||
part 'theme.g.dart';
|
||||
|
||||
class ThemeNotifier extends StateNotifier<ThemeSet?> {
|
||||
ThemeNotifier() : super(null) {
|
||||
_loadTheme();
|
||||
}
|
||||
|
||||
Future<void> _loadTheme() async {
|
||||
final theme = await createAppThemeSet();
|
||||
state = theme;
|
||||
}
|
||||
|
||||
void reloadTheme({
|
||||
Color? seedColorOverride,
|
||||
bool? useMaterial3,
|
||||
String? customFonts,
|
||||
}) async {
|
||||
final theme = await createAppThemeSet(
|
||||
seedColorOverride: seedColorOverride,
|
||||
useMaterial3: useMaterial3,
|
||||
customFonts: customFonts,
|
||||
);
|
||||
state = theme;
|
||||
}
|
||||
@riverpod
|
||||
ThemeSet theme(Ref ref) {
|
||||
final settings = ref.watch(appSettingsNotifierProvider);
|
||||
return createAppThemeSet(settings);
|
||||
}
|
||||
|
||||
const kMaterialYouToggleStoreKey = 'app_theme_material_you';
|
||||
|
||||
class ThemeSet {
|
||||
ThemeData light;
|
||||
ThemeData dark;
|
||||
@@ -40,52 +18,50 @@ class ThemeSet {
|
||||
ThemeSet({required this.light, required this.dark});
|
||||
}
|
||||
|
||||
Future<ThemeSet> createAppThemeSet({
|
||||
Color? seedColorOverride,
|
||||
bool? useMaterial3,
|
||||
String? customFonts,
|
||||
}) async {
|
||||
ThemeSet createAppThemeSet(AppSettings settings) {
|
||||
return ThemeSet(
|
||||
light: await createAppTheme(
|
||||
Brightness.light,
|
||||
useMaterial3: useMaterial3,
|
||||
customFonts: customFonts,
|
||||
),
|
||||
dark: await createAppTheme(
|
||||
Brightness.dark,
|
||||
useMaterial3: useMaterial3,
|
||||
customFonts: customFonts,
|
||||
),
|
||||
light: createAppTheme(Brightness.light, settings),
|
||||
dark: createAppTheme(Brightness.dark, settings),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ThemeData> createAppTheme(
|
||||
Brightness brightness, {
|
||||
Color? seedColorOverride,
|
||||
bool? useMaterial3,
|
||||
String? customFonts,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final seedColorString = prefs.getInt(kAppColorSchemeStoreKey);
|
||||
ThemeData createAppTheme(Brightness brightness, AppSettings settings) {
|
||||
final seedColor =
|
||||
seedColorString != null ? Color(seedColorString) : Colors.indigo;
|
||||
settings.appColorScheme != null
|
||||
? Color(settings.appColorScheme!)
|
||||
: Colors.indigo;
|
||||
|
||||
final colorScheme = ColorScheme.fromSeed(
|
||||
seedColor: seedColorOverride ?? seedColor,
|
||||
var colorScheme = ColorScheme.fromSeed(
|
||||
seedColor: seedColor,
|
||||
brightness: brightness,
|
||||
);
|
||||
|
||||
final hasAppBarTransparent =
|
||||
prefs.getBool(kAppbarTransparentStoreKey) ?? false;
|
||||
final useM3 =
|
||||
useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
|
||||
final customColors = settings.customColors;
|
||||
if (customColors != null) {
|
||||
colorScheme = colorScheme.copyWith(
|
||||
primary:
|
||||
customColors.primary != null ? Color(customColors.primary!) : null,
|
||||
secondary:
|
||||
customColors.secondary != null
|
||||
? Color(customColors.secondary!)
|
||||
: null,
|
||||
tertiary:
|
||||
customColors.tertiary != null ? Color(customColors.tertiary!) : null,
|
||||
surface:
|
||||
customColors.surface != null ? Color(customColors.surface!) : null,
|
||||
background:
|
||||
customColors.background != null
|
||||
? Color(customColors.background!)
|
||||
: null,
|
||||
error: customColors.error != null ? Color(customColors.error!) : null,
|
||||
);
|
||||
}
|
||||
|
||||
final hasAppBarTransparent = settings.appBarTransparent;
|
||||
final useM3 = settings.useMaterial3;
|
||||
|
||||
final inUseFonts =
|
||||
(customFonts ?? prefs.getString(kAppCustomFonts))
|
||||
?.split(',')
|
||||
.map((ele) => ele.trim())
|
||||
.toList() ??
|
||||
settings.customFonts?.split(',').map((ele) => ele.trim()).toList() ??
|
||||
['Nunito'];
|
||||
|
||||
return ThemeData(
|
||||
@@ -100,10 +76,6 @@ Future<ThemeData> createAppTheme(
|
||||
opticalSize: 20,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed,
|
||||
width: 480,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
centerTitle: true,
|
||||
elevation: hasAppBarTransparent ? 0 : null,
|
||||
@@ -112,6 +84,11 @@ Future<ThemeData> createAppTheme(
|
||||
foregroundColor:
|
||||
hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
|
||||
),
|
||||
cardTheme: CardThemeData(
|
||||
color: colorScheme.surfaceContainer.withOpacity(
|
||||
settings.cardTransparency,
|
||||
),
|
||||
),
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.android: ZoomPageTransitionsBuilder(),
|
||||
|
||||
26
lib/pods/theme.g.dart
Normal file
26
lib/pods/theme.g.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'theme.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$themeHash() => r'a12dbf8b83d75713b7ae4c68e9cdd1a1cc3a35f0';
|
||||
|
||||
/// See also [theme].
|
||||
@ProviderFor(theme)
|
||||
final themeProvider = AutoDisposeProvider<ThemeSet>.internal(
|
||||
theme,
|
||||
name: r'themeProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$themeHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef ThemeRef = AutoDisposeProviderRef<ThemeSet>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
@@ -12,6 +11,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/talker.dart';
|
||||
|
||||
class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
||||
final Ref _ref;
|
||||
@@ -21,7 +21,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
||||
Future<void> fetchUser() async {
|
||||
final token = _ref.watch(tokenProvider);
|
||||
if (token == null) {
|
||||
log('[UserInfo] No token found, not going to fetch...');
|
||||
talker.info('[UserInfo] No token found, not going to fetch...');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -75,11 +75,10 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
|
||||
}
|
||||
});
|
||||
}
|
||||
log(
|
||||
talker.error(
|
||||
"[UserInfo] Failed to fetch user info...",
|
||||
name: 'UserInfoNotifier',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
state = AsyncValue.data(null);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@@ -8,6 +7,7 @@ import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import 'package:island/talker.dart';
|
||||
|
||||
part 'websocket.freezed.dart';
|
||||
part 'websocket.g.dart';
|
||||
@@ -64,7 +64,7 @@ class WebSocketService {
|
||||
|
||||
final url = '$baseUrl/ws'.replaceFirst('http', 'ws');
|
||||
|
||||
log('[WebSocket] Trying connecting to $url');
|
||||
talker.info('[WebSocket] Trying connecting to $url');
|
||||
try {
|
||||
if (kIsWeb) {
|
||||
_channel = WebSocketChannel.connect(Uri.parse('$url?tk=$token'));
|
||||
@@ -88,24 +88,24 @@ class WebSocketService {
|
||||
return;
|
||||
}
|
||||
_streamController.sink.add(packet);
|
||||
log(
|
||||
talker.info(
|
||||
"[WebSocket] Received packet: ${packet.type} ${packet.errorMessage}",
|
||||
);
|
||||
if (packet.type == 'pong' && _heartbeatAt != null) {
|
||||
var now = DateTime.now();
|
||||
heartbeatDelay = now.difference(_heartbeatAt!);
|
||||
log(
|
||||
talker.info(
|
||||
"[WebSocket] Server respond last heartbeat for ${heartbeatDelay!.inMilliseconds} ms",
|
||||
);
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
log('[WebSocket] Connection closed, attempting to reconnect...');
|
||||
talker.info('[WebSocket] Connection closed, attempting to reconnect...');
|
||||
_scheduleReconnect();
|
||||
_statusStreamController.sink.add(WebSocketState.disconnected());
|
||||
},
|
||||
onError: (error) {
|
||||
log('[WebSocket] Error occurred: $error, attempting to reconnect...');
|
||||
talker.error('[WebSocket] Error occurred: $error, attempting to reconnect...');
|
||||
_scheduleReconnect();
|
||||
_statusStreamController.sink.add(
|
||||
WebSocketState.error(error.toString()),
|
||||
@@ -113,7 +113,7 @@ class WebSocketService {
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
log('[WebSocket] Failed to connect: $err');
|
||||
talker.error('[WebSocket] Failed to connect: $err');
|
||||
_scheduleReconnect();
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ class WebSocketService {
|
||||
|
||||
void _beatTheHeart() {
|
||||
_heartbeatAt = DateTime.now();
|
||||
log('[WebSocket] We\'re beating the heart! $_heartbeatAt');
|
||||
talker.info('[WebSocket] We\'re beating the heart! $_heartbeatAt');
|
||||
sendMessage(jsonEncode(WebSocketPacket(type: 'ping', data: null)));
|
||||
}
|
||||
|
||||
|
||||
458
lib/route.dart
458
lib/route.dart
@@ -10,13 +10,11 @@ import 'package:island/screens/developers/app_detail.dart';
|
||||
import 'package:island/screens/developers/bot_detail.dart';
|
||||
import 'package:island/screens/developers/edit_app.dart';
|
||||
import 'package:island/screens/developers/edit_bot.dart';
|
||||
import 'package:island/screens/developers/new_app.dart';
|
||||
import 'package:island/screens/developers/hub.dart';
|
||||
import 'package:island/screens/developers/new_app.dart';
|
||||
import 'package:island/screens/developers/new_bot.dart';
|
||||
import 'package:island/screens/developers/projects.dart';
|
||||
import 'package:island/screens/developers/edit_project.dart';
|
||||
import 'package:island/screens/developers/new_project.dart';
|
||||
import 'package:island/screens/developers/project_detail.dart';
|
||||
import 'package:island/screens/discovery/articles.dart';
|
||||
import 'package:island/screens/files/file_list.dart';
|
||||
import 'package:island/screens/posts/post_categories_list.dart';
|
||||
@@ -35,6 +33,7 @@ import 'package:island/screens/account/me/profile_update.dart';
|
||||
import 'package:island/screens/account/leveling.dart';
|
||||
import 'package:island/screens/account/me/account_settings.dart';
|
||||
import 'package:island/screens/chat/chat.dart';
|
||||
import 'package:island/screens/chat/chat_form.dart';
|
||||
import 'package:island/screens/chat/room.dart';
|
||||
import 'package:island/screens/chat/room_detail.dart';
|
||||
import 'package:island/screens/chat/call.dart';
|
||||
@@ -42,15 +41,13 @@ import 'package:island/screens/chat/search_messages.dart';
|
||||
import 'package:island/screens/creators/hub.dart';
|
||||
import 'package:island/screens/creators/posts/post_manage_list.dart';
|
||||
import 'package:island/screens/creators/stickers/stickers.dart';
|
||||
import 'package:island/screens/creators/stickers/pack_detail.dart';
|
||||
import 'package:island/screens/stickers/sticker_marketplace.dart';
|
||||
import 'package:island/screens/stickers/pack_detail.dart';
|
||||
import 'package:island/screens/discovery/feeds/feed_marketplace.dart';
|
||||
import 'package:island/screens/discovery/feeds/feed_detail.dart';
|
||||
import 'package:island/screens/creators/poll/poll_list.dart';
|
||||
import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/screens/creators/publishers_form.dart';
|
||||
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
|
||||
import 'package:island/screens/creators/webfeed/webfeed_edit.dart';
|
||||
import 'package:island/screens/poll/poll_editor.dart';
|
||||
import 'package:island/screens/posts/compose.dart';
|
||||
import 'package:island/screens/posts/post_detail.dart';
|
||||
@@ -59,12 +56,14 @@ import 'package:island/screens/auth/login.dart';
|
||||
import 'package:island/screens/auth/create_account.dart';
|
||||
import 'package:island/screens/settings.dart';
|
||||
import 'package:island/screens/realm/realms.dart';
|
||||
import 'package:island/screens/realm/realm_form.dart';
|
||||
import 'package:island/screens/realm/realm_detail.dart';
|
||||
import 'package:island/screens/account/event_calendar.dart';
|
||||
import 'package:island/screens/discovery/realms.dart';
|
||||
import 'package:island/screens/reports/report_detail.dart';
|
||||
import 'package:island/screens/reports/report_list.dart';
|
||||
import 'package:island/talker.dart';
|
||||
import 'package:island/widgets/post/post_shuffle.dart';
|
||||
import 'package:talker_flutter/talker_flutter.dart';
|
||||
|
||||
// Shell route keys for nested navigation
|
||||
final rootNavigatorKey = GlobalKey<NavigatorState>();
|
||||
@@ -96,6 +95,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
observers: [
|
||||
if (_supportsAnalytics)
|
||||
FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance),
|
||||
TalkerRouteObserver(talker),
|
||||
],
|
||||
routes: [
|
||||
ShellRoute(
|
||||
@@ -133,262 +133,9 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'accountCalendar',
|
||||
path: '/account/:name/calendar',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return EventCalanderScreen(name: name);
|
||||
},
|
||||
),
|
||||
ShellRoute(
|
||||
builder:
|
||||
(context, state, child) => CreatorHubShellScreen(child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'creatorHub',
|
||||
path: '/creators',
|
||||
builder: (context, state) => const CreatorHubScreen(),
|
||||
),
|
||||
// Web Feed Routes
|
||||
GoRoute(
|
||||
name: 'creatorFeeds',
|
||||
path: '/creators/:name/feeds',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return WebFeedListScreen(pubName: name);
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'creatorFeedNew',
|
||||
path: 'new',
|
||||
builder: (context, state) {
|
||||
return WebFeedNewScreen(
|
||||
pubName: state.pathParameters['name']!,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorFeedEdit',
|
||||
path: ':feedId',
|
||||
builder: (context, state) {
|
||||
return WebFeedEditScreen(
|
||||
pubName: state.pathParameters['name']!,
|
||||
feedId: state.pathParameters['feedId'],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorPosts',
|
||||
path: '/creators/:name/posts',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return CreatorPostListScreen(pubName: name);
|
||||
},
|
||||
),
|
||||
// Poll list route
|
||||
GoRoute(
|
||||
name: 'creatorPolls',
|
||||
path: '/creators/:name/polls',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return CreatorPollListScreen(pubName: name);
|
||||
},
|
||||
),
|
||||
// Poll routes
|
||||
GoRoute(
|
||||
name: 'creatorPollNew',
|
||||
path: '/creators/:name/polls/new',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
// initialPollId left null for create; initialPublisher prefilled
|
||||
return PollEditorScreen(initialPublisher: name);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorPollEdit',
|
||||
path: '/creators/:name/polls/:id/edit',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
final id = state.pathParameters['id']!;
|
||||
return PollEditorScreen(
|
||||
initialPollId: id,
|
||||
initialPublisher: name,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorStickers',
|
||||
path: '/creators/:name/stickers',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return StickersScreen(pubName: name);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorStickerPackNew',
|
||||
path: '/creators/:name/stickers/new',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return NewStickerPacksScreen(pubName: name);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorStickerPackEdit',
|
||||
path: '/creators/:name/stickers/:packId/edit',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
final packId = state.pathParameters['packId']!;
|
||||
return EditStickerPacksScreen(pubName: name, packId: packId);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorStickerPackDetail',
|
||||
path: '/creators/:name/stickers/:packId',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
final packId = state.pathParameters['packId']!;
|
||||
return StickerPackDetailScreen(pubName: name, id: packId);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorStickerNew',
|
||||
path: '/creators/:name/stickers/:packId/new',
|
||||
builder: (context, state) {
|
||||
final packId = state.pathParameters['packId']!;
|
||||
return NewStickersScreen(packId: packId);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorStickerEdit',
|
||||
path: '/creators/:name/stickers/:packId/:id/edit',
|
||||
builder: (context, state) {
|
||||
final packId = state.pathParameters['packId']!;
|
||||
final id = state.pathParameters['id']!;
|
||||
return EditStickersScreen(id: id, packId: packId);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorNew',
|
||||
path: '/creators/new',
|
||||
builder: (context, state) => const NewPublisherScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorEdit',
|
||||
path: '/creators/:name/edit',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return EditPublisherScreen(name: name);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder:
|
||||
(context, state, child) =>
|
||||
DeveloperHubShellScreen(child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'developerHub',
|
||||
path: '/developers',
|
||||
builder: (context, state) => const DeveloperHubScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerProjects',
|
||||
path: '/developers/:name/projects',
|
||||
builder:
|
||||
(context, state) => DevProjectsScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerProjectNew',
|
||||
path: '/developers/:name/projects/new',
|
||||
builder:
|
||||
(context, state) => NewProjectScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerProjectEdit',
|
||||
path: '/developers/:name/projects/:id/edit',
|
||||
builder:
|
||||
(context, state) => EditProjectScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerProjectDetail',
|
||||
path: '/developers/:name/projects/:projectId',
|
||||
builder:
|
||||
(context, state) => ProjectDetailScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'developerAppNew',
|
||||
path: 'apps/new',
|
||||
builder:
|
||||
(context, state) => NewCustomAppScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerAppEdit',
|
||||
path: 'apps/:id/edit',
|
||||
builder:
|
||||
(context, state) => EditAppScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerAppDetail',
|
||||
path: 'apps/:appId',
|
||||
builder:
|
||||
(context, state) => AppDetailScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
appId: state.pathParameters['appId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerBotDetail',
|
||||
path: 'bots/:botId',
|
||||
builder:
|
||||
(context, state) => BotDetailScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
botId: state.pathParameters['botId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerBotNew',
|
||||
path: 'bots/new',
|
||||
builder:
|
||||
(context, state) => NewBotScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerBotEdit',
|
||||
path: 'bots/:id/edit',
|
||||
builder:
|
||||
(context, state) => EditBotScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
name: 'logs',
|
||||
path: '/logs',
|
||||
builder: (context, state) => TalkerScreen(talker: talker),
|
||||
),
|
||||
|
||||
// Web articles
|
||||
@@ -698,12 +445,195 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
|
||||
GoRoute(
|
||||
name: 'accountProfile',
|
||||
path: '/account/:name',
|
||||
path: '/accounts/:name',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return AccountProfileScreen(name: name);
|
||||
},
|
||||
),
|
||||
// Creator hub tab
|
||||
GoRoute(
|
||||
name: 'creatorHub',
|
||||
path: '/creators',
|
||||
builder: (context, state) => const CreatorHubScreen(),
|
||||
routes: [
|
||||
// Web Feed Routes
|
||||
GoRoute(
|
||||
name: 'creatorFeeds',
|
||||
path: ':name/feeds',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return WebFeedListScreen(pubName: name);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorPosts',
|
||||
path: ':name/posts',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return CreatorPostListScreen(pubName: name);
|
||||
},
|
||||
),
|
||||
// Poll list route
|
||||
GoRoute(
|
||||
name: 'creatorPolls',
|
||||
path: ':name/polls',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return CreatorPollListScreen(pubName: name);
|
||||
},
|
||||
),
|
||||
// Poll routes
|
||||
GoRoute(
|
||||
name: 'creatorPollNew',
|
||||
path: ':name/polls/new',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
// initialPollId left null for create; initialPublisher prefilled
|
||||
return PollEditorScreen(initialPublisher: name);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorPollEdit',
|
||||
path: ':name/polls/:id/edit',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
final id = state.pathParameters['id']!;
|
||||
return PollEditorScreen(
|
||||
initialPollId: id,
|
||||
initialPublisher: name,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorStickers',
|
||||
path: ':name/stickers',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return StickersScreen(pubName: name);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorNew',
|
||||
path: 'new',
|
||||
builder: (context, state) => const NewPublisherScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'creatorEdit',
|
||||
path: ':name/edit',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
return EditPublisherScreen(name: name);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Developer hub tab
|
||||
GoRoute(
|
||||
name: 'developerHub',
|
||||
path: '/developers',
|
||||
builder:
|
||||
(context, state) => DeveloperHubScreen(
|
||||
initialPublisherName:
|
||||
state.uri.queryParameters['publisher'],
|
||||
initialProjectId: state.uri.queryParameters['project'],
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'developerProjectNew',
|
||||
path: ':name/projects/new',
|
||||
builder:
|
||||
(context, state) => NewProjectScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerProjectEdit',
|
||||
path: ':name/projects/:id/edit',
|
||||
builder:
|
||||
(context, state) => EditProjectScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerProjectDetail',
|
||||
path: ':name/projects/:projectId',
|
||||
builder: (context, state) {
|
||||
final name = state.pathParameters['name']!;
|
||||
final projectId = state.pathParameters['projectId']!;
|
||||
// Redirect to hub with project selected
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.go(
|
||||
'/developers?publisher=$name&project=$projectId',
|
||||
);
|
||||
});
|
||||
return const SizedBox.shrink(); // Temporary placeholder
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
name: 'developerAppNew',
|
||||
path: 'apps/new',
|
||||
builder:
|
||||
(context, state) => NewCustomAppScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerAppEdit',
|
||||
path: 'apps/:id/edit',
|
||||
builder:
|
||||
(context, state) => EditAppScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerAppDetail',
|
||||
path: 'apps/:appId',
|
||||
builder:
|
||||
(context, state) => AppDetailScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
appId: state.pathParameters['appId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerBotNew',
|
||||
path: 'bots/new',
|
||||
builder:
|
||||
(context, state) => NewBotScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerBotDetail',
|
||||
path: 'bots/:botId',
|
||||
builder:
|
||||
(context, state) => BotDetailScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
botId: state.pathParameters['botId']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
name: 'developerBotEdit',
|
||||
path: 'bots/:id/edit',
|
||||
builder:
|
||||
(context, state) => EditBotScreen(
|
||||
publisherName: state.pathParameters['name']!,
|
||||
projectId: state.pathParameters['projectId']!,
|
||||
id: state.pathParameters['id']!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -211,21 +211,11 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
|
||||
icon: Symbols.system_update,
|
||||
title: 'Check for updates',
|
||||
onTap: () async {
|
||||
// Fetch latest release and show the unified sheet
|
||||
final svc = UpdateService();
|
||||
// Reuse service fetch + compare to decide content
|
||||
showLoadingModal(context);
|
||||
final release = await svc.fetchLatestRelease();
|
||||
svc.checkForUpdates(context);
|
||||
if (!context.mounted) return;
|
||||
hideLoadingModal(context);
|
||||
if (release != null) {
|
||||
await svc.showUpdateSheet(context, release);
|
||||
} else {
|
||||
showInfoAlert(
|
||||
'Currently cannot get update from the GitHub.',
|
||||
'Unable to check for updates',
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
_buildListTile(
|
||||
|
||||
@@ -66,7 +66,6 @@ class AccountScreen extends HookConsumerWidget {
|
||||
isNoBackground: isWide,
|
||||
appBar: AppBar(backgroundColor: Colors.transparent, toolbarHeight: 0),
|
||||
body: SingleChildScrollView(
|
||||
padding: getTabbedPadding(context),
|
||||
child: Column(
|
||||
spacing: 4,
|
||||
children: <Widget>[
|
||||
@@ -150,132 +149,124 @@ class AccountScreen extends HookConsumerWidget {
|
||||
context.pushNamed('leveling');
|
||||
},
|
||||
).padding(horizontal: 12),
|
||||
if (!isWideScreen(context)) const SizedBox.shrink(),
|
||||
if (!isWideScreen(context))
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Symbols.draw, size: 28).padding(bottom: 8),
|
||||
Text(
|
||||
'creatorHub',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr().fontSize(16).bold(),
|
||||
Text(
|
||||
'creatorHubDescription',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {
|
||||
context.goNamed('creatorHub');
|
||||
},
|
||||
),
|
||||
).height(140),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Symbols.code, size: 28).padding(bottom: 8),
|
||||
Text(
|
||||
'developerPortal',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr().fontSize(16).bold(),
|
||||
Text(
|
||||
'developerPortalDescription',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {
|
||||
context.pushNamed('developerHub');
|
||||
},
|
||||
),
|
||||
).height(140),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12),
|
||||
const SizedBox.shrink(),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Icon(Symbols.draw, size: 28).padding(bottom: 8),
|
||||
Text(
|
||||
'creatorHub',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr().fontSize(16).bold(),
|
||||
Text(
|
||||
'creatorHubDescription',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {
|
||||
context.pushNamed('creatorHub');
|
||||
},
|
||||
),
|
||||
).height(140),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Symbols.code, size: 28).padding(bottom: 8),
|
||||
Text(
|
||||
'developerPortal',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr().fontSize(16).bold(),
|
||||
Text(
|
||||
'developerPortalDescription',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {
|
||||
context.pushNamed('developerHub');
|
||||
},
|
||||
),
|
||||
).height(140),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12),
|
||||
const SizedBox.shrink(),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Symbols.settings, size: 28).padding(bottom: 8),
|
||||
Text('appSettings').tr().fontSize(16).bold(),
|
||||
Icon(Symbols.settings, size: 20),
|
||||
Text('appSettings').tr().fontSize(13).bold(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {
|
||||
context.pushNamed('settings');
|
||||
},
|
||||
),
|
||||
).height(120),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.person_edit,
|
||||
size: 28,
|
||||
).padding(bottom: 8),
|
||||
Text('updateYourProfile').tr().fontSize(16).bold(),
|
||||
Icon(Symbols.person_edit, size: 20),
|
||||
Text('updateYourProfile').tr().fontSize(13).bold(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {
|
||||
context.pushNamed('profileUpdate');
|
||||
},
|
||||
),
|
||||
).height(120),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.manage_accounts,
|
||||
size: 28,
|
||||
).padding(bottom: 8),
|
||||
Text('accountSettings').tr().fontSize(16).bold(),
|
||||
Icon(Symbols.manage_accounts, size: 20),
|
||||
Text('accountSettings').tr().fontSize(13).bold(),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 12),
|
||||
onTap: () {
|
||||
context.pushNamed('accountSettings');
|
||||
},
|
||||
),
|
||||
).height(120),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12),
|
||||
).height(48),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.notifications),
|
||||
|
||||
@@ -1,42 +1,21 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/models/wallet.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/screens/account/credits.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/services/time.dart';
|
||||
import 'package:island/widgets/account/leveling_progress.dart';
|
||||
import 'package:island/widgets/account/restore_purchase_sheet.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/account/stellar_program_tab.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/payment/payment_overlay.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'leveling.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<SnWalletSubscription?> accountStellarSubscription(Ref ref) async {
|
||||
try {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/id/subscriptions/fuzzy/solian.stellar');
|
||||
return SnWalletSubscription.fromJson(resp.data);
|
||||
} catch (err) {
|
||||
if (err is DioException && err.response?.statusCode == 404) return null;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class LevelingHistoryNotifier extends _$LevelingHistoryNotifier
|
||||
with CursorPagingNotifierMixin<SnExperienceRecord> {
|
||||
@@ -129,7 +108,7 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
children: [
|
||||
_buildLevelingTab(context, ref, user.value!),
|
||||
const SocialCreditsTab(),
|
||||
_buildStellarProgramTab(context, ref),
|
||||
const StellarProgramTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -259,457 +238,12 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
|
||||
SliverGap(getTabbedPadding(context, vertical: 20).vertical),
|
||||
SliverGap(20),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStellarProgramTab(BuildContext context, WidgetRef ref) {
|
||||
final stellarSubscription = ref.watch(accountStellarSubscriptionProvider);
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: getTabbedPadding(context, horizontal: 20, vertical: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildMembershipSection(context, ref, stellarSubscription),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMembershipSection(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AsyncValue<SnWalletSubscription?> stellarSubscriptionAsync,
|
||||
) {
|
||||
return stellarSubscriptionAsync.when(
|
||||
data: (membership) => _buildMembershipContent(context, ref, membership),
|
||||
loading:
|
||||
() => Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error:
|
||||
(error, stack) => Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text('Error loading membership: $error'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMembershipContent(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
SnWalletSubscription? membership,
|
||||
) {
|
||||
final isActive = membership?.isActive ?? false;
|
||||
|
||||
Future<void> membershipCancel() async {
|
||||
if (!isActive || membership == null) return;
|
||||
|
||||
final confirm = await showConfirmAlert(
|
||||
'membershipCancelHint'.tr(),
|
||||
'membershipCancelConfirm'.tr(),
|
||||
);
|
||||
if (!confirm || !context.mounted) return;
|
||||
|
||||
try {
|
||||
showLoadingModal(context);
|
||||
final client = ref.watch(apiClientProvider);
|
||||
await client.post('/id/subscriptions/${membership.identifier}/cancel');
|
||||
ref.invalidate(accountStellarSubscriptionProvider);
|
||||
ref.read(userInfoProvider.notifier).fetchUser();
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
showSnackBar('membershipCancelSuccess'.tr());
|
||||
}
|
||||
} catch (err) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
showErrorAlert(err);
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primaryContainer,
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isActive ? Icons.star : Icons.star_border,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'stellarMembership'.tr(),
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
|
||||
if (isActive) ...[
|
||||
_buildCurrentMembershipCard(context, membership!),
|
||||
const Gap(12),
|
||||
FilledButton.icon(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Theme.of(context).colorScheme.error,
|
||||
),
|
||||
foregroundColor: WidgetStateProperty.all(
|
||||
Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
),
|
||||
onPressed: membershipCancel,
|
||||
icon: const Icon(Symbols.cancel),
|
||||
label: Text('membershipCancel'.tr()),
|
||||
),
|
||||
],
|
||||
|
||||
if (!isActive) ...[
|
||||
Text(
|
||||
'chooseYourPlan'.tr(),
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const Gap(12),
|
||||
_buildMembershipTiers(context, ref, membership),
|
||||
],
|
||||
|
||||
// Restore Purchase Button
|
||||
// As you know Apple platform need IAP
|
||||
if (kIsWeb || !(Platform.isIOS || Platform.isMacOS))
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _showRestorePurchaseSheet(context, ref),
|
||||
icon: const Icon(Icons.restore),
|
||||
label: Text('restorePurchase'.tr()),
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
).padding(top: 12),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCurrentMembershipCard(
|
||||
BuildContext context,
|
||||
SnWalletSubscription membership,
|
||||
) {
|
||||
final tierName = _getMembershipTierName(membership.identifier);
|
||||
final tierColor = _getMembershipTierColor(context, membership.identifier);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: tierColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: tierColor, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.verified, color: tierColor, size: 20),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'currentMembership'.tr(args: [tierName]),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: tierColor,
|
||||
),
|
||||
),
|
||||
if (membership.endedAt != null)
|
||||
Text(
|
||||
'membershipExpires'.tr(
|
||||
args: [membership.endedAt!.formatSystem()],
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMembershipTiers(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
SnWalletSubscription? currentMembership,
|
||||
) {
|
||||
final tiers = [
|
||||
{
|
||||
'id': 'solian.stellar.primary',
|
||||
'name': 'membershipTierStellar'.tr(),
|
||||
'price': 'membershipPriceStellar'.tr(),
|
||||
'color': Colors.blue,
|
||||
},
|
||||
{
|
||||
'id': 'solian.stellar.nova',
|
||||
'name': 'membershipTierNova'.tr(),
|
||||
'price': 'membershipPriceNova'.tr(),
|
||||
'color': Color.fromRGBO(57, 197, 187, 1),
|
||||
},
|
||||
{
|
||||
'id': 'solian.stellar.supernova',
|
||||
'name': 'membershipTierSupernova'.tr(),
|
||||
'price': 'membershipPriceSupernova'.tr(),
|
||||
'color': Colors.orange,
|
||||
},
|
||||
];
|
||||
|
||||
return Column(
|
||||
children:
|
||||
tiers.map((tier) {
|
||||
final isCurrentTier = currentMembership?.identifier == tier['id'];
|
||||
final tierColor = tier['color'] as Color;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap:
|
||||
isCurrentTier
|
||||
? null
|
||||
: () => _purchaseMembership(
|
||||
context,
|
||||
ref,
|
||||
tier['id'] as String,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isCurrentTier
|
||||
? tierColor.withOpacity(0.1)
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color:
|
||||
isCurrentTier
|
||||
? tierColor
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: isCurrentTier ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 4,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: tierColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const Gap(12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
tier['name'] as String,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isCurrentTier ? tierColor : null,
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
if (isCurrentTier)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: tierColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'membershipCurrentBadge'.tr(),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
tier['price'] as String,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isCurrentTier)
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
String _getMembershipTierName(String identifier) {
|
||||
switch (identifier) {
|
||||
case 'solian.stellar.primary':
|
||||
return 'membershipTierStellar'.tr();
|
||||
case 'solian.stellar.nova':
|
||||
return 'membershipTierNova'.tr();
|
||||
case 'solian.stellar.supernova':
|
||||
return 'membershipTierSupernova'.tr();
|
||||
default:
|
||||
return 'membershipTierUnknown'.tr();
|
||||
}
|
||||
}
|
||||
|
||||
Color _getMembershipTierColor(BuildContext context, String identifier) {
|
||||
switch (identifier) {
|
||||
case 'solian.stellar.primary':
|
||||
return Colors.blue;
|
||||
case 'solian.stellar.nova':
|
||||
return Colors.purple;
|
||||
case 'solian.stellar.supernova':
|
||||
return Colors.orange;
|
||||
default:
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showRestorePurchaseSheet(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) async {
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => const RestorePurchaseSheet(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _purchaseMembership(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
String tierId,
|
||||
) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
try {
|
||||
showLoadingModal(context);
|
||||
final resp = await client.post(
|
||||
'/id/subscriptions',
|
||||
data: {
|
||||
'identifier': tierId,
|
||||
'payment_method': 'solian.wallet',
|
||||
'payment_details': {'currency': 'golds'},
|
||||
'cycle_duration_days': 30,
|
||||
},
|
||||
options: Options(headers: {'X-Noop': true}),
|
||||
);
|
||||
final subscription = SnWalletSubscription.fromJson(resp.data);
|
||||
if (subscription.status == 1) return;
|
||||
final orderResp = await client.post(
|
||||
'/id/subscriptions/${subscription.identifier}/order',
|
||||
);
|
||||
final order = SnWalletOrder.fromJson(orderResp.data);
|
||||
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
|
||||
// Show payment overlay to complete the payment
|
||||
if (!context.mounted) return;
|
||||
final paidOrder = await PaymentOverlay.show(
|
||||
context: context,
|
||||
order: order,
|
||||
enableBiometric: true,
|
||||
);
|
||||
|
||||
if (context.mounted) showLoadingModal(context);
|
||||
|
||||
if (paidOrder != null) {
|
||||
// Wait for server to handle order
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
ref.invalidate(accountStellarSubscriptionProvider);
|
||||
ref.read(userInfoProvider.notifier).fetchUser();
|
||||
if (context.mounted) {
|
||||
showSnackBar('membershipPurchaseSuccess'.tr());
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LevelStairsPainter extends CustomPainter {
|
||||
|
||||
@@ -6,27 +6,6 @@ part of 'leveling.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$accountStellarSubscriptionHash() =>
|
||||
r'80abcdefb3868775fd8fe3c980215713efff5948';
|
||||
|
||||
/// See also [accountStellarSubscription].
|
||||
@ProviderFor(accountStellarSubscription)
|
||||
final accountStellarSubscriptionProvider =
|
||||
AutoDisposeFutureProvider<SnWalletSubscription?>.internal(
|
||||
accountStellarSubscription,
|
||||
name: r'accountStellarSubscriptionProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$accountStellarSubscriptionHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef AccountStellarSubscriptionRef =
|
||||
AutoDisposeFutureProviderRef<SnWalletSubscription?>;
|
||||
String _$levelingHistoryNotifierHash() =>
|
||||
r'e795f9b7911c9e50f15c095ea237cb0e87bf1e89';
|
||||
|
||||
|
||||
@@ -9,11 +9,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/account.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/services/file.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/services/timezone.dart';
|
||||
import 'package:island/widgets/account/account_name.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
@@ -24,6 +25,17 @@ const kServerSupportedLanguages = {'en-US': 'en-us', 'zh-CN': 'zh-hans'};
|
||||
const kServerSupportedRegions = ['US', 'JP', 'CN'];
|
||||
|
||||
class UpdateProfileScreen extends HookConsumerWidget {
|
||||
bool _isValidHexColor(String color) {
|
||||
if (!color.startsWith('#')) return false;
|
||||
if (color.length != 7) return false; // #RRGGBB format
|
||||
try {
|
||||
int.parse(color.substring(1), radix: 16);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const UpdateProfileScreen({super.key});
|
||||
|
||||
@override
|
||||
@@ -62,19 +74,13 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final baseUrl = ref.watch(serverUrlProvider);
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putFileToCloud(
|
||||
await FileUploader.createCloudFile(
|
||||
client: ref.read(apiClientProvider),
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: result.name,
|
||||
mimetype: result.mimeType ?? 'image/jpeg',
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
@@ -154,12 +160,39 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
text: user.value!.profile.timeZone,
|
||||
);
|
||||
|
||||
// Username color state
|
||||
final usernameColorType = useState<String>(
|
||||
user.value!.profile.usernameColor?.type ?? 'plain',
|
||||
);
|
||||
final usernameColorValue = useTextEditingController(
|
||||
text: user.value!.profile.usernameColor?.value,
|
||||
);
|
||||
final usernameColorDirection = useTextEditingController(
|
||||
text: user.value!.profile.usernameColor?.direction,
|
||||
);
|
||||
final usernameColorColors = useState<List<String>>(
|
||||
user.value!.profile.usernameColor?.colors ?? [],
|
||||
);
|
||||
|
||||
void updateProfile() async {
|
||||
if (!formKeyProfile.currentState!.validate()) return;
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final usernameColorData = {
|
||||
'type': usernameColorType.value,
|
||||
if (usernameColorType.value == 'plain' &&
|
||||
usernameColorValue.text.isNotEmpty)
|
||||
'value': usernameColorValue.text,
|
||||
if (usernameColorType.value == 'gradient') ...{
|
||||
if (usernameColorDirection.text.isNotEmpty)
|
||||
'direction': usernameColorDirection.text,
|
||||
'colors':
|
||||
usernameColorColors.value.where((c) => c.isNotEmpty).toList(),
|
||||
},
|
||||
};
|
||||
|
||||
await client.patch(
|
||||
'/id/accounts/me/profile',
|
||||
data: {
|
||||
@@ -172,6 +205,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
'location': locationController.text,
|
||||
'time_zone': timeZoneController.text,
|
||||
'birthday': birthday.value?.toUtc().toIso8601String(),
|
||||
'username_color': usernameColorData,
|
||||
'links':
|
||||
links.value
|
||||
.where((e) => e.name.isNotEmpty && e.url.isNotEmpty)
|
||||
@@ -599,6 +633,320 @@ class UpdateProfileScreen extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'usernameColor',
|
||||
).tr().bold().fontSize(18).padding(top: 16),
|
||||
Column(
|
||||
spacing: 16,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Preview section
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('preview').tr().bold().fontSize(14),
|
||||
const Gap(8),
|
||||
// Create a preview account with the current settings
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final previewAccount = user.value!.copyWith(
|
||||
profile: user.value!.profile.copyWith(
|
||||
usernameColor: UsernameColor(
|
||||
type: usernameColorType.value,
|
||||
value:
|
||||
usernameColorType.value == 'plain' &&
|
||||
usernameColorValue
|
||||
.text
|
||||
.isNotEmpty
|
||||
? usernameColorValue.text
|
||||
: null,
|
||||
direction:
|
||||
usernameColorType.value ==
|
||||
'gradient' &&
|
||||
usernameColorDirection
|
||||
.text
|
||||
.isNotEmpty
|
||||
? usernameColorDirection.text
|
||||
: null,
|
||||
colors:
|
||||
usernameColorType.value == 'gradient'
|
||||
? usernameColorColors.value
|
||||
.where((c) => c.isNotEmpty)
|
||||
.toList()
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Check if user can use this color
|
||||
final tier =
|
||||
user.value!.perkSubscription?.identifier;
|
||||
final canUseColor = switch (tier) {
|
||||
'solian.stellar.primary' =>
|
||||
usernameColorType.value == 'plain' &&
|
||||
(kUsernamePlainColors.containsKey(
|
||||
usernameColorValue.text,
|
||||
) ||
|
||||
(usernameColorValue.text.startsWith(
|
||||
'#',
|
||||
) &&
|
||||
_isValidHexColor(
|
||||
usernameColorValue.text,
|
||||
))),
|
||||
'solian.stellar.nova' =>
|
||||
usernameColorType.value == 'plain',
|
||||
'solian.stellar.supernova' => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AccountName(
|
||||
account: previewAccount,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
ignorePermissions: true,
|
||||
),
|
||||
const Gap(4),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
canUseColor
|
||||
? Symbols.check_circle
|
||||
: Symbols.error,
|
||||
size: 16,
|
||||
color:
|
||||
canUseColor
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
canUseColor
|
||||
? 'availableWithYourPlan'.tr()
|
||||
: 'upgradeRequired'.tr(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
canUseColor
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
DropdownButtonFormField2<String>(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'colorType'.tr(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'plain',
|
||||
child: Text('plain'.tr()),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'gradient',
|
||||
child: Text('gradient'.tr()),
|
||||
),
|
||||
],
|
||||
value: usernameColorType.value,
|
||||
onChanged: (value) {
|
||||
usernameColorType.value = value ?? 'plain';
|
||||
},
|
||||
customButton: Row(
|
||||
children: [
|
||||
Expanded(child: Text(usernameColorType.value).tr()),
|
||||
Icon(Symbols.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (usernameColorType.value == 'plain')
|
||||
Autocomplete<String>(
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
final options = kUsernamePlainColors.keys.toList();
|
||||
if (textEditingValue.text == '') {
|
||||
return options;
|
||||
}
|
||||
return options.where(
|
||||
(option) => option.toLowerCase().contains(
|
||||
textEditingValue.text.toLowerCase(),
|
||||
),
|
||||
);
|
||||
},
|
||||
onSelected: (String selection) {
|
||||
usernameColorValue.text = selection;
|
||||
},
|
||||
fieldViewBuilder: (
|
||||
context,
|
||||
controller,
|
||||
focusNode,
|
||||
onFieldSubmitted,
|
||||
) {
|
||||
// Initialize the controller with the current value
|
||||
if (controller.text.isEmpty &&
|
||||
usernameColorValue.text.isNotEmpty) {
|
||||
controller.text = usernameColorValue.text;
|
||||
}
|
||||
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'colorValue'.tr(),
|
||||
hintText: 'e.g. red or #ff6600',
|
||||
),
|
||||
onChanged: (value) {
|
||||
usernameColorValue.text = value;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (usernameColorType.value == 'gradient') ...[
|
||||
DropdownButtonFormField2<String>(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'gradientDirection'.tr(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'to right',
|
||||
child: Text('gradientDirectionToRight'.tr()),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'to left',
|
||||
child: Text('gradientDirectionToLeft'.tr()),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'to bottom',
|
||||
child: Text('gradientDirectionToBottom'.tr()),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'to top',
|
||||
child: Text('gradientDirectionToTop'.tr()),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'to bottom right',
|
||||
child: Text(
|
||||
'gradientDirectionToBottomRight'.tr(),
|
||||
),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'to bottom left',
|
||||
child: Text('gradientDirectionToBottomLeft'.tr()),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'to top right',
|
||||
child: Text('gradientDirectionToTopRight'.tr()),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'to top left',
|
||||
child: Text('gradientDirectionToTopLeft'.tr()),
|
||||
),
|
||||
],
|
||||
value:
|
||||
usernameColorDirection.text.isNotEmpty
|
||||
? usernameColorDirection.text
|
||||
: 'to right',
|
||||
onChanged: (value) {
|
||||
usernameColorDirection.text = value ?? 'to right';
|
||||
},
|
||||
customButton: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
usernameColorDirection.text.isNotEmpty
|
||||
? usernameColorDirection.text
|
||||
: 'to right',
|
||||
),
|
||||
),
|
||||
Icon(Symbols.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'gradientColors',
|
||||
).tr().bold().fontSize(14).padding(top: 8),
|
||||
Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
for (
|
||||
var i = 0;
|
||||
i < usernameColorColors.value.length;
|
||||
i++
|
||||
)
|
||||
Row(
|
||||
key: ValueKey(
|
||||
usernameColorColors.value[i].hashCode,
|
||||
),
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue:
|
||||
usernameColorColors.value[i],
|
||||
decoration: InputDecoration(
|
||||
labelText: 'color'.tr(),
|
||||
hintText: 'e.g. #ff0000',
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: (value) {
|
||||
usernameColorColors.value[i] = value;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete),
|
||||
onPressed: () {
|
||||
usernameColorColors.value =
|
||||
usernameColorColors.value
|
||||
.whereIndexed(
|
||||
(idx, _) => idx != i,
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FilledButton.icon(
|
||||
onPressed: () {
|
||||
usernameColorColors.value = List.from(
|
||||
usernameColorColors.value,
|
||||
)..add('');
|
||||
},
|
||||
label: Text('addColor').tr(),
|
||||
icon: const Icon(Symbols.add),
|
||||
).padding(top: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
Text('links').tr().bold().fontSize(18).padding(top: 16),
|
||||
Column(
|
||||
spacing: 8,
|
||||
|
||||
@@ -168,7 +168,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
|
||||
scopes: [AppleIDAuthorizationScopes.email],
|
||||
webAuthenticationOptions: WebAuthenticationOptions(
|
||||
clientId: 'dev.solsynth.solarpass',
|
||||
redirectUri: Uri.parse('https://id.solian.app/auth/callback'),
|
||||
redirectUri: Uri.parse('https://solian.app/auth/callback'),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -97,9 +97,7 @@ class _AccountBasicInfo extends StatelessWidget {
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
SharePlus.instance.share(
|
||||
ShareParams(
|
||||
uri: Uri.parse('https://id.solian.app/@${data.name}'),
|
||||
),
|
||||
ShareParams(uri: Uri.parse('https://solian.app/@${data.name}')),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.share),
|
||||
@@ -879,7 +877,7 @@ class AccountProfileScreen extends HookConsumerWidget {
|
||||
child: Card(
|
||||
child: FortuneGraphWidget(
|
||||
events: accountEvents,
|
||||
eventCalanderUser: data.name,
|
||||
eventCalandarUser: data.name,
|
||||
margin: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
@@ -1004,7 +1002,7 @@ class AccountProfileScreen extends HookConsumerWidget {
|
||||
child: Card(
|
||||
child: FortuneGraphWidget(
|
||||
events: accountEvents,
|
||||
eventCalanderUser: data.name,
|
||||
eventCalandarUser: data.name,
|
||||
),
|
||||
).padding(horizontal: 4),
|
||||
),
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'captcha.config.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<String> captchaUrl(Ref ref) async {
|
||||
const baseUrl = "https://solian.app";
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final baseUrl = await apiClient.get('/config/site');
|
||||
return '$baseUrl/auth/captcha';
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'captcha.config.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$captchaUrlHash() => r'd46bc43032cef504547cd528a40c23cf76f27cc8';
|
||||
String _$captchaUrlHash() => r'5d59de4f26a0544bf4fbd5209943f0b111959ce6';
|
||||
|
||||
/// See also [captchaUrl].
|
||||
@ProviderFor(captchaUrl)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart' hide ConnectionState;
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/talker.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/chat/call_button.dart';
|
||||
import 'package:island/widgets/chat/call_overlay.dart';
|
||||
@@ -26,14 +25,14 @@ class CallScreen extends HookConsumerWidget {
|
||||
final callNotifier = ref.watch(callNotifierProvider.notifier);
|
||||
|
||||
useEffect(() {
|
||||
log('[Call] Joining the call...');
|
||||
talker.info('[Call] Joining the call...');
|
||||
callNotifier.joinRoom(roomId).catchError((_) {
|
||||
showConfirmAlert(
|
||||
'Seems there already has a call connected, do you want override it?',
|
||||
'Call already connected',
|
||||
).then((value) {
|
||||
if (value != true) return;
|
||||
log('[Call] Joining the call... with overrides');
|
||||
talker.info('[Call] Joining the call... with overrides');
|
||||
callNotifier.disconnect();
|
||||
callNotifier.dispose();
|
||||
callNotifier.joinRoom(roomId);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:croppy/croppy.dart' hide cropImage;
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -7,25 +5,18 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/realm.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/pods/chat_summary.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/chat/call.dart';
|
||||
import 'package:island/pods/chat/chat_summary.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/realm/realms.dart';
|
||||
import 'package:island/services/file.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/account/account_picker.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/realm/realm_selection_dropdown.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:island/screens/tabs.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:relative_time/relative_time.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -178,6 +169,101 @@ Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
|
||||
.toList();
|
||||
}
|
||||
|
||||
class ChatListBodyWidget extends HookConsumerWidget {
|
||||
final bool isFloating;
|
||||
final TabController tabController;
|
||||
final ValueNotifier<int> selectedTab;
|
||||
|
||||
const ChatListBodyWidget({
|
||||
super.key,
|
||||
this.isFloating = false,
|
||||
required this.tabController,
|
||||
required this.selectedTab,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final chats = ref.watch(chatroomsJoinedProvider);
|
||||
final callState = ref.watch(callNotifierProvider);
|
||||
|
||||
Widget bodyWidget = Column(
|
||||
children: [
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final summaryState = ref.watch(chatSummaryProvider);
|
||||
return summaryState.maybeWhen(
|
||||
loading:
|
||||
() => const LinearProgressIndicator(
|
||||
minHeight: 2,
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: chats.when(
|
||||
data:
|
||||
(items) => RefreshIndicator(
|
||||
onRefresh:
|
||||
() => Future.sync(() {
|
||||
ref.invalidate(chatroomsJoinedProvider);
|
||||
}),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: callState.isConnected ? 96 : 0,
|
||||
),
|
||||
itemCount:
|
||||
items
|
||||
.where(
|
||||
(item) =>
|
||||
selectedTab.value == 0 ||
|
||||
(selectedTab.value == 1 && item.type == 1) ||
|
||||
(selectedTab.value == 2 && item.type != 1),
|
||||
)
|
||||
.length,
|
||||
itemBuilder: (context, index) {
|
||||
final filteredItems =
|
||||
items
|
||||
.where(
|
||||
(item) =>
|
||||
selectedTab.value == 0 ||
|
||||
(selectedTab.value == 1 &&
|
||||
item.type == 1) ||
|
||||
(selectedTab.value == 2 && item.type != 1),
|
||||
)
|
||||
.toList();
|
||||
final item = filteredItems[index];
|
||||
return ChatRoomListTile(
|
||||
room: item,
|
||||
isDirect: item.type == 1,
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {'id': item.id},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, stack) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () {
|
||||
ref.invalidate(chatroomsJoinedProvider);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return isFloating ? Card(child: bodyWidget) : bodyWidget;
|
||||
}
|
||||
}
|
||||
|
||||
class ChatShellScreen extends HookConsumerWidget {
|
||||
final Widget child;
|
||||
const ChatShellScreen({super.key, required this.child});
|
||||
@@ -191,9 +277,23 @@ class ChatShellScreen extends HookConsumerWidget {
|
||||
isRoot: true,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(flex: 2, child: ChatListScreen(isAside: true)),
|
||||
const VerticalDivider(width: 1),
|
||||
Flexible(flex: 4, child: child),
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: ChatListScreen(
|
||||
isAside: true,
|
||||
isFloating: true,
|
||||
).padding(left: 16, vertical: 16),
|
||||
),
|
||||
const Gap(8),
|
||||
Flexible(
|
||||
flex: 4,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
),
|
||||
child: child,
|
||||
).padding(top: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -205,24 +305,23 @@ class ChatShellScreen extends HookConsumerWidget {
|
||||
|
||||
class ChatListScreen extends HookConsumerWidget {
|
||||
final bool isAside;
|
||||
const ChatListScreen({super.key, this.isAside = false});
|
||||
final bool isFloating;
|
||||
const ChatListScreen({
|
||||
super.key,
|
||||
this.isAside = false,
|
||||
this.isFloating = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isWide = isWideScreen(context);
|
||||
if (isWide && !isAside) {
|
||||
return const EmptyPageHolder();
|
||||
}
|
||||
|
||||
final chats = ref.watch(chatroomsJoinedProvider);
|
||||
final chatInvites = ref.watch(chatroomInvitesProvider);
|
||||
final tabController = useTabController(initialLength: 3);
|
||||
final selectedTab = useState(
|
||||
0,
|
||||
); // 0 for All, 1 for Direct Messages, 2 for Group Chats
|
||||
|
||||
final callState = ref.watch(callNotifierProvider);
|
||||
|
||||
useEffect(() {
|
||||
tabController.addListener(() {
|
||||
selectedTab.value = tabController.index;
|
||||
@@ -250,6 +349,76 @@ class ChatListScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
if (isAside) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBar(
|
||||
dividerColor: Colors.transparent,
|
||||
controller: tabController,
|
||||
tabAlignment: TabAlignment.start,
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
const Tab(icon: Icon(Symbols.chat)),
|
||||
const Tab(icon: Icon(Symbols.person)),
|
||||
const Tab(icon: Icon(Symbols.group)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: IconButton(
|
||||
icon: Badge(
|
||||
label: Text(
|
||||
chatInvites.when(
|
||||
data: (invites) => invites.length.toString(),
|
||||
error: (_, _) => '0',
|
||||
loading: () => '0',
|
||||
),
|
||||
),
|
||||
isLabelVisible: chatInvites.when(
|
||||
data: (invites) => invites.isNotEmpty,
|
||||
error: (_, _) => false,
|
||||
loading: () => false,
|
||||
),
|
||||
child: const Icon(Symbols.email),
|
||||
),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const _ChatInvitesSheet(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: ChatListBodyWidget(
|
||||
isFloating: false,
|
||||
tabController: tabController,
|
||||
selectedTab: selectedTab,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isWide && !isAside) {
|
||||
return const EmptyPageHolder();
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
extendBody: false, // Prevent conflicts with tabs navigation
|
||||
appBar: AppBar(
|
||||
@@ -352,82 +521,10 @@ class ChatListScreen extends HookConsumerWidget {
|
||||
},
|
||||
child: const Icon(Symbols.add),
|
||||
),
|
||||
floatingActionButtonLocation: TabbedFabLocation(context),
|
||||
body: Column(
|
||||
children: [
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final summaryState = ref.watch(chatSummaryProvider);
|
||||
return summaryState.maybeWhen(
|
||||
loading:
|
||||
() => const LinearProgressIndicator(
|
||||
minHeight: 2,
|
||||
borderRadius: BorderRadius.zero,
|
||||
),
|
||||
orElse: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: chats.when(
|
||||
data:
|
||||
(items) => RefreshIndicator(
|
||||
onRefresh:
|
||||
() => Future.sync(() {
|
||||
ref.invalidate(chatroomsJoinedProvider);
|
||||
}),
|
||||
child: ListView.builder(
|
||||
padding: getTabbedPadding(
|
||||
context,
|
||||
bottom: callState.isConnected ? 96 : null,
|
||||
),
|
||||
itemCount:
|
||||
items
|
||||
.where(
|
||||
(item) =>
|
||||
selectedTab.value == 0 ||
|
||||
(selectedTab.value == 1 &&
|
||||
item.type == 1) ||
|
||||
(selectedTab.value == 2 && item.type != 1),
|
||||
)
|
||||
.length,
|
||||
itemBuilder: (context, index) {
|
||||
final filteredItems =
|
||||
items
|
||||
.where(
|
||||
(item) =>
|
||||
selectedTab.value == 0 ||
|
||||
(selectedTab.value == 1 &&
|
||||
item.type == 1) ||
|
||||
(selectedTab.value == 2 &&
|
||||
item.type != 1),
|
||||
)
|
||||
.toList();
|
||||
final item = filteredItems[index];
|
||||
return ChatRoomListTile(
|
||||
room: item,
|
||||
isDirect: item.type == 1,
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'chatRoom',
|
||||
pathParameters: {'id': item.id},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, stack) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry: () {
|
||||
ref.invalidate(chatroomsJoinedProvider);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: ChatListBodyWidget(
|
||||
isFloating: false,
|
||||
tabController: tabController,
|
||||
selectedTab: selectedTab,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -463,275 +560,6 @@ Future<SnChatMember?> chatroomIdentity(Ref ref, String? identifier) async {
|
||||
}
|
||||
}
|
||||
|
||||
class NewChatScreen extends StatelessWidget {
|
||||
const NewChatScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const EditChatScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class EditChatScreen extends HookConsumerWidget {
|
||||
final String? id;
|
||||
const EditChatScreen({super.key, this.id});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>(), []);
|
||||
|
||||
final submitting = useState(false);
|
||||
|
||||
final nameController = useTextEditingController();
|
||||
final descriptionController = useTextEditingController();
|
||||
final picture = useState<SnCloudFile?>(null);
|
||||
final background = useState<SnCloudFile?>(null);
|
||||
final isPublic = useState(true);
|
||||
final isCommunity = useState(false);
|
||||
|
||||
final chat = ref.watch(chatroomProvider(id));
|
||||
|
||||
final joinedRealms = ref.watch(realmsJoinedProvider);
|
||||
final currentRealm = useState<SnRealm?>(null);
|
||||
|
||||
useEffect(() {
|
||||
if (chat.value != null) {
|
||||
nameController.text = chat.value!.name ?? '';
|
||||
descriptionController.text = chat.value!.description ?? '';
|
||||
picture.value = chat.value!.picture;
|
||||
background.value = chat.value!.background;
|
||||
isPublic.value = chat.value!.isPublic;
|
||||
isCommunity.value = chat.value!.isCommunity;
|
||||
currentRealm.value = joinedRealms.value?.firstWhereOrNull(
|
||||
(realm) => realm.id == chat.value!.realmId,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}, [chat, joinedRealms]);
|
||||
|
||||
void setPicture(String position) async {
|
||||
showLoadingModal(context);
|
||||
var result = await ref
|
||||
.read(imagePickerProvider)
|
||||
.pickImage(source: ImageSource.gallery);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
hideLoadingModal(context);
|
||||
result = await cropImage(
|
||||
context,
|
||||
image: result,
|
||||
allowedAspectRatios: [
|
||||
if (position == 'background')
|
||||
const CropAspectRatio(height: 7, width: 16)
|
||||
else
|
||||
const CropAspectRatio(height: 1, width: 1),
|
||||
],
|
||||
);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
showLoadingModal(context);
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final baseUrl = ref.watch(serverUrlProvider);
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putFileToCloud(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: result.name,
|
||||
mimetype: result.mimeType ?? 'image/jpeg',
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
switch (position) {
|
||||
case 'picture':
|
||||
picture.value = cloudFile;
|
||||
case 'background':
|
||||
background.value = cloudFile;
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> performAction() async {
|
||||
if (!formKey.currentState!.validate()) return;
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.request(
|
||||
id == null ? '/sphere/chat' : '/sphere/chat/$id',
|
||||
data: {
|
||||
'name': nameController.text,
|
||||
'description': descriptionController.text,
|
||||
'background_id': background.value?.id,
|
||||
'picture_id': picture.value?.id,
|
||||
'realm_id': currentRealm.value?.id,
|
||||
'is_public': isPublic.value,
|
||||
'is_community': isCommunity.value,
|
||||
},
|
||||
options: Options(method: id == null ? 'POST' : 'PATCH'),
|
||||
);
|
||||
if (context.mounted) {
|
||||
context.pop(SnChatRoom.fromJson(resp.data));
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(id == null ? 'createChatRoom' : 'editChatRoom').tr(),
|
||||
leading: const PageBackButton(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
RealmSelectionDropdown(
|
||||
value: currentRealm.value,
|
||||
realms: joinedRealms.when(
|
||||
data: (realms) => realms,
|
||||
loading: () => [],
|
||||
error: (_, _) => [],
|
||||
),
|
||||
onChanged: (SnRealm? value) {
|
||||
currentRealm.value = value;
|
||||
},
|
||||
isLoading: joinedRealms.isLoading,
|
||||
error: joinedRealms.error?.toString(),
|
||||
),
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child:
|
||||
background.value != null
|
||||
? CloudFileWidget(
|
||||
item: background.value!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('background');
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: -32,
|
||||
child: GestureDetector(
|
||||
child: ProfilePictureWidget(
|
||||
fileId: picture.value?.id,
|
||||
radius: 40,
|
||||
fallbackIcon: Symbols.group,
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('picture');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).padding(bottom: 32),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(labelText: 'Name'),
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description',
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
minLines: 3,
|
||||
maxLines: null,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.public),
|
||||
title: Text('publicChat').tr(),
|
||||
subtitle: Text('publicChatDescription').tr(),
|
||||
value: isPublic.value,
|
||||
onChanged: (value) {
|
||||
isPublic.value = value ?? true;
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.travel_explore),
|
||||
title: Text('communityChat').tr(),
|
||||
subtitle: Text('communityChatDescription').tr(),
|
||||
value: isCommunity.value,
|
||||
onChanged: (value) {
|
||||
isCommunity.value = value ?? false;
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
label: const Text('Save'),
|
||||
icon: const Icon(Symbols.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(all: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<SnChatMember>> chatroomInvites(Ref ref) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
|
||||
299
lib/screens/chat/chat_form.dart
Normal file
299
lib/screens/chat/chat_form.dart
Normal file
@@ -0,0 +1,299 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:croppy/croppy.dart' hide cropImage;
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/chat.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/realm.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/chat/chat.dart';
|
||||
import 'package:island/screens/realm/realms.dart';
|
||||
import 'package:island/services/file.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class NewChatScreen extends StatelessWidget {
|
||||
const NewChatScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const EditChatScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class EditChatScreen extends HookConsumerWidget {
|
||||
final String? id;
|
||||
const EditChatScreen({super.key, this.id});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>(), []);
|
||||
|
||||
final submitting = useState(false);
|
||||
|
||||
final nameController = useTextEditingController();
|
||||
final descriptionController = useTextEditingController();
|
||||
final picture = useState<SnCloudFile?>(null);
|
||||
final background = useState<SnCloudFile?>(null);
|
||||
final isPublic = useState(true);
|
||||
final isCommunity = useState(false);
|
||||
|
||||
final chat = ref.watch(chatroomProvider(id));
|
||||
|
||||
final joinedRealms = ref.watch(realmsJoinedProvider);
|
||||
final currentRealm = useState<SnRealm?>(null);
|
||||
|
||||
useEffect(() {
|
||||
if (chat.value != null) {
|
||||
nameController.text = chat.value!.name ?? '';
|
||||
descriptionController.text = chat.value!.description ?? '';
|
||||
picture.value = chat.value!.picture;
|
||||
background.value = chat.value!.background;
|
||||
isPublic.value = chat.value!.isPublic;
|
||||
isCommunity.value = chat.value!.isCommunity;
|
||||
currentRealm.value = joinedRealms.value?.firstWhereOrNull(
|
||||
(realm) => realm.id == chat.value!.realmId,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}, [chat, joinedRealms]);
|
||||
|
||||
void setPicture(String position) async {
|
||||
showLoadingModal(context);
|
||||
var result = await ref
|
||||
.read(imagePickerProvider)
|
||||
.pickImage(source: ImageSource.gallery);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
hideLoadingModal(context);
|
||||
result = await cropImage(
|
||||
context,
|
||||
image: result,
|
||||
allowedAspectRatios: [
|
||||
if (position == 'background')
|
||||
const CropAspectRatio(height: 7, width: 16)
|
||||
else
|
||||
const CropAspectRatio(height: 1, width: 1),
|
||||
],
|
||||
);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
showLoadingModal(context);
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
client: ref.read(apiClientProvider),
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
switch (position) {
|
||||
case 'picture':
|
||||
picture.value = cloudFile;
|
||||
case 'background':
|
||||
background.value = cloudFile;
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> performAction() async {
|
||||
if (!formKey.currentState!.validate()) return;
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.request(
|
||||
id == null ? '/sphere/chat' : '/sphere/chat/$id',
|
||||
data: {
|
||||
'name': nameController.text,
|
||||
'description': descriptionController.text,
|
||||
'background_id': background.value?.id,
|
||||
'picture_id': picture.value?.id,
|
||||
'realm_id': currentRealm.value?.id,
|
||||
'is_public': isPublic.value,
|
||||
'is_community': isCommunity.value,
|
||||
},
|
||||
options: Options(method: id == null ? 'POST' : 'PATCH'),
|
||||
);
|
||||
if (context.mounted) {
|
||||
context.pop(SnChatRoom.fromJson(resp.data));
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(id == null ? 'createChatRoom' : 'editChatRoom').tr(),
|
||||
leading: const PageBackButton(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child:
|
||||
background.value != null
|
||||
? CloudFileWidget(
|
||||
item: background.value!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('background');
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: -32,
|
||||
child: GestureDetector(
|
||||
child: ProfilePictureWidget(
|
||||
fileId: picture.value?.id,
|
||||
radius: 40,
|
||||
fallbackIcon: Symbols.group,
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('picture');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).padding(bottom: 32),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: const InputDecoration(labelText: 'Name'),
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description',
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
minLines: 3,
|
||||
maxLines: null,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<SnRealm>(
|
||||
value: currentRealm.value,
|
||||
decoration: InputDecoration(labelText: 'realm'.tr()),
|
||||
items: [
|
||||
DropdownMenuItem<SnRealm>(
|
||||
value: null,
|
||||
child: Text('none'.tr()),
|
||||
),
|
||||
...joinedRealms.maybeWhen(
|
||||
data:
|
||||
(realms) => realms.map(
|
||||
(realm) => DropdownMenuItem(
|
||||
value: realm,
|
||||
child: Text(realm.name),
|
||||
),
|
||||
),
|
||||
orElse: () => [],
|
||||
),
|
||||
],
|
||||
onChanged:
|
||||
joinedRealms.isLoading
|
||||
? null
|
||||
: (SnRealm? value) {
|
||||
currentRealm.value = value;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.public),
|
||||
title: Text('publicChat').tr(),
|
||||
subtitle: Text('publicChatDescription').tr(),
|
||||
value: isPublic.value,
|
||||
onChanged: (value) {
|
||||
isPublic.value = value ?? true;
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.travel_explore),
|
||||
title: Text('communityChat').tr(),
|
||||
subtitle: Text('communityChatDescription').tr(),
|
||||
value: isCommunity.value,
|
||||
onChanged: (value) {
|
||||
isCommunity.value = value ?? false;
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
label: const Text('Save'),
|
||||
icon: const Icon(Symbols.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(all: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import "package:island/widgets/chat/message_item.dart";
|
||||
import "package:island/widgets/response.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
import "package:island/services/responsive.dart";
|
||||
import "package:island/pods/messages_notifier.dart";
|
||||
import "package:island/pods/chat/messages_notifier.dart";
|
||||
|
||||
class PublicRoomPreview extends HookConsumerWidget {
|
||||
final String id;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -410,7 +410,7 @@ class ChatDetailScreen extends HookConsumerWidget {
|
||||
if (result is SearchMessagesResult) {
|
||||
// Navigate back to room screen with message to jump to
|
||||
if (context.mounted) {
|
||||
context.pop(result.messageId);
|
||||
context.pop(result);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,11 +3,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/messages_notifier.dart';
|
||||
import 'package:island/pods/chat/messages_notifier.dart';
|
||||
import 'package:island/pods/chat/chat_rooms.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/chat/message_list_tile.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'dart:async';
|
||||
|
||||
// Class to represent the result when popping from search messages
|
||||
class SearchMessagesResult {
|
||||
@@ -15,6 +18,93 @@ class SearchMessagesResult {
|
||||
const SearchMessagesResult(this.messageId);
|
||||
}
|
||||
|
||||
// Search states for better UX
|
||||
enum SearchState { idle, searching, results, noResults, error }
|
||||
|
||||
class _SearchFilters extends StatelessWidget {
|
||||
final ValueNotifier<bool> withLinks;
|
||||
final ValueNotifier<bool> withAttachments;
|
||||
final void Function(String) performSearch;
|
||||
final TextEditingController searchController;
|
||||
final bool isLarge;
|
||||
|
||||
const _SearchFilters({
|
||||
required this.withLinks,
|
||||
required this.withAttachments,
|
||||
required this.performSearch,
|
||||
required this.searchController,
|
||||
required this.isLarge,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isLarge) {
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Symbols.link,
|
||||
color:
|
||||
withLinks.value
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
onPressed: () {
|
||||
withLinks.value = !withLinks.value;
|
||||
performSearch(searchController.text);
|
||||
},
|
||||
tooltip: 'searchLinks'.tr(),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Symbols.file_copy,
|
||||
color:
|
||||
withAttachments.value
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
onPressed: () {
|
||||
withAttachments.value = !withAttachments.value;
|
||||
performSearch(searchController.text);
|
||||
},
|
||||
tooltip: 'searchAttachments'.tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Row(
|
||||
children: [
|
||||
FilterChip(
|
||||
avatar: const Icon(Symbols.link, size: 16),
|
||||
label: const Text('searchLinks').tr(),
|
||||
selected: withLinks.value,
|
||||
onSelected: (bool? value) {
|
||||
withLinks.value = value!;
|
||||
performSearch(searchController.text);
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilterChip(
|
||||
avatar: const Icon(Symbols.file_copy, size: 16),
|
||||
label: const Text('searchAttachments').tr(),
|
||||
selected: withAttachments.value,
|
||||
onSelected: (bool? value) {
|
||||
withAttachments.value = value!;
|
||||
performSearch(searchController.text);
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SearchMessagesScreen extends HookConsumerWidget {
|
||||
final String roomId;
|
||||
|
||||
@@ -25,118 +115,390 @@ class SearchMessagesScreen extends HookConsumerWidget {
|
||||
final searchController = useTextEditingController();
|
||||
final withLinks = useState(false);
|
||||
final withAttachments = useState(false);
|
||||
final searchState = useState(SearchState.idle);
|
||||
final searchResultCount = useState<int?>(null);
|
||||
final searchResults = useState<AsyncValue<List<dynamic>>>(
|
||||
const AsyncValue.data([]),
|
||||
);
|
||||
|
||||
// Debounce timer for search optimization
|
||||
final debounceTimer = useRef<Timer?>(null);
|
||||
|
||||
final messagesNotifier = ref.read(
|
||||
messagesNotifierProvider(roomId).notifier,
|
||||
);
|
||||
final messages = ref.watch(messagesNotifierProvider(roomId));
|
||||
|
||||
// Optimized search function with debouncing
|
||||
void performSearch(String query) async {
|
||||
final trimmedQuery = query.trim();
|
||||
final hasFilters = withLinks.value || withAttachments.value;
|
||||
|
||||
if (trimmedQuery.isEmpty && !hasFilters) {
|
||||
searchState.value = SearchState.idle;
|
||||
searchResultCount.value = null;
|
||||
searchResults.value = const AsyncValue.data([]);
|
||||
return;
|
||||
}
|
||||
|
||||
searchState.value = SearchState.searching;
|
||||
searchResults.value = const AsyncValue.loading();
|
||||
|
||||
// Cancel previous search if still active
|
||||
debounceTimer.value?.cancel();
|
||||
|
||||
// Debounce search to avoid excessive API calls
|
||||
debounceTimer.value = Timer(const Duration(milliseconds: 300), () async {
|
||||
try {
|
||||
final results = await messagesNotifier.getSearchResults(
|
||||
query.trim(),
|
||||
withLinks: withLinks.value,
|
||||
withAttachments: withAttachments.value,
|
||||
);
|
||||
searchResults.value = AsyncValue.data(results);
|
||||
searchState.value =
|
||||
results.isEmpty ? SearchState.noResults : SearchState.results;
|
||||
searchResultCount.value = results.length;
|
||||
} catch (error, stackTrace) {
|
||||
searchResults.value = AsyncValue.error(error, stackTrace);
|
||||
searchState.value = SearchState.error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Search state is now managed locally in performSearch
|
||||
|
||||
useEffect(() {
|
||||
// Clear search when screen is disposed
|
||||
return () {
|
||||
messagesNotifier.clearSearch();
|
||||
debounceTimer.value?.cancel();
|
||||
// Note: Don't access ref here as widget may be disposed
|
||||
// Flashing messages will be cleared by the next screen or jump operation
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Clear flashing messages when screen initializes (safer than in dispose)
|
||||
useEffect(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Clear flashing messages when entering search screen
|
||||
ref.read(flashingMessagesProvider.notifier).state = {};
|
||||
});
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
final isLarge = isWideScreen(context);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: const Text('searchMessages').tr()),
|
||||
appBar: AppBar(
|
||||
title: const Text('searchMessages').tr(),
|
||||
bottom:
|
||||
searchState.value == SearchState.searching
|
||||
? const PreferredSize(
|
||||
preferredSize: Size.fromHeight(2),
|
||||
child: LinearProgressIndicator(),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'searchMessagesHint'.tr(),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
top: 12,
|
||||
bottom: 16,
|
||||
),
|
||||
suffix: IconButton(
|
||||
iconSize: 18,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
messagesNotifier.clearSearch();
|
||||
},
|
||||
),
|
||||
),
|
||||
onChanged: (query) {
|
||||
messagesNotifier.searchMessages(
|
||||
query,
|
||||
withLinks: withLinks.value,
|
||||
withAttachments: withAttachments.value,
|
||||
);
|
||||
},
|
||||
// Search input section
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||||
child:
|
||||
isLarge
|
||||
? Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'searchMessagesHint'.tr(),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (searchResultCount.value != null &&
|
||||
searchState.value ==
|
||||
SearchState.results)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(
|
||||
right: 8,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'${searchResultCount.value}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (searchController.text.isNotEmpty)
|
||||
IconButton(
|
||||
iconSize: 18,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
performSearch('');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onChanged: performSearch,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: _SearchFilters(
|
||||
withLinks: withLinks,
|
||||
withAttachments: withAttachments,
|
||||
performSearch: performSearch,
|
||||
searchController: searchController,
|
||||
isLarge: isLarge,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: searchController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'searchMessagesHint'.tr(),
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 12,
|
||||
),
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (searchResultCount.value != null &&
|
||||
searchState.value ==
|
||||
SearchState.results)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'${searchResultCount.value}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (searchController.text.isNotEmpty)
|
||||
IconButton(
|
||||
iconSize: 18,
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
performSearch('');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onChanged: performSearch,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
_SearchFilters(
|
||||
withLinks: withLinks,
|
||||
withAttachments: withAttachments,
|
||||
performSearch: performSearch,
|
||||
searchController: searchController,
|
||||
isLarge: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CheckboxListTile(
|
||||
secondary: const Icon(Symbols.link),
|
||||
title: const Text('searchLinks').tr(),
|
||||
value: withLinks.value,
|
||||
onChanged: (bool? value) {
|
||||
withLinks.value = value!;
|
||||
messagesNotifier.searchMessages(
|
||||
searchController.text,
|
||||
withLinks: withLinks.value,
|
||||
withAttachments: withAttachments.value,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: CheckboxListTile(
|
||||
secondary: const Icon(Symbols.file_copy),
|
||||
title: const Text('searchAttachments').tr(),
|
||||
value: withAttachments.value,
|
||||
onChanged: (bool? value) {
|
||||
withAttachments.value = value!;
|
||||
messagesNotifier.searchMessages(
|
||||
searchController.text,
|
||||
withLinks: withLinks.value,
|
||||
withAttachments: withAttachments.value,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
|
||||
// Search results section
|
||||
Expanded(
|
||||
child: messages.when(
|
||||
data:
|
||||
(messageList) =>
|
||||
messageList.isEmpty
|
||||
? Center(child: Text('noMessagesFound'.tr()))
|
||||
: SuperListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
reverse: false, // Show newest messages at the top
|
||||
itemCount: messageList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messageList[index];
|
||||
return MessageListTile(
|
||||
message: message,
|
||||
onJump: (messageId) {
|
||||
// Return the search result and pop back to room detail
|
||||
context.pop(SearchMessagesResult(messageId));
|
||||
},
|
||||
);
|
||||
},
|
||||
child: searchResults.value.when(
|
||||
data: (messageList) {
|
||||
switch (searchState.value) {
|
||||
case SearchState.idle:
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search,
|
||||
size: 64,
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, _) => Center(
|
||||
child: Text('errorGeneric'.tr(args: [error.toString()])),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'searchMessagesHint'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
case SearchState.noResults:
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off,
|
||||
size: 64,
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'noMessagesFound'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'tryDifferentKeywords'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
case SearchState.results:
|
||||
return SuperListView.builder(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
reverse: false, // Show newest messages at the top
|
||||
itemCount: messageList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messageList[index];
|
||||
return MessageListTile(
|
||||
message: message,
|
||||
onJump: (messageId) {
|
||||
// Return the search result and pop back to room detail
|
||||
context.pop(SearchMessagesResult(messageId));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
loading: () {
|
||||
if (searchState.value == SearchState.searching) {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Searching...'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
},
|
||||
error: (error, _) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'searchError'.tr(),
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => performSearch(searchController.text),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('retry').tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -8,8 +8,9 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/models/publisher.dart';
|
||||
import 'package:island/models/heatmap.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/screens/creators/publishers_form.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/utils/text.dart';
|
||||
import 'package:island/widgets/account/account_picker.dart';
|
||||
@@ -18,6 +19,7 @@ import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:island/widgets/activity_heatmap.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
@@ -33,6 +35,14 @@ Future<SnPublisherStats?> publisherStats(Ref ref, String? uname) async {
|
||||
return SnPublisherStats.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<SnHeatmap?> publisherHeatmap(Ref ref, String? uname) async {
|
||||
if (uname == null) return null;
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/sphere/publishers/$uname/heatmap');
|
||||
return SnHeatmap.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<SnPublisherMember?> publisherIdentity(Ref ref, String uname) async {
|
||||
try {
|
||||
@@ -102,42 +112,168 @@ class PublisherMemberListNotifier extends _$PublisherMemberListNotifier
|
||||
}
|
||||
}
|
||||
|
||||
class CreatorHubShellScreen extends StatelessWidget {
|
||||
final Widget child;
|
||||
const CreatorHubShellScreen({super.key, required this.child});
|
||||
class PublisherSelector extends StatelessWidget {
|
||||
final SnPublisher? currentPublisher;
|
||||
final List<DropdownMenuItem<SnPublisher>> publishersMenu;
|
||||
final ValueChanged<SnPublisher?>? onChanged;
|
||||
final bool isReadOnly;
|
||||
|
||||
const PublisherSelector({
|
||||
super.key,
|
||||
required this.currentPublisher,
|
||||
required this.publishersMenu,
|
||||
this.onChanged,
|
||||
this.isReadOnly = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isWide = isWideScreen(context);
|
||||
if (isWide) {
|
||||
return AppBackground(
|
||||
isRoot: true,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(flex: 2, child: const CreatorHubScreen(isAside: true)),
|
||||
const VerticalDivider(width: 1),
|
||||
Flexible(flex: 3, child: child),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (isReadOnly || currentPublisher == null) {
|
||||
return ProfilePictureWidget(
|
||||
radius: 16,
|
||||
fileId: currentPublisher?.picture?.id,
|
||||
).center().padding(right: 8);
|
||||
}
|
||||
return AppBackground(isRoot: true, child: child);
|
||||
|
||||
return DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SnPublisher>(
|
||||
value: currentPublisher,
|
||||
hint: CircleAvatar(
|
||||
radius: 16,
|
||||
child: Icon(
|
||||
Symbols.person,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSecondaryContainer.withOpacity(0.9),
|
||||
fill: 1,
|
||||
),
|
||||
).center().padding(right: 8),
|
||||
items: publishersMenu,
|
||||
onChanged: onChanged,
|
||||
selectedItemBuilder: (context) {
|
||||
return publishersMenu
|
||||
.map(
|
||||
(e) => ProfilePictureWidget(
|
||||
radius: 16,
|
||||
fileId: e.value?.picture?.id,
|
||||
).center().padding(right: 8),
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
buttonStyleData: ButtonStyleData(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.only(left: 14, right: 8),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)),
|
||||
),
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
width: 320,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 64,
|
||||
padding: EdgeInsets.only(left: 14, right: 14),
|
||||
),
|
||||
iconStyleData: IconStyleData(
|
||||
icon: Icon(Icons.arrow_drop_down),
|
||||
iconSize: 19,
|
||||
iconEnabledColor: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
iconDisabledColor: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PublisherUnselectedWidget extends HookConsumerWidget {
|
||||
final ValueChanged<SnPublisher> onPublisherSelected;
|
||||
|
||||
const _PublisherUnselectedWidget({required this.onPublisherSelected});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final publishers = ref.watch(publishersManagedProvider);
|
||||
final publisherInvites = ref.watch(publisherInvitesProvider);
|
||||
|
||||
final hasPublishers = publishers.value?.isNotEmpty ?? false;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
if (!hasPublishers) ...[
|
||||
const Icon(
|
||||
Symbols.info,
|
||||
fill: 1,
|
||||
size: 32,
|
||||
).padding(bottom: 6, top: 24),
|
||||
Text(
|
||||
'creatorHubUnselectedHint',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
).tr(),
|
||||
const Gap(24),
|
||||
],
|
||||
if (hasPublishers)
|
||||
...(publishers.value?.map(
|
||||
(publisher) => ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
leading: ProfilePictureWidget(file: publisher.picture),
|
||||
title: Text(publisher.nick),
|
||||
subtitle: Text('@${publisher.name}'),
|
||||
onTap: () => onPublisherSelected(publisher),
|
||||
),
|
||||
) ??
|
||||
[]),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
leading: const CircleAvatar(child: Icon(Symbols.mail)),
|
||||
title: Text('publisherCollabInvitation').tr(),
|
||||
subtitle: Text(
|
||||
'publisherCollabInvitationCount',
|
||||
).plural(publisherInvites.value?.length ?? 0),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => const _PublisherInviteSheet(),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
leading: const CircleAvatar(child: Icon(Symbols.add)),
|
||||
title: Text('createPublisher').tr(),
|
||||
subtitle: Text('createPublisherHint').tr(),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context.pushNamed('creatorNew').then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(publishersManagedProvider);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CreatorHubScreen extends HookConsumerWidget {
|
||||
final bool isAside;
|
||||
const CreatorHubScreen({super.key, this.isAside = false});
|
||||
const CreatorHubScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isWide = isWideScreen(context);
|
||||
if (isWide && !isAside) {
|
||||
return Container(color: Theme.of(context).colorScheme.surface);
|
||||
}
|
||||
|
||||
final publishers = ref.watch(publishersManagedProvider);
|
||||
final publisherInvites = ref.watch(publisherInvitesProvider);
|
||||
final currentPublisher = useState<SnPublisher?>(
|
||||
publishers.value?.firstOrNull,
|
||||
);
|
||||
@@ -203,303 +339,261 @@ class CreatorHubScreen extends HookConsumerWidget {
|
||||
publisherStatsProvider(currentPublisher.value?.name),
|
||||
);
|
||||
|
||||
final publisherHeatmap = ref.watch(
|
||||
publisherHeatmapProvider(currentPublisher.value?.name),
|
||||
);
|
||||
|
||||
final publisherFeatures = ref.watch(
|
||||
publisherFeaturesProvider(currentPublisher.value?.name),
|
||||
);
|
||||
|
||||
Widget buildNavigationWidget(bool isWide) {
|
||||
final leftItems = [
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
minTileHeight: 48,
|
||||
title: Text('stickers').tr(),
|
||||
trailing: Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.ar_stickers),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'creatorStickers',
|
||||
pathParameters: {'name': currentPublisher.value!.name},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
minTileHeight: 48,
|
||||
title: Text('posts').tr(),
|
||||
trailing: Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.sticky_note_2),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'creatorPosts',
|
||||
pathParameters: {'name': currentPublisher.value!.name},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
minTileHeight: 48,
|
||||
title: Text('polls').tr(),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.poll),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'creatorPolls',
|
||||
pathParameters: {'name': currentPublisher.value!.name},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
minTileHeight: 48,
|
||||
title: const Text('webFeeds').tr(),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.rss_feed),
|
||||
onTap: () {
|
||||
context.push('/creators/${currentPublisher.value!.name}/feeds');
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
final rightItems = [
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
minTileHeight: 48,
|
||||
title: Text('publisherMembers').tr(),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.group),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder:
|
||||
(context) => _PublisherMemberListSheet(
|
||||
publisherUname: currentPublisher.value!.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ExpansionTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
title: Text('publisherFeatures').tr(),
|
||||
leading: const Icon(Symbols.flag),
|
||||
tilePadding: const EdgeInsets.only(left: 16, right: 24),
|
||||
minTileHeight: 48,
|
||||
children: [
|
||||
...publisherFeatures.when(
|
||||
data: (data) {
|
||||
return data.entries.map((entry) {
|
||||
final keyPrefix =
|
||||
'publisherFeature${entry.key.capitalizeEachWord()}';
|
||||
return ListTile(
|
||||
minTileHeight: 48,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
leading: Icon(
|
||||
Symbols.circle,
|
||||
color: entry.value ? Colors.green : Colors.red,
|
||||
fill: 1,
|
||||
size: 16,
|
||||
).padding(left: 2, top: 4),
|
||||
title: Text(keyPrefix).tr(),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${keyPrefix}Description').tr(),
|
||||
if (!entry.value) Text('${keyPrefix}Hint').tr().bold(),
|
||||
],
|
||||
),
|
||||
isThreeLine: true,
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
error: (_, _) => [],
|
||||
loading: () => [],
|
||||
),
|
||||
],
|
||||
),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
minTileHeight: 48,
|
||||
title: Text('editPublisher').tr(),
|
||||
trailing: Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.edit),
|
||||
onTap: updatePublisher,
|
||||
),
|
||||
ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
minTileHeight: 48,
|
||||
title: Text('deletePublisher').tr(),
|
||||
trailing: Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.delete),
|
||||
onTap: deletePublisher,
|
||||
),
|
||||
];
|
||||
|
||||
if (isWide) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(children: leftItems),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(children: rightItems),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12);
|
||||
} else {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [...leftItems, const Divider(height: 8), ...rightItems],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
leading: !isWide ? const PageBackButton() : null,
|
||||
leading: const PageBackButton(),
|
||||
title: Text('creatorHub').tr(),
|
||||
actions: [
|
||||
DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<SnPublisher>(
|
||||
alignment: Alignment.centerRight,
|
||||
value: currentPublisher.value,
|
||||
hint: CircleAvatar(
|
||||
radius: 16,
|
||||
child: Icon(
|
||||
Symbols.person,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSecondaryContainer.withOpacity(0.9),
|
||||
fill: 1,
|
||||
),
|
||||
).center().padding(right: 8),
|
||||
items: [...publishersMenu],
|
||||
if (!isWideScreen(context))
|
||||
PublisherSelector(
|
||||
currentPublisher: currentPublisher.value,
|
||||
publishersMenu: publishersMenu,
|
||||
onChanged: (value) {
|
||||
currentPublisher.value = value;
|
||||
},
|
||||
selectedItemBuilder: (context) {
|
||||
return [
|
||||
...publishersMenu.map(
|
||||
(e) => ProfilePictureWidget(
|
||||
radius: 16,
|
||||
fileId: e.value?.picture?.id,
|
||||
).center().padding(right: 8),
|
||||
),
|
||||
];
|
||||
},
|
||||
buttonStyleData: ButtonStyleData(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.only(left: 14, right: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
dropdownStyleData: DropdownStyleData(
|
||||
width: 320,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
menuItemStyleData: const MenuItemStyleData(
|
||||
height: 64,
|
||||
padding: EdgeInsets.only(left: 14, right: 14),
|
||||
),
|
||||
iconStyleData: IconStyleData(
|
||||
icon: Icon(Icons.arrow_drop_down),
|
||||
iconSize: 19,
|
||||
iconEnabledColor:
|
||||
Theme.of(context).appBarTheme.foregroundColor!,
|
||||
iconDisabledColor:
|
||||
Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
body: publisherStats.when(
|
||||
data:
|
||||
(stats) => SingleChildScrollView(
|
||||
child:
|
||||
currentPublisher.value == null
|
||||
? Column(
|
||||
children: [
|
||||
const Gap(24),
|
||||
const Icon(Symbols.info, size: 32).padding(bottom: 4),
|
||||
Text(
|
||||
'creatorHubUnselectedHint',
|
||||
textAlign: TextAlign.center,
|
||||
).tr(),
|
||||
const Gap(24),
|
||||
const Divider(height: 1),
|
||||
...(publishers.value?.map(
|
||||
(publisher) => ListTile(
|
||||
leading: ProfilePictureWidget(
|
||||
file: publisher.picture,
|
||||
),
|
||||
title: Text(publisher.nick),
|
||||
subtitle: Text('@${publisher.name}'),
|
||||
onTap: () {
|
||||
body: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = isWideScreen(context);
|
||||
final maxWidth = isWide ? 800.0 : double.infinity;
|
||||
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
child: publisherStats.when(
|
||||
data:
|
||||
(stats) => SingleChildScrollView(
|
||||
child:
|
||||
currentPublisher.value == null
|
||||
? ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: _PublisherUnselectedWidget(
|
||||
onPublisherSelected: (publisher) {
|
||||
currentPublisher.value = publisher;
|
||||
},
|
||||
),
|
||||
) ??
|
||||
[]),
|
||||
ListTile(
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Symbols.mail),
|
||||
),
|
||||
title: Text('publisherCollabInvitation').tr(),
|
||||
subtitle: Text(
|
||||
'publisherCollabInvitationCount',
|
||||
).plural(publisherInvites.value?.length ?? 0),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => const _PublisherInviteSheet(),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const CircleAvatar(
|
||||
child: Icon(Symbols.add),
|
||||
),
|
||||
title: Text('createPublisher').tr(),
|
||||
subtitle: Text('createPublisherHint').tr(),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context.pushNamed('creatorNew').then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(publishersManagedProvider);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
if (stats != null)
|
||||
_PublisherStatsWidget(
|
||||
stats: stats,
|
||||
).padding(vertical: 12, horizontal: 12),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
title: Text('stickers').tr(),
|
||||
trailing: Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.ar_stickers),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'creatorStickers',
|
||||
pathParameters: {
|
||||
'name': currentPublisher.value!.name,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
title: Text('posts').tr(),
|
||||
trailing: Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.sticky_note_2),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'creatorPosts',
|
||||
pathParameters: {
|
||||
'name': currentPublisher.value!.name,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
title: Text('polls').tr(),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.poll),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'creatorPolls',
|
||||
pathParameters: {
|
||||
'name': currentPublisher.value!.name,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
title: Text('publisherMembers').tr(),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.group),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder:
|
||||
(context) => _PublisherMemberListSheet(
|
||||
publisherUname:
|
||||
currentPublisher.value!.name,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
title: const Text('webFeeds').tr(),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.rss_feed),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
onTap: () {
|
||||
context.push(
|
||||
'/creators/${currentPublisher.value!.name}/feeds',
|
||||
);
|
||||
},
|
||||
),
|
||||
ExpansionTile(
|
||||
title: Text('publisherFeatures').tr(),
|
||||
leading: const Icon(Symbols.flag),
|
||||
tilePadding: EdgeInsets.symmetric(horizontal: 24),
|
||||
minTileHeight: 48,
|
||||
children: [
|
||||
...publisherFeatures.when(
|
||||
data: (data) {
|
||||
return data.entries.map((entry) {
|
||||
final keyPrefix =
|
||||
'publisherFeature${entry.key.capitalizeEachWord()}';
|
||||
return ListTile(
|
||||
minTileHeight: 48,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
leading: Icon(
|
||||
Symbols.circle,
|
||||
color:
|
||||
entry.value
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
fill: 1,
|
||||
size: 16,
|
||||
).padding(left: 2, top: 4),
|
||||
title: Text(keyPrefix).tr(),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${keyPrefix}Description').tr(),
|
||||
if (!entry.value)
|
||||
Text(
|
||||
'${keyPrefix}Hint',
|
||||
).tr().bold(),
|
||||
],
|
||||
),
|
||||
isThreeLine: true,
|
||||
);
|
||||
}).toList();
|
||||
},
|
||||
error: (_, _) => [],
|
||||
loading: () => [],
|
||||
).center()
|
||||
: isWide
|
||||
? Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
PublisherSelector(
|
||||
currentPublisher: currentPublisher.value,
|
||||
publishersMenu: publishersMenu,
|
||||
onChanged: (value) {
|
||||
currentPublisher.value = value;
|
||||
},
|
||||
),
|
||||
if (stats != null)
|
||||
_PublisherStatsWidget(
|
||||
stats: stats,
|
||||
heatmap: publisherHeatmap.value,
|
||||
).padding(horizontal: 12),
|
||||
buildNavigationWidget(true),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (stats != null)
|
||||
_PublisherStatsWidget(
|
||||
stats: stats,
|
||||
heatmap: publisherHeatmap.value,
|
||||
).padding(horizontal: 16),
|
||||
buildNavigationWidget(false),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Divider(height: 1).padding(vertical: 8),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
title: Text('editPublisher').tr(),
|
||||
trailing: Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.edit),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
onTap: () {
|
||||
updatePublisher();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
minTileHeight: 48,
|
||||
title: Text('deletePublisher').tr(),
|
||||
trailing: Icon(Symbols.chevron_right),
|
||||
leading: const Icon(Symbols.delete),
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
onTap: () {
|
||||
deletePublisher();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -507,7 +601,8 @@ class CreatorHubScreen extends HookConsumerWidget {
|
||||
|
||||
class _PublisherStatsWidget extends StatelessWidget {
|
||||
final SnPublisherStats stats;
|
||||
const _PublisherStatsWidget({required this.stats});
|
||||
final SnHeatmap? heatmap;
|
||||
const _PublisherStatsWidget({required this.stats, this.heatmap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -560,6 +655,7 @@ class _PublisherStatsWidget extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (heatmap != null) ActivityHeatmapWidget(heatmap: heatmap!),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -576,21 +672,36 @@ class _PublisherStatsWidget extends StatelessWidget {
|
||||
height: 100,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
statValue,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
statLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(),
|
||||
],
|
||||
child: Tooltip(
|
||||
richMessage: TextSpan(
|
||||
text: statLabel.tr(),
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
children: [
|
||||
TextSpan(text: ' '),
|
||||
TextSpan(
|
||||
text: statValue,
|
||||
style: TextStyle(fontWeight: FontWeight.normal),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
statValue,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
statLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -149,6 +149,127 @@ class _PublisherStatsProviderElement
|
||||
String? get uname => (origin as PublisherStatsProvider).uname;
|
||||
}
|
||||
|
||||
String _$publisherHeatmapHash() => r'5f70c55e14629ec8628445a317888e02fccd9af2';
|
||||
|
||||
/// See also [publisherHeatmap].
|
||||
@ProviderFor(publisherHeatmap)
|
||||
const publisherHeatmapProvider = PublisherHeatmapFamily();
|
||||
|
||||
/// See also [publisherHeatmap].
|
||||
class PublisherHeatmapFamily extends Family<AsyncValue<SnHeatmap?>> {
|
||||
/// See also [publisherHeatmap].
|
||||
const PublisherHeatmapFamily();
|
||||
|
||||
/// See also [publisherHeatmap].
|
||||
PublisherHeatmapProvider call(String? uname) {
|
||||
return PublisherHeatmapProvider(uname);
|
||||
}
|
||||
|
||||
@override
|
||||
PublisherHeatmapProvider getProviderOverride(
|
||||
covariant PublisherHeatmapProvider provider,
|
||||
) {
|
||||
return call(provider.uname);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'publisherHeatmapProvider';
|
||||
}
|
||||
|
||||
/// See also [publisherHeatmap].
|
||||
class PublisherHeatmapProvider extends AutoDisposeFutureProvider<SnHeatmap?> {
|
||||
/// See also [publisherHeatmap].
|
||||
PublisherHeatmapProvider(String? uname)
|
||||
: this._internal(
|
||||
(ref) => publisherHeatmap(ref as PublisherHeatmapRef, uname),
|
||||
from: publisherHeatmapProvider,
|
||||
name: r'publisherHeatmapProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$publisherHeatmapHash,
|
||||
dependencies: PublisherHeatmapFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
PublisherHeatmapFamily._allTransitiveDependencies,
|
||||
uname: uname,
|
||||
);
|
||||
|
||||
PublisherHeatmapProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.uname,
|
||||
}) : super.internal();
|
||||
|
||||
final String? uname;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<SnHeatmap?> Function(PublisherHeatmapRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: PublisherHeatmapProvider._internal(
|
||||
(ref) => create(ref as PublisherHeatmapRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
uname: uname,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<SnHeatmap?> createElement() {
|
||||
return _PublisherHeatmapProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is PublisherHeatmapProvider && other.uname == uname;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, uname.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin PublisherHeatmapRef on AutoDisposeFutureProviderRef<SnHeatmap?> {
|
||||
/// The parameter `uname` of this provider.
|
||||
String? get uname;
|
||||
}
|
||||
|
||||
class _PublisherHeatmapProviderElement
|
||||
extends AutoDisposeFutureProviderElement<SnHeatmap?>
|
||||
with PublisherHeatmapRef {
|
||||
_PublisherHeatmapProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String? get uname => (origin as PublisherHeatmapProvider).uname;
|
||||
}
|
||||
|
||||
String _$publisherIdentityHash() => r'299372f25fa4b2bf8e11a8ba2d645100fc38e76f';
|
||||
|
||||
/// See also [publisherIdentity].
|
||||
|
||||
@@ -84,6 +84,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: const Text('Polls')),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _createPoll(context),
|
||||
@@ -105,10 +106,13 @@ class CreatorPollListScreen extends HookConsumerWidget {
|
||||
return endItemView;
|
||||
}
|
||||
final pollWithStats = data.items[index];
|
||||
return _CreatorPollItem(
|
||||
pollWithStats: pollWithStats,
|
||||
pubName: pubName,
|
||||
);
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: _CreatorPollItem(
|
||||
pollWithStats: pollWithStats,
|
||||
pubName: pubName,
|
||||
),
|
||||
).center();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/post/post_list.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class CreatorPostListScreen extends HookConsumerWidget {
|
||||
final String pubName;
|
||||
@@ -16,62 +13,20 @@ class CreatorPostListScreen extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final refreshKey = useState(0);
|
||||
|
||||
void showCreatePostSheet() {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'create'.tr(),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.edit),
|
||||
title: Text('Post'),
|
||||
subtitle: Text('Create a regular post'),
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
final result = await context.pushNamed(
|
||||
'postCompose',
|
||||
queryParameters: {'type': '0'},
|
||||
);
|
||||
if (result == true) {
|
||||
refreshKey.value++;
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Symbols.article),
|
||||
title: Text('Article'),
|
||||
subtitle: Text('Create a detailed article'),
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
final result = await context.pushNamed(
|
||||
'postCompose',
|
||||
queryParameters: {'type': '1'},
|
||||
);
|
||||
if (result == true) {
|
||||
refreshKey.value++;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: Text('posts').tr()),
|
||||
body: CustomScrollView(
|
||||
key: ValueKey(refreshKey.value),
|
||||
slivers: [
|
||||
SliverPostList(pubName: pubName, itemType: PostItemType.creator),
|
||||
SliverPostList(
|
||||
pubName: pubName,
|
||||
itemType: PostItemType.creator,
|
||||
maxWidth: 640,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: showCreatePostSheet,
|
||||
child: const Icon(Symbols.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,21 +10,19 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/publisher.dart';
|
||||
import 'package:island/models/realm.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/screens/realm/realms.dart';
|
||||
import 'package:island/services/file.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/realm/realm_selection_dropdown.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'publishers.g.dart';
|
||||
part 'publishers_form.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<SnPublisher>> publishersManaged(Ref ref) async {
|
||||
@@ -95,19 +93,13 @@ class EditPublisherScreen extends HookConsumerWidget {
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final baseUrl = ref.watch(serverUrlProvider);
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putFileToCloud(
|
||||
await FileUploader.createCloudFile(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: result.name,
|
||||
mimetype: result.mimeType ?? 'image/jpeg',
|
||||
client: ref.read(apiClientProvider),
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
@@ -186,27 +178,15 @@ class EditPublisherScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text(name == null ? 'createPublisher' : 'editPublisher').tr(),
|
||||
leading: const PageBackButton(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: getTabbedPadding(context, bottom: 16),
|
||||
padding: EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
RealmSelectionDropdown(
|
||||
value: currentRealm.value,
|
||||
realms: joinedRealms.when(
|
||||
data: (realms) => realms,
|
||||
loading: () => [],
|
||||
error: (_, _) => [],
|
||||
),
|
||||
onChanged: (SnRealm? value) {
|
||||
currentRealm.value = value;
|
||||
},
|
||||
isLoading: joinedRealms.isLoading,
|
||||
error: joinedRealms.error?.toString(),
|
||||
),
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
@@ -280,6 +260,32 @@ class EditPublisherScreen extends HookConsumerWidget {
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
DropdownButtonFormField<SnRealm>(
|
||||
value: currentRealm.value,
|
||||
decoration: InputDecoration(labelText: 'realm'.tr()),
|
||||
items: [
|
||||
DropdownMenuItem<SnRealm>(
|
||||
value: null,
|
||||
child: Text('individual'.tr()),
|
||||
),
|
||||
...joinedRealms.maybeWhen(
|
||||
data:
|
||||
(realms) => realms.map(
|
||||
(realm) => DropdownMenuItem(
|
||||
value: realm,
|
||||
child: Text(realm.name),
|
||||
),
|
||||
),
|
||||
orElse: () => [],
|
||||
),
|
||||
],
|
||||
onChanged:
|
||||
joinedRealms.isLoading
|
||||
? null
|
||||
: (SnRealm? value) {
|
||||
currentRealm.value = value;
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'publishers.dart';
|
||||
part of 'publishers_form.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
@@ -8,13 +8,14 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/sticker.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/creators/stickers/stickers.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_file_picker.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
@@ -33,13 +34,13 @@ Future<List<SnSticker>> stickerPackContent(Ref ref, String packId) async {
|
||||
.toList();
|
||||
}
|
||||
|
||||
class StickerPackDetailScreen extends HookConsumerWidget {
|
||||
class StickerPackDetailContent extends HookConsumerWidget {
|
||||
final String id;
|
||||
final String pubName;
|
||||
const StickerPackDetailScreen({
|
||||
const StickerPackDetailContent({
|
||||
super.key,
|
||||
required this.pubName,
|
||||
required this.id,
|
||||
required this.pubName,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -67,193 +68,170 @@ class StickerPackDetailScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(pack.value?.name ?? 'loading'.tr()),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
onPressed: () {
|
||||
context
|
||||
.pushNamed(
|
||||
'creatorStickerNew',
|
||||
pathParameters: {'name': pubName, 'packId': id},
|
||||
)
|
||||
.then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(stickerPackContentProvider(id));
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
_StickerPackActionMenu(
|
||||
pubName: pubName,
|
||||
packId: id,
|
||||
iconShadow: Shadow(),
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
body: pack.when(
|
||||
data:
|
||||
(pack) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(pack!.description),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.folder, size: 16),
|
||||
Text(
|
||||
'${packContent.value?.length ?? 0}/24',
|
||||
style: GoogleFonts.robotoMono(),
|
||||
),
|
||||
],
|
||||
).opacity(0.85),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.sell, size: 16),
|
||||
Text(pack.prefix, style: GoogleFonts.robotoMono()),
|
||||
],
|
||||
).opacity(0.85),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.tag, size: 16),
|
||||
SelectableText(
|
||||
return pack.when(
|
||||
data:
|
||||
(pack) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(pack!.description),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.folder, size: 16),
|
||||
Text(
|
||||
'${packContent.value?.length ?? 0}/24',
|
||||
style: GoogleFonts.robotoMono(),
|
||||
),
|
||||
],
|
||||
).opacity(0.85),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.sell, size: 16),
|
||||
Text(pack.prefix, style: GoogleFonts.robotoMono()),
|
||||
],
|
||||
).opacity(0.85),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.tag, size: 16),
|
||||
Flexible(
|
||||
child: SelectableText(
|
||||
pack.id,
|
||||
maxLines: 1,
|
||||
style: GoogleFonts.robotoMono(),
|
||||
),
|
||||
],
|
||||
).opacity(0.85),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 24),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: packContent.when(
|
||||
data:
|
||||
(stickers) => RefreshIndicator(
|
||||
onRefresh:
|
||||
() => ref.refresh(
|
||||
stickerPackContentProvider(id).future,
|
||||
),
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 20,
|
||||
),
|
||||
],
|
||||
).opacity(0.85),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 24),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: packContent.when(
|
||||
data:
|
||||
(stickers) => RefreshIndicator(
|
||||
onRefresh:
|
||||
() => ref.refresh(
|
||||
stickerPackContentProvider(id).future,
|
||||
),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 48,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemCount: stickers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final sticker = stickers[index];
|
||||
return ContextMenuWidget(
|
||||
menuProvider: (_) {
|
||||
return Menu(
|
||||
children: [
|
||||
MenuAction(
|
||||
title: 'stickerCopyPlaceholder'.tr(),
|
||||
image: MenuImage.icon(Symbols.copy_all),
|
||||
callback: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text:
|
||||
':${pack.prefix}${sticker.slug}:',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuSeparator(),
|
||||
MenuAction(
|
||||
title: 'edit'.tr(),
|
||||
image: MenuImage.icon(Symbols.edit),
|
||||
callback: () {
|
||||
context
|
||||
.pushNamed(
|
||||
'creatorStickerEdit',
|
||||
pathParameters: {
|
||||
'name': pubName,
|
||||
'packId': id,
|
||||
'id': sticker.id,
|
||||
},
|
||||
)
|
||||
.then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(
|
||||
stickerPackContentProvider(
|
||||
id,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
title: 'delete'.tr(),
|
||||
image: MenuImage.icon(Symbols.delete),
|
||||
callback: () {
|
||||
deleteSticker(sticker);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: CloudImageWidget(
|
||||
fileId: sticker.imageId,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 20,
|
||||
),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 80,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemCount: stickers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final sticker = stickers[index];
|
||||
return ContextMenuWidget(
|
||||
menuProvider: (_) {
|
||||
return Menu(
|
||||
children: [
|
||||
MenuAction(
|
||||
title: 'stickerCopyPlaceholder'.tr(),
|
||||
image: MenuImage.icon(Symbols.copy_all),
|
||||
callback: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text:
|
||||
':${pack.prefix}+${sticker.slug}:',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuSeparator(),
|
||||
MenuAction(
|
||||
title: 'edit'.tr(),
|
||||
image: MenuImage.icon(Symbols.edit),
|
||||
callback: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'editSticker'.tr(),
|
||||
child: StickerForm(
|
||||
packId: id,
|
||||
id: sticker.id,
|
||||
),
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(
|
||||
stickerPackContentProvider(id),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
title: 'delete'.tr(),
|
||||
image: MenuImage.icon(Symbols.delete),
|
||||
callback: () {
|
||||
deleteSticker(sticker);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: CloudImageWidget(
|
||||
fileId: sticker.imageId,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
error:
|
||||
(err, _) =>
|
||||
Text(
|
||||
'Error: $err',
|
||||
).textAlignment(TextAlign.center).center(),
|
||||
loading: () => const CircularProgressIndicator().center(),
|
||||
),
|
||||
),
|
||||
error:
|
||||
(err, _) =>
|
||||
Text(
|
||||
'Error: $err',
|
||||
).textAlignment(TextAlign.center).center(),
|
||||
loading: () => const CircularProgressIndicator().center(),
|
||||
),
|
||||
],
|
||||
),
|
||||
error:
|
||||
(err, _) =>
|
||||
Text('Error: $err').textAlignment(TextAlign.center).center(),
|
||||
loading: () => const CircularProgressIndicator().center(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
error:
|
||||
(err, _) =>
|
||||
Text('Error: $err').textAlignment(TextAlign.center).center(),
|
||||
loading: () => const CircularProgressIndicator().center(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StickerPackActionMenu extends HookConsumerWidget {
|
||||
class StickerPackActionMenu extends HookConsumerWidget {
|
||||
final String pubName;
|
||||
final String packId;
|
||||
final Shadow iconShadow;
|
||||
|
||||
const _StickerPackActionMenu({
|
||||
const StickerPackActionMenu({
|
||||
super.key,
|
||||
required this.pubName,
|
||||
required this.packId,
|
||||
required this.iconShadow,
|
||||
@@ -267,7 +245,22 @@ class _StickerPackActionMenu extends HookConsumerWidget {
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
onTap: () {
|
||||
context.push('/creators/$pubName/stickers/$packId/edit');
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'editStickerPack'.tr(),
|
||||
child: StickerPackForm(
|
||||
pubName: pubName,
|
||||
packId: packId,
|
||||
),
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(stickerPackProvider(packId));
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -310,42 +303,10 @@ class _StickerPackActionMenu extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class StickerWithPackQuery with _$StickerWithPackQuery {
|
||||
const factory StickerWithPackQuery({
|
||||
required String packId,
|
||||
required String id,
|
||||
}) = _StickerWithPackQuery;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<SnSticker?> stickerPackSticker(
|
||||
Ref ref,
|
||||
StickerWithPackQuery? query,
|
||||
) async {
|
||||
if (query == null) return null;
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get(
|
||||
'/sphere/stickers/${query.packId}/content/${query.id}',
|
||||
);
|
||||
if (resp.data == null) return null;
|
||||
return SnSticker.fromJson(resp.data);
|
||||
}
|
||||
|
||||
class NewStickersScreen extends StatelessWidget {
|
||||
final String packId;
|
||||
const NewStickersScreen({super.key, required this.packId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return EditStickersScreen(packId: packId, id: null);
|
||||
}
|
||||
}
|
||||
|
||||
class EditStickersScreen extends HookConsumerWidget {
|
||||
class StickerForm extends HookConsumerWidget {
|
||||
final String packId;
|
||||
final String? id;
|
||||
const EditStickersScreen({super.key, required this.packId, required this.id});
|
||||
const StickerForm({super.key, required this.packId, this.id});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -395,87 +356,102 @@ class EditStickersScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(id == null ? 'createSticker' : 'editSticker').tr(),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 96,
|
||||
width: 96,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 80,
|
||||
width: 80,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child:
|
||||
(image.value?.isEmpty ?? true)
|
||||
? const SizedBox.shrink()
|
||||
: CloudImageWidget(fileId: image.value!),
|
||||
),
|
||||
child:
|
||||
(image.value?.isEmpty ?? true)
|
||||
? const SizedBox.shrink()
|
||||
: CloudImageWidget(fileId: image.value!),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: imageController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'stickerImage'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
suffix: InkWell(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => CloudFilePicker(),
|
||||
).then((value) {
|
||||
if (value == null) return;
|
||||
image.value = value[0].id;
|
||||
imageController.text = image.value!;
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
child: const Icon(
|
||||
Symbols.cloud_upload,
|
||||
).padding(horizontal: 4),
|
||||
),
|
||||
),
|
||||
readOnly: true,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'stickerSlug'.tr(),
|
||||
helperText: 'stickerSlugHint'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
),
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
IconButton.filledTonal(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => CloudFilePicker(
|
||||
allowedTypes: {UniversalFileType.image},
|
||||
),
|
||||
).then((value) {
|
||||
if (value == null) return;
|
||||
image.value = value[0].id;
|
||||
imageController.text = image.value!;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Symbols.cloud_upload),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(16),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'stickerSlug'.tr(),
|
||||
helperText: 'stickerSlugHint'.tr(),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : submit,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text(id == null ? 'create' : 'saveChanges').tr(),
|
||||
),
|
||||
),
|
||||
const Gap(12),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : submit,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text(id == null ? 'create' : 'saveChanges').tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 24),
|
||||
);
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class StickerWithPackQuery with _$StickerWithPackQuery {
|
||||
const factory StickerWithPackQuery({
|
||||
required String packId,
|
||||
required String id,
|
||||
}) = _StickerWithPackQuery;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<SnSticker?> stickerPackSticker(
|
||||
Ref ref,
|
||||
StickerWithPackQuery? query,
|
||||
) async {
|
||||
if (query == null) return null;
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get(
|
||||
'/sphere/stickers/${query.packId}/content/${query.id}',
|
||||
);
|
||||
if (resp.data == null) return null;
|
||||
return SnSticker.fromJson(resp.data);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/sticker.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/screens/creators/stickers/pack_detail.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
@@ -22,29 +24,50 @@ class StickersScreen extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final content = SliverStickerPacksList(pubName: pubName);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: const Text('stickers').tr(),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context
|
||||
.pushNamed(
|
||||
'creatorStickerPackNew',
|
||||
pathParameters: {'name': pubName},
|
||||
)
|
||||
.then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(stickerPacksNotifierProvider(pubName));
|
||||
}
|
||||
});
|
||||
},
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
actions: [const Gap(8)],
|
||||
),
|
||||
body: SliverStickerPacksList(pubName: pubName),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'createStickerPack'.tr(),
|
||||
child: StickerPackForm(pubName: pubName),
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(stickerPacksNotifierProvider(pubName));
|
||||
}
|
||||
});
|
||||
},
|
||||
child: const Icon(Symbols.add),
|
||||
),
|
||||
body:
|
||||
isWideScreen(context)
|
||||
? Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.only(top: 16),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
)
|
||||
: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -70,13 +93,52 @@ class SliverStickerPacksList extends HookConsumerWidget {
|
||||
|
||||
final sticker = data.items[index];
|
||||
return ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
title: Text(sticker.name),
|
||||
subtitle: Text(sticker.description),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'creatorStickerPackDetail',
|
||||
pathParameters: {'name': pubName, 'packId': sticker.id},
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: sticker.name,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
onPressed: () {
|
||||
final id = sticker.id;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'createSticker'.tr(),
|
||||
child: StickerForm(packId: id),
|
||||
),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
ref.invalidate(
|
||||
stickerPackContentProvider(id),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
StickerPackActionMenu(
|
||||
pubName: pubName,
|
||||
packId: sticker.id,
|
||||
iconShadow: Shadow(),
|
||||
),
|
||||
],
|
||||
child: StickerPackDetailContent(
|
||||
id: sticker.id,
|
||||
pubName: pubName,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -135,20 +197,10 @@ Future<SnStickerPack?> stickerPack(Ref ref, String? packId) async {
|
||||
return SnStickerPack.fromJson(resp.data);
|
||||
}
|
||||
|
||||
class NewStickerPacksScreen extends HookConsumerWidget {
|
||||
final String pubName;
|
||||
const NewStickerPacksScreen({super.key, required this.pubName});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return EditStickerPacksScreen(pubName: pubName);
|
||||
}
|
||||
}
|
||||
|
||||
class EditStickerPacksScreen extends HookConsumerWidget {
|
||||
class StickerPackForm extends HookConsumerWidget {
|
||||
final String pubName;
|
||||
final String? packId;
|
||||
const EditStickerPacksScreen({super.key, required this.pubName, this.packId});
|
||||
const StickerPackForm({super.key, required this.pubName, this.packId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -187,7 +239,7 @@ class EditStickerPacksScreen extends HookConsumerWidget {
|
||||
options: Options(method: packId == null ? 'POST' : 'PATCH'),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
context.pop(SnStickerPack.fromJson(resp.data));
|
||||
Navigator.of(context).pop(SnStickerPack.fromJson(resp.data));
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
@@ -195,76 +247,76 @@ class EditStickerPacksScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title:
|
||||
Text(packId == null ? 'createStickerPack' : 'editStickerPack').tr(),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'name'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
return Column(
|
||||
children: [
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 16,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'name'.tr(),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'fieldCannotBeEmpty'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
alignLabelWithHint: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'fieldCannotBeEmpty'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
minLines: 3,
|
||||
maxLines: null,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
TextFormField(
|
||||
controller: prefixController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'stickerPackPrefix'.tr(),
|
||||
border: const UnderlineInputBorder(),
|
||||
helperText: 'deleteStickerHint'.tr(),
|
||||
minLines: 3,
|
||||
maxLines: null,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextFormField(
|
||||
controller: prefixController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'stickerPackPrefix'.tr(),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'fieldCannotBeEmpty'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
helperText: 'stickerPackPrefixHint'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'fieldCannotBeEmpty'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(12),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : submit,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text(packId == null ? 'create'.tr() : 'saveChanges'.tr()),
|
||||
),
|
||||
),
|
||||
const Gap(12),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : submit,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text(packId == null ? 'create'.tr() : 'saveChanges'.tr()),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16),
|
||||
);
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 24, vertical: 16);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,25 +6,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/webfeed.dart';
|
||||
import 'package:island/pods/webfeed.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class WebFeedNewScreen extends StatelessWidget {
|
||||
final String pubName;
|
||||
const WebFeedNewScreen({super.key, required this.pubName});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WebFeedEditScreen(pubName: pubName, feedId: null);
|
||||
}
|
||||
}
|
||||
|
||||
class WebFeedEditScreen extends HookConsumerWidget {
|
||||
class WebfeedForm extends HookConsumerWidget {
|
||||
final String pubName;
|
||||
final String? feedId;
|
||||
|
||||
const WebFeedEditScreen({super.key, required this.pubName, this.feedId});
|
||||
const WebfeedForm({super.key, required this.pubName, this.feedId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -113,14 +103,14 @@ class WebFeedEditScreen extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
return feedAsync.when(
|
||||
loading:
|
||||
() => const AppScaffold(
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(error, stack) => AppScaffold(
|
||||
appBar: AppBar(title: const Text('Error')),
|
||||
body: Center(child: Text('Error: $error')),
|
||||
(error, _) => ResponseErrorWidget(
|
||||
error: error,
|
||||
onRetry:
|
||||
() => ref.invalidate(
|
||||
webFeedNotifierProvider((pubName: pubName, feedId: feedId)),
|
||||
),
|
||||
),
|
||||
data: (feed) {
|
||||
// Initialize form fields if they're empty and we have a feed
|
||||
@@ -131,159 +121,142 @@ class WebFeedEditScreen extends HookConsumerWidget {
|
||||
isScrapEnabled.value = feed.config.scrapPage;
|
||||
}
|
||||
|
||||
return _buildForm(
|
||||
context,
|
||||
formKey: formKey,
|
||||
titleController: titleController,
|
||||
urlController: urlController,
|
||||
descriptionController: descriptionController,
|
||||
isScrapEnabled: isScrapEnabled.value,
|
||||
onScrapEnabledChanged: (value) => isScrapEnabled.value = value,
|
||||
onSave: saveFeed,
|
||||
onDelete: deleteFeed,
|
||||
isLoading: isLoading.value,
|
||||
ref: ref,
|
||||
hasFeedId: feedId != null,
|
||||
final scrapNow = useCallback(() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
await ref
|
||||
.read(
|
||||
webFeedNotifierProvider((
|
||||
pubName: pubName,
|
||||
feedId: feedId!,
|
||||
)).notifier,
|
||||
)
|
||||
.scrapFeed();
|
||||
|
||||
if (context.mounted) {
|
||||
showSnackBar('Feed scraping successfully.');
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}, [pubName, feedId, ref, context, isLoading]);
|
||||
|
||||
final formFields = Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: titleController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Title',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter a title';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: urlController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'URL',
|
||||
hintText: 'https://example.com/feed',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter a URL';
|
||||
}
|
||||
final uri = Uri.tryParse(value);
|
||||
if (uri == null || !uri.hasAbsolutePath) {
|
||||
return 'Please enter a valid URL';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description',
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
),
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text('Scrape web page for content'),
|
||||
subtitle: const Text(
|
||||
'When enabled, the system will attempt to extract full content from the web page',
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
value: isScrapEnabled.value,
|
||||
onChanged: (value) => isScrapEnabled.value = value,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (feedId != null) ...[
|
||||
TextButton.icon(
|
||||
onPressed: isLoading.value ? null : scrapNow,
|
||||
icon: const Icon(Symbols.refresh),
|
||||
label: const Text('Scrape Now'),
|
||||
).alignment(Alignment.centerRight),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
],
|
||||
).padding(all: 20);
|
||||
|
||||
final formWidget = Form(
|
||||
key: formKey,
|
||||
child: SingleChildScrollView(child: formFields),
|
||||
);
|
||||
|
||||
final buttonsRow = Row(
|
||||
children: [
|
||||
if (feedId != null)
|
||||
TextButton.icon(
|
||||
onPressed: isLoading.value ? null : deleteFeed,
|
||||
icon: const Icon(Symbols.delete_forever),
|
||||
label: const Text('Delete Web Feed'),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: isLoading.value ? null : saveFeed,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text('saveChanges').tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 20, vertical: 12);
|
||||
|
||||
return Column(children: [Expanded(child: formWidget), buttonsRow]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForm(
|
||||
BuildContext context, {
|
||||
required WidgetRef ref,
|
||||
required GlobalKey<FormState> formKey,
|
||||
required TextEditingController titleController,
|
||||
required TextEditingController urlController,
|
||||
required TextEditingController descriptionController,
|
||||
required bool isScrapEnabled,
|
||||
required ValueChanged<bool> onScrapEnabledChanged,
|
||||
required VoidCallback onSave,
|
||||
required VoidCallback onDelete,
|
||||
required bool isLoading,
|
||||
required bool hasFeedId,
|
||||
}) {
|
||||
final scrapNow = useCallback(() async {
|
||||
showLoadingModal(context);
|
||||
try {
|
||||
await ref
|
||||
.read(
|
||||
webFeedNotifierProvider((
|
||||
pubName: pubName,
|
||||
feedId: feedId!,
|
||||
)).notifier,
|
||||
)
|
||||
.scrapFeed();
|
||||
|
||||
if (context.mounted) {
|
||||
showSnackBar('Feed scraping successfully.');
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorAlert(e);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
}, [pubName, feedId, ref, context]);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(hasFeedId ? 'Edit Web Feed' : 'New Web Feed'),
|
||||
actions: [
|
||||
if (hasFeedId)
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.delete_forever),
|
||||
onPressed: isLoading ? null : onDelete,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
body: Form(
|
||||
key: formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: titleController,
|
||||
decoration: const InputDecoration(labelText: 'Title'),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter a title';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: urlController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'URL',
|
||||
hintText: 'https://example.com/feed',
|
||||
),
|
||||
keyboardType: TextInputType.url,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter a URL';
|
||||
}
|
||||
final uri = Uri.tryParse(value);
|
||||
if (uri == null || !uri.hasAbsolutePath) {
|
||||
return 'Please enter a valid URL';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description',
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text('Scrape web page for content'),
|
||||
subtitle: const Text(
|
||||
'When enabled, the system will attempt to extract full content from the web page',
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
value: isScrapEnabled,
|
||||
onChanged: onScrapEnabledChanged,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (hasFeedId) ...[
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: isLoading ? null : scrapNow,
|
||||
icon: const Icon(Symbols.refresh),
|
||||
label: const Text('Scrape Now'),
|
||||
).alignment(Alignment.centerRight),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
FilledButton.icon(
|
||||
onPressed: isLoading ? null : onSave,
|
||||
icon: const Icon(Symbols.save),
|
||||
label: Text('saveChanges').tr(),
|
||||
).alignment(Alignment.centerRight),
|
||||
],
|
||||
).padding(all: 20),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:island/pods/webfeed.dart';
|
||||
import 'package:island/screens/creators/webfeed/webfeed_edit.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:island/widgets/empty_state.dart';
|
||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class WebFeedListScreen extends ConsumerWidget {
|
||||
final String pubName;
|
||||
@@ -17,13 +19,19 @@ class WebFeedListScreen extends ConsumerWidget {
|
||||
final feedsAsync = ref.watch(webFeedListProvider(pubName));
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: const Text('Web Feeds')),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
child: const Icon(Symbols.add),
|
||||
onPressed: () {
|
||||
context.pushNamed(
|
||||
'creatorFeedNew',
|
||||
pathParameters: {'name': pubName},
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'New Web Feed',
|
||||
child: WebfeedForm(pubName: pubName, feedId: null),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -43,36 +51,47 @@ class WebFeedListScreen extends ConsumerWidget {
|
||||
itemCount: feeds.length,
|
||||
itemBuilder: (context, index) {
|
||||
final feed = feeds[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
child: ListTile(
|
||||
leading: const Icon(Symbols.rss_feed, size: 32),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
title: Text(
|
||||
feed.title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
feed.url,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => SheetScaffold(
|
||||
titleText: 'Edit Web Feed',
|
||||
child: WebfeedForm(
|
||||
pubName: pubName,
|
||||
feedId: feed.id,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: const Icon(Symbols.rss_feed, size: 32),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
title: Text(
|
||||
feed.title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
feed.url,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'creatorFeedEdit',
|
||||
pathParameters: {'name': pubName, 'feedId': feed.id},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
).center();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/custom_app.dart';
|
||||
@@ -32,6 +33,7 @@ class AppDetailScreen extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text(appData.value?.name ?? 'appDetails'.tr()),
|
||||
actions: [
|
||||
@@ -51,6 +53,7 @@ class AppDetailScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: tabController,
|
||||
|
||||
@@ -30,6 +30,7 @@ class BotDetailScreen extends HookConsumerWidget {
|
||||
final botData = ref.watch(botProvider(publisherName, projectId, botId));
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text(botData.value?.account.nick ?? 'botDetails'.tr()),
|
||||
actions: [
|
||||
|
||||
@@ -6,10 +6,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/custom_app.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/developers/apps.dart';
|
||||
import 'package:island/services/file.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
@@ -137,19 +137,13 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final baseUrl = ref.watch(serverUrlProvider);
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putFileToCloud(
|
||||
await FileUploader.createCloudFile(
|
||||
client: ref.read(apiClientProvider),
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: result.name,
|
||||
mimetype: result.mimeType ?? 'image/jpeg',
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
@@ -323,6 +317,7 @@ class EditAppScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
|
||||
),
|
||||
|
||||
@@ -7,9 +7,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/bot.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/services/file.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
@@ -123,19 +123,13 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final baseUrl = ref.watch(serverUrlProvider);
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
final cloudFile =
|
||||
await putFileToCloud(
|
||||
await FileUploader.createCloudFile(
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename: result.name,
|
||||
mimetype: result.mimeType ?? 'image/jpeg',
|
||||
client: ref.read(apiClientProvider),
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
@@ -198,6 +192,7 @@ class EditBotScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
|
||||
body:
|
||||
botData == null && !isNew
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/dev_project.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/developers/projects.dart';
|
||||
import 'package:island/screens/developers/hub.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -168,5 +168,125 @@ final developersProvider =
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnDeveloper>>;
|
||||
String _$devProjectsHash() => r'715b395bebda785d38691ffee3b88e50b498c91a';
|
||||
|
||||
/// See also [devProjects].
|
||||
@ProviderFor(devProjects)
|
||||
const devProjectsProvider = DevProjectsFamily();
|
||||
|
||||
/// See also [devProjects].
|
||||
class DevProjectsFamily extends Family<AsyncValue<List<DevProject>>> {
|
||||
/// See also [devProjects].
|
||||
const DevProjectsFamily();
|
||||
|
||||
/// See also [devProjects].
|
||||
DevProjectsProvider call(String pubName) {
|
||||
return DevProjectsProvider(pubName);
|
||||
}
|
||||
|
||||
@override
|
||||
DevProjectsProvider getProviderOverride(
|
||||
covariant DevProjectsProvider provider,
|
||||
) {
|
||||
return call(provider.pubName);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'devProjectsProvider';
|
||||
}
|
||||
|
||||
/// See also [devProjects].
|
||||
class DevProjectsProvider extends AutoDisposeFutureProvider<List<DevProject>> {
|
||||
/// See also [devProjects].
|
||||
DevProjectsProvider(String pubName)
|
||||
: this._internal(
|
||||
(ref) => devProjects(ref as DevProjectsRef, pubName),
|
||||
from: devProjectsProvider,
|
||||
name: r'devProjectsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$devProjectsHash,
|
||||
dependencies: DevProjectsFamily._dependencies,
|
||||
allTransitiveDependencies: DevProjectsFamily._allTransitiveDependencies,
|
||||
pubName: pubName,
|
||||
);
|
||||
|
||||
DevProjectsProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.pubName,
|
||||
}) : super.internal();
|
||||
|
||||
final String pubName;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<List<DevProject>> Function(DevProjectsRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: DevProjectsProvider._internal(
|
||||
(ref) => create(ref as DevProjectsRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
pubName: pubName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<List<DevProject>> createElement() {
|
||||
return _DevProjectsProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is DevProjectsProvider && other.pubName == pubName;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, pubName.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin DevProjectsRef on AutoDisposeFutureProviderRef<List<DevProject>> {
|
||||
/// The parameter `pubName` of this provider.
|
||||
String get pubName;
|
||||
}
|
||||
|
||||
class _DevProjectsProviderElement
|
||||
extends AutoDisposeFutureProviderElement<List<DevProject>>
|
||||
with DevProjectsRef {
|
||||
_DevProjectsProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get pubName => (origin as DevProjectsProvider).pubName;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/screens/developers/apps.dart';
|
||||
import 'package:island/screens/developers/bots.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class ProjectDetailScreen extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final String projectId;
|
||||
|
||||
const ProjectDetailScreen({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.projectId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabController = useTabController(initialLength: 2);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('projectDetails').tr(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.add),
|
||||
onPressed: () {
|
||||
// Get current tab index
|
||||
final index = tabController.index;
|
||||
switch (index) {
|
||||
case 0:
|
||||
context.pushNamed(
|
||||
'developerAppNew',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
context.pushNamed(
|
||||
'developerBotNew',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': projectId,
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
bottom: TabBar(
|
||||
controller: tabController,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(
|
||||
'customApps'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
child: Text(
|
||||
'bots'.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
CustomAppsScreen(publisherName: publisherName, projectId: projectId),
|
||||
BotsScreen(publisherName: publisherName, projectId: projectId),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
106
lib/screens/developers/project_detail_view.dart
Normal file
106
lib/screens/developers/project_detail_view.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/dev_project.dart';
|
||||
import 'package:island/screens/developers/apps.dart';
|
||||
import 'package:island/screens/developers/bots.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
|
||||
class ProjectDetailView extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
final DevProject project;
|
||||
final VoidCallback onBackToHub;
|
||||
|
||||
const ProjectDetailView({
|
||||
super.key,
|
||||
required this.publisherName,
|
||||
required this.project,
|
||||
required this.onBackToHub,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabController = useTabController(initialLength: 2);
|
||||
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
if (isWide) {
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Card(
|
||||
margin: const EdgeInsets.only(left: 16, bottom: 16, top: 12),
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, -56),
|
||||
child: NavigationRail(
|
||||
extended: isWiderScreen(context),
|
||||
scrollable: true,
|
||||
labelType:
|
||||
isWiderScreen(context)
|
||||
? null
|
||||
: NavigationRailLabelType.selected,
|
||||
backgroundColor: Colors.transparent,
|
||||
selectedIndex: tabController.index,
|
||||
onDestinationSelected:
|
||||
(index) => tabController.animateTo(index),
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.apps),
|
||||
label: Text('customApps'.tr()),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: Icon(Icons.smart_toy),
|
||||
label: Text('bots'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
CustomAppsScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: project.id,
|
||||
),
|
||||
BotsScreen(publisherName: publisherName, projectId: project.id),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: TabBar(
|
||||
dividerColor: Colors.transparent,
|
||||
controller: tabController,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text('customApps'.tr(), textAlign: TextAlign.center),
|
||||
),
|
||||
Tab(child: Text('bots'.tr(), textAlign: TextAlign.center)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
CustomAppsScreen(
|
||||
publisherName: publisherName,
|
||||
projectId: project.id,
|
||||
),
|
||||
BotsScreen(publisherName: publisherName, projectId: project.id),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/dev_project.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/response.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'projects.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<DevProject>> devProjects(Ref ref, String pubName) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.get('/develop/developers/$pubName/projects');
|
||||
return (resp.data as List)
|
||||
.map((e) => DevProject.fromJson(e))
|
||||
.cast<DevProject>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
class DevProjectsScreen extends HookConsumerWidget {
|
||||
final String publisherName;
|
||||
const DevProjectsScreen({super.key, required this.publisherName});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final projects = ref.watch(devProjectsProvider(publisherName));
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('projects').tr(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.add),
|
||||
onPressed: () {
|
||||
context.pushNamed(
|
||||
'developerProjectNew',
|
||||
pathParameters: {'name': publisherName},
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(8),
|
||||
],
|
||||
),
|
||||
body: projects.when(
|
||||
data: (data) {
|
||||
if (data.isEmpty) {
|
||||
return Center(child: Text('noProjects').tr());
|
||||
}
|
||||
return RefreshIndicator(
|
||||
onRefresh:
|
||||
() => ref.refresh(devProjectsProvider(publisherName).future),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.only(top: 4),
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
final project = data[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
child: ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
contentPadding: EdgeInsets.only(left: 20, right: 12),
|
||||
title: Text(project.name),
|
||||
subtitle: Text(project.description ?? ''),
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
value: 'edit',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.edit),
|
||||
const SizedBox(width: 12),
|
||||
Text('edit').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.delete, color: Colors.red),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'delete',
|
||||
style: TextStyle(color: Colors.red),
|
||||
).tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelected: (value) {
|
||||
if (value == 'edit') {
|
||||
context.pushNamed(
|
||||
'developerProjectEdit',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'id': project.id,
|
||||
},
|
||||
);
|
||||
} else if (value == 'delete') {
|
||||
showConfirmAlert(
|
||||
'deleteProjectHint'.tr(),
|
||||
'deleteProject'.tr(),
|
||||
).then((confirm) {
|
||||
if (confirm) {
|
||||
final client = ref.read(apiClientProvider);
|
||||
client.delete(
|
||||
'/develop/developers/$publisherName/projects/${project.id}',
|
||||
);
|
||||
ref.invalidate(
|
||||
devProjectsProvider(publisherName),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed(
|
||||
'developerProjectDetail',
|
||||
pathParameters: {
|
||||
'name': publisherName,
|
||||
'projectId': project.id,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error:
|
||||
(err, stack) => ResponseErrorWidget(
|
||||
error: err,
|
||||
onRetry: () => ref.invalidate(devProjectsProvider(publisherName)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'projects.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$devProjectsHash() => r'87fdcab47cd7d79ab019a5625617abeb1ffa1f39';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [devProjects].
|
||||
@ProviderFor(devProjects)
|
||||
const devProjectsProvider = DevProjectsFamily();
|
||||
|
||||
/// See also [devProjects].
|
||||
class DevProjectsFamily extends Family<AsyncValue<List<DevProject>>> {
|
||||
/// See also [devProjects].
|
||||
const DevProjectsFamily();
|
||||
|
||||
/// See also [devProjects].
|
||||
DevProjectsProvider call(String pubName) {
|
||||
return DevProjectsProvider(pubName);
|
||||
}
|
||||
|
||||
@override
|
||||
DevProjectsProvider getProviderOverride(
|
||||
covariant DevProjectsProvider provider,
|
||||
) {
|
||||
return call(provider.pubName);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'devProjectsProvider';
|
||||
}
|
||||
|
||||
/// See also [devProjects].
|
||||
class DevProjectsProvider extends AutoDisposeFutureProvider<List<DevProject>> {
|
||||
/// See also [devProjects].
|
||||
DevProjectsProvider(String pubName)
|
||||
: this._internal(
|
||||
(ref) => devProjects(ref as DevProjectsRef, pubName),
|
||||
from: devProjectsProvider,
|
||||
name: r'devProjectsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$devProjectsHash,
|
||||
dependencies: DevProjectsFamily._dependencies,
|
||||
allTransitiveDependencies: DevProjectsFamily._allTransitiveDependencies,
|
||||
pubName: pubName,
|
||||
);
|
||||
|
||||
DevProjectsProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.pubName,
|
||||
}) : super.internal();
|
||||
|
||||
final String pubName;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<List<DevProject>> Function(DevProjectsRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: DevProjectsProvider._internal(
|
||||
(ref) => create(ref as DevProjectsRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
pubName: pubName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<List<DevProject>> createElement() {
|
||||
return _DevProjectsProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is DevProjectsProvider && other.pubName == pubName;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, pubName.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin DevProjectsRef on AutoDisposeFutureProviderRef<List<DevProject>> {
|
||||
/// The parameter `pubName` of this provider.
|
||||
String get pubName;
|
||||
}
|
||||
|
||||
class _DevProjectsProviderElement
|
||||
extends AutoDisposeFutureProviderElement<List<DevProject>>
|
||||
with DevProjectsRef {
|
||||
_DevProjectsProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get pubName => (origin as DevProjectsProvider).pubName;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
@@ -13,13 +13,13 @@ import 'package:island/pods/event_calendar.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/screens/notification.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/account/fortune_graph.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/widgets/check_in.dart';
|
||||
import 'package:island/widgets/post/post_featured.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:island/screens/tabs.dart';
|
||||
import 'package:island/widgets/post/compose_card.dart';
|
||||
import 'package:island/widgets/post/compose_dialog.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
||||
@@ -42,6 +42,7 @@ Widget notificationIndicatorWidget(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
minTileHeight: 48,
|
||||
leading: const Icon(Symbols.notifications),
|
||||
title: Row(
|
||||
children: [
|
||||
@@ -51,7 +52,6 @@ Widget notificationIndicatorWidget(
|
||||
],
|
||||
),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
minTileHeight: 40,
|
||||
contentPadding: EdgeInsets.only(left: 16, right: 15),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('notifications');
|
||||
@@ -99,10 +99,6 @@ class ExploreScreen extends HookConsumerWidget {
|
||||
final events = ref.watch(eventCalendarProvider(query.value));
|
||||
|
||||
final selectedDay = useState(now);
|
||||
// Function to handle day selection for synchronizing between widgets
|
||||
void onDaySelected(DateTime day) {
|
||||
selectedDay.value = day;
|
||||
}
|
||||
|
||||
final user = ref.watch(userInfoProvider);
|
||||
|
||||
@@ -110,251 +106,143 @@ class ExploreScreen extends HookConsumerWidget {
|
||||
notificationUnreadCountNotifierProvider,
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
toolbarHeight: 0,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(48),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBar(
|
||||
controller: tabController,
|
||||
tabAlignment: TabAlignment.start,
|
||||
isScrollable: true,
|
||||
dividerColor: Colors.transparent,
|
||||
tabs: [
|
||||
Tab(
|
||||
icon: Tooltip(
|
||||
message: 'explore'.tr(),
|
||||
child: Icon(
|
||||
Symbols.explore,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Tooltip(
|
||||
message: 'exploreFilterSubscriptions'.tr(),
|
||||
child: Icon(
|
||||
Symbols.subscriptions,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Tooltip(
|
||||
message: 'exploreFilterFriends'.tr(),
|
||||
child: Icon(
|
||||
Symbols.people,
|
||||
color:
|
||||
Theme.of(
|
||||
context,
|
||||
).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
final filterBar = Card(
|
||||
margin: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBar(
|
||||
controller: tabController,
|
||||
tabAlignment: TabAlignment.start,
|
||||
isScrollable: true,
|
||||
dividerColor: Colors.transparent,
|
||||
tabs: [
|
||||
Tab(
|
||||
icon: Tooltip(
|
||||
message: 'explore'.tr(),
|
||||
child: Icon(Symbols.explore),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Tooltip(
|
||||
message: 'exploreFilterSubscriptions'.tr(),
|
||||
child: Icon(Symbols.subscriptions),
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
icon: Tooltip(
|
||||
message: 'exploreFilterFriends'.tr(),
|
||||
child: Icon(Symbols.people),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.pushNamed('articles');
|
||||
},
|
||||
icon: Icon(Symbols.auto_stories),
|
||||
tooltip: 'webArticlesStand'.tr(),
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.category),
|
||||
const Gap(12),
|
||||
Text('categories').tr(),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.pushNamed('articles');
|
||||
onTap: () {
|
||||
context.pushNamed('postCategories');
|
||||
},
|
||||
icon: Icon(
|
||||
Symbols.auto_stories,
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
tooltip: 'webArticlesStand'.tr(),
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.category),
|
||||
const Gap(12),
|
||||
Text('categories').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postCategories');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.label),
|
||||
const Gap(12),
|
||||
Text('tags').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postTags');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.shuffle),
|
||||
const Gap(12),
|
||||
Text('postShuffle').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postShuffle');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.search),
|
||||
const Gap(12),
|
||||
Text('search').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postSearch');
|
||||
},
|
||||
),
|
||||
],
|
||||
icon: Icon(
|
||||
Symbols.action_key,
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.label),
|
||||
const Gap(12),
|
||||
Text('tags').tr(),
|
||||
],
|
||||
),
|
||||
tooltip: 'search'.tr(),
|
||||
onTap: () {
|
||||
context.pushNamed('postTags');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.shuffle),
|
||||
const Gap(12),
|
||||
Text('postShuffle').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postShuffle');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.search),
|
||||
const Gap(12),
|
||||
Text('search').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postSearch');
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
.padding(horizontal: 8)
|
||||
.border(
|
||||
bottom: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
color: Theme.of(context).dividerColor,
|
||||
icon: Icon(Symbols.action_key),
|
||||
tooltip: 'search'.tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8),
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
floatingActionButton:
|
||||
isWide
|
||||
? null
|
||||
: InkWell(
|
||||
onLongPress: () async {
|
||||
final result = await PostComposeDialog.show(context);
|
||||
if (result != null) {
|
||||
activitiesNotifier.forceRefresh();
|
||||
}
|
||||
},
|
||||
child: FloatingActionButton(
|
||||
heroTag: Key("explore-page-fab"),
|
||||
onPressed: () async {
|
||||
final result = await PostComposeDialog.show(context);
|
||||
if (result != null) {
|
||||
activitiesNotifier.forceRefresh();
|
||||
}
|
||||
},
|
||||
child: const Icon(Symbols.edit),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: InkWell(
|
||||
onLongPress: () {
|
||||
context.pushNamed('postCompose', queryParameters: {'type': '1'}).then(
|
||||
(value) {
|
||||
if (value != null) {
|
||||
activitiesNotifier.forceRefresh();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
child: FloatingActionButton(
|
||||
heroTag: Key("explore-page-fab"),
|
||||
onPressed: () {
|
||||
context.pushNamed('postCompose').then((value) {
|
||||
if (value != null) {
|
||||
activitiesNotifier.forceRefresh();
|
||||
}
|
||||
});
|
||||
},
|
||||
child: const Icon(Symbols.edit),
|
||||
),
|
||||
),
|
||||
floatingActionButtonLocation: TabbedFabLocation(context),
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
final bodyView = _buildActivityList(
|
||||
context,
|
||||
ref,
|
||||
currentFilter.value,
|
||||
);
|
||||
|
||||
if (isWide) {
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(flex: 3, child: bodyView.padding(left: 8)),
|
||||
if (user.value != null)
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
CheckInWidget(
|
||||
margin: EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 12,
|
||||
top: 16,
|
||||
),
|
||||
onChecked: () {
|
||||
ref.invalidate(
|
||||
eventCalendarProvider(query.value),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (notificationCount.value != null &&
|
||||
notificationCount.value! > 0)
|
||||
notificationIndicatorWidget(
|
||||
context,
|
||||
count: notificationCount.value ?? 0,
|
||||
margin: EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 12,
|
||||
top: 8,
|
||||
),
|
||||
),
|
||||
PostFeaturedList().padding(
|
||||
left: 8,
|
||||
right: 12,
|
||||
top: 8,
|
||||
),
|
||||
FortuneGraphWidget(
|
||||
margin: EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 12,
|
||||
top: 8,
|
||||
),
|
||||
events: events,
|
||||
constrainWidth: true,
|
||||
onPointSelected: onDaySelected,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Welcome to\nthe Solar Network',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).bold(),
|
||||
const Gap(2),
|
||||
Text(
|
||||
'Login to explore more!',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 36, vertical: 16),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return bodyView;
|
||||
},
|
||||
),
|
||||
body:
|
||||
isWide
|
||||
? _buildWideBody(
|
||||
context,
|
||||
ref,
|
||||
filterBar,
|
||||
user,
|
||||
notificationCount,
|
||||
query,
|
||||
events,
|
||||
selectedDay,
|
||||
currentFilter.value,
|
||||
)
|
||||
: _buildNarrowBody(context, ref, filterBar, currentFilter.value),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -369,23 +257,167 @@ class ExploreScreen extends HookConsumerWidget {
|
||||
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
return ExtendedRefreshIndicator(
|
||||
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
|
||||
child: PagingHelperView(
|
||||
provider: activityListNotifierProvider(filter),
|
||||
futureRefreshable: activityListNotifierProvider(filter).future,
|
||||
notifierRefreshable: activityListNotifierProvider(filter).notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => Center(
|
||||
child: _ActivityListView(
|
||||
data: data,
|
||||
widgetCount: widgetCount,
|
||||
endItemView: endItemView,
|
||||
activitiesNotifier: activitiesNotifier,
|
||||
contentOnly: isWide || filter != null,
|
||||
return PagingHelperSliverView(
|
||||
provider: activityListNotifierProvider(filter),
|
||||
futureRefreshable: activityListNotifierProvider(filter).future,
|
||||
notifierRefreshable: activityListNotifierProvider(filter).notifier,
|
||||
contentBuilder:
|
||||
(data, widgetCount, endItemView) => _ActivityListView(
|
||||
data: data,
|
||||
widgetCount: widgetCount,
|
||||
endItemView: endItemView,
|
||||
activitiesNotifier: activitiesNotifier,
|
||||
isWide: isWide,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWideBody(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Widget filterBar,
|
||||
AsyncValue<dynamic> user,
|
||||
AsyncValue<int?> notificationCount,
|
||||
ValueNotifier<EventCalendarQuery> query,
|
||||
AsyncValue<List<dynamic>> events,
|
||||
ValueNotifier<DateTime> selectedDay,
|
||||
String? currentFilter,
|
||||
) {
|
||||
final bodyView = _buildActivityList(context, ref, currentFilter);
|
||||
|
||||
final activitiesNotifier = ref.watch(
|
||||
activityListNotifierProvider(currentFilter).notifier,
|
||||
);
|
||||
|
||||
return Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: ExtendedRefreshIndicator(
|
||||
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverGap(12),
|
||||
SliverToBoxAdapter(child: filterBar),
|
||||
const SliverGap(8),
|
||||
bodyView,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (user.value != null)
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
CheckInWidget(
|
||||
margin: EdgeInsets.only(top: 12),
|
||||
onChecked: () {
|
||||
ref.invalidate(eventCalendarProvider(query.value));
|
||||
},
|
||||
),
|
||||
if (notificationCount.value != null &&
|
||||
notificationCount.value! > 0)
|
||||
notificationIndicatorWidget(
|
||||
context,
|
||||
count: notificationCount.value ?? 0,
|
||||
margin: EdgeInsets.zero,
|
||||
),
|
||||
PostFeaturedList(),
|
||||
PostComposeCard(
|
||||
onSubmit: (post) {
|
||||
activitiesNotifier.forceRefresh();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Welcome to\nthe Solar Network',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
).bold(),
|
||||
const Gap(2),
|
||||
Text(
|
||||
'Login to explore more!',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 36, vertical: 16),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12);
|
||||
}
|
||||
|
||||
Widget _buildNarrowBody(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Widget filterBar,
|
||||
String? currentFilter,
|
||||
) {
|
||||
final user = ref.watch(userInfoProvider);
|
||||
final notificationCount = ref.watch(
|
||||
notificationUnreadCountNotifierProvider,
|
||||
);
|
||||
|
||||
final activitiesNotifier = ref.watch(
|
||||
activityListNotifierProvider(currentFilter).notifier,
|
||||
);
|
||||
|
||||
final bodyView = _buildActivityList(context, ref, currentFilter);
|
||||
|
||||
return Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
filterBar.padding(horizontal: 8, top: 8),
|
||||
Expanded(
|
||||
child: ExtendedRefreshIndicator(
|
||||
onRefresh: () => Future.sync(activitiesNotifier.forceRefresh),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
if (user.value != null)
|
||||
SliverToBoxAdapter(
|
||||
child: CheckInWidget(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: PostFeaturedList(),
|
||||
),
|
||||
),
|
||||
if (notificationCount.value != null &&
|
||||
notificationCount.value! > 0)
|
||||
SliverToBoxAdapter(
|
||||
child: notificationIndicatorWidget(
|
||||
context,
|
||||
count: notificationCount.value ?? 0,
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
),
|
||||
),
|
||||
bodyView,
|
||||
],
|
||||
),
|
||||
).padding(horizontal: 8),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -464,7 +496,7 @@ class _DiscoveryActivityItem extends StatelessWidget {
|
||||
};
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -505,92 +537,60 @@ class _ActivityListView extends HookConsumerWidget {
|
||||
final CursorPagingData<SnActivity> data;
|
||||
final int widgetCount;
|
||||
final Widget endItemView;
|
||||
final bool contentOnly;
|
||||
final ActivityListNotifier activitiesNotifier;
|
||||
final bool isWide;
|
||||
|
||||
const _ActivityListView({
|
||||
required this.data,
|
||||
required this.widgetCount,
|
||||
required this.endItemView,
|
||||
required this.activitiesNotifier,
|
||||
this.contentOnly = false,
|
||||
required this.isWide,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final user = ref.watch(userInfoProvider);
|
||||
return SliverList.separated(
|
||||
itemCount: widgetCount,
|
||||
separatorBuilder: (_, _) => const Gap(8),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
|
||||
final notificationCount = ref.watch(
|
||||
notificationUnreadCountNotifierProvider,
|
||||
);
|
||||
final item = data.items[index];
|
||||
if (item.data == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
Widget itemWidget;
|
||||
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverGap(12),
|
||||
if (user.value != null && !contentOnly)
|
||||
SliverToBoxAdapter(
|
||||
child: CheckInWidget(
|
||||
margin: EdgeInsets.only(left: 8, right: 8, bottom: 4),
|
||||
),
|
||||
),
|
||||
if (!contentOnly)
|
||||
SliverToBoxAdapter(
|
||||
child: PostFeaturedList().padding(horizontal: 8, bottom: 4, top: 4),
|
||||
),
|
||||
if (!contentOnly && (notificationCount.value ?? 0) > 0)
|
||||
SliverToBoxAdapter(
|
||||
child: notificationIndicatorWidget(
|
||||
context,
|
||||
count: notificationCount.value ?? 0,
|
||||
margin: EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4),
|
||||
),
|
||||
),
|
||||
SliverList.builder(
|
||||
itemCount: widgetCount,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == widgetCount - 1) {
|
||||
return endItemView;
|
||||
}
|
||||
|
||||
final item = data.items[index];
|
||||
if (item.data == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
Widget itemWidget;
|
||||
|
||||
switch (item.type) {
|
||||
case 'posts.new':
|
||||
case 'posts.new.replies':
|
||||
itemWidget = PostActionableItem(
|
||||
borderRadius: 8,
|
||||
item: SnPost.fromJson(item.data!),
|
||||
onRefresh: () {
|
||||
activitiesNotifier.forceRefresh();
|
||||
},
|
||||
onUpdate: (post) {
|
||||
activitiesNotifier.updateOne(
|
||||
index,
|
||||
item.copyWith(data: post.toJson()),
|
||||
);
|
||||
},
|
||||
switch (item.type) {
|
||||
case 'posts.new':
|
||||
case 'posts.new.replies':
|
||||
itemWidget = PostActionableItem(
|
||||
borderRadius: 8,
|
||||
item: SnPost.fromJson(item.data!),
|
||||
onRefresh: () {
|
||||
activitiesNotifier.forceRefresh();
|
||||
},
|
||||
onUpdate: (post) {
|
||||
activitiesNotifier.updateOne(
|
||||
index,
|
||||
item.copyWith(data: post.toJson()),
|
||||
);
|
||||
itemWidget = Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: itemWidget,
|
||||
);
|
||||
break;
|
||||
case 'discovery':
|
||||
itemWidget = _DiscoveryActivityItem(data: item.data!);
|
||||
break;
|
||||
default:
|
||||
itemWidget = const Placeholder();
|
||||
}
|
||||
},
|
||||
);
|
||||
itemWidget = Card(margin: EdgeInsets.zero, child: itemWidget);
|
||||
break;
|
||||
case 'discovery':
|
||||
itemWidget = _DiscoveryActivityItem(data: item.data!);
|
||||
break;
|
||||
default:
|
||||
itemWidget = const Placeholder();
|
||||
}
|
||||
|
||||
return itemWidget;
|
||||
},
|
||||
),
|
||||
SliverGap(getTabbedPadding(context).bottom),
|
||||
],
|
||||
return itemWidget;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -7,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/talker.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/models/poll.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
@@ -92,10 +91,10 @@ class PollEditor extends Notifier<PollEditorState> {
|
||||
questions: poll.questions,
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
log('Failed to load poll $id: ${e.message}');
|
||||
talker.error('Failed to load poll $id: ${e.message}');
|
||||
// Keep state with id set; UI may handle error display.
|
||||
} catch (e) {
|
||||
log('Unexpected error loading poll $id: $e');
|
||||
talker.error('Unexpected error loading poll $id: $e');
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
}
|
||||
@@ -417,6 +416,7 @@ class PollEditorScreen extends ConsumerWidget {
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text(model.id == null ? 'pollCreate'.tr() : 'pollEdit'.tr()),
|
||||
actions: [
|
||||
@@ -434,177 +434,198 @@ class PollEditorScreen extends ConsumerWidget {
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Form(
|
||||
key: ValueKey(model.id),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
TextFormField(
|
||||
initialValue: model.title ?? '',
|
||||
decoration: InputDecoration(
|
||||
labelText: 'title'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
maxLength: 256,
|
||||
onChanged: notifier.setTitle,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) {
|
||||
return 'pollTitleRequired'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
TextFormField(
|
||||
initialValue: model.description ?? '',
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
),
|
||||
maxLines: 3,
|
||||
maxLength: 4096,
|
||||
onChanged: notifier.setDescription,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
_EndDatePicker(
|
||||
value: model.endedAt,
|
||||
onChanged: notifier.setEndedAt,
|
||||
),
|
||||
const Gap(24),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'questions'.tr(),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const Spacer(),
|
||||
MenuAnchor(
|
||||
builder: (context, controller, child) {
|
||||
return FilledButton.icon(
|
||||
onPressed: () {
|
||||
controller.isOpen
|
||||
? controller.close()
|
||||
: controller.open();
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text('pollAddQuestion'.tr()),
|
||||
);
|
||||
},
|
||||
menuChildren:
|
||||
SnPollQuestionType.values
|
||||
.map(
|
||||
(t) => MenuItemButton(
|
||||
leadingIcon: Icon(_iconForType(t)),
|
||||
onPressed: () => notifier.addQuestion(t),
|
||||
child: Text(_labelForType(t)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
if (model.questions.isEmpty)
|
||||
_EmptyState(
|
||||
title: 'pollNoQuestionsYet'.tr(),
|
||||
subtitle: 'pollNoQuestionsHint'.tr(),
|
||||
)
|
||||
else
|
||||
ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: model.questions.length,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
// Convert to stepwise moves using provided functions
|
||||
if (newIndex > oldIndex) newIndex -= 1;
|
||||
final steps = newIndex - oldIndex;
|
||||
if (steps == 0) return;
|
||||
if (steps > 0) {
|
||||
for (int i = 0; i < steps; i++) {
|
||||
notifier.moveQuestionDown(oldIndex + i);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i > steps; i--) {
|
||||
notifier.moveQuestionUp(oldIndex + i);
|
||||
}
|
||||
}
|
||||
},
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, index) {
|
||||
final q = model.questions[index];
|
||||
return Card(
|
||||
key: ValueKey('q_$index'),
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
children: [
|
||||
_QuestionHeader(
|
||||
index: index,
|
||||
question: q,
|
||||
onMoveUp:
|
||||
index > 0
|
||||
? () => notifier.moveQuestionUp(index)
|
||||
: null,
|
||||
onMoveDown:
|
||||
index < model.questions.length - 1
|
||||
? () => notifier.moveQuestionDown(index)
|
||||
: null,
|
||||
onDelete: () => notifier.removeQuestion(index),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child:
|
||||
Form(
|
||||
key: ValueKey(model.id),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
TextFormField(
|
||||
initialValue: model.title ?? '',
|
||||
decoration: InputDecoration(
|
||||
labelText: 'postTitle'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(16),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: _QuestionEditor(
|
||||
index: index,
|
||||
question: q,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
textInputAction: TextInputAction.next,
|
||||
maxLength: 256,
|
||||
onChanged: notifier.setTitle,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
validator: (v) {
|
||||
if (v == null || v.trim().isEmpty) {
|
||||
return 'pollTitleRequired'.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const Gap(12),
|
||||
TextFormField(
|
||||
initialValue: model.description ?? '',
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
maxLines: 3,
|
||||
maxLength: 4096,
|
||||
onChanged: notifier.setDescription,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
_EndDatePicker(
|
||||
value: model.endedAt,
|
||||
onChanged: notifier.setEndedAt,
|
||||
),
|
||||
const Gap(24),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'questions'.tr(),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const Spacer(),
|
||||
MenuAnchor(
|
||||
builder: (context, controller, child) {
|
||||
return FilledButton.icon(
|
||||
onPressed: () {
|
||||
controller.isOpen
|
||||
? controller.close()
|
||||
: controller.open();
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text('pollAddQuestion'.tr()),
|
||||
);
|
||||
},
|
||||
menuChildren:
|
||||
SnPollQuestionType.values
|
||||
.map(
|
||||
(t) => MenuItemButton(
|
||||
leadingIcon: Icon(_iconForType(t)),
|
||||
onPressed:
|
||||
() => notifier.addQuestion(t),
|
||||
child: Text(_labelForType(t)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
if (model.questions.isEmpty)
|
||||
_EmptyState(
|
||||
title: 'pollNoQuestionsYet'.tr(),
|
||||
subtitle: 'pollNoQuestionsHint'.tr(),
|
||||
)
|
||||
else
|
||||
ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: model.questions.length,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
// Convert to stepwise moves using provided functions
|
||||
if (newIndex > oldIndex) newIndex -= 1;
|
||||
final steps = newIndex - oldIndex;
|
||||
if (steps == 0) return;
|
||||
if (steps > 0) {
|
||||
for (int i = 0; i < steps; i++) {
|
||||
notifier.moveQuestionDown(oldIndex + i);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i > steps; i--) {
|
||||
notifier.moveQuestionUp(oldIndex + i);
|
||||
}
|
||||
}
|
||||
},
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, index) {
|
||||
final q = model.questions[index];
|
||||
return Card(
|
||||
key: ValueKey('q_$index'),
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
children: [
|
||||
_QuestionHeader(
|
||||
index: index,
|
||||
question: q,
|
||||
onMoveUp:
|
||||
index > 0
|
||||
? () =>
|
||||
notifier.moveQuestionUp(index)
|
||||
: null,
|
||||
onMoveDown:
|
||||
index < model.questions.length - 1
|
||||
? () => notifier.moveQuestionDown(
|
||||
index,
|
||||
)
|
||||
: null,
|
||||
onDelete:
|
||||
() => notifier.removeQuestion(index),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: _QuestionEditor(
|
||||
index: index,
|
||||
question: q,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(96),
|
||||
],
|
||||
),
|
||||
const Gap(96),
|
||||
],
|
||||
),
|
||||
).center(),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
elevation: 2,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
label: Text('cancel'.tr()),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
_submitPoll(context, ref);
|
||||
},
|
||||
icon: const Icon(Icons.cloud_upload_outlined),
|
||||
label: Text(model.id == null ? 'create'.tr() : 'update'.tr()),
|
||||
),
|
||||
],
|
||||
).padding(
|
||||
horizontal: 24,
|
||||
top: 16,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
),
|
||||
child:
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 640),
|
||||
child: Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
label: Text('cancel'.tr()),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
_submitPoll(context, ref);
|
||||
},
|
||||
icon: const Icon(Icons.cloud_upload_outlined),
|
||||
label: Text(
|
||||
model.id == null ? 'create'.tr() : 'update'.tr(),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(
|
||||
horizontal: 24,
|
||||
top: 16,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
),
|
||||
).center(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -6,21 +6,20 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/screens/creators/publishers_form.dart';
|
||||
import 'package:island/screens/posts/compose_article.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/screens/posts/post_detail.dart';
|
||||
import 'package:island/services/compose_storage_db.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/attachment_uploader.dart';
|
||||
import 'package:island/widgets/content/attachment_preview.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/post/compose_attachments.dart';
|
||||
import 'package:island/widgets/post/compose_form_fields.dart';
|
||||
import 'package:island/widgets/post/compose_info_banner.dart';
|
||||
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
||||
import 'package:island/widgets/post/compose_shared.dart';
|
||||
import 'package:island/widgets/post/compose_toolbar.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:island/widgets/post/publishers_modal.dart';
|
||||
import 'package:island/screens/posts/post_detail.dart';
|
||||
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
||||
import 'package:island/services/compose_storage_db.dart';
|
||||
// DraftManagerSheet is now imported through compose_toolbar.dart
|
||||
import 'package:island/widgets/post/compose_toolbar.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@@ -92,9 +91,6 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
return ArticleComposeScreen(originalPost: originalPost);
|
||||
}
|
||||
|
||||
// Otherwise, continue with regular post compose
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// When editing, preserve the original replied/forwarded post references
|
||||
final effectiveRepliedPost = repliedPost ?? originalPost?.repliedPost;
|
||||
final effectiveForwardedPost = forwardedPost ?? originalPost?.forwardedPost;
|
||||
@@ -210,109 +206,6 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildWideAttachmentGrid() {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemCount: state.attachments.value.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final progressMap = state.attachmentProgress.value;
|
||||
return AttachmentPreview(
|
||||
item: state.attachments.value[idx],
|
||||
progress: progressMap[idx],
|
||||
onRequestUpload: () async {
|
||||
final config = await showModalBottomSheet<AttachmentUploadConfig>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
state: state,
|
||||
index: idx,
|
||||
),
|
||||
);
|
||||
if (config != null) {
|
||||
await ComposeLogic.uploadAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
poolId: config.poolId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onDelete: () => ComposeLogic.deleteAttachment(ref, state, idx),
|
||||
onUpdate:
|
||||
(value) => ComposeLogic.updateAttachment(state, value, idx),
|
||||
onMove: (delta) {
|
||||
state.attachments.value = ComposeLogic.moveAttachment(
|
||||
state.attachments.value,
|
||||
idx,
|
||||
delta,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildNarrowAttachmentList() {
|
||||
return Column(
|
||||
children: [
|
||||
for (var idx = 0; idx < state.attachments.value.length; idx++)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: () {
|
||||
final progressMap = state.attachmentProgress.value;
|
||||
return AttachmentPreview(
|
||||
item: state.attachments.value[idx],
|
||||
progress: progressMap[idx],
|
||||
onRequestUpload: () async {
|
||||
final config =
|
||||
await showModalBottomSheet<AttachmentUploadConfig>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => AttachmentUploaderSheet(
|
||||
ref: ref,
|
||||
state: state,
|
||||
index: idx,
|
||||
),
|
||||
);
|
||||
if (config != null) {
|
||||
await ComposeLogic.uploadAttachment(
|
||||
ref,
|
||||
state,
|
||||
idx,
|
||||
poolId: config.poolId,
|
||||
);
|
||||
}
|
||||
},
|
||||
onDelete:
|
||||
() => ComposeLogic.deleteAttachment(ref, state, idx),
|
||||
onUpdate:
|
||||
(value) =>
|
||||
ComposeLogic.updateAttachment(state, value, idx),
|
||||
onMove: (delta) {
|
||||
state.attachments.value = ComposeLogic.moveAttachment(
|
||||
state.attachments.value,
|
||||
idx,
|
||||
delta,
|
||||
);
|
||||
},
|
||||
);
|
||||
}(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Build UI
|
||||
return PopScope(
|
||||
onPopInvoked: (_) {
|
||||
if (originalPost == null) {
|
||||
@@ -362,7 +255,57 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Reply/Forward info section
|
||||
_buildInfoBanner(context),
|
||||
ComposeInfoBanner(
|
||||
originalPost: originalPost,
|
||||
replyingTo: repliedPost,
|
||||
forwardingTo: forwardedPost,
|
||||
onReferencePostTap: (context, post) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder:
|
||||
(context) => DraggableScrollableSheet(
|
||||
initialChildSize: 0.7,
|
||||
maxChildSize: 0.9,
|
||||
minChildSize: 0.5,
|
||||
builder:
|
||||
(context, scrollController) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.outline,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: PostItem(item: post),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Main content area
|
||||
Expanded(
|
||||
@@ -414,78 +357,27 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: state.titleController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'postTitle'.tr(),
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
),
|
||||
style: theme.textTheme.titleMedium,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
ComposeFormFields(
|
||||
state: state,
|
||||
showPublisherAvatar: false,
|
||||
onPublisherTap: () {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder:
|
||||
(context) => const PublisherModal(),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
state.currentPublisher.value = value;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
TextField(
|
||||
controller: state.descriptionController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'postDescription'.tr(),
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.fromLTRB(
|
||||
8,
|
||||
4,
|
||||
8,
|
||||
12,
|
||||
),
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
// Content field with borderless design
|
||||
TextField(
|
||||
controller: state.contentController,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: 'postContent'.tr(),
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
),
|
||||
maxLines: null,
|
||||
onTapOutside:
|
||||
(_) =>
|
||||
FocusManager.instance.primaryFocus
|
||||
?.unfocus(),
|
||||
),
|
||||
|
||||
const Gap(8),
|
||||
|
||||
// Attachments preview
|
||||
if (state.attachments.value.isNotEmpty)
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = isWideScreen(context);
|
||||
return isWide
|
||||
? buildWideAttachmentGrid()
|
||||
: buildNarrowAttachmentList();
|
||||
},
|
||||
)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
ComposeAttachments(
|
||||
state: state,
|
||||
isCompact: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -503,262 +395,4 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoBanner(BuildContext context) {
|
||||
// When editing, preserve the original replied/forwarded post references
|
||||
final effectiveRepliedPost =
|
||||
initialState?.replyingTo ?? originalPost?.repliedPost;
|
||||
final effectiveForwardedPost =
|
||||
initialState?.forwardingTo ?? originalPost?.forwardedPost;
|
||||
|
||||
// Show editing banner when editing a post
|
||||
if (originalPost != null) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.edit,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'postEditing'.tr(),
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 16, vertical: 8),
|
||||
),
|
||||
// Show reply/forward banners below editing banner if they exist
|
||||
if (effectiveRepliedPost != null)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Symbols.reply, size: 16),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'postReplyingTo'.tr(),
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
_buildCompactReferencePost(context, effectiveRepliedPost),
|
||||
],
|
||||
).padding(all: 16),
|
||||
),
|
||||
if (effectiveForwardedPost != null)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Symbols.forward, size: 16),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'postForwardingTo'.tr(),
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
_buildCompactReferencePost(context, effectiveForwardedPost),
|
||||
],
|
||||
).padding(all: 16),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Show banner for replies (including when editing a reply)
|
||||
if (effectiveRepliedPost != null) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Symbols.reply, size: 16),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'postReplyingTo'.tr(),
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
_buildCompactReferencePost(context, effectiveRepliedPost),
|
||||
],
|
||||
).padding(all: 16),
|
||||
);
|
||||
}
|
||||
|
||||
// Show banner for forwards (including when editing a forward)
|
||||
if (effectiveForwardedPost != null) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Symbols.forward, size: 16),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'postForwardingTo'.tr(),
|
||||
style: Theme.of(context).textTheme.labelMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
_buildCompactReferencePost(context, effectiveForwardedPost),
|
||||
],
|
||||
).padding(all: 16),
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildCompactReferencePost(BuildContext context, SnPost post) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder:
|
||||
(context) => DraggableScrollableSheet(
|
||||
initialChildSize: 0.7,
|
||||
maxChildSize: 0.9,
|
||||
minChildSize: 0.5,
|
||||
builder:
|
||||
(context, scrollController) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: PostItem(item: post),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
ProfilePictureWidget(
|
||||
fileId: post.publisher.picture?.id,
|
||||
radius: 16,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
post.publisher.nick,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (post.title?.isNotEmpty ?? false)
|
||||
Text(
|
||||
post.title!,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (post.content?.isNotEmpty ?? false)
|
||||
Text(
|
||||
post.content!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (post.attachments.isNotEmpty)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.attach_file,
|
||||
size: 12,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
const Gap(4),
|
||||
Text(
|
||||
'postHasAttachments'.plural(post.attachments.length),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Symbols.open_in_full,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,20 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
|
||||
import 'package:island/screens/creators/publishers.dart';
|
||||
import 'package:island/screens/creators/publishers_form.dart';
|
||||
import 'package:island/screens/posts/post_detail.dart';
|
||||
import 'package:island/services/compose_storage_db.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/attachment_uploader.dart';
|
||||
import 'package:island/screens/posts/post_detail.dart';
|
||||
import 'package:island/widgets/content/attachment_preview.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/markdown.dart';
|
||||
import 'package:island/widgets/post/compose_form_fields.dart';
|
||||
import 'package:island/widgets/post/compose_shared.dart';
|
||||
import 'package:island/widgets/post/compose_settings_sheet.dart';
|
||||
import 'package:island/services/compose_storage_db.dart';
|
||||
import 'package:island/widgets/post/compose_toolbar.dart';
|
||||
import 'package:island/widgets/post/publishers_modal.dart';
|
||||
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@@ -233,64 +232,20 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextField(
|
||||
controller: state.titleController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'postTitle'.tr(),
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
),
|
||||
style: theme.textTheme.titleMedium,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
TextField(
|
||||
controller: state.descriptionController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'postDescription'.tr(),
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 12),
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
Expanded(
|
||||
child: KeyboardListener(
|
||||
focusNode: FocusNode(),
|
||||
onKeyEvent:
|
||||
(event) => ComposeLogic.handleKeyPress(
|
||||
event,
|
||||
state,
|
||||
ref,
|
||||
context,
|
||||
originalPost: originalPost,
|
||||
),
|
||||
child: TextField(
|
||||
controller: state.contentController,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: 'postContent'.tr(),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 8,
|
||||
),
|
||||
),
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
),
|
||||
ComposeFormFields(
|
||||
state: state,
|
||||
showPublisherAvatar: false,
|
||||
onPublisherTap: () {
|
||||
showModalBottomSheet(
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const PublisherModal(),
|
||||
).then((value) {
|
||||
if (value != null) {
|
||||
state.currentPublisher.value = value;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
// Attachments preview
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:island/screens/posts/compose.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||
import 'package:island/widgets/post/post_award_sheet.dart';
|
||||
import 'package:island/widgets/post/post_item.dart';
|
||||
import 'package:island/widgets/post/post_award_history_sheet.dart';
|
||||
import 'package:island/widgets/post/post_pin_sheet.dart';
|
||||
@@ -273,7 +274,14 @@ class PostActionButtons extends HookConsumerWidget {
|
||||
|
||||
actions.add(
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: () {},
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder: (context) => PostAwardSheet(post: post),
|
||||
);
|
||||
},
|
||||
onLongPress: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -482,14 +490,13 @@ class PostDetailScreen extends HookConsumerWidget {
|
||||
),
|
||||
if (user.value != null)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: postState
|
||||
.when(
|
||||
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child:
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 660),
|
||||
child: postState.when(
|
||||
data:
|
||||
(post) => PostQuickReply(
|
||||
parent: post!,
|
||||
@@ -501,13 +508,8 @@ class PostDetailScreen extends HookConsumerWidget {
|
||||
),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
)
|
||||
.padding(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 8,
|
||||
top: 8,
|
||||
horizontal: 16,
|
||||
),
|
||||
),
|
||||
).center(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
276
lib/screens/realm/realm_form.dart
Normal file
276
lib/screens/realm/realm_form.dart
Normal file
@@ -0,0 +1,276 @@
|
||||
import 'package:croppy/croppy.dart' show CropAspectRatio;
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/models/realm.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/screens/realm/realms.dart';
|
||||
import 'package:island/services/file.dart';
|
||||
import 'package:island/services/file_uploader.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
class NewRealmScreen extends StatelessWidget {
|
||||
const NewRealmScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const EditRealmScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class EditRealmScreen extends HookConsumerWidget {
|
||||
final String? slug;
|
||||
const EditRealmScreen({super.key, this.slug});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final submitting = useState(false);
|
||||
|
||||
final picture = useState<SnCloudFile?>(null);
|
||||
final background = useState<SnCloudFile?>(null);
|
||||
final isPublic = useState(true);
|
||||
final isCommunity = useState(false);
|
||||
|
||||
final slugController = useTextEditingController();
|
||||
final nameController = useTextEditingController();
|
||||
final descriptionController = useTextEditingController();
|
||||
|
||||
final formKey = useMemoized(GlobalKey<FormState>.new, const []);
|
||||
|
||||
final realm = ref.watch(realmProvider(slug));
|
||||
|
||||
useEffect(() {
|
||||
if (realm.value != null) {
|
||||
picture.value = realm.value!.picture;
|
||||
background.value = realm.value!.background;
|
||||
slugController.text = realm.value!.slug;
|
||||
nameController.text = realm.value!.name;
|
||||
descriptionController.text = realm.value!.description;
|
||||
isPublic.value = realm.value!.isPublic;
|
||||
isCommunity.value = realm.value!.isCommunity;
|
||||
}
|
||||
return null;
|
||||
}, [realm]);
|
||||
|
||||
void setPicture(String position) async {
|
||||
showLoadingModal(context);
|
||||
var result = await ref
|
||||
.read(imagePickerProvider)
|
||||
.pickImage(source: ImageSource.gallery);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
hideLoadingModal(context);
|
||||
result = await cropImage(
|
||||
context,
|
||||
image: result,
|
||||
allowedAspectRatios: [
|
||||
if (position == 'background')
|
||||
const CropAspectRatio(height: 7, width: 16)
|
||||
else
|
||||
const CropAspectRatio(height: 1, width: 1),
|
||||
],
|
||||
);
|
||||
if (result == null) {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
return;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
showLoadingModal(context);
|
||||
submitting.value = true;
|
||||
try {
|
||||
final cloudFile =
|
||||
await FileUploader.createCloudFile(
|
||||
client: ref.read(apiClientProvider),
|
||||
fileData: UniversalFile(
|
||||
data: result,
|
||||
type: UniversalFileType.image,
|
||||
),
|
||||
).future;
|
||||
if (cloudFile == null) {
|
||||
throw ArgumentError('Failed to upload the file...');
|
||||
}
|
||||
switch (position) {
|
||||
case 'picture':
|
||||
picture.value = cloudFile;
|
||||
case 'background':
|
||||
background.value = cloudFile;
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
if (context.mounted) hideLoadingModal(context);
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> performAction() async {
|
||||
if (!formKey.currentState!.validate()) return;
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final resp = await client.request(
|
||||
'/sphere${slug == null ? '/realms' : '/realms/$slug'}',
|
||||
data: {
|
||||
'slug': slugController.text,
|
||||
'name': nameController.text,
|
||||
'description': descriptionController.text,
|
||||
'background_id': background.value?.id,
|
||||
'picture_id': picture.value?.id,
|
||||
'is_public': isPublic.value,
|
||||
'is_community': isCommunity.value,
|
||||
},
|
||||
options: Options(method: slug == null ? 'POST' : 'PATCH'),
|
||||
);
|
||||
if (context.mounted) {
|
||||
context.pop(SnRealm.fromJson(resp.data));
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: Text(slug == null ? 'createRealm'.tr() : 'editRealm'.tr()),
|
||||
leading: const PageBackButton(),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 7,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child:
|
||||
background.value != null
|
||||
? CloudFileWidget(
|
||||
item: background.value!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('background');
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
left: 20,
|
||||
bottom: -32,
|
||||
child: GestureDetector(
|
||||
child: ProfilePictureWidget(
|
||||
fileId: picture.value?.id,
|
||||
radius: 40,
|
||||
fallbackIcon: Symbols.group,
|
||||
),
|
||||
onTap: () {
|
||||
setPicture('picture');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).padding(bottom: 32),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: slugController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'slug'.tr(),
|
||||
helperText: 'slugHint'.tr(),
|
||||
),
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(labelText: 'name'.tr()),
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: descriptionController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'description'.tr(),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
minLines: 3,
|
||||
maxLines: null,
|
||||
onTapOutside:
|
||||
(_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
children: [
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.public),
|
||||
title: Text('publicRealm').tr(),
|
||||
subtitle: Text('publicRealmDescription').tr(),
|
||||
value: isPublic.value,
|
||||
onChanged: (value) {
|
||||
isPublic.value = value ?? true;
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
secondary: const Icon(Symbols.travel_explore),
|
||||
title: Text('communityRealm').tr(),
|
||||
subtitle: Text('communityRealmDescription').tr(),
|
||||
value: isCommunity.value,
|
||||
onChanged: (value) {
|
||||
isCommunity.value = value ?? false;
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: submitting.value ? null : performAction,
|
||||
label: Text('saveChanges'.tr()),
|
||||
icon: const Icon(Symbols.save),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padding(all: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user