Compare commits
	
		
			82 Commits
		
	
	
		
			4beda9200e
			...
			3.2.0+131
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6f9d51673b | |||
| f8c6887769 | |||
| cd2a507b7f | |||
| 3cafce00a2 | |||
| 837f3fbe98 | |||
| ca7cc5d7ee | |||
| ef2c14daa2 | |||
| 3a17837cc6 | |||
| 2617a64acf | |||
| afe1e12a3b | |||
| be80f5ff85 | |||
| 3281d69eba | |||
| 77b6ce9937 | |||
| 39275f61b5 | |||
| 72193ba8f3 | |||
| 98dd9b6617 | |||
| a22b94a263 | |||
| 9c75eafdb3 | |||
| 28fda3d0c7 | |||
| 187c2ea43e | |||
| ae7d967461 | |||
| 1ce71f1fa1 | |||
| 9b68808c77 | |||
|  | 99b7bf8199 | ||
|  | eb9bb73c31 | ||
|  | a8c3830d67 | ||
|  | 07a5a19141 | ||
| ecc100ac45 | |||
| 573b76d3ff | |||
| f7dad5e419 | |||
| 9f2f1c0848 | |||
| 580d9fd979 | |||
| 3b375abc09 | |||
| c527b5e67c | |||
| e9f09bbe54 | |||
| 3aece9316c | |||
| a61c889c6c | |||
| 0dd3221a56 | |||
| 66918521f8 | |||
| bb1846e462 | |||
| a976a6eaf4 | |||
| 4252f66fd3 | |||
| f2d780b48f | |||
| 300541f9bb | |||
| 43787bb813 | |||
| 3417c51a3b | |||
| f98e603e82 | |||
| c9b71701c8 | |||
| 28e98488f1 | |||
| b4d476613e | |||
| b48a1aac44 | |||
| 596d212593 | |||
| 54f290327e | |||
| 16f248ceab | |||
| 856d811187 | |||
| d07b194c04 | |||
| 2554b58be6 | |||
| a627b5838e | |||
| c479a9f381 | |||
| 02057e663b | |||
| 6501594100 | |||
| c6599edc3d | |||
| 709a0620b6 | |||
| f9b2a96c7c | |||
| 4dca6189cb | |||
| c7f5b63fe5 | |||
| 96c2f45c85 | |||
| 06f04eb3a5 | |||
| 8af97e43b4 | |||
| d1e8234b93 | |||
| a03d6015a6 | |||
| 246ac52d0a | |||
| abf395ff9a | |||
| 4fdc8eb1d0 | |||
| d7dcde898c | |||
| f85484d3ed | |||
| 5060bd30c9 | |||
| 3959f2260b | |||
| 6f4f1216ad | |||
| f401ffbf81 | |||
| 0251697951 | |||
| 178c12b893 | 
| @@ -62,4 +62,3 @@ If you want to build the release version, use the flutter build command. Learn m | ||||
| ```bash | ||||
| flutter build <platform> | ||||
| ``` | ||||
|  | ||||
|   | ||||
| @@ -195,6 +195,7 @@ | ||||
|   "checkInResultLevel2": "A Normal Day", | ||||
|   "checkInResultLevel3": "Good Luck", | ||||
|   "checkInResultLevel4": "Best Luck", | ||||
|   "checkInResultLevel5": "Happy Birthday 🥳", | ||||
|   "checkInActivityTitle": "{} checked in on {} and got a {}", | ||||
|   "eventCalander": "Event Calander", | ||||
|   "eventCalanderEmpty": "No events on that day.", | ||||
| @@ -228,6 +229,8 @@ | ||||
|   "settings": "Settings", | ||||
|   "language": "Language", | ||||
|   "accountLanguageHint": "This language will be used for email and push notifications.", | ||||
|   "region": "Region", | ||||
|   "accountRegionHint": "This region will be used for content delivery and localization.", | ||||
|   "settingsDisplayLanguage": "Display Language", | ||||
|   "languageFollowSystem": "Follow System", | ||||
|   "postsCreatedCount": "Posts", | ||||
| @@ -338,6 +341,7 @@ | ||||
|   "notifications": "Notifications", | ||||
|   "posts": "Posts", | ||||
|   "settingsBackgroundImage": "Background Image", | ||||
|   "settingsBackgroundImageEnable": "Show Background Image", | ||||
|   "settingsBackgroundImageClear": "Clear Background Image", | ||||
|   "settingsBackgroundGenerateColor": "Generate color scheme from Bacground Image", | ||||
|   "messageNone": "No content to display", | ||||
| @@ -348,6 +352,8 @@ | ||||
|   "chatBreakNone": "None", | ||||
|   "settingsRealmCompactView": "Compact Realm View", | ||||
|   "settingsMixedFeed": "Mixed Feed", | ||||
|   "settingsDataSavingMode": "Data Saving Mode", | ||||
|   "dataSavingHint": "Data Saving Mode", | ||||
|   "settingsAutoTranslate": "Auto Translate", | ||||
|   "settingsHideBottomNav": "Hide Bottom Navigation", | ||||
|   "settingsSoundEffects": "Sound Effects", | ||||
| @@ -386,6 +392,7 @@ | ||||
|   "postSettings": "Settings", | ||||
|   "postPublisherUnselected": "Publisher Unspecified", | ||||
|   "postType": "Post Type", | ||||
|   "postTypePost": "Post", | ||||
|   "articleAttachmentHint": "Attachments must be uploaded and inserted into the article body to be visible.", | ||||
|   "postVisibility": "Post Visibility", | ||||
|   "postVisibilityPublic": "Public", | ||||
| @@ -633,8 +640,9 @@ | ||||
|   "chatJoin": "Join the Chat", | ||||
|   "realmJoin": "Join the Realm", | ||||
|   "realmJoinSuccess": "Successfully joined the realm.", | ||||
|   "discoverRealms": "Discover realms", | ||||
|   "discoverPublishers": "Discover publishers", | ||||
|   "discoverRealms": "Realms", | ||||
|   "discoverPublishers": "Publishers", | ||||
|   "discoverShuffledPost": "Random Posts", | ||||
|   "search": "Search", | ||||
|   "publisherMembers": "Collaborators", | ||||
|   "developerHub": "Developer Hub", | ||||
| @@ -651,6 +659,10 @@ | ||||
|   "editProject": "Edit Project", | ||||
|   "projectDetails": "Project Details", | ||||
|   "createBot": "Create Bot", | ||||
|   "bots": "Bots", | ||||
|   "noBots": "No bots yet.", | ||||
|   "deleteBotHint": "Are you sure you want to delete this bot? This action cannot be undone.", | ||||
|   "deleteBot": "Delete Bot", | ||||
|   "customApps": "Custom Apps", | ||||
|   "noCustomApps": "No custom apps yet.", | ||||
|   "createCustomApp": "Create Custom App", | ||||
| @@ -688,7 +700,7 @@ | ||||
|   "publisherFeatureDevelopDescription": "Unlock development abilities for your publisher, including custom apps, API keys, and more.", | ||||
|   "publisherFeatureDevelopHint": "Currently, this feature is under active development, you need send a request to unlock this feature.", | ||||
|   "learnMore": "Learn More", | ||||
|   "discoverWebArticles": "Articles from external sites", | ||||
|   "discoverWebArticles": "Web Feed Articles", | ||||
|   "webArticlesStand": "Article Stand", | ||||
|   "about": "About", | ||||
|   "membershipCancel": "Cancel Membership", | ||||
| @@ -863,7 +875,7 @@ | ||||
|   "failedToLoadUserInfoNetwork": "It seems be network issue, you can tap the button below to try again.", | ||||
|   "failedToLoadUserInfoUnauthorized": "It seems your session has been logged out or not available anymore, you can still try agian to fetch the user info if you want.", | ||||
|   "okay": "Okay", | ||||
|   "postDetails": "Post Details", | ||||
|   "postDetail": "Post Detail", | ||||
|   "postCount": { | ||||
|     "zero": "No posts", | ||||
|     "one": "{} post", | ||||
| @@ -879,11 +891,91 @@ | ||||
|   "stellarProgram": "Stellar Program", | ||||
|   "socialCredits": "Social Credits", | ||||
|   "credits": "Credits", | ||||
|   "creditsStatus": "Credits Status", | ||||
|   "socialCreditsDescription": "Social Credit is a way for Solar Network to evaluate users. It is calculated based on their behavior and interactions. With a base score of 100, higher scores indicate a user's credibility within the community. Scores change over time to reflect a user's recent behavior. Users with higher credit ratings enjoy more benefits, while users with lower credit ratings may have some functionality restricted.", | ||||
|   "socialCreditsLevelPoor": "Poor", | ||||
|   "socialCreditsLevelNormal": "Normal", | ||||
|   "socialCreditsLevelGood": "Good", | ||||
|   "socialCreditsLevelExcellent": "Excellent", | ||||
|   "orderByPopularity": "Sort by popularity", | ||||
|   "orderByReleaseDate": "Sort by release date" | ||||
|   "orderByReleaseDate": "Sort by release date", | ||||
|   "editBot": "Edit Bot", | ||||
|   "botAutomatedBy": "Automated by {}", | ||||
|   "botDetails": "Bot Details", | ||||
|   "overview": "Overview", | ||||
|   "keys": "Keys", | ||||
|   "botNotFound": "Bot not found.", | ||||
|   "newBotKey": "New Bot Key", | ||||
|   "newBotKeyHint": "Enter a name for your new key. The key will be shown only once.", | ||||
|   "revokeBotKey": "Revoke Bot Key", | ||||
|   "revokeBotKeyHint": "Are you sure you want to revoke this key? This action cannot be undone and any application using this key will stop working.", | ||||
|   "noBotKeys": "No bot keys yet.", | ||||
|   "revoke": "Revoke", | ||||
|   "keyName": "Key Name", | ||||
|   "newKeyGenerated": "New Key Generated", | ||||
|   "copyKeyHint": "Please copy this key and store it somewhere safe. You will not be able to see it again.", | ||||
|   "rotateKey": "Rotate Key", | ||||
|   "rotateBotKey": "Rotate Bot Key", | ||||
|   "rotateBotKeyHint": "Are you sure you want to rotate this key? The old key will become invalid immediately. This action cannot be undone.", | ||||
|   "webFeedArticleCount": { | ||||
|     "zero": "No articles", | ||||
|     "one": "{} article", | ||||
|     "other": "{} articles" | ||||
|   }, | ||||
|   "webFeedSubscribed": "The feed has been subscribed", | ||||
|   "webFeedUnsubscribed": "The feed has been unsubscribed", | ||||
|   "appDetails": "App Details", | ||||
|   "secrets": "Secrets", | ||||
|   "appNotFound": "App not found.", | ||||
|   "secretCopied": "Secret copied to clipboard.", | ||||
|   "deleteSecret": "Delete Secret", | ||||
|   "deleteSecretHint": "Are you sure you want to delete this secret? This action cannot be undone.", | ||||
|   "generateSecret": "Generate New Secret", | ||||
|   "createdAt": "Created at {}", | ||||
|   "newSecretGenerated": "New Secret Generated", | ||||
|   "copySecretHint": "Please copy this secret and store it somewhere safe. You will not be able to see it again.", | ||||
|   "expiresIn": "Expires In (seconds)", | ||||
|   "isOidc": "OIDC Compliant", | ||||
|   "pinPost": "Pin Post", | ||||
|   "unpinPost": "Unpin Post", | ||||
|   "pinnedPost": "Pinned", | ||||
|   "publisherPage": "Publisher Page", | ||||
|   "realmPage": "Realm Page", | ||||
|   "replyPage": "Reply Page", | ||||
|   "pinPostPublisherHint": "Pin this post to your publisher page", | ||||
|   "pinPostRealmHint": "Pin this post to the realm page", | ||||
|   "pinPostRealmDisabledHint": "This post doesn't belong to any realm", | ||||
|   "pinPostReplyHint": "Pin this post to the reply page", | ||||
|   "pinPostReplyDisabledHint": "This post is not a reply", | ||||
|   "pin": "Pin", | ||||
|   "unpinPostHint": "Are you sure you want to unpin this post?", | ||||
|   "all": "All", | ||||
|   "statusPresent": "Present", | ||||
|   "accountAutomated": "Automated", | ||||
|   "chatBreakClearButton": "Clear", | ||||
|   "chatBreak5m": "5m", | ||||
|   "chatBreak10m": "10m", | ||||
|   "chatBreak15m": "15m", | ||||
|   "chatBreak30m": "30m", | ||||
|   "chatBreakCustomMinutes": "Custom (minutes)", | ||||
|   "errorGeneric": "Error: {}", | ||||
|   "searchMessages": "Search Messages", | ||||
|   "messagesCount": "{} messages", | ||||
|   "dotSeparator": "·", | ||||
|   "roleValidationHint": "Role must be between 0 and 100", | ||||
|   "searchMessagesHint": "Search messages...", | ||||
|   "searchLinks": "Links", | ||||
|   "searchAttachments": "Attachments", | ||||
|   "noMessagesFound": "No messages found", | ||||
|   "openInBrowser": "Open in Browser", | ||||
|   "highlightPost": "Highlight Post", | ||||
|   "filters": "Filters", | ||||
|   "apply": "Apply", | ||||
|   "pubName": "Pub Name", | ||||
|   "realm": "Realm", | ||||
|   "shuffle": "Shuffle", | ||||
|   "pinned": "Pinned", | ||||
|   "noResultsFound": "No results found", | ||||
|   "toggleFilters": "Toggle filters", | ||||
|   "notableDayNext": "{} is in" | ||||
| } | ||||
|   | ||||
| @@ -158,11 +158,12 @@ | ||||
|   "checkIn": "签到", | ||||
|   "checkInNone": "尚未签到", | ||||
|   "checkInNoneHint": "通过签到获取您的财富提示和每日奖励。", | ||||
|   "checkInResultLevel0": "最差运气", | ||||
|   "checkInResultLevel1": "坏运气", | ||||
|   "checkInResultLevel2": "一个普通的日常", | ||||
|   "checkInResultLevel3": "好运", | ||||
|   "checkInResultLevel4": "最佳运气", | ||||
|   "checkInResultLevel0": "大凶", | ||||
|   "checkInResultLevel1": "凶", | ||||
|   "checkInResultLevel2": "中平", | ||||
|   "checkInResultLevel3": "吉", | ||||
|   "checkInResultLevel4": "大吉", | ||||
|   "checkInResultLevel5": "生日快乐 🥳", | ||||
|   "checkInActivityTitle": "{} 在 {} 签到并获得了 {}", | ||||
|   "eventCalander": "活动日历", | ||||
|   "eventCalanderEmpty": "该日无活动。", | ||||
| @@ -304,6 +305,7 @@ | ||||
|   "notifications": "通知", | ||||
|   "posts": "帖子", | ||||
|   "settingsBackgroundImage": "背景图片", | ||||
|   "settingsBackgroundImageEnable": "显示背景图片", | ||||
|   "settingsBackgroundImageClear": "清除背景图片", | ||||
|   "settingsBackgroundGenerateColor": "从背景图像生成主题色", | ||||
|   "messageNone": "没有内容可显示", | ||||
| @@ -314,6 +316,8 @@ | ||||
|   "chatBreakNone": "无", | ||||
|   "settingsRealmCompactView": "紧凑领域视图", | ||||
|   "settingsMixedFeed": "混合动态", | ||||
|   "settingsDataSavingMode": "流量节省模式", | ||||
|   "dataSavingHint": "流量节省模式", | ||||
|   "settingsAutoTranslate": "自动翻译", | ||||
|   "settingsHideBottomNav": "隐藏底部导航", | ||||
|   "settingsSoundEffects": "音效", | ||||
| @@ -345,7 +349,7 @@ | ||||
|   "accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。", | ||||
|   "unauthorized": "未授权", | ||||
|   "unauthorizedHint": "您未登录或会话已过期,请重新登录。", | ||||
|   "publisherBelongsTo": "属于", | ||||
|   "publisherBelongsTo": "属于 {}", | ||||
|   "postContent": "内容", | ||||
|   "postSettings": "设置", | ||||
|   "postPublisherUnselected": "未指定发布者", | ||||
| @@ -829,7 +833,7 @@ | ||||
|   "failedToLoadUserInfoNetwork": "这看起来是个网络问题,你可以按下面的按钮来重试", | ||||
|   "failedToLoadUserInfoUnauthorized": "看来您的会话已被注销或不再可用,如果您愿意,您仍然可以再次尝试获取用户信息。", | ||||
|   "okay": "了解", | ||||
|   "postDetails": "帖子详情", | ||||
|   "postDetail": "帖子详情", | ||||
|   "mimeType": "类型", | ||||
|   "fileSize": "大小", | ||||
|   "fileHash": "哈希", | ||||
| @@ -843,5 +847,22 @@ | ||||
|   "socialCreditsLevelPoor": "糟糕", | ||||
|   "socialCreditsLevelNormal": "正常", | ||||
|   "socialCreditsLevelGood": "良好", | ||||
|   "socialCreditsLevelExcellent": "优秀" | ||||
|   "socialCreditsLevelExcellent": "优秀", | ||||
|   "appDetails": "应用详情", | ||||
|   "secrets": "密钥", | ||||
|   "appNotFound": "应用未找到。", | ||||
|   "secretCopied": "密钥已复制到剪贴板。", | ||||
|   "deleteSecret": "删除密钥", | ||||
|   "deleteSecretHint": "您确定要删除此密钥吗?此操作无法撤销。", | ||||
|   "generateSecret": "生成新密钥", | ||||
|   "createdAt": "创建于 {}", | ||||
|   "newSecretGenerated": "已生成新密钥", | ||||
|   "copySecretHint": "请复制此密钥并将其存放在安全的地方。您将无法再次看到它。", | ||||
|   "expiresIn": "过期时间(秒)", | ||||
|   "isOidc": "OIDC 兼容", | ||||
|   "statusPresent": "至今", | ||||
|   "accountAutomated": "机器人", | ||||
|   "openInBrowser": "在浏览器中打开", | ||||
|   "highlightPost": "精选帖子", | ||||
|   "notableDayNext": "距离 {} 还有" | ||||
| } | ||||
|   | ||||
| @@ -303,6 +303,7 @@ | ||||
|     "notifications": "通知", | ||||
|     "posts": "帖子", | ||||
|     "settingsBackgroundImage": "背景圖片", | ||||
|   "settingsBackgroundImageEnable": "顯示背景圖片", | ||||
|   "settingsBackgroundImageClear": "清除背景圖片", | ||||
|     "settingsBackgroundGenerateColor": "從背景圖像生成主題色", | ||||
|     "messageNone": "沒有內容可顯示", | ||||
| @@ -314,6 +315,8 @@ | ||||
|     "settingsRealmCompactView": "緊湊領域視圖", | ||||
|     "settingsMixedFeed": "混合動態", | ||||
|     "settingsAutoTranslate": "自動翻譯", | ||||
|     "settingsDataSavingMode": "低數據模式", | ||||
|     "dataSavingHint": "低數據模式", | ||||
|     "settingsHideBottomNav": "隱藏底部導航", | ||||
|     "settingsSoundEffects": "音效", | ||||
|     "settingsAprilFoolFeatures": "愚人節功能", | ||||
| @@ -811,5 +814,17 @@ | ||||
|     "filesListAdditional": { | ||||
|         "one": "+{} 個文件被摺疊", | ||||
|         "other": "+{} 個文件被摺疊" | ||||
|     } | ||||
|     }, | ||||
|     "appDetails": "應用程式詳情", | ||||
|     "secrets": "密鑰", | ||||
|     "appNotFound": "找不到應用程式。", | ||||
|     "secretCopied": "密鑰已複製到剪貼簿。", | ||||
|     "deleteSecret": "刪除密鑰", | ||||
|     "deleteSecretHint": "您確定要刪除此密鑰嗎?此操作無法復原。", | ||||
|     "generateSecret": "產生新密鑰", | ||||
|     "createdAt": "建立於 {}", | ||||
|     "newSecretGenerated": "已產生新密鑰", | ||||
|     "copySecretHint": "請複製此密鑰並將其存放在安全的地方。您將無法再次看到它。", | ||||
|     "expiresIn": "過期時間(秒)", | ||||
|     "isOidc": "OIDC 相容" | ||||
| } | ||||
							
								
								
									
										12
									
								
								assets/icons/icon-outline.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								assets/icons/icon-outline.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" fill="none"> | ||||
|     <path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="12" | ||||
|         d="M54 147h86" /> | ||||
|     <path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="10" | ||||
|         d="M57 111s-2-4.5-2-10m22 22s-4 7-11 4m9-22s-2-4.5-2-10" /> | ||||
|     <path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="12" | ||||
|         d="M54 147a32 32 0 0 1-11.999-61.665A39 39 0 0 1 81 46m59 101a30 30 0 0 0 29.933-28" /> | ||||
|     <circle cx="132" cy="75" r="4" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" | ||||
|         stroke-width="8" /> | ||||
|     <path stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="10" | ||||
|         d="M112.5 41.217C100.843 47.961 93 60.564 93 75c0 6.375 1.53 12.393 4.242 17.707m69.513-35.419A38.84 38.84 0 0 1 171 75c0 14.433-7.84 27.034-19.493 33.779m-.793-43.317A20.9 20.9 0 0 1 153 75c0 7.77-4.221 14.556-10.495 18.188m-21.003-36.38C115.224 60.44 111 67.226 111 75a20.9 20.9 0 0 0 2.284 9.533" /> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/images/media-offline.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/images/media-offline.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 461 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 307 KiB | 
| @@ -21,6 +21,6 @@ | ||||
|   <key>CFBundleVersion</key> | ||||
|   <string>1.0</string> | ||||
|   <key>MinimumOSVersion</key> | ||||
|   <string>12.0</string> | ||||
|   <string>13.0</string> | ||||
| </dict> | ||||
| </plist> | ||||
|   | ||||
							
								
								
									
										168
									
								
								ios/Podfile.lock
									
									
									
									
									
								
							
							
						
						
									
										168
									
								
								ios/Podfile.lock
									
									
									
									
									
								
							| @@ -40,83 +40,85 @@ PODS: | ||||
|   - file_picker (0.0.1): | ||||
|     - DKImagePickerController/PhotoGallery | ||||
|     - Flutter | ||||
|   - Firebase/CoreOnly (12.0.0): | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|   - Firebase/Crashlytics (12.0.0): | ||||
|   - file_saver (0.0.1): | ||||
|     - Flutter | ||||
|   - Firebase/CoreOnly (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|   - Firebase/Crashlytics (12.2.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseCrashlytics (~> 12.0.0) | ||||
|   - Firebase/Messaging (12.0.0): | ||||
|     - FirebaseCrashlytics (~> 12.2.0) | ||||
|   - Firebase/Messaging (12.2.0): | ||||
|     - Firebase/CoreOnly | ||||
|     - FirebaseMessaging (~> 12.0.0) | ||||
|   - firebase_analytics (12.0.0): | ||||
|     - FirebaseMessaging (~> 12.2.0) | ||||
|   - firebase_analytics (12.0.1): | ||||
|     - firebase_core | ||||
|     - FirebaseAnalytics (= 12.0.0) | ||||
|     - FirebaseAnalytics (= 12.2.0) | ||||
|     - Flutter | ||||
|   - firebase_core (4.0.0): | ||||
|     - Firebase/CoreOnly (= 12.0.0) | ||||
|   - firebase_core (4.1.0): | ||||
|     - Firebase/CoreOnly (= 12.2.0) | ||||
|     - Flutter | ||||
|   - firebase_crashlytics (5.0.0): | ||||
|     - Firebase/Crashlytics (= 12.0.0) | ||||
|   - firebase_crashlytics (5.0.1): | ||||
|     - Firebase/Crashlytics (= 12.2.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
|   - firebase_messaging (16.0.0): | ||||
|     - Firebase/Messaging (= 12.0.0) | ||||
|   - firebase_messaging (16.0.1): | ||||
|     - Firebase/Messaging (= 12.2.0) | ||||
|     - firebase_core | ||||
|     - Flutter | ||||
|   - FirebaseAnalytics (12.0.0): | ||||
|     - FirebaseAnalytics/Default (= 12.0.0) | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|     - FirebaseInstallations (~> 12.0.0) | ||||
|   - FirebaseAnalytics (12.2.0): | ||||
|     - FirebaseAnalytics/Default (= 12.2.0) | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|     - FirebaseInstallations (~> 12.2.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Network (~> 8.1) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseAnalytics/Default (12.0.0): | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|     - FirebaseInstallations (~> 12.0.0) | ||||
|     - GoogleAppMeasurement/Default (= 12.0.0) | ||||
|   - FirebaseAnalytics/Default (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|     - FirebaseInstallations (~> 12.2.0) | ||||
|     - GoogleAppMeasurement/Default (= 12.2.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Network (~> 8.1) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseCore (12.0.0): | ||||
|     - FirebaseCoreInternal (~> 12.0.0) | ||||
|   - FirebaseCore (12.2.0): | ||||
|     - FirebaseCoreInternal (~> 12.2.0) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/Logger (~> 8.1) | ||||
|   - FirebaseCoreExtension (12.0.0): | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|   - FirebaseCoreInternal (12.0.0): | ||||
|   - FirebaseCoreExtension (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|   - FirebaseCoreInternal (12.2.0): | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|   - FirebaseCrashlytics (12.0.0): | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|     - FirebaseInstallations (~> 12.0.0) | ||||
|     - FirebaseRemoteConfigInterop (~> 12.0.0) | ||||
|     - FirebaseSessions (~> 12.0.0) | ||||
|   - FirebaseCrashlytics (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|     - FirebaseInstallations (~> 12.2.0) | ||||
|     - FirebaseRemoteConfigInterop (~> 12.2.0) | ||||
|     - FirebaseSessions (~> 12.2.0) | ||||
|     - GoogleDataTransport (~> 10.1) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - nanopb (~> 3.30910.0) | ||||
|     - PromisesObjC (~> 2.4) | ||||
|   - FirebaseInstallations (12.0.0): | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|   - FirebaseInstallations (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.1) | ||||
|     - PromisesObjC (~> 2.4) | ||||
|   - FirebaseMessaging (12.0.0): | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|     - FirebaseInstallations (~> 12.0.0) | ||||
|   - FirebaseMessaging (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|     - FirebaseInstallations (~> 12.2.0) | ||||
|     - GoogleDataTransport (~> 10.1) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/Reachability (~> 8.1) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.1) | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - FirebaseRemoteConfigInterop (12.0.0) | ||||
|   - FirebaseSessions (12.0.0): | ||||
|     - FirebaseCore (~> 12.0.0) | ||||
|     - FirebaseCoreExtension (~> 12.0.0) | ||||
|     - FirebaseInstallations (~> 12.0.0) | ||||
|   - FirebaseRemoteConfigInterop (12.2.0) | ||||
|   - FirebaseSessions (12.2.0): | ||||
|     - FirebaseCore (~> 12.2.0) | ||||
|     - FirebaseCoreExtension (~> 12.2.0) | ||||
|     - FirebaseInstallations (~> 12.2.0) | ||||
|     - GoogleDataTransport (~> 10.1) | ||||
|     - GoogleUtilities/Environment (~> 8.1) | ||||
|     - GoogleUtilities/UserDefaults (~> 8.1) | ||||
| @@ -134,6 +136,8 @@ PODS: | ||||
|     - OrderedSet (~> 6.0.3) | ||||
|   - flutter_keyboard_visibility (0.0.1): | ||||
|     - Flutter | ||||
|   - flutter_local_notifications (0.0.1): | ||||
|     - Flutter | ||||
|   - flutter_native_splash (2.4.3): | ||||
|     - Flutter | ||||
|   - flutter_platform_alert (0.0.1): | ||||
| @@ -145,33 +149,33 @@ PODS: | ||||
|   - flutter_udid (0.0.1): | ||||
|     - Flutter | ||||
|     - SAMKeychain | ||||
|   - flutter_webrtc (1.0.0): | ||||
|   - flutter_webrtc (1.1.0): | ||||
|     - Flutter | ||||
|     - WebRTC-SDK (= 137.7151.02) | ||||
|     - WebRTC-SDK (= 137.7151.03) | ||||
|   - gal (1.0.0): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - GoogleAdsOnDeviceConversion (2.1.0): | ||||
|   - GoogleAdsOnDeviceConversion (2.3.0): | ||||
|     - GoogleUtilities/Logger (~> 8.1) | ||||
|     - GoogleUtilities/Network (~> 8.1) | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/Core (12.0.0): | ||||
|   - GoogleAppMeasurement/Core (12.2.0): | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Network (~> 8.1) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/Default (12.0.0): | ||||
|     - GoogleAdsOnDeviceConversion (= 2.1.0) | ||||
|     - GoogleAppMeasurement/Core (= 12.0.0) | ||||
|     - GoogleAppMeasurement/IdentitySupport (= 12.0.0) | ||||
|   - GoogleAppMeasurement/Default (12.2.0): | ||||
|     - GoogleAdsOnDeviceConversion (= 2.3.0) | ||||
|     - GoogleAppMeasurement/Core (= 12.2.0) | ||||
|     - GoogleAppMeasurement/IdentitySupport (= 12.2.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Network (~> 8.1) | ||||
|     - "GoogleUtilities/NSData+zlib (~> 8.1)" | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - GoogleAppMeasurement/IdentitySupport (12.0.0): | ||||
|     - GoogleAppMeasurement/Core (= 12.0.0) | ||||
|   - GoogleAppMeasurement/IdentitySupport (12.2.0): | ||||
|     - GoogleAppMeasurement/Core (= 12.2.0) | ||||
|     - GoogleUtilities/AppDelegateSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/MethodSwizzler (~> 8.1) | ||||
|     - GoogleUtilities/Network (~> 8.1) | ||||
| @@ -215,7 +219,7 @@ PODS: | ||||
|   - livekit_client (2.5.0): | ||||
|     - Flutter | ||||
|     - flutter_webrtc | ||||
|     - WebRTC-SDK (= 137.7151.02) | ||||
|     - WebRTC-SDK (= 137.7151.03) | ||||
|   - local_auth_darwin (0.0.1): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
| @@ -248,9 +252,9 @@ PODS: | ||||
|   - record_ios (1.1.0): | ||||
|     - Flutter | ||||
|   - SAMKeychain (1.5.3) | ||||
|   - SDWebImage (5.21.1): | ||||
|     - SDWebImage/Core (= 5.21.1) | ||||
|   - SDWebImage/Core (5.21.1) | ||||
|   - SDWebImage (5.21.2): | ||||
|     - SDWebImage/Core (= 5.21.2) | ||||
|   - SDWebImage/Core (5.21.2) | ||||
|   - share_plus (0.0.1): | ||||
|     - Flutter | ||||
|   - shared_preferences_foundation (0.0.1): | ||||
| @@ -295,7 +299,7 @@ PODS: | ||||
|     - Flutter | ||||
|   - wakelock_plus (0.0.1): | ||||
|     - Flutter | ||||
|   - WebRTC-SDK (137.7151.02) | ||||
|   - WebRTC-SDK (137.7151.03) | ||||
|  | ||||
| DEPENDENCIES: | ||||
|   - Alamofire | ||||
| @@ -303,6 +307,7 @@ DEPENDENCIES: | ||||
|   - croppy (from `.symlinks/plugins/croppy/ios`) | ||||
|   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) | ||||
|   - file_picker (from `.symlinks/plugins/file_picker/ios`) | ||||
|   - file_saver (from `.symlinks/plugins/file_saver/ios`) | ||||
|   - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) | ||||
|   - firebase_core (from `.symlinks/plugins/firebase_core/ios`) | ||||
|   - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) | ||||
| @@ -311,6 +316,7 @@ DEPENDENCIES: | ||||
|   - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`) | ||||
|   - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) | ||||
|   - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) | ||||
|   - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) | ||||
|   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) | ||||
|   - flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`) | ||||
|   - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) | ||||
| @@ -381,6 +387,8 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/device_info_plus/ios" | ||||
|   file_picker: | ||||
|     :path: ".symlinks/plugins/file_picker/ios" | ||||
|   file_saver: | ||||
|     :path: ".symlinks/plugins/file_saver/ios" | ||||
|   firebase_analytics: | ||||
|     :path: ".symlinks/plugins/firebase_analytics/ios" | ||||
|   firebase_core: | ||||
| @@ -397,6 +405,8 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" | ||||
|   flutter_keyboard_visibility: | ||||
|     :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" | ||||
|   flutter_local_notifications: | ||||
|     :path: ".symlinks/plugins/flutter_local_notifications/ios" | ||||
|   flutter_native_splash: | ||||
|     :path: ".symlinks/plugins/flutter_native_splash/ios" | ||||
|   flutter_platform_alert: | ||||
| @@ -464,39 +474,41 @@ SPEC CHECKSUMS: | ||||
|   DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c | ||||
|   DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 | ||||
|   file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be | ||||
|   Firebase: 800d487043c0557d9faed71477a38d9aafb08a41 | ||||
|   firebase_analytics: cd56fc56f75c1df30a6ff5290cd56e230996a76d | ||||
|   firebase_core: 633e1851ffe1b9ab875f6467a4f574c79cef02e4 | ||||
|   firebase_crashlytics: 2c6c1a17900a38081d938330e9f48e60ec5b255d | ||||
|   firebase_messaging: d17feef781edc84ebefe62624fb384358ad96361 | ||||
|   FirebaseAnalytics: 6d790cd1b159b4eb61a99948df0934ce505a34f7 | ||||
|   FirebaseCore: 055f4ab117d5964158c833f3d5e7ec6d91648d4a | ||||
|   FirebaseCoreExtension: 639afb3de6abd611952be78a794c54a47fa0f361 | ||||
|   FirebaseCoreInternal: dedc28e569a4be85f38f3d6af1070a2e12018d55 | ||||
|   FirebaseCrashlytics: db75aa0cab8d00f68406fa247c32fe17ade884d7 | ||||
|   FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988 | ||||
|   FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde | ||||
|   FirebaseRemoteConfigInterop: bfa0ea72ba3dc5af739777296424e46bd6f42613 | ||||
|   FirebaseSessions: 4e784acda213108aafef536535cdfc03504acc42 | ||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||
|   file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 | ||||
|   Firebase: 26f6f8d460603af3df970ad505b16b15f5e2e9a1 | ||||
|   firebase_analytics: 111ff65791a430356bd6c7e4d7339537fc6a15ae | ||||
|   firebase_core: 3ff52146406557dddd01d570e807e203ec7e1302 | ||||
|   firebase_crashlytics: 3637078b718a52dc9fb4d64e37c969e86b87ff6f | ||||
|   firebase_messaging: 3dcc998dd98e1e54af75d0cccae8606eba43553c | ||||
|   FirebaseAnalytics: e04e23bc070e3014aa5cf4980f9df7ce5cd79ec8 | ||||
|   FirebaseCore: 311c48a147ad4a0ab7febbaed89e8025c67510cd | ||||
|   FirebaseCoreExtension: 73af080c22a2f7b44cefa391dc08f7e4ee162cb5 | ||||
|   FirebaseCoreInternal: 56ea29f3dad2894f81b060f706f9d53509b6ed3b | ||||
|   FirebaseCrashlytics: f83cbf176d5c637ade108c0aacf1ccbd5ec499bf | ||||
|   FirebaseInstallations: 3e884b01feabdf67582a80f3250425a00979b4ed | ||||
|   FirebaseMessaging: 43ec73bbfedd0c385a849bb91593ab4ad4b9e48e | ||||
|   FirebaseRemoteConfigInterop: 0896fd52ab72586a355c8f389ff85aaa9e5375e1 | ||||
|   FirebaseSessions: f4692789e770bec66ce17d772c0e9561c4f11737 | ||||
|   Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 | ||||
|   flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9 | ||||
|   flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 | ||||
|   flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 | ||||
|   flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb | ||||
|   flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf | ||||
|   flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3 | ||||
|   flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 | ||||
|   flutter_timezone: 7c838e17ffd4645d261e87037e5bebf6d38fe544 | ||||
|   flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9 | ||||
|   flutter_webrtc: 6f7da106613d52ade777d5b4875a43f48c28b457 | ||||
|   flutter_webrtc: b0b2e04411747142962164a1cfa43a1af9a0afac | ||||
|   gal: baecd024ebfd13c441269ca7404792a7152fde89 | ||||
|   GoogleAdsOnDeviceConversion: 2be6297a4f048459e0ae17fad9bfd2844e10cf64 | ||||
|   GoogleAppMeasurement: 8f6ab04ad6ae493b53fcf56bd26323fb2f1384f3 | ||||
|   GoogleAdsOnDeviceConversion: 9090c435cde08903e8dd1ba2c77fbec9e46d9afe | ||||
|   GoogleAppMeasurement: 09f341dfa8527d1612a09cbfe809a242c0b737af | ||||
|   GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 | ||||
|   GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 | ||||
|   image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a | ||||
|   irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 | ||||
|   Kingfisher: ff0d31a1f07bdff6a1ebb3ba08b8e6e567b6500c | ||||
|   livekit_client: e3b79b99405428aac439b6b76a254cd9a11dbbfb | ||||
|   livekit_client: f810c81bbbc229a84f60b09e66603ac4e93f7599 | ||||
|   local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19 | ||||
|   media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854 | ||||
|   media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474 | ||||
| @@ -512,7 +524,7 @@ SPEC CHECKSUMS: | ||||
|   receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 | ||||
|   record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374 | ||||
|   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c | ||||
|   SDWebImage: f29024626962457f3470184232766516dee8dfea | ||||
|   SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a | ||||
|   share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a | ||||
|   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 | ||||
|   sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 | ||||
| @@ -524,7 +536,7 @@ SPEC CHECKSUMS: | ||||
|   url_launcher_ios: 694010445543906933d732453a59da0a173ae33d | ||||
|   volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 | ||||
|   wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 | ||||
|   WebRTC-SDK: d20de357dcbf7c9696b124b39f3ff62125107e4b | ||||
|   WebRTC-SDK: 69d4e56b0b4b27d788e87bab9b9a1326ed05b1e3 | ||||
|  | ||||
| PODFILE CHECKSUM: c818292390b02fa379036ea099713a332bd7193f | ||||
|  | ||||
|   | ||||
| @@ -853,7 +853,7 @@ | ||||
| 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | ||||
| 				GCC_WARN_UNUSED_FUNCTION = YES; | ||||
| 				GCC_WARN_UNUSED_VARIABLE = YES; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 12.0; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				MTL_ENABLE_DEBUG_INFO = NO; | ||||
| 				SDKROOT = iphoneos; | ||||
| 				SUPPORTED_PLATFORMS = iphoneos; | ||||
| @@ -897,6 +897,7 @@ | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				CURRENT_PROJECT_VERSION = 1; | ||||
| 				GENERATE_INFOPLIST_FILE = YES; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				MARKETING_VERSION = 1.0; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| @@ -915,6 +916,7 @@ | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				CURRENT_PROJECT_VERSION = 1; | ||||
| 				GENERATE_INFOPLIST_FILE = YES; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				MARKETING_VERSION = 1.0; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| @@ -931,6 +933,7 @@ | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				CURRENT_PROJECT_VERSION = 1; | ||||
| 				GENERATE_INFOPLIST_FILE = YES; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				MARKETING_VERSION = 1.0; | ||||
| 				PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.RunnerTests; | ||||
| 				PRODUCT_NAME = "$(TARGET_NAME)"; | ||||
| @@ -1078,7 +1081,7 @@ | ||||
| 				INFOPLIST_FILE = SolianShareExtension/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension; | ||||
| 				INFOPLIST_KEY_NSHumanReadableCopyright = ""; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| @@ -1121,7 +1124,7 @@ | ||||
| 				INFOPLIST_FILE = SolianShareExtension/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension; | ||||
| 				INFOPLIST_KEY_NSHumanReadableCopyright = ""; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| @@ -1161,7 +1164,7 @@ | ||||
| 				INFOPLIST_FILE = SolianShareExtension/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension; | ||||
| 				INFOPLIST_KEY_NSHumanReadableCopyright = ""; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 15.0; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/Frameworks", | ||||
| @@ -1348,7 +1351,7 @@ | ||||
| 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | ||||
| 				GCC_WARN_UNUSED_FUNCTION = YES; | ||||
| 				GCC_WARN_UNUSED_VARIABLE = YES; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 12.0; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				MTL_ENABLE_DEBUG_INFO = YES; | ||||
| 				ONLY_ACTIVE_ARCH = YES; | ||||
| 				SDKROOT = iphoneos; | ||||
| @@ -1399,7 +1402,7 @@ | ||||
| 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | ||||
| 				GCC_WARN_UNUSED_FUNCTION = YES; | ||||
| 				GCC_WARN_UNUSED_VARIABLE = YES; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 12.0; | ||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 13.0; | ||||
| 				MTL_ENABLE_DEBUG_INFO = NO; | ||||
| 				SDKROOT = iphoneos; | ||||
| 				SUPPORTED_PLATFORMS = iphoneos; | ||||
|   | ||||
| @@ -47,6 +47,7 @@ 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) | ||||
| @@ -60,8 +61,6 @@ class NotificationService: UNNotificationServiceExtension { | ||||
|          | ||||
|         let pfpIdentifier = meta["pfp"] as? String | ||||
|          | ||||
|         content.categoryIdentifier = "REPLYABLE_MESSAGE" | ||||
|          | ||||
|         let metaCopy = meta as? [String: Any] ?? [:] | ||||
|         let pfpUrl = pfpIdentifier != nil ? getAttachmentUrl(for: pfpIdentifier!) : nil | ||||
|          | ||||
|   | ||||
| @@ -68,6 +68,34 @@ class AppDatabase extends _$AppDatabase { | ||||
|     return (delete(chatMessages)..where((m) => m.id.equals(id))).go(); | ||||
|   } | ||||
|  | ||||
|   Future<int> getTotalMessagesForRoom(String roomId) { | ||||
|     return (select(chatMessages)..where((m) => m.roomId.equals(roomId))).get().then((list) => list.length); | ||||
|   } | ||||
|  | ||||
|   Future<List<LocalChatMessage>> searchMessages( | ||||
|     String roomId, | ||||
|     String query, | ||||
|   ) async { | ||||
|     var selectStatement = select(chatMessages) | ||||
|       ..where((m) => m.roomId.equals(roomId)); | ||||
|  | ||||
|     if (query.isNotEmpty) { | ||||
|       selectStatement = | ||||
|           selectStatement | ||||
|             ..where((m) => m.content.like('%${query.toLowerCase()}%')); | ||||
|     } | ||||
|  | ||||
|      | ||||
|  | ||||
|      | ||||
|  | ||||
|     final messages = | ||||
|         await (selectStatement | ||||
|               ..orderBy([(m) => OrderingTerm.desc(m.createdAt)])) | ||||
|             .get(); | ||||
|     return messages.map((msg) => companionToMessage(msg)).toList(); | ||||
|   } | ||||
|  | ||||
|   // Convert between Drift and model objects | ||||
|   ChatMessagesCompanion messageToCompanion(LocalChatMessage message) { | ||||
|     return ChatMessagesCompanion( | ||||
|   | ||||
| @@ -8,7 +8,7 @@ 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/services.dart'; | ||||
|  | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| @@ -30,7 +30,6 @@ 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:url_launcher/url_launcher_string.dart'; | ||||
| import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect; | ||||
|  | ||||
| @pragma('vm:entry-point') | ||||
| Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { | ||||
| @@ -52,7 +51,6 @@ void main() async { | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     await langdetect.initLangDetect(); | ||||
|     await EasyLocalization.ensureInitialized(); | ||||
|  | ||||
|     if (kIsWeb || !Platform.isLinux) { | ||||
| @@ -169,12 +167,12 @@ class IslandApp extends HookConsumerWidget { | ||||
|     final theme = ref.watch(themeProvider); | ||||
|  | ||||
|     void handleMessage(RemoteMessage notification) { | ||||
|       if (notification.data['action_uri'] != null) { | ||||
|         var uri = notification.data['action_uri'] as String; | ||||
|       if (notification.data['meta']?['action_uri'] != null) { | ||||
|         var uri = notification.data['meta']['action_uri'] as String; | ||||
|         if (uri.startsWith('/')) { | ||||
|           // In-app routes | ||||
|           final router = ref.read(routerProvider); | ||||
|           router.go(notification.data['action_uri']); | ||||
|           router.push(notification.data['meta']['action_uri']); | ||||
|         } else { | ||||
|           // External links | ||||
|           launchUrlString(uri); | ||||
| @@ -186,27 +184,6 @@ class IslandApp extends HookConsumerWidget { | ||||
|       if (!kIsWeb && Platform.isLinux) { | ||||
|         return null; | ||||
|       } | ||||
|       const channel = MethodChannel('dev.solsynth.solian/notifications'); | ||||
|  | ||||
|       Future<void> handleInitialLink() async { | ||||
|         final String? link = await channel.invokeMethod('initialLink'); | ||||
|         if (link != null) { | ||||
|           final router = ref.read(routerProvider); | ||||
|           router.go(link); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (!kIsWeb && Platform.isAndroid) { | ||||
|         handleInitialLink(); | ||||
|       } | ||||
|  | ||||
|       channel.setMethodCallHandler((call) async { | ||||
|         if (call.method == 'newLink') { | ||||
|           final String link = call.arguments; | ||||
|           final router = ref.read(routerProvider); | ||||
|           router.go(link); | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|       // When the app is opened from a terminated state. | ||||
|       FirebaseMessaging.instance.getInitialMessage().then((message) { | ||||
| @@ -246,6 +223,7 @@ class IslandApp extends HookConsumerWidget { | ||||
|           if (user.value != null) { | ||||
|             final apiClient = ref.read(apiClientProvider); | ||||
|             subscribePushNotification(apiClient); | ||||
|             initializeLocalNotifications(); | ||||
|             final wsNotifier = ref.read(websocketStateProvider.notifier); | ||||
|             wsNotifier.connect(); | ||||
|           } | ||||
| @@ -262,6 +240,7 @@ class IslandApp extends HookConsumerWidget { | ||||
|       themeMode: ThemeMode.system, | ||||
|       routerConfig: router, | ||||
|       supportedLocales: context.supportedLocales, | ||||
|       scrollBehavior: AppScrollBehavior(), | ||||
|       localizationsDelegates: [ | ||||
|         ...context.localizationDelegates, | ||||
|         CroppyLocalizations.delegate, | ||||
|   | ||||
| @@ -13,7 +13,9 @@ sealed class SnAccount with _$SnAccount { | ||||
|     required String name, | ||||
|     required String nick, | ||||
|     required String language, | ||||
|     @Default("") String region, | ||||
|     required bool isSuperuser, | ||||
|     required String? automatedId, | ||||
|     required SnAccountProfile profile, | ||||
|     required SnWalletSubscriptionRef? perkSubscription, | ||||
|     @Default([]) List<SnAccountBadge> badges, | ||||
| @@ -70,6 +72,8 @@ sealed class SnAccountProfile with _$SnAccountProfile { | ||||
|     SnAccountBadge? activeBadge, | ||||
|     required int experience, | ||||
|     required int level, | ||||
|     @Default(100) double socialCredits, | ||||
|     @Default(0) int socialCreditsLevel, | ||||
|     required double levelingProgress, | ||||
|     required SnCloudFile? picture, | ||||
|     required SnCloudFile? background, | ||||
|   | ||||
| @@ -15,7 +15,7 @@ T _$identity<T>(T value) => value; | ||||
| /// @nodoc | ||||
| mixin _$SnAccount { | ||||
|  | ||||
|  String get id; String get name; String get nick; String get language; bool get isSuperuser; SnAccountProfile get profile; SnWalletSubscriptionRef? get perkSubscription; List<SnAccountBadge> get badges; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
|  String get id; String get name; String get nick; String get language; String get region; bool get isSuperuser; String? get automatedId; SnAccountProfile get profile; SnWalletSubscriptionRef? get perkSubscription; List<SnAccountBadge> get badges; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
| /// Create a copy of SnAccount | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -28,16 +28,16 @@ $SnAccountCopyWith<SnAccount> get copyWith => _$SnAccountCopyWithImpl<SnAccount> | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other.badges, badges)&&(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 SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.region, region) || other.region == region)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.automatedId, automatedId) || other.automatedId == automatedId)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other.badges, badges)&&(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.hash(runtimeType,id,name,nick,language,isSuperuser,profile,perkSubscription,const DeepCollectionEquality().hash(badges),createdAt,updatedAt,deletedAt); | ||||
| int get hashCode => Object.hash(runtimeType,id,name,nick,language,region,isSuperuser,automatedId,profile,perkSubscription,const DeepCollectionEquality().hash(badges),createdAt,updatedAt,deletedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
|   return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, region: $region, isSuperuser: $isSuperuser, automatedId: $automatedId, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -48,7 +48,7 @@ abstract mixin class $SnAccountCopyWith<$Res>  { | ||||
|   factory $SnAccountCopyWith(SnAccount value, $Res Function(SnAccount) _then) = _$SnAccountCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String name, String nick, String language, bool isSuperuser, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
|  String id, String name, String nick, String language, String region, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -65,14 +65,16 @@ class _$SnAccountCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnAccount | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? region = null,Object? isSuperuser = null,Object? automatedId = freezed,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,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,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable | ||||
| as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable | ||||
| as String,region: null == region ? _self.region : region // ignore: cast_nullable_to_non_nullable | ||||
| as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable | ||||
| as bool,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable | ||||
| as bool,automatedId: freezed == automatedId ? _self.automatedId : automatedId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable | ||||
| as SnAccountProfile,perkSubscription: freezed == perkSubscription ? _self.perkSubscription : perkSubscription // ignore: cast_nullable_to_non_nullable | ||||
| as SnWalletSubscriptionRef?,badges: null == badges ? _self.badges : badges // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnAccountBadge>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| @@ -181,10 +183,10 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String nick,  String language,  bool isSuperuser,  SnAccountProfile profile,  SnWalletSubscriptionRef? perkSubscription,  List<SnAccountBadge> badges,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String nick,  String language,  String region,  bool isSuperuser,  String? automatedId,  SnAccountProfile profile,  SnWalletSubscriptionRef? perkSubscription,  List<SnAccountBadge> badges,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccount() when $default != null: | ||||
| return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
| return $default(_that.id,_that.name,_that.nick,_that.language,_that.region,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| @@ -202,10 +204,10 @@ return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser, | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String nick,  String language,  bool isSuperuser,  SnAccountProfile profile,  SnWalletSubscriptionRef? perkSubscription,  List<SnAccountBadge> badges,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String nick,  String language,  String region,  bool isSuperuser,  String? automatedId,  SnAccountProfile profile,  SnWalletSubscriptionRef? perkSubscription,  List<SnAccountBadge> badges,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccount(): | ||||
| return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||
| return $default(_that.id,_that.name,_that.nick,_that.language,_that.region,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| @@ -219,10 +221,10 @@ return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser, | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String nick,  String language,  bool isSuperuser,  SnAccountProfile profile,  SnWalletSubscriptionRef? perkSubscription,  List<SnAccountBadge> badges,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String nick,  String language,  String region,  bool isSuperuser,  String? automatedId,  SnAccountProfile profile,  SnWalletSubscriptionRef? perkSubscription,  List<SnAccountBadge> badges,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccount() when $default != null: | ||||
| return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
| return $default(_that.id,_that.name,_that.nick,_that.language,_that.region,_that.isSuperuser,_that.automatedId,_that.profile,_that.perkSubscription,_that.badges,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| @@ -234,14 +236,16 @@ return $default(_that.id,_that.name,_that.nick,_that.language,_that.isSuperuser, | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnAccount implements SnAccount { | ||||
|   const _SnAccount({required this.id, required this.name, required this.nick, required this.language, required this.isSuperuser, required this.profile, required this.perkSubscription, final  List<SnAccountBadge> badges = const [], required this.createdAt, required this.updatedAt, required this.deletedAt}): _badges = badges; | ||||
|   const _SnAccount({required this.id, required this.name, required this.nick, required this.language, this.region = "", required this.isSuperuser, required this.automatedId, required this.profile, required this.perkSubscription, final  List<SnAccountBadge> badges = const [], required this.createdAt, required this.updatedAt, required this.deletedAt}): _badges = badges; | ||||
|   factory _SnAccount.fromJson(Map<String, dynamic> json) => _$SnAccountFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String name; | ||||
| @override final  String nick; | ||||
| @override final  String language; | ||||
| @override@JsonKey() final  String region; | ||||
| @override final  bool isSuperuser; | ||||
| @override final  String? automatedId; | ||||
| @override final  SnAccountProfile profile; | ||||
| @override final  SnWalletSubscriptionRef? perkSubscription; | ||||
|  final  List<SnAccountBadge> _badges; | ||||
| @@ -268,16 +272,16 @@ Map<String, dynamic> toJson() { | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other._badges, _badges)&&(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 _SnAccount&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.language, language) || other.language == language)&&(identical(other.region, region) || other.region == region)&&(identical(other.isSuperuser, isSuperuser) || other.isSuperuser == isSuperuser)&&(identical(other.automatedId, automatedId) || other.automatedId == automatedId)&&(identical(other.profile, profile) || other.profile == profile)&&(identical(other.perkSubscription, perkSubscription) || other.perkSubscription == perkSubscription)&&const DeepCollectionEquality().equals(other._badges, _badges)&&(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.hash(runtimeType,id,name,nick,language,isSuperuser,profile,perkSubscription,const DeepCollectionEquality().hash(_badges),createdAt,updatedAt,deletedAt); | ||||
| int get hashCode => Object.hash(runtimeType,id,name,nick,language,region,isSuperuser,automatedId,profile,perkSubscription,const DeepCollectionEquality().hash(_badges),createdAt,updatedAt,deletedAt); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, isSuperuser: $isSuperuser, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
|   return 'SnAccount(id: $id, name: $name, nick: $nick, language: $language, region: $region, isSuperuser: $isSuperuser, automatedId: $automatedId, profile: $profile, perkSubscription: $perkSubscription, badges: $badges, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -288,7 +292,7 @@ abstract mixin class _$SnAccountCopyWith<$Res> implements $SnAccountCopyWith<$Re | ||||
|   factory _$SnAccountCopyWith(_SnAccount value, $Res Function(_SnAccount) _then) = __$SnAccountCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String name, String nick, String language, bool isSuperuser, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
|  String id, String name, String nick, String language, String region, bool isSuperuser, String? automatedId, SnAccountProfile profile, SnWalletSubscriptionRef? perkSubscription, List<SnAccountBadge> badges, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -305,14 +309,16 @@ class __$SnAccountCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnAccount | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? isSuperuser = null,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? nick = null,Object? language = null,Object? region = null,Object? isSuperuser = null,Object? automatedId = freezed,Object? profile = null,Object? perkSubscription = freezed,Object? badges = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||
|   return _then(_SnAccount( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable | ||||
| as String,language: null == language ? _self.language : language // ignore: cast_nullable_to_non_nullable | ||||
| as String,region: null == region ? _self.region : region // ignore: cast_nullable_to_non_nullable | ||||
| as String,isSuperuser: null == isSuperuser ? _self.isSuperuser : isSuperuser // ignore: cast_nullable_to_non_nullable | ||||
| as bool,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable | ||||
| as bool,automatedId: freezed == automatedId ? _self.automatedId : automatedId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,profile: null == profile ? _self.profile : profile // ignore: cast_nullable_to_non_nullable | ||||
| as SnAccountProfile,perkSubscription: freezed == perkSubscription ? _self.perkSubscription : perkSubscription // ignore: cast_nullable_to_non_nullable | ||||
| as SnWalletSubscriptionRef?,badges: null == badges ? _self._badges : badges // ignore: cast_nullable_to_non_nullable | ||||
| as List<SnAccountBadge>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| @@ -610,7 +616,7 @@ as 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 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; 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) | ||||
| @@ -623,16 +629,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.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.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,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,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, 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, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -643,7 +649,7 @@ 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 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, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -660,7 +666,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? 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? 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 | ||||
| @@ -677,6 +683,8 @@ as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : last | ||||
| as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable | ||||
| as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable | ||||
| as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable | ||||
| as int,socialCredits: null == socialCredits ? _self.socialCredits : socialCredits // ignore: cast_nullable_to_non_nullable | ||||
| as double,socialCreditsLevel: null == socialCreditsLevel ? _self.socialCreditsLevel : socialCreditsLevel // ignore: cast_nullable_to_non_nullable | ||||
| as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : levelingProgress // ignore: cast_nullable_to_non_nullable | ||||
| 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 | ||||
| @@ -814,10 +822,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 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,  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.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.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| @@ -835,10 +843,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 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,  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.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.createdAt,_that.updatedAt,_that.deletedAt);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| @@ -852,10 +860,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 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,  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.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.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| @@ -867,7 +875,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, 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, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links; | ||||
|   factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @@ -891,6 +899,8 @@ class _SnAccountProfile implements SnAccountProfile { | ||||
| @override final  SnAccountBadge? activeBadge; | ||||
| @override final  int experience; | ||||
| @override final  int level; | ||||
| @override@JsonKey() final  double socialCredits; | ||||
| @override@JsonKey() final  int socialCreditsLevel; | ||||
| @override final  double levelingProgress; | ||||
| @override final  SnCloudFile? picture; | ||||
| @override final  SnCloudFile? background; | ||||
| @@ -912,16 +922,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.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.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,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,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, 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, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -932,7 +942,7 @@ 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 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, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -949,7 +959,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? 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? 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 | ||||
| @@ -966,6 +976,8 @@ as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : last | ||||
| as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable | ||||
| as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable | ||||
| as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable | ||||
| as int,socialCredits: null == socialCredits ? _self.socialCredits : socialCredits // ignore: cast_nullable_to_non_nullable | ||||
| as double,socialCreditsLevel: null == socialCreditsLevel ? _self.socialCreditsLevel : socialCreditsLevel // ignore: cast_nullable_to_non_nullable | ||||
| as int,levelingProgress: null == levelingProgress ? _self.levelingProgress : levelingProgress // ignore: cast_nullable_to_non_nullable | ||||
| 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 | ||||
|   | ||||
| @@ -11,7 +11,9 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount( | ||||
|   name: json['name'] as String, | ||||
|   nick: json['nick'] as String, | ||||
|   language: json['language'] as String, | ||||
|   region: json['region'] as String? ?? "", | ||||
|   isSuperuser: json['is_superuser'] as bool, | ||||
|   automatedId: json['automated_id'] as String?, | ||||
|   profile: SnAccountProfile.fromJson(json['profile'] as Map<String, dynamic>), | ||||
|   perkSubscription: | ||||
|       json['perk_subscription'] == null | ||||
| @@ -38,7 +40,9 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) => | ||||
|       'name': instance.name, | ||||
|       'nick': instance.nick, | ||||
|       'language': instance.language, | ||||
|       'region': instance.region, | ||||
|       'is_superuser': instance.isSuperuser, | ||||
|       'automated_id': instance.automatedId, | ||||
|       'profile': instance.profile.toJson(), | ||||
|       'perk_subscription': instance.perkSubscription?.toJson(), | ||||
|       'badges': instance.badges.map((e) => e.toJson()).toList(), | ||||
| @@ -84,6 +88,8 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) => | ||||
|               ), | ||||
|       experience: (json['experience'] as num).toInt(), | ||||
|       level: (json['level'] as num).toInt(), | ||||
|       socialCredits: (json['social_credits'] as num?)?.toDouble() ?? 100, | ||||
|       socialCreditsLevel: (json['social_credits_level'] as num?)?.toInt() ?? 0, | ||||
|       levelingProgress: (json['leveling_progress'] as num).toDouble(), | ||||
|       picture: | ||||
|           json['picture'] == null | ||||
| @@ -126,6 +132,8 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) => | ||||
|       'active_badge': instance.activeBadge?.toJson(), | ||||
|       'experience': instance.experience, | ||||
|       'level': instance.level, | ||||
|       'social_credits': instance.socialCredits, | ||||
|       'social_credits_level': instance.socialCreditsLevel, | ||||
|       'leveling_progress': instance.levelingProgress, | ||||
|       'picture': instance.picture?.toJson(), | ||||
|       'background': instance.background?.toJson(), | ||||
|   | ||||
| @@ -4,6 +4,20 @@ import 'package:island/models/account.dart'; | ||||
| part 'activity.freezed.dart'; | ||||
| part 'activity.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| sealed class SnNotableDay with _$SnNotableDay { | ||||
|   const factory SnNotableDay({ | ||||
|     required DateTime date, | ||||
|     required String localName, | ||||
|     required String globalName, | ||||
|     required String countryCode, | ||||
|     required List<int> holidays, | ||||
|   }) = _SnNotableDay; | ||||
|  | ||||
|   factory SnNotableDay.fromJson(Map<String, dynamic> json) => | ||||
|       _$SnNotableDayFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class SnActivity with _$SnActivity { | ||||
|   const factory SnActivity({ | ||||
| @@ -54,7 +68,7 @@ sealed class SnEventCalendarEntry with _$SnEventCalendarEntry { | ||||
|   const factory SnEventCalendarEntry({ | ||||
|     required DateTime date, | ||||
|     required SnCheckInResult? checkInResult, | ||||
|     required List<dynamic> statuses, | ||||
|     required List<SnAccountStatus> statuses, | ||||
|   }) = _SnEventCalendarEntry; | ||||
|  | ||||
|   factory SnEventCalendarEntry.fromJson(Map<String, dynamic> json) => | ||||
|   | ||||
| @@ -12,6 +12,281 @@ part of 'activity.dart'; | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnNotableDay { | ||||
|  | ||||
|  DateTime get date; String get localName; String get globalName; String get countryCode; List<int> get holidays; | ||||
| /// Create a copy of SnNotableDay | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnNotableDayCopyWith<SnNotableDay> get copyWith => _$SnNotableDayCopyWithImpl<SnNotableDay>(this as SnNotableDay, _$identity); | ||||
|  | ||||
|   /// Serializes this SnNotableDay to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnNotableDay&&(identical(other.date, date) || other.date == date)&&(identical(other.localName, localName) || other.localName == localName)&&(identical(other.globalName, globalName) || other.globalName == globalName)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&const DeepCollectionEquality().equals(other.holidays, holidays)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,date,localName,globalName,countryCode,const DeepCollectionEquality().hash(holidays)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnNotableDay(date: $date, localName: $localName, globalName: $globalName, countryCode: $countryCode, holidays: $holidays)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $SnNotableDayCopyWith<$Res>  { | ||||
|   factory $SnNotableDayCopyWith(SnNotableDay value, $Res Function(SnNotableDay) _then) = _$SnNotableDayCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  DateTime date, String localName, String globalName, String countryCode, List<int> holidays | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$SnNotableDayCopyWithImpl<$Res> | ||||
|     implements $SnNotableDayCopyWith<$Res> { | ||||
|   _$SnNotableDayCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final SnNotableDay _self; | ||||
|   final $Res Function(SnNotableDay) _then; | ||||
|  | ||||
| /// Create a copy of SnNotableDay | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? date = null,Object? localName = null,Object? globalName = null,Object? countryCode = null,Object? holidays = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,localName: null == localName ? _self.localName : localName // ignore: cast_nullable_to_non_nullable | ||||
| as String,globalName: null == globalName ? _self.globalName : globalName // ignore: cast_nullable_to_non_nullable | ||||
| as String,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable | ||||
| as String,holidays: null == holidays ? _self.holidays : holidays // ignore: cast_nullable_to_non_nullable | ||||
| as List<int>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Adds pattern-matching-related methods to [SnNotableDay]. | ||||
| extension SnNotableDayPatterns on SnNotableDay { | ||||
| /// 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( _SnNotableDay value)?  $default,{required TResult orElse(),}){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnNotableDay() 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( _SnNotableDay value)  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnNotableDay(): | ||||
| 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( _SnNotableDay value)?  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnNotableDay() 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,  String localName,  String globalName,  String countryCode,  List<int> holidays)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnNotableDay() when $default != null: | ||||
| return $default(_that.date,_that.localName,_that.globalName,_that.countryCode,_that.holidays);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,  String localName,  String globalName,  String countryCode,  List<int> holidays)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnNotableDay(): | ||||
| return $default(_that.date,_that.localName,_that.globalName,_that.countryCode,_that.holidays);} | ||||
| } | ||||
| /// 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,  String localName,  String globalName,  String countryCode,  List<int> holidays)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnNotableDay() when $default != null: | ||||
| return $default(_that.date,_that.localName,_that.globalName,_that.countryCode,_that.holidays);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnNotableDay implements SnNotableDay { | ||||
|   const _SnNotableDay({required this.date, required this.localName, required this.globalName, required this.countryCode, required final  List<int> holidays}): _holidays = holidays; | ||||
|   factory _SnNotableDay.fromJson(Map<String, dynamic> json) => _$SnNotableDayFromJson(json); | ||||
|  | ||||
| @override final  DateTime date; | ||||
| @override final  String localName; | ||||
| @override final  String globalName; | ||||
| @override final  String countryCode; | ||||
|  final  List<int> _holidays; | ||||
| @override List<int> get holidays { | ||||
|   if (_holidays is EqualUnmodifiableListView) return _holidays; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_holidays); | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Create a copy of SnNotableDay | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$SnNotableDayCopyWith<_SnNotableDay> get copyWith => __$SnNotableDayCopyWithImpl<_SnNotableDay>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$SnNotableDayToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnNotableDay&&(identical(other.date, date) || other.date == date)&&(identical(other.localName, localName) || other.localName == localName)&&(identical(other.globalName, globalName) || other.globalName == globalName)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&const DeepCollectionEquality().equals(other._holidays, _holidays)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,date,localName,globalName,countryCode,const DeepCollectionEquality().hash(_holidays)); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnNotableDay(date: $date, localName: $localName, globalName: $globalName, countryCode: $countryCode, holidays: $holidays)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$SnNotableDayCopyWith<$Res> implements $SnNotableDayCopyWith<$Res> { | ||||
|   factory _$SnNotableDayCopyWith(_SnNotableDay value, $Res Function(_SnNotableDay) _then) = __$SnNotableDayCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  DateTime date, String localName, String globalName, String countryCode, List<int> holidays | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$SnNotableDayCopyWithImpl<$Res> | ||||
|     implements _$SnNotableDayCopyWith<$Res> { | ||||
|   __$SnNotableDayCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _SnNotableDay _self; | ||||
|   final $Res Function(_SnNotableDay) _then; | ||||
|  | ||||
| /// Create a copy of SnNotableDay | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? date = null,Object? localName = null,Object? globalName = null,Object? countryCode = null,Object? holidays = null,}) { | ||||
|   return _then(_SnNotableDay( | ||||
| date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,localName: null == localName ? _self.localName : localName // ignore: cast_nullable_to_non_nullable | ||||
| as String,globalName: null == globalName ? _self.globalName : globalName // ignore: cast_nullable_to_non_nullable | ||||
| as String,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable | ||||
| as String,holidays: null == holidays ? _self._holidays : holidays // ignore: cast_nullable_to_non_nullable | ||||
| as List<int>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnActivity { | ||||
|  | ||||
| @@ -861,7 +1136,7 @@ as String, | ||||
| /// @nodoc | ||||
| mixin _$SnEventCalendarEntry { | ||||
|  | ||||
|  DateTime get date; SnCheckInResult? get checkInResult; List<dynamic> get statuses; | ||||
|  DateTime get date; SnCheckInResult? get checkInResult; List<SnAccountStatus> get statuses; | ||||
| /// Create a copy of SnEventCalendarEntry | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -894,7 +1169,7 @@ abstract mixin class $SnEventCalendarEntryCopyWith<$Res>  { | ||||
|   factory $SnEventCalendarEntryCopyWith(SnEventCalendarEntry value, $Res Function(SnEventCalendarEntry) _then) = _$SnEventCalendarEntryCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  DateTime date, SnCheckInResult? checkInResult, List<dynamic> statuses | ||||
|  DateTime date, SnCheckInResult? checkInResult, List<SnAccountStatus> statuses | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -916,7 +1191,7 @@ class _$SnEventCalendarEntryCopyWithImpl<$Res> | ||||
| date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,checkInResult: freezed == checkInResult ? _self.checkInResult : checkInResult // ignore: cast_nullable_to_non_nullable | ||||
| as SnCheckInResult?,statuses: null == statuses ? _self.statuses : statuses // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>, | ||||
| as List<SnAccountStatus>, | ||||
|   )); | ||||
| } | ||||
| /// Create a copy of SnEventCalendarEntry | ||||
| @@ -1010,7 +1285,7 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime date,  SnCheckInResult? checkInResult,  List<dynamic> statuses)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DateTime date,  SnCheckInResult? checkInResult,  List<SnAccountStatus> statuses)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnEventCalendarEntry() when $default != null: | ||||
| return $default(_that.date,_that.checkInResult,_that.statuses);case _: | ||||
| @@ -1031,7 +1306,7 @@ return $default(_that.date,_that.checkInResult,_that.statuses);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime date,  SnCheckInResult? checkInResult,  List<dynamic> statuses)  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DateTime date,  SnCheckInResult? checkInResult,  List<SnAccountStatus> statuses)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnEventCalendarEntry(): | ||||
| return $default(_that.date,_that.checkInResult,_that.statuses);} | ||||
| @@ -1048,7 +1323,7 @@ return $default(_that.date,_that.checkInResult,_that.statuses);} | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime date,  SnCheckInResult? checkInResult,  List<dynamic> statuses)?  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DateTime date,  SnCheckInResult? checkInResult,  List<SnAccountStatus> statuses)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnEventCalendarEntry() when $default != null: | ||||
| return $default(_that.date,_that.checkInResult,_that.statuses);case _: | ||||
| @@ -1063,13 +1338,13 @@ return $default(_that.date,_that.checkInResult,_that.statuses);case _: | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnEventCalendarEntry implements SnEventCalendarEntry { | ||||
|   const _SnEventCalendarEntry({required this.date, required this.checkInResult, required final  List<dynamic> statuses}): _statuses = statuses; | ||||
|   const _SnEventCalendarEntry({required this.date, required this.checkInResult, required final  List<SnAccountStatus> statuses}): _statuses = statuses; | ||||
|   factory _SnEventCalendarEntry.fromJson(Map<String, dynamic> json) => _$SnEventCalendarEntryFromJson(json); | ||||
|  | ||||
| @override final  DateTime date; | ||||
| @override final  SnCheckInResult? checkInResult; | ||||
|  final  List<dynamic> _statuses; | ||||
| @override List<dynamic> get statuses { | ||||
|  final  List<SnAccountStatus> _statuses; | ||||
| @override List<SnAccountStatus> get statuses { | ||||
|   if (_statuses is EqualUnmodifiableListView) return _statuses; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableListView(_statuses); | ||||
| @@ -1109,7 +1384,7 @@ abstract mixin class _$SnEventCalendarEntryCopyWith<$Res> implements $SnEventCal | ||||
|   factory _$SnEventCalendarEntryCopyWith(_SnEventCalendarEntry value, $Res Function(_SnEventCalendarEntry) _then) = __$SnEventCalendarEntryCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  DateTime date, SnCheckInResult? checkInResult, List<dynamic> statuses | ||||
|  DateTime date, SnCheckInResult? checkInResult, List<SnAccountStatus> statuses | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -1131,7 +1406,7 @@ class __$SnEventCalendarEntryCopyWithImpl<$Res> | ||||
| date: null == date ? _self.date : date // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,checkInResult: freezed == checkInResult ? _self.checkInResult : checkInResult // ignore: cast_nullable_to_non_nullable | ||||
| as SnCheckInResult?,statuses: null == statuses ? _self._statuses : statuses // ignore: cast_nullable_to_non_nullable | ||||
| as List<dynamic>, | ||||
| as List<SnAccountStatus>, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,27 @@ part of 'activity.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _SnNotableDay _$SnNotableDayFromJson(Map<String, dynamic> json) => | ||||
|     _SnNotableDay( | ||||
|       date: DateTime.parse(json['date'] as String), | ||||
|       localName: json['local_name'] as String, | ||||
|       globalName: json['global_name'] as String, | ||||
|       countryCode: json['country_code'] as String, | ||||
|       holidays: | ||||
|           (json['holidays'] as List<dynamic>) | ||||
|               .map((e) => (e as num).toInt()) | ||||
|               .toList(), | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$SnNotableDayToJson(_SnNotableDay instance) => | ||||
|     <String, dynamic>{ | ||||
|       'date': instance.date.toIso8601String(), | ||||
|       'local_name': instance.localName, | ||||
|       'global_name': instance.globalName, | ||||
|       'country_code': instance.countryCode, | ||||
|       'holidays': instance.holidays, | ||||
|     }; | ||||
|  | ||||
| _SnActivity _$SnActivityFromJson(Map<String, dynamic> json) => _SnActivity( | ||||
|   id: json['id'] as String, | ||||
|   type: json['type'] as String, | ||||
| @@ -87,7 +108,10 @@ _SnEventCalendarEntry _$SnEventCalendarEntryFromJson( | ||||
|           : SnCheckInResult.fromJson( | ||||
|             json['check_in_result'] as Map<String, dynamic>, | ||||
|           ), | ||||
|   statuses: json['statuses'] as List<dynamic>, | ||||
|   statuses: | ||||
|       (json['statuses'] as List<dynamic>) | ||||
|           .map((e) => SnAccountStatus.fromJson(e as Map<String, dynamic>)) | ||||
|           .toList(), | ||||
| ); | ||||
|  | ||||
| Map<String, dynamic> _$SnEventCalendarEntryToJson( | ||||
| @@ -95,5 +119,5 @@ Map<String, dynamic> _$SnEventCalendarEntryToJson( | ||||
| ) => <String, dynamic>{ | ||||
|   'date': instance.date.toIso8601String(), | ||||
|   'check_in_result': instance.checkInResult?.toJson(), | ||||
|   'statuses': instance.statuses, | ||||
|   'statuses': instance.statuses.map((e) => e.toJson()).toList(), | ||||
| }; | ||||
|   | ||||
| @@ -11,6 +11,20 @@ sealed class AppToken with _$AppToken { | ||||
|       _$AppTokenFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class GeoIpLocation with _$GeoIpLocation { | ||||
|   const factory GeoIpLocation({ | ||||
|     required double latitude, | ||||
|     required double longitude, | ||||
|     required String countryCode, | ||||
|     required String country, | ||||
|     required String city, | ||||
|   }) = _GeoIpLocation; | ||||
|  | ||||
|   factory GeoIpLocation.fromJson(Map<String, dynamic> json) => | ||||
|       _$GeoIpLocationFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class SnAuthChallenge with _$SnAuthChallenge { | ||||
|   const factory SnAuthChallenge({ | ||||
| @@ -26,7 +40,7 @@ sealed class SnAuthChallenge with _$SnAuthChallenge { | ||||
|     required String ipAddress, | ||||
|     required String userAgent, | ||||
|     required String? nonce, | ||||
|     required String? location, | ||||
|     required GeoIpLocation? location, | ||||
|     required String accountId, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|   | ||||
| @@ -269,10 +269,279 @@ as String, | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$GeoIpLocation { | ||||
|  | ||||
|  double get latitude; double get longitude; String get countryCode; String get country; String get city; | ||||
| /// Create a copy of GeoIpLocation | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $GeoIpLocationCopyWith<GeoIpLocation> get copyWith => _$GeoIpLocationCopyWithImpl<GeoIpLocation>(this as GeoIpLocation, _$identity); | ||||
|  | ||||
|   /// Serializes this GeoIpLocation to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is GeoIpLocation&&(identical(other.latitude, latitude) || other.latitude == latitude)&&(identical(other.longitude, longitude) || other.longitude == longitude)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&(identical(other.country, country) || other.country == country)&&(identical(other.city, city) || other.city == city)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,latitude,longitude,countryCode,country,city); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'GeoIpLocation(latitude: $latitude, longitude: $longitude, countryCode: $countryCode, country: $country, city: $city)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $GeoIpLocationCopyWith<$Res>  { | ||||
|   factory $GeoIpLocationCopyWith(GeoIpLocation value, $Res Function(GeoIpLocation) _then) = _$GeoIpLocationCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  double latitude, double longitude, String countryCode, String country, String city | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$GeoIpLocationCopyWithImpl<$Res> | ||||
|     implements $GeoIpLocationCopyWith<$Res> { | ||||
|   _$GeoIpLocationCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final GeoIpLocation _self; | ||||
|   final $Res Function(GeoIpLocation) _then; | ||||
|  | ||||
| /// Create a copy of GeoIpLocation | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable | ||||
| as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable | ||||
| as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable | ||||
| as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable | ||||
| as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable | ||||
| as String, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Adds pattern-matching-related methods to [GeoIpLocation]. | ||||
| extension GeoIpLocationPatterns on GeoIpLocation { | ||||
| /// 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( _GeoIpLocation value)?  $default,{required TResult orElse(),}){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _GeoIpLocation() 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( _GeoIpLocation value)  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _GeoIpLocation(): | ||||
| 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( _GeoIpLocation value)?  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _GeoIpLocation() 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( double latitude,  double longitude,  String countryCode,  String country,  String city)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _GeoIpLocation() when $default != null: | ||||
| return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);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( double latitude,  double longitude,  String countryCode,  String country,  String city)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _GeoIpLocation(): | ||||
| return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);} | ||||
| } | ||||
| /// 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( double latitude,  double longitude,  String countryCode,  String country,  String city)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _GeoIpLocation() when $default != null: | ||||
| return $default(_that.latitude,_that.longitude,_that.countryCode,_that.country,_that.city);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _GeoIpLocation implements GeoIpLocation { | ||||
|   const _GeoIpLocation({required this.latitude, required this.longitude, required this.countryCode, required this.country, required this.city}); | ||||
|   factory _GeoIpLocation.fromJson(Map<String, dynamic> json) => _$GeoIpLocationFromJson(json); | ||||
|  | ||||
| @override final  double latitude; | ||||
| @override final  double longitude; | ||||
| @override final  String countryCode; | ||||
| @override final  String country; | ||||
| @override final  String city; | ||||
|  | ||||
| /// Create a copy of GeoIpLocation | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$GeoIpLocationCopyWith<_GeoIpLocation> get copyWith => __$GeoIpLocationCopyWithImpl<_GeoIpLocation>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$GeoIpLocationToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _GeoIpLocation&&(identical(other.latitude, latitude) || other.latitude == latitude)&&(identical(other.longitude, longitude) || other.longitude == longitude)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&(identical(other.country, country) || other.country == country)&&(identical(other.city, city) || other.city == city)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,latitude,longitude,countryCode,country,city); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'GeoIpLocation(latitude: $latitude, longitude: $longitude, countryCode: $countryCode, country: $country, city: $city)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$GeoIpLocationCopyWith<$Res> implements $GeoIpLocationCopyWith<$Res> { | ||||
|   factory _$GeoIpLocationCopyWith(_GeoIpLocation value, $Res Function(_GeoIpLocation) _then) = __$GeoIpLocationCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  double latitude, double longitude, String countryCode, String country, String city | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$GeoIpLocationCopyWithImpl<$Res> | ||||
|     implements _$GeoIpLocationCopyWith<$Res> { | ||||
|   __$GeoIpLocationCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _GeoIpLocation _self; | ||||
|   final $Res Function(_GeoIpLocation) _then; | ||||
|  | ||||
| /// Create a copy of GeoIpLocation | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? latitude = null,Object? longitude = null,Object? countryCode = null,Object? country = null,Object? city = null,}) { | ||||
|   return _then(_GeoIpLocation( | ||||
| latitude: null == latitude ? _self.latitude : latitude // ignore: cast_nullable_to_non_nullable | ||||
| as double,longitude: null == longitude ? _self.longitude : longitude // ignore: cast_nullable_to_non_nullable | ||||
| as double,countryCode: null == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable | ||||
| as String,country: null == country ? _self.country : country // ignore: cast_nullable_to_non_nullable | ||||
| as String,city: null == city ? _self.city : city // ignore: cast_nullable_to_non_nullable | ||||
| as String, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnAuthChallenge { | ||||
|  | ||||
|  String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; String? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
|  String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String? get nonce; GeoIpLocation? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
| /// Create a copy of SnAuthChallenge | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -305,11 +574,11 @@ abstract mixin class $SnAuthChallengeCopyWith<$Res>  { | ||||
|   factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
|  String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
| $GeoIpLocationCopyWith<$Res>? get location; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| @@ -337,14 +606,26 @@ as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // i | ||||
| as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable | ||||
| as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable | ||||
| as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable | ||||
| as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||
| as GeoIpLocation?,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?, | ||||
|   )); | ||||
| } | ||||
| /// Create a copy of SnAuthChallenge | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $GeoIpLocationCopyWith<$Res>? get location { | ||||
|     if (_self.location == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $GeoIpLocationCopyWith<$Res>(_self.location!, (value) { | ||||
|     return _then(_self.copyWith(location: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -423,7 +704,7 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  String? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAuthChallenge() when $default != null: | ||||
| return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
| @@ -444,7 +725,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that. | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  String? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAuthChallenge(): | ||||
| return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||
| @@ -461,7 +742,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that. | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  String? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String? nonce,  GeoIpLocation? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAuthChallenge() when $default != null: | ||||
| return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
| @@ -509,7 +790,7 @@ class _SnAuthChallenge implements SnAuthChallenge { | ||||
| @override final  String ipAddress; | ||||
| @override final  String userAgent; | ||||
| @override final  String? nonce; | ||||
| @override final  String? location; | ||||
| @override final  GeoIpLocation? location; | ||||
| @override final  String accountId; | ||||
| @override final  DateTime createdAt; | ||||
| @override final  DateTime updatedAt; | ||||
| @@ -548,11 +829,11 @@ abstract mixin class _$SnAuthChallengeCopyWith<$Res> implements $SnAuthChallenge | ||||
|   factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
|  String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String? nonce, GeoIpLocation? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
| @override $GeoIpLocationCopyWith<$Res>? get location; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| @@ -580,7 +861,7 @@ as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // i | ||||
| as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable | ||||
| as String,nonce: freezed == nonce ? _self.nonce : nonce // ignore: cast_nullable_to_non_nullable | ||||
| as String?,location: freezed == location ? _self.location : location // ignore: cast_nullable_to_non_nullable | ||||
| as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||
| as GeoIpLocation?,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,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable | ||||
| @@ -588,7 +869,19 @@ as DateTime?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| /// Create a copy of SnAuthChallenge | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $GeoIpLocationCopyWith<$Res>? get location { | ||||
|     if (_self.location == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $GeoIpLocationCopyWith<$Res>(_self.location!, (value) { | ||||
|     return _then(_self.copyWith(location: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -13,6 +13,24 @@ Map<String, dynamic> _$AppTokenToJson(_AppToken instance) => <String, dynamic>{ | ||||
|   'token': instance.token, | ||||
| }; | ||||
|  | ||||
| _GeoIpLocation _$GeoIpLocationFromJson(Map<String, dynamic> json) => | ||||
|     _GeoIpLocation( | ||||
|       latitude: (json['latitude'] as num).toDouble(), | ||||
|       longitude: (json['longitude'] as num).toDouble(), | ||||
|       countryCode: json['country_code'] as String, | ||||
|       country: json['country'] as String, | ||||
|       city: json['city'] as String, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$GeoIpLocationToJson(_GeoIpLocation instance) => | ||||
|     <String, dynamic>{ | ||||
|       'latitude': instance.latitude, | ||||
|       'longitude': instance.longitude, | ||||
|       'country_code': instance.countryCode, | ||||
|       'country': instance.country, | ||||
|       'city': instance.city, | ||||
|     }; | ||||
|  | ||||
| _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) => | ||||
|     _SnAuthChallenge( | ||||
|       id: json['id'] as String, | ||||
| @@ -30,7 +48,12 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) => | ||||
|       ipAddress: json['ip_address'] as String, | ||||
|       userAgent: json['user_agent'] as String, | ||||
|       nonce: json['nonce'] as String?, | ||||
|       location: json['location'] as String?, | ||||
|       location: | ||||
|           json['location'] == null | ||||
|               ? null | ||||
|               : GeoIpLocation.fromJson( | ||||
|                 json['location'] as Map<String, dynamic>, | ||||
|               ), | ||||
|       accountId: json['account_id'] as String, | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
| @@ -54,7 +77,7 @@ Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) => | ||||
|       'ip_address': instance.ipAddress, | ||||
|       'user_agent': instance.userAgent, | ||||
|       'nonce': instance.nonce, | ||||
|       'location': instance.location, | ||||
|       'location': instance.location?.toJson(), | ||||
|       'account_id': instance.accountId, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:island/models/file.dart'; | ||||
| import 'package:island/models/account.dart'; | ||||
| import 'package:island/models/developer.dart'; | ||||
|  | ||||
| part 'bot.freezed.dart'; | ||||
| part 'bot.g.dart'; | ||||
| @@ -8,20 +8,14 @@ part 'bot.g.dart'; | ||||
| @freezed | ||||
| sealed class Bot with _$Bot { | ||||
|   const factory Bot({ | ||||
|     @Default('') String id, | ||||
|     @Default('') String name, | ||||
|     @Default('') String slug, | ||||
|     String? description, | ||||
|     @Default(0) int status, | ||||
|     SnCloudFile? picture, | ||||
|     SnCloudFile? background, | ||||
|     SnVerificationMark? verification, | ||||
|     BotConfig? config, | ||||
|     BotLinks? links, | ||||
|     @Default('') String publisherId, | ||||
|     @Default('') String appId, | ||||
|     DateTime? createdAt, | ||||
|     DateTime? updatedAt, | ||||
|     required String id, | ||||
|     required String slug, | ||||
|     required bool isActive, | ||||
|     required String projectId, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     required SnAccount account, | ||||
|     SnDeveloper? developer, | ||||
|   }) = _Bot; | ||||
|  | ||||
|   factory Bot.fromJson(Map<String, dynamic> json) => _$BotFromJson(json); | ||||
|   | ||||
| @@ -15,7 +15,7 @@ T _$identity<T>(T value) => value; | ||||
| /// @nodoc | ||||
| mixin _$Bot { | ||||
|  | ||||
|  String get id; String get name; String get slug; String? get description; int get status; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; BotConfig? get config; BotLinks? get links; String get publisherId; String get appId; DateTime? get createdAt; DateTime? get updatedAt; | ||||
|  String get id; String get slug; bool get isActive; String get projectId; DateTime get createdAt; DateTime get updatedAt; SnAccount get account; SnDeveloper? get developer; | ||||
| /// Create a copy of Bot | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -28,16 +28,16 @@ $BotCopyWith<Bot> get copyWith => _$BotCopyWithImpl<Bot>(this as Bot, _$identity | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is Bot&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.description, description) || other.description == description)&&(identical(other.status, status) || other.status == status)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.config, config) || other.config == config)&&(identical(other.links, links) || other.links == links)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.appId, appId) || other.appId == appId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is Bot&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.isActive, isActive) || other.isActive == isActive)&&(identical(other.projectId, projectId) || other.projectId == projectId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.account, account) || other.account == account)&&(identical(other.developer, developer) || other.developer == developer)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,name,slug,description,status,picture,background,verification,config,links,publisherId,appId,createdAt,updatedAt); | ||||
| int get hashCode => Object.hash(runtimeType,id,slug,isActive,projectId,createdAt,updatedAt,account,developer); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'Bot(id: $id, name: $name, slug: $slug, description: $description, status: $status, picture: $picture, background: $background, verification: $verification, config: $config, links: $links, publisherId: $publisherId, appId: $appId, createdAt: $createdAt, updatedAt: $updatedAt)'; | ||||
|   return 'Bot(id: $id, slug: $slug, isActive: $isActive, projectId: $projectId, createdAt: $createdAt, updatedAt: $updatedAt, account: $account, developer: $developer)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -48,11 +48,11 @@ abstract mixin class $BotCopyWith<$Res>  { | ||||
|   factory $BotCopyWith(Bot value, $Res Function(Bot) _then) = _$BotCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String name, String slug, String? description, int status, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, BotConfig? config, BotLinks? links, String publisherId, String appId, DateTime? createdAt, DateTime? updatedAt | ||||
|  String id, String slug, bool isActive, String projectId, DateTime createdAt, DateTime updatedAt, SnAccount account, SnDeveloper? developer | ||||
| }); | ||||
|  | ||||
|  | ||||
| $SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification;$BotConfigCopyWith<$Res>? get config;$BotLinksCopyWith<$Res>? get links; | ||||
| $SnAccountCopyWith<$Res> get account;$SnDeveloperCopyWith<$Res>? get developer; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| @@ -65,84 +65,39 @@ class _$BotCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of Bot | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? slug = null,Object? description = freezed,Object? status = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? config = freezed,Object? links = freezed,Object? publisherId = null,Object? appId = null,Object? createdAt = freezed,Object? updatedAt = freezed,}) { | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? isActive = null,Object? projectId = null,Object? createdAt = null,Object? updatedAt = null,Object? account = null,Object? developer = freezed,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||
| as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||
| as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable | ||||
| as int,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?,config: freezed == config ? _self.config : config // ignore: cast_nullable_to_non_nullable | ||||
| as BotConfig?,links: freezed == links ? _self.links : links // ignore: cast_nullable_to_non_nullable | ||||
| as BotLinks?,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable | ||||
| as String,appId: null == appId ? _self.appId : appId // ignore: cast_nullable_to_non_nullable | ||||
| as String,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?, | ||||
| as String,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable | ||||
| as bool,projectId: null == projectId ? _self.projectId : projectId // 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: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable | ||||
| as SnAccount,developer: freezed == developer ? _self.developer : developer // ignore: cast_nullable_to_non_nullable | ||||
| as SnDeveloper?, | ||||
|   )); | ||||
| } | ||||
| /// Create a copy of Bot | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnCloudFileCopyWith<$Res>? get picture { | ||||
|     if (_self.picture == null) { | ||||
|     return null; | ||||
|   } | ||||
| $SnAccountCopyWith<$Res> get account { | ||||
|    | ||||
|   return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) { | ||||
|     return _then(_self.copyWith(picture: value)); | ||||
|   return $SnAccountCopyWith<$Res>(_self.account, (value) { | ||||
|     return _then(_self.copyWith(account: value)); | ||||
|   }); | ||||
| }/// Create a copy of Bot | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnCloudFileCopyWith<$Res>? get background { | ||||
|     if (_self.background == null) { | ||||
| $SnDeveloperCopyWith<$Res>? get developer { | ||||
|     if (_self.developer == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnCloudFileCopyWith<$Res>(_self.background!, (value) { | ||||
|     return _then(_self.copyWith(background: value)); | ||||
|   }); | ||||
| }/// Create a copy of Bot | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnVerificationMarkCopyWith<$Res>? get verification { | ||||
|     if (_self.verification == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) { | ||||
|     return _then(_self.copyWith(verification: value)); | ||||
|   }); | ||||
| }/// Create a copy of Bot | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $BotConfigCopyWith<$Res>? get config { | ||||
|     if (_self.config == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $BotConfigCopyWith<$Res>(_self.config!, (value) { | ||||
|     return _then(_self.copyWith(config: value)); | ||||
|   }); | ||||
| }/// Create a copy of Bot | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $BotLinksCopyWith<$Res>? get links { | ||||
|     if (_self.links == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $BotLinksCopyWith<$Res>(_self.links!, (value) { | ||||
|     return _then(_self.copyWith(links: value)); | ||||
|   return $SnDeveloperCopyWith<$Res>(_self.developer!, (value) { | ||||
|     return _then(_self.copyWith(developer: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
| @@ -223,10 +178,10 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String name,  String slug,  String? description,  int status,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  BotConfig? config,  BotLinks? links,  String publisherId,  String appId,  DateTime? createdAt,  DateTime? updatedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String slug,  bool isActive,  String projectId,  DateTime createdAt,  DateTime updatedAt,  SnAccount account,  SnDeveloper? developer)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _Bot() when $default != null: | ||||
| return $default(_that.id,_that.name,_that.slug,_that.description,_that.status,_that.picture,_that.background,_that.verification,_that.config,_that.links,_that.publisherId,_that.appId,_that.createdAt,_that.updatedAt);case _: | ||||
| return $default(_that.id,_that.slug,_that.isActive,_that.projectId,_that.createdAt,_that.updatedAt,_that.account,_that.developer);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| @@ -244,10 +199,10 @@ return $default(_that.id,_that.name,_that.slug,_that.description,_that.status,_t | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String name,  String slug,  String? description,  int status,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  BotConfig? config,  BotLinks? links,  String publisherId,  String appId,  DateTime? createdAt,  DateTime? updatedAt)  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String slug,  bool isActive,  String projectId,  DateTime createdAt,  DateTime updatedAt,  SnAccount account,  SnDeveloper? developer)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _Bot(): | ||||
| return $default(_that.id,_that.name,_that.slug,_that.description,_that.status,_that.picture,_that.background,_that.verification,_that.config,_that.links,_that.publisherId,_that.appId,_that.createdAt,_that.updatedAt);} | ||||
| return $default(_that.id,_that.slug,_that.isActive,_that.projectId,_that.createdAt,_that.updatedAt,_that.account,_that.developer);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| @@ -261,10 +216,10 @@ return $default(_that.id,_that.name,_that.slug,_that.description,_that.status,_t | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String name,  String slug,  String? description,  int status,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  BotConfig? config,  BotLinks? links,  String publisherId,  String appId,  DateTime? createdAt,  DateTime? updatedAt)?  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String slug,  bool isActive,  String projectId,  DateTime createdAt,  DateTime updatedAt,  SnAccount account,  SnDeveloper? developer)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _Bot() when $default != null: | ||||
| return $default(_that.id,_that.name,_that.slug,_that.description,_that.status,_that.picture,_that.background,_that.verification,_that.config,_that.links,_that.publisherId,_that.appId,_that.createdAt,_that.updatedAt);case _: | ||||
| return $default(_that.id,_that.slug,_that.isActive,_that.projectId,_that.createdAt,_that.updatedAt,_that.account,_that.developer);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| @@ -276,23 +231,17 @@ return $default(_that.id,_that.name,_that.slug,_that.description,_that.status,_t | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _Bot implements Bot { | ||||
|   const _Bot({this.id = '', this.name = '', this.slug = '', this.description, this.status = 0, this.picture, this.background, this.verification, this.config, this.links, this.publisherId = '', this.appId = '', this.createdAt, this.updatedAt}); | ||||
|   const _Bot({required this.id, required this.slug, required this.isActive, required this.projectId, required this.createdAt, required this.updatedAt, required this.account, this.developer}); | ||||
|   factory _Bot.fromJson(Map<String, dynamic> json) => _$BotFromJson(json); | ||||
|  | ||||
| @override@JsonKey() final  String id; | ||||
| @override@JsonKey() final  String name; | ||||
| @override@JsonKey() final  String slug; | ||||
| @override final  String? description; | ||||
| @override@JsonKey() final  int status; | ||||
| @override final  SnCloudFile? picture; | ||||
| @override final  SnCloudFile? background; | ||||
| @override final  SnVerificationMark? verification; | ||||
| @override final  BotConfig? config; | ||||
| @override final  BotLinks? links; | ||||
| @override@JsonKey() final  String publisherId; | ||||
| @override@JsonKey() final  String appId; | ||||
| @override final  DateTime? createdAt; | ||||
| @override final  DateTime? updatedAt; | ||||
| @override final  String id; | ||||
| @override final  String slug; | ||||
| @override final  bool isActive; | ||||
| @override final  String projectId; | ||||
| @override final  DateTime createdAt; | ||||
| @override final  DateTime updatedAt; | ||||
| @override final  SnAccount account; | ||||
| @override final  SnDeveloper? developer; | ||||
|  | ||||
| /// Create a copy of Bot | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @@ -307,16 +256,16 @@ Map<String, dynamic> toJson() { | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _Bot&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.description, description) || other.description == description)&&(identical(other.status, status) || other.status == status)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.config, config) || other.config == config)&&(identical(other.links, links) || other.links == links)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.appId, appId) || other.appId == appId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _Bot&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.isActive, isActive) || other.isActive == isActive)&&(identical(other.projectId, projectId) || other.projectId == projectId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.account, account) || other.account == account)&&(identical(other.developer, developer) || other.developer == developer)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,name,slug,description,status,picture,background,verification,config,links,publisherId,appId,createdAt,updatedAt); | ||||
| int get hashCode => Object.hash(runtimeType,id,slug,isActive,projectId,createdAt,updatedAt,account,developer); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'Bot(id: $id, name: $name, slug: $slug, description: $description, status: $status, picture: $picture, background: $background, verification: $verification, config: $config, links: $links, publisherId: $publisherId, appId: $appId, createdAt: $createdAt, updatedAt: $updatedAt)'; | ||||
|   return 'Bot(id: $id, slug: $slug, isActive: $isActive, projectId: $projectId, createdAt: $createdAt, updatedAt: $updatedAt, account: $account, developer: $developer)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -327,11 +276,11 @@ abstract mixin class _$BotCopyWith<$Res> implements $BotCopyWith<$Res> { | ||||
|   factory _$BotCopyWith(_Bot value, $Res Function(_Bot) _then) = __$BotCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String name, String slug, String? description, int status, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, BotConfig? config, BotLinks? links, String publisherId, String appId, DateTime? createdAt, DateTime? updatedAt | ||||
|  String id, String slug, bool isActive, String projectId, DateTime createdAt, DateTime updatedAt, SnAccount account, SnDeveloper? developer | ||||
| }); | ||||
|  | ||||
|  | ||||
| @override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification;@override $BotConfigCopyWith<$Res>? get config;@override $BotLinksCopyWith<$Res>? get links; | ||||
| @override $SnAccountCopyWith<$Res> get account;@override $SnDeveloperCopyWith<$Res>? get developer; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| @@ -344,23 +293,17 @@ class __$BotCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of Bot | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? slug = null,Object? description = freezed,Object? status = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? config = freezed,Object? links = freezed,Object? publisherId = null,Object? appId = null,Object? createdAt = freezed,Object? updatedAt = freezed,}) { | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? isActive = null,Object? projectId = null,Object? createdAt = null,Object? updatedAt = null,Object? account = null,Object? developer = freezed,}) { | ||||
|   return _then(_Bot( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||
| as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||
| as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable | ||||
| as int,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?,config: freezed == config ? _self.config : config // ignore: cast_nullable_to_non_nullable | ||||
| as BotConfig?,links: freezed == links ? _self.links : links // ignore: cast_nullable_to_non_nullable | ||||
| as BotLinks?,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable | ||||
| as String,appId: null == appId ? _self.appId : appId // ignore: cast_nullable_to_non_nullable | ||||
| as String,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?, | ||||
| as String,isActive: null == isActive ? _self.isActive : isActive // ignore: cast_nullable_to_non_nullable | ||||
| as bool,projectId: null == projectId ? _self.projectId : projectId // 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: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable | ||||
| as SnAccount,developer: freezed == developer ? _self.developer : developer // ignore: cast_nullable_to_non_nullable | ||||
| as SnDeveloper?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| @@ -368,61 +311,22 @@ as DateTime?, | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnCloudFileCopyWith<$Res>? get picture { | ||||
|     if (_self.picture == null) { | ||||
|     return null; | ||||
|   } | ||||
| $SnAccountCopyWith<$Res> get account { | ||||
|    | ||||
|   return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) { | ||||
|     return _then(_self.copyWith(picture: value)); | ||||
|   return $SnAccountCopyWith<$Res>(_self.account, (value) { | ||||
|     return _then(_self.copyWith(account: value)); | ||||
|   }); | ||||
| }/// Create a copy of Bot | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnCloudFileCopyWith<$Res>? get background { | ||||
|     if (_self.background == null) { | ||||
| $SnDeveloperCopyWith<$Res>? get developer { | ||||
|     if (_self.developer == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnCloudFileCopyWith<$Res>(_self.background!, (value) { | ||||
|     return _then(_self.copyWith(background: value)); | ||||
|   }); | ||||
| }/// Create a copy of Bot | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnVerificationMarkCopyWith<$Res>? get verification { | ||||
|     if (_self.verification == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) { | ||||
|     return _then(_self.copyWith(verification: value)); | ||||
|   }); | ||||
| }/// Create a copy of Bot | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $BotConfigCopyWith<$Res>? get config { | ||||
|     if (_self.config == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $BotConfigCopyWith<$Res>(_self.config!, (value) { | ||||
|     return _then(_self.copyWith(config: value)); | ||||
|   }); | ||||
| }/// Create a copy of Bot | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $BotLinksCopyWith<$Res>? get links { | ||||
|     if (_self.links == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $BotLinksCopyWith<$Res>(_self.links!, (value) { | ||||
|     return _then(_self.copyWith(links: value)); | ||||
|   return $SnDeveloperCopyWith<$Res>(_self.developer!, (value) { | ||||
|     return _then(_self.copyWith(developer: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -7,60 +7,28 @@ part of 'bot.dart'; | ||||
| // ************************************************************************** | ||||
|  | ||||
| _Bot _$BotFromJson(Map<String, dynamic> json) => _Bot( | ||||
|   id: json['id'] as String? ?? '', | ||||
|   name: json['name'] as String? ?? '', | ||||
|   slug: json['slug'] as String? ?? '', | ||||
|   description: json['description'] as String?, | ||||
|   status: (json['status'] as num?)?.toInt() ?? 0, | ||||
|   picture: | ||||
|       json['picture'] == null | ||||
|   id: json['id'] as String, | ||||
|   slug: json['slug'] as String, | ||||
|   isActive: json['is_active'] as bool, | ||||
|   projectId: json['project_id'] as String, | ||||
|   createdAt: DateTime.parse(json['created_at'] as String), | ||||
|   updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
|   account: SnAccount.fromJson(json['account'] as Map<String, dynamic>), | ||||
|   developer: | ||||
|       json['developer'] == null | ||||
|           ? null | ||||
|           : SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>), | ||||
|   background: | ||||
|       json['background'] == null | ||||
|           ? null | ||||
|           : SnCloudFile.fromJson(json['background'] as Map<String, dynamic>), | ||||
|   verification: | ||||
|       json['verification'] == null | ||||
|           ? null | ||||
|           : SnVerificationMark.fromJson( | ||||
|             json['verification'] as Map<String, dynamic>, | ||||
|           ), | ||||
|   config: | ||||
|       json['config'] == null | ||||
|           ? null | ||||
|           : BotConfig.fromJson(json['config'] as Map<String, dynamic>), | ||||
|   links: | ||||
|       json['links'] == null | ||||
|           ? null | ||||
|           : BotLinks.fromJson(json['links'] as Map<String, dynamic>), | ||||
|   publisherId: json['publisher_id'] as String? ?? '', | ||||
|   appId: json['app_id'] as String? ?? '', | ||||
|   createdAt: | ||||
|       json['created_at'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['created_at'] as String), | ||||
|   updatedAt: | ||||
|       json['updated_at'] == null | ||||
|           ? null | ||||
|           : DateTime.parse(json['updated_at'] as String), | ||||
|           : SnDeveloper.fromJson(json['developer'] as Map<String, dynamic>), | ||||
| ); | ||||
|  | ||||
| Map<String, dynamic> _$BotToJson(_Bot instance) => <String, dynamic>{ | ||||
|   'id': instance.id, | ||||
|   'name': instance.name, | ||||
|   'slug': instance.slug, | ||||
|   'description': instance.description, | ||||
|   'status': instance.status, | ||||
|   'picture': instance.picture?.toJson(), | ||||
|   'background': instance.background?.toJson(), | ||||
|   'verification': instance.verification?.toJson(), | ||||
|   'config': instance.config?.toJson(), | ||||
|   'links': instance.links?.toJson(), | ||||
|   'publisher_id': instance.publisherId, | ||||
|   'app_id': instance.appId, | ||||
|   'created_at': instance.createdAt?.toIso8601String(), | ||||
|   'updated_at': instance.updatedAt?.toIso8601String(), | ||||
|   'is_active': instance.isActive, | ||||
|   'project_id': instance.projectId, | ||||
|   'created_at': instance.createdAt.toIso8601String(), | ||||
|   'updated_at': instance.updatedAt.toIso8601String(), | ||||
|   'account': instance.account.toJson(), | ||||
|   'developer': instance.developer?.toJson(), | ||||
| }; | ||||
|  | ||||
| _BotConfig _$BotConfigFromJson(Map<String, dynamic> json) => _BotConfig( | ||||
|   | ||||
							
								
								
									
										20
									
								
								lib/models/bot_key.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								lib/models/bot_key.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
|  | ||||
| part 'bot_key.freezed.dart'; | ||||
| part 'bot_key.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| sealed class SnAccountApiKey with _$SnAccountApiKey { | ||||
|   const factory SnAccountApiKey({ | ||||
|     required String id, | ||||
|     required String label, | ||||
|     required String accountId, | ||||
|     required String sessionId, | ||||
|     required DateTime createdAt, | ||||
|     required DateTime updatedAt, | ||||
|     String? key, | ||||
|   }) = _SnAccountApiKey; | ||||
|  | ||||
|   factory SnAccountApiKey.fromJson(Map<String, dynamic> json) => | ||||
|       _$SnAccountApiKeyFromJson(json); | ||||
| } | ||||
							
								
								
									
										289
									
								
								lib/models/bot_key.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										289
									
								
								lib/models/bot_key.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,289 @@ | ||||
| // 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 'bot_key.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnAccountApiKey { | ||||
|  | ||||
|  String get id; String get label; String get accountId; String get sessionId; DateTime get createdAt; DateTime get updatedAt; String? get key; | ||||
| /// Create a copy of SnAccountApiKey | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnAccountApiKeyCopyWith<SnAccountApiKey> get copyWith => _$SnAccountApiKeyCopyWithImpl<SnAccountApiKey>(this as SnAccountApiKey, _$identity); | ||||
|  | ||||
|   /// Serializes this SnAccountApiKey to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAccountApiKey&&(identical(other.id, id) || other.id == id)&&(identical(other.label, label) || other.label == label)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.sessionId, sessionId) || other.sessionId == sessionId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.key, key) || other.key == key)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,label,accountId,sessionId,createdAt,updatedAt,key); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnAccountApiKey(id: $id, label: $label, accountId: $accountId, sessionId: $sessionId, createdAt: $createdAt, updatedAt: $updatedAt, key: $key)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $SnAccountApiKeyCopyWith<$Res>  { | ||||
|   factory $SnAccountApiKeyCopyWith(SnAccountApiKey value, $Res Function(SnAccountApiKey) _then) = _$SnAccountApiKeyCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String label, String accountId, String sessionId, DateTime createdAt, DateTime updatedAt, String? key | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$SnAccountApiKeyCopyWithImpl<$Res> | ||||
|     implements $SnAccountApiKeyCopyWith<$Res> { | ||||
|   _$SnAccountApiKeyCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final SnAccountApiKey _self; | ||||
|   final $Res Function(SnAccountApiKey) _then; | ||||
|  | ||||
| /// Create a copy of SnAccountApiKey | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? label = null,Object? accountId = null,Object? sessionId = null,Object? createdAt = null,Object? updatedAt = null,Object? key = freezed,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | ||||
| as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||
| as String,sessionId: null == sessionId ? _self.sessionId : sessionId // 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,key: freezed == key ? _self.key : key // ignore: cast_nullable_to_non_nullable | ||||
| as String?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Adds pattern-matching-related methods to [SnAccountApiKey]. | ||||
| extension SnAccountApiKeyPatterns on SnAccountApiKey { | ||||
| /// 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( _SnAccountApiKey value)?  $default,{required TResult orElse(),}){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccountApiKey() 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( _SnAccountApiKey value)  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccountApiKey(): | ||||
| 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( _SnAccountApiKey value)?  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccountApiKey() 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 label,  String accountId,  String sessionId,  DateTime createdAt,  DateTime updatedAt,  String? key)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccountApiKey() when $default != null: | ||||
| return $default(_that.id,_that.label,_that.accountId,_that.sessionId,_that.createdAt,_that.updatedAt,_that.key);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 label,  String accountId,  String sessionId,  DateTime createdAt,  DateTime updatedAt,  String? key)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccountApiKey(): | ||||
| return $default(_that.id,_that.label,_that.accountId,_that.sessionId,_that.createdAt,_that.updatedAt,_that.key);} | ||||
| } | ||||
| /// 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 label,  String accountId,  String sessionId,  DateTime createdAt,  DateTime updatedAt,  String? key)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccountApiKey() when $default != null: | ||||
| return $default(_that.id,_that.label,_that.accountId,_that.sessionId,_that.createdAt,_that.updatedAt,_that.key);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnAccountApiKey implements SnAccountApiKey { | ||||
|   const _SnAccountApiKey({required this.id, required this.label, required this.accountId, required this.sessionId, required this.createdAt, required this.updatedAt, this.key}); | ||||
|   factory _SnAccountApiKey.fromJson(Map<String, dynamic> json) => _$SnAccountApiKeyFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String label; | ||||
| @override final  String accountId; | ||||
| @override final  String sessionId; | ||||
| @override final  DateTime createdAt; | ||||
| @override final  DateTime updatedAt; | ||||
| @override final  String? key; | ||||
|  | ||||
| /// Create a copy of SnAccountApiKey | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$SnAccountApiKeyCopyWith<_SnAccountApiKey> get copyWith => __$SnAccountApiKeyCopyWithImpl<_SnAccountApiKey>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$SnAccountApiKeyToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAccountApiKey&&(identical(other.id, id) || other.id == id)&&(identical(other.label, label) || other.label == label)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.sessionId, sessionId) || other.sessionId == sessionId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.key, key) || other.key == key)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,label,accountId,sessionId,createdAt,updatedAt,key); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnAccountApiKey(id: $id, label: $label, accountId: $accountId, sessionId: $sessionId, createdAt: $createdAt, updatedAt: $updatedAt, key: $key)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$SnAccountApiKeyCopyWith<$Res> implements $SnAccountApiKeyCopyWith<$Res> { | ||||
|   factory _$SnAccountApiKeyCopyWith(_SnAccountApiKey value, $Res Function(_SnAccountApiKey) _then) = __$SnAccountApiKeyCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String label, String accountId, String sessionId, DateTime createdAt, DateTime updatedAt, String? key | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$SnAccountApiKeyCopyWithImpl<$Res> | ||||
|     implements _$SnAccountApiKeyCopyWith<$Res> { | ||||
|   __$SnAccountApiKeyCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _SnAccountApiKey _self; | ||||
|   final $Res Function(_SnAccountApiKey) _then; | ||||
|  | ||||
| /// Create a copy of SnAccountApiKey | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? label = null,Object? accountId = null,Object? sessionId = null,Object? createdAt = null,Object? updatedAt = null,Object? key = freezed,}) { | ||||
|   return _then(_SnAccountApiKey( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,label: null == label ? _self.label : label // ignore: cast_nullable_to_non_nullable | ||||
| as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||
| as String,sessionId: null == sessionId ? _self.sessionId : sessionId // 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,key: freezed == key ? _self.key : key // ignore: cast_nullable_to_non_nullable | ||||
| as String?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
							
								
								
									
										29
									
								
								lib/models/bot_key.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								lib/models/bot_key.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'bot_key.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _SnAccountApiKey _$SnAccountApiKeyFromJson(Map<String, dynamic> json) => | ||||
|     _SnAccountApiKey( | ||||
|       id: json['id'] as String, | ||||
|       label: json['label'] as String, | ||||
|       accountId: json['account_id'] as String, | ||||
|       sessionId: json['session_id'] as String, | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       updatedAt: DateTime.parse(json['updated_at'] as String), | ||||
|       key: json['key'] as String?, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$SnAccountApiKeyToJson(_SnAccountApiKey instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'label': instance.label, | ||||
|       'account_id': instance.accountId, | ||||
|       'session_id': instance.sessionId, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'updated_at': instance.updatedAt.toIso8601String(), | ||||
|       'key': instance.key, | ||||
|     }; | ||||
							
								
								
									
										19
									
								
								lib/models/custom_app_secret.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								lib/models/custom_app_secret.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
|  | ||||
| part 'custom_app_secret.freezed.dart'; | ||||
| part 'custom_app_secret.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| sealed class CustomAppSecret with _$CustomAppSecret { | ||||
|   const factory CustomAppSecret({ | ||||
|     required String id, | ||||
|     required String? secret, | ||||
|     required DateTime createdAt, | ||||
|     String? description, | ||||
|     int? expiresIn, | ||||
|     bool? isOidc, | ||||
|   }) = _CustomAppSecret; | ||||
|  | ||||
|   factory CustomAppSecret.fromJson(Map<String, dynamic> json) => | ||||
|       _$CustomAppSecretFromJson(json); | ||||
| } | ||||
							
								
								
									
										286
									
								
								lib/models/custom_app_secret.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										286
									
								
								lib/models/custom_app_secret.freezed.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,286 @@ | ||||
| // 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 'custom_app_secret.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // FreezedGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$CustomAppSecret { | ||||
|  | ||||
|  String get id; String? get secret; DateTime get createdAt; String? get description; int? get expiresIn; bool? get isOidc; | ||||
| /// Create a copy of CustomAppSecret | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $CustomAppSecretCopyWith<CustomAppSecret> get copyWith => _$CustomAppSecretCopyWithImpl<CustomAppSecret>(this as CustomAppSecret, _$identity); | ||||
|  | ||||
|   /// Serializes this CustomAppSecret to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiresIn, expiresIn) || other.expiresIn == expiresIn)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description,expiresIn,isOidc); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description, expiresIn: $expiresIn, isOidc: $isOidc)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $CustomAppSecretCopyWith<$Res>  { | ||||
|   factory $CustomAppSecretCopyWith(CustomAppSecret value, $Res Function(CustomAppSecret) _then) = _$CustomAppSecretCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$CustomAppSecretCopyWithImpl<$Res> | ||||
|     implements $CustomAppSecretCopyWith<$Res> { | ||||
|   _$CustomAppSecretCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final CustomAppSecret _self; | ||||
|   final $Res Function(CustomAppSecret) _then; | ||||
|  | ||||
| /// Create a copy of CustomAppSecret | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? secret = freezed,Object? createdAt = null,Object? description = freezed,Object? expiresIn = freezed,Object? isOidc = freezed,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,secret: freezed == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable | ||||
| as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||
| as String?,expiresIn: freezed == expiresIn ? _self.expiresIn : expiresIn // ignore: cast_nullable_to_non_nullable | ||||
| as int?,isOidc: freezed == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable | ||||
| as bool?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Adds pattern-matching-related methods to [CustomAppSecret]. | ||||
| extension CustomAppSecretPatterns on CustomAppSecret { | ||||
| /// 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( _CustomAppSecret value)?  $default,{required TResult orElse(),}){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _CustomAppSecret() 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( _CustomAppSecret value)  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _CustomAppSecret(): | ||||
| 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( _CustomAppSecret value)?  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _CustomAppSecret() 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? secret,  DateTime createdAt,  String? description,  int? expiresIn,  bool? isOidc)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _CustomAppSecret() when $default != null: | ||||
| return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);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? secret,  DateTime createdAt,  String? description,  int? expiresIn,  bool? isOidc)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _CustomAppSecret(): | ||||
| return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);} | ||||
| } | ||||
| /// 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? secret,  DateTime createdAt,  String? description,  int? expiresIn,  bool? isOidc)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _CustomAppSecret() when $default != null: | ||||
| return $default(_that.id,_that.secret,_that.createdAt,_that.description,_that.expiresIn,_that.isOidc);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _CustomAppSecret implements CustomAppSecret { | ||||
|   const _CustomAppSecret({required this.id, required this.secret, required this.createdAt, this.description, this.expiresIn, this.isOidc}); | ||||
|   factory _CustomAppSecret.fromJson(Map<String, dynamic> json) => _$CustomAppSecretFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String? secret; | ||||
| @override final  DateTime createdAt; | ||||
| @override final  String? description; | ||||
| @override final  int? expiresIn; | ||||
| @override final  bool? isOidc; | ||||
|  | ||||
| /// Create a copy of CustomAppSecret | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$CustomAppSecretCopyWith<_CustomAppSecret> get copyWith => __$CustomAppSecretCopyWithImpl<_CustomAppSecret>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$CustomAppSecretToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiresIn, expiresIn) || other.expiresIn == expiresIn)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,secret,createdAt,description,expiresIn,isOidc); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'CustomAppSecret(id: $id, secret: $secret, createdAt: $createdAt, description: $description, expiresIn: $expiresIn, isOidc: $isOidc)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$CustomAppSecretCopyWith<$Res> implements $CustomAppSecretCopyWith<$Res> { | ||||
|   factory _$CustomAppSecretCopyWith(_CustomAppSecret value, $Res Function(_CustomAppSecret) _then) = __$CustomAppSecretCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String? secret, DateTime createdAt, String? description, int? expiresIn, bool? isOidc | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$CustomAppSecretCopyWithImpl<$Res> | ||||
|     implements _$CustomAppSecretCopyWith<$Res> { | ||||
|   __$CustomAppSecretCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _CustomAppSecret _self; | ||||
|   final $Res Function(_CustomAppSecret) _then; | ||||
|  | ||||
| /// Create a copy of CustomAppSecret | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? secret = freezed,Object? createdAt = null,Object? description = freezed,Object? expiresIn = freezed,Object? isOidc = freezed,}) { | ||||
|   return _then(_CustomAppSecret( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,secret: freezed == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable | ||||
| as String?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||
| as String?,expiresIn: freezed == expiresIn ? _self.expiresIn : expiresIn // ignore: cast_nullable_to_non_nullable | ||||
| as int?,isOidc: freezed == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable | ||||
| as bool?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| // dart format on | ||||
							
								
								
									
										27
									
								
								lib/models/custom_app_secret.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								lib/models/custom_app_secret.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'custom_app_secret.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _CustomAppSecret _$CustomAppSecretFromJson(Map<String, dynamic> json) => | ||||
|     _CustomAppSecret( | ||||
|       id: json['id'] as String, | ||||
|       secret: json['secret'] as String?, | ||||
|       createdAt: DateTime.parse(json['created_at'] as String), | ||||
|       description: json['description'] as String?, | ||||
|       expiresIn: (json['expires_in'] as num?)?.toInt(), | ||||
|       isOidc: json['is_oidc'] as bool?, | ||||
|     ); | ||||
|  | ||||
| Map<String, dynamic> _$CustomAppSecretToJson(_CustomAppSecret instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'secret': instance.secret, | ||||
|       'created_at': instance.createdAt.toIso8601String(), | ||||
|       'description': instance.description, | ||||
|       'expires_in': instance.expiresIn, | ||||
|       'is_oidc': instance.isOidc, | ||||
|     }; | ||||
| @@ -27,6 +27,7 @@ sealed class SnPost with _$SnPost { | ||||
|     @Default(0) int upvotes, | ||||
|     @Default(0) int downvotes, | ||||
|     @Default(0) int repliesCount, | ||||
|     int? pinMode, | ||||
|     String? threadedPostId, | ||||
|     SnPost? threadedPost, | ||||
|     String? repliedPostId, | ||||
|   | ||||
| @@ -15,7 +15,7 @@ T _$identity<T>(T value) => value; | ||||
| /// @nodoc | ||||
| mixin _$SnPost { | ||||
|  | ||||
|  String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; String? get slug; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; String? get realmId; SnRealm? get realm; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<SnPostTag> get tags; List<SnPostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated; | ||||
|  String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; String? get slug; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; int? get pinMode; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; String? get realmId; SnRealm? get realm; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; Map<String, bool> get reactionsMade; List<dynamic> get reactions; List<SnPostTag> get tags; List<SnPostCategory> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated; | ||||
| /// Create a copy of SnPost | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -28,16 +28,16 @@ $SnPostCopyWith<SnPost> get copyWith => _$SnPostCopyWithImpl<SnPost>(this as SnP | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactionsMade, reactionsMade)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other.reactionsCount, reactionsCount)&&const DeepCollectionEquality().equals(other.reactionsMade, reactionsMade)&&const DeepCollectionEquality().equals(other.reactions, reactions)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.categories, categories)&&const DeepCollectionEquality().equals(other.collections, collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactionsMade),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]); | ||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(attachments),publisher,const DeepCollectionEquality().hash(reactionsCount),const DeepCollectionEquality().hash(reactionsMade),const DeepCollectionEquality().hash(reactions),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(categories),const DeepCollectionEquality().hash(collections),createdAt,updatedAt,deletedAt,isTruncated]); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)'; | ||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -48,7 +48,7 @@ abstract mixin class $SnPostCopyWith<$Res>  { | ||||
|   factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -65,7 +65,7 @@ class _$SnPostCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnPost | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) { | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||
| @@ -83,7 +83,8 @@ as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: | ||||
| as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable | ||||
| as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable | ||||
| as int,repliesCount: null == repliesCount ? _self.repliesCount : repliesCount // ignore: cast_nullable_to_non_nullable | ||||
| as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable | ||||
| as int,pinMode: freezed == pinMode ? _self.pinMode : pinMode // ignore: cast_nullable_to_non_nullable | ||||
| as int?,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable | ||||
| as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,repliedPost: freezed == repliedPost ? _self.repliedPost : repliedPost // ignore: cast_nullable_to_non_nullable | ||||
| @@ -242,10 +243,10 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String? title,  String? description,  String? language,  DateTime? editedAt,  DateTime? publishedAt,  int visibility,  String? content,  String? slug,  int type,  Map<String, dynamic>? meta,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  String? realmId,  SnRealm? realm,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<SnPostTag> tags,  List<SnPostCategory> categories,  List<dynamic> collections,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool isTruncated)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String? title,  String? description,  String? language,  DateTime? editedAt,  DateTime? publishedAt,  int visibility,  String? content,  String? slug,  int type,  Map<String, dynamic>? meta,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  int? pinMode,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  String? realmId,  SnRealm? realm,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<SnPostTag> tags,  List<SnPostCategory> categories,  List<dynamic> collections,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool isTruncated)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnPost() when $default != null: | ||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _: | ||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| @@ -263,10 +264,10 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String? title,  String? description,  String? language,  DateTime? editedAt,  DateTime? publishedAt,  int visibility,  String? content,  String? slug,  int type,  Map<String, dynamic>? meta,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  String? realmId,  SnRealm? realm,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<SnPostTag> tags,  List<SnPostCategory> categories,  List<dynamic> collections,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool isTruncated)  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String? title,  String? description,  String? language,  DateTime? editedAt,  DateTime? publishedAt,  int visibility,  String? content,  String? slug,  int type,  Map<String, dynamic>? meta,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  int? pinMode,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  String? realmId,  SnRealm? realm,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<SnPostTag> tags,  List<SnPostCategory> categories,  List<dynamic> collections,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool isTruncated)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnPost(): | ||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);} | ||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| @@ -280,10 +281,10 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String? title,  String? description,  String? language,  DateTime? editedAt,  DateTime? publishedAt,  int visibility,  String? content,  String? slug,  int type,  Map<String, dynamic>? meta,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  String? realmId,  SnRealm? realm,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<SnPostTag> tags,  List<SnPostCategory> categories,  List<dynamic> collections,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool isTruncated)?  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String? title,  String? description,  String? language,  DateTime? editedAt,  DateTime? publishedAt,  int visibility,  String? content,  String? slug,  int type,  Map<String, dynamic>? meta,  int viewsUnique,  int viewsTotal,  int upvotes,  int downvotes,  int repliesCount,  int? pinMode,  String? threadedPostId,  SnPost? threadedPost,  String? repliedPostId,  SnPost? repliedPost,  String? forwardedPostId,  SnPost? forwardedPost,  String? realmId,  SnRealm? realm,  List<SnCloudFile> attachments,  SnPublisher publisher,  Map<String, int> reactionsCount,  Map<String, bool> reactionsMade,  List<dynamic> reactions,  List<SnPostTag> tags,  List<SnPostCategory> categories,  List<dynamic> collections,  DateTime? createdAt,  DateTime? updatedAt,  DateTime? deletedAt,  bool isTruncated)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnPost() when $default != null: | ||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _: | ||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_that.slug,_that.type,_that.meta,_that.viewsUnique,_that.viewsTotal,_that.upvotes,_that.downvotes,_that.repliesCount,_that.pinMode,_that.threadedPostId,_that.threadedPost,_that.repliedPostId,_that.repliedPost,_that.forwardedPostId,_that.forwardedPost,_that.realmId,_that.realm,_that.attachments,_that.publisher,_that.reactionsCount,_that.reactionsMade,_that.reactions,_that.tags,_that.categories,_that.collections,_that.createdAt,_that.updatedAt,_that.deletedAt,_that.isTruncated);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| @@ -295,7 +296,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnPost implements SnPost { | ||||
|   const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.slug, this.type = 0, final  Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, this.realmId, this.realm, final  List<SnCloudFile> attachments = const [], required this.publisher, final  Map<String, int> reactionsCount = const {}, final  Map<String, bool> reactionsMade = const {}, final  List<dynamic> reactions = const [], final  List<SnPostTag> tags = const [], final  List<SnPostCategory> categories = const [], final  List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections; | ||||
|   const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.slug, this.type = 0, final  Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.pinMode, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, this.realmId, this.realm, final  List<SnCloudFile> attachments = const [], required this.publisher, final  Map<String, int> reactionsCount = const {}, final  Map<String, bool> reactionsMade = const {}, final  List<dynamic> reactions = const [], final  List<SnPostTag> tags = const [], final  List<SnPostCategory> categories = const [], final  List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactionsMade = reactionsMade,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections; | ||||
|   factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @@ -322,6 +323,7 @@ class _SnPost implements SnPost { | ||||
| @override@JsonKey() final  int upvotes; | ||||
| @override@JsonKey() final  int downvotes; | ||||
| @override@JsonKey() final  int repliesCount; | ||||
| @override final  int? pinMode; | ||||
| @override final  String? threadedPostId; | ||||
| @override final  SnPost? threadedPost; | ||||
| @override final  String? repliedPostId; | ||||
| @@ -398,16 +400,16 @@ Map<String, dynamic> toJson() { | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactionsMade, _reactionsMade)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPost&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.language, language) || other.language == language)&&(identical(other.editedAt, editedAt) || other.editedAt == editedAt)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.visibility, visibility) || other.visibility == visibility)&&(identical(other.content, content) || other.content == content)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.viewsUnique, viewsUnique) || other.viewsUnique == viewsUnique)&&(identical(other.viewsTotal, viewsTotal) || other.viewsTotal == viewsTotal)&&(identical(other.upvotes, upvotes) || other.upvotes == upvotes)&&(identical(other.downvotes, downvotes) || other.downvotes == downvotes)&&(identical(other.repliesCount, repliesCount) || other.repliesCount == repliesCount)&&(identical(other.pinMode, pinMode) || other.pinMode == pinMode)&&(identical(other.threadedPostId, threadedPostId) || other.threadedPostId == threadedPostId)&&(identical(other.threadedPost, threadedPost) || other.threadedPost == threadedPost)&&(identical(other.repliedPostId, repliedPostId) || other.repliedPostId == repliedPostId)&&(identical(other.repliedPost, repliedPost) || other.repliedPost == repliedPost)&&(identical(other.forwardedPostId, forwardedPostId) || other.forwardedPostId == forwardedPostId)&&(identical(other.forwardedPost, forwardedPost) || other.forwardedPost == forwardedPost)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.realm, realm) || other.realm == realm)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&const DeepCollectionEquality().equals(other._reactionsCount, _reactionsCount)&&const DeepCollectionEquality().equals(other._reactionsMade, _reactionsMade)&&const DeepCollectionEquality().equals(other._reactions, _reactions)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._categories, _categories)&&const DeepCollectionEquality().equals(other._collections, _collections)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.isTruncated, isTruncated) || other.isTruncated == isTruncated)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactionsMade),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]); | ||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,slug,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,pinMode,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,realmId,realm,const DeepCollectionEquality().hash(_attachments),publisher,const DeepCollectionEquality().hash(_reactionsCount),const DeepCollectionEquality().hash(_reactionsMade),const DeepCollectionEquality().hash(_reactions),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_categories),const DeepCollectionEquality().hash(_collections),createdAt,updatedAt,deletedAt,isTruncated]); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)'; | ||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, slug: $slug, type: $type, meta: $meta, viewsUnique: $viewsUnique, viewsTotal: $viewsTotal, upvotes: $upvotes, downvotes: $downvotes, repliesCount: $repliesCount, pinMode: $pinMode, threadedPostId: $threadedPostId, threadedPost: $threadedPost, repliedPostId: $repliedPostId, repliedPost: $repliedPost, forwardedPostId: $forwardedPostId, forwardedPost: $forwardedPost, realmId: $realmId, realm: $realm, attachments: $attachments, publisher: $publisher, reactionsCount: $reactionsCount, reactionsMade: $reactionsMade, reactions: $reactions, tags: $tags, categories: $categories, collections: $collections, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, isTruncated: $isTruncated)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -418,7 +420,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> { | ||||
|   factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, String? slug, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, int? pinMode, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, String? realmId, SnRealm? realm, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, Map<String, bool> reactionsMade, List<dynamic> reactions, List<SnPostTag> tags, List<SnPostCategory> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -435,7 +437,7 @@ class __$SnPostCopyWithImpl<$Res> | ||||
|  | ||||
| /// Create a copy of SnPost | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) { | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? slug = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? pinMode = freezed,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? realmId = freezed,Object? realm = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactionsMade = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) { | ||||
|   return _then(_SnPost( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||
| @@ -453,7 +455,8 @@ as int,viewsTotal: null == viewsTotal ? _self.viewsTotal : viewsTotal // ignore: | ||||
| as int,upvotes: null == upvotes ? _self.upvotes : upvotes // ignore: cast_nullable_to_non_nullable | ||||
| as int,downvotes: null == downvotes ? _self.downvotes : downvotes // ignore: cast_nullable_to_non_nullable | ||||
| as int,repliesCount: null == repliesCount ? _self.repliesCount : repliesCount // ignore: cast_nullable_to_non_nullable | ||||
| as int,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable | ||||
| as int,pinMode: freezed == pinMode ? _self.pinMode : pinMode // ignore: cast_nullable_to_non_nullable | ||||
| as int?,threadedPostId: freezed == threadedPostId ? _self.threadedPostId : threadedPostId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,threadedPost: freezed == threadedPost ? _self.threadedPost : threadedPost // ignore: cast_nullable_to_non_nullable | ||||
| as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repliedPostId // ignore: cast_nullable_to_non_nullable | ||||
| as String?,repliedPost: freezed == repliedPost ? _self.repliedPost : repliedPost // ignore: cast_nullable_to_non_nullable | ||||
|   | ||||
| @@ -29,6 +29,7 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost( | ||||
|   upvotes: (json['upvotes'] as num?)?.toInt() ?? 0, | ||||
|   downvotes: (json['downvotes'] as num?)?.toInt() ?? 0, | ||||
|   repliesCount: (json['replies_count'] as num?)?.toInt() ?? 0, | ||||
|   pinMode: (json['pin_mode'] as num?)?.toInt(), | ||||
|   threadedPostId: json['threaded_post_id'] as String?, | ||||
|   threadedPost: | ||||
|       json['threaded_post'] == null | ||||
| @@ -109,6 +110,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{ | ||||
|   'upvotes': instance.upvotes, | ||||
|   'downvotes': instance.downvotes, | ||||
|   'replies_count': instance.repliesCount, | ||||
|   'pin_mode': instance.pinMode, | ||||
|   'threaded_post_id': instance.threadedPostId, | ||||
|   'threaded_post': instance.threadedPost?.toJson(), | ||||
|   'replied_post_id': instance.repliedPostId, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/services/text.dart'; | ||||
| import 'package:island/utils/text.dart'; | ||||
|  | ||||
| part 'post_category.freezed.dart'; | ||||
| part 'post_category.g.dart'; | ||||
|   | ||||
| @@ -9,6 +9,7 @@ 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'; | ||||
|  | ||||
| part 'call.g.dart'; | ||||
| part 'call.freezed.dart'; | ||||
| @@ -54,7 +55,7 @@ sealed class CallParticipantLive with _$CallParticipantLive { | ||||
|   bool get hasAudio => remoteParticipant.hasAudio; | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| @Riverpod(keepAlive: true) | ||||
| class CallNotifier extends _$CallNotifier { | ||||
|   Room? _room; | ||||
|   LocalParticipant? _localParticipant; | ||||
| @@ -277,14 +278,27 @@ class CallNotifier extends _$CallNotifier { | ||||
|  | ||||
|         // Listen for connection updates | ||||
|         _room!.addListener(() { | ||||
|           final wasConnected = state.isConnected; | ||||
|           final isNowConnected = | ||||
|               _room!.connectionState == ConnectionState.connected; | ||||
|           state = state.copyWith( | ||||
|             isConnected: _room!.connectionState == ConnectionState.connected, | ||||
|             isConnected: isNowConnected, | ||||
|             isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(), | ||||
|             isCameraEnabled: _localParticipant!.isCameraEnabled(), | ||||
|             isScreenSharing: _localParticipant!.isScreenShareEnabled(), | ||||
|           ); | ||||
|           // Enable wakelock when call connects | ||||
|           if (!wasConnected && isNowConnected) { | ||||
|             WakelockPlus.enable(); | ||||
|           } | ||||
|           // Disable wakelock when call disconnects | ||||
|           else if (wasConnected && !isNowConnected) { | ||||
|             WakelockPlus.disable(); | ||||
|           } | ||||
|         }); | ||||
|         state = state.copyWith(isConnected: true); | ||||
|         // Enable wakelock when call connects | ||||
|         WakelockPlus.enable(); | ||||
|       } else { | ||||
|         state = state.copyWith(error: 'Failed to join room'); | ||||
|       } | ||||
| @@ -344,6 +358,8 @@ class CallNotifier extends _$CallNotifier { | ||||
|         isCameraEnabled: false, | ||||
|         isScreenSharing: false, | ||||
|       ); | ||||
|       // Disable wakelock when call disconnects | ||||
|       WakelockPlus.disable(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -381,5 +397,7 @@ class CallNotifier extends _$CallNotifier { | ||||
|     _durationTimer?.cancel(); | ||||
|     _roomId = null; | ||||
|     participantsVolumes = {}; | ||||
|     // Disable wakelock when disposing | ||||
|     WakelockPlus.disable(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -6,22 +6,19 @@ part of 'call.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$callNotifierHash() => r'18fb807f067eecd3ea42631c1426c3e5f1fb4280'; | ||||
| String _$callNotifierHash() => r'eb9bd41b97e9b5e9d54007c8327edb6567458846'; | ||||
|  | ||||
| /// See also [CallNotifier]. | ||||
| @ProviderFor(CallNotifier) | ||||
| final callNotifierProvider = | ||||
|     AutoDisposeNotifierProvider<CallNotifier, CallState>.internal( | ||||
| final callNotifierProvider = NotifierProvider<CallNotifier, CallState>.internal( | ||||
|   CallNotifier.new, | ||||
|   name: r'callNotifierProvider', | ||||
|   debugGetCreateSourceHash: | ||||
|           const bool.fromEnvironment('dart.vm.product') | ||||
|               ? null | ||||
|               : _$callNotifierHash, | ||||
|       const bool.fromEnvironment('dart.vm.product') ? null : _$callNotifierHash, | ||||
|   dependencies: null, | ||||
|   allTransitiveDependencies: null, | ||||
| ); | ||||
|  | ||||
| typedef _$CallNotifier = AutoDisposeNotifier<CallState>; | ||||
| typedef _$CallNotifier = Notifier<CallState>; | ||||
| // 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 | ||||
|   | ||||
| @@ -15,10 +15,12 @@ const kNetworkServerStoreKey = 'app_server_url'; | ||||
|  | ||||
| const kAppbarTransparentStoreKey = 'app_bar_transparent'; | ||||
| const kAppBackgroundStoreKey = 'app_has_background'; | ||||
| const kAppShowBackgroundImage = 'app_show_background_image'; | ||||
| const kAppColorSchemeStoreKey = 'app_color_scheme'; | ||||
| const kAppNotifyWithHaptic = 'app_notify_with_haptic'; | ||||
| const kAppCustomFonts = 'app_custom_fonts'; | ||||
| const kAppAutoTranslate = 'app_auto_translate'; | ||||
| const kAppDataSavingMode = 'app_data_saving_mode'; | ||||
| const kAppSoundEffects = 'app_sound_effects'; | ||||
| const kAppAprilFoolFeatures = 'app_april_fool_features'; | ||||
| const kAppWindowSize = 'app_window_size'; | ||||
| @@ -54,10 +56,12 @@ final serverUrlProvider = Provider<String>((ref) { | ||||
| sealed class AppSettings with _$AppSettings { | ||||
|   const factory AppSettings({ | ||||
|     required bool autoTranslate, | ||||
|     required bool dataSavingMode, | ||||
|     required bool soundEffects, | ||||
|     required bool aprilFoolFeatures, | ||||
|     required bool enterToSend, | ||||
|     required bool appBarTransparent, | ||||
|     required bool showBackgroundImage, | ||||
|     required String? customFonts, | ||||
|     required int? appColorScheme, // The color stored via the int type | ||||
|     required Size? windowSize, // The window size for desktop platforms | ||||
| @@ -71,10 +75,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { | ||||
|     final prefs = ref.watch(sharedPreferencesProvider); | ||||
|     return AppSettings( | ||||
|       autoTranslate: prefs.getBool(kAppAutoTranslate) ?? false, | ||||
|       dataSavingMode: prefs.getBool(kAppDataSavingMode) ?? false, | ||||
|       soundEffects: prefs.getBool(kAppSoundEffects) ?? true, | ||||
|       aprilFoolFeatures: prefs.getBool(kAppAprilFoolFeatures) ?? true, | ||||
|       enterToSend: prefs.getBool(kAppEnterToSend) ?? true, | ||||
|       appBarTransparent: prefs.getBool(kAppbarTransparentStoreKey) ?? false, | ||||
|       showBackgroundImage: prefs.getBool(kAppShowBackgroundImage) ?? true, | ||||
|       customFonts: prefs.getString(kAppCustomFonts), | ||||
|       appColorScheme: prefs.getInt(kAppColorSchemeStoreKey), | ||||
|       windowSize: _getWindowSizeFromPrefs(prefs), | ||||
| @@ -104,6 +110,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { | ||||
|     state = state.copyWith(autoTranslate: value); | ||||
|   } | ||||
|  | ||||
|   void setDataSavingMode(bool value){ | ||||
|     final prefs = ref.read(sharedPreferencesProvider); | ||||
|     prefs.setBool(kAppDataSavingMode, value); | ||||
|     state = state.copyWith(dataSavingMode: value); | ||||
|   } | ||||
|  | ||||
|   void setSoundEffects(bool value) { | ||||
|     final prefs = ref.read(sharedPreferencesProvider); | ||||
|     prefs.setBool(kAppSoundEffects, value); | ||||
| @@ -129,6 +141,12 @@ class AppSettingsNotifier extends _$AppSettingsNotifier { | ||||
|     ref.read(themeProvider.notifier).reloadTheme(); | ||||
|   } | ||||
|  | ||||
|   void setShowBackgroundImage(bool value) { | ||||
|     final prefs = ref.read(sharedPreferencesProvider); | ||||
|     prefs.setBool(kAppShowBackgroundImage, value); | ||||
|     state = state.copyWith(showBackgroundImage: value); | ||||
|   } | ||||
|  | ||||
|   void setCustomFonts(String? value) { | ||||
|     final prefs = ref.read(sharedPreferencesProvider); | ||||
|     prefs.setString(kAppCustomFonts, value ?? ''); | ||||
|   | ||||
| @@ -14,7 +14,7 @@ T _$identity<T>(T value) => value; | ||||
| /// @nodoc | ||||
| mixin _$AppSettings { | ||||
|  | ||||
|  bool get autoTranslate; bool get soundEffects; bool get aprilFoolFeatures; bool get enterToSend; bool get appBarTransparent; String? get customFonts; int? get appColorScheme;// The color stored via the int type | ||||
|  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; | ||||
| /// Create a copy of AppSettings | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @@ -26,16 +26,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.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.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)); | ||||
|   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)); | ||||
| } | ||||
|  | ||||
|  | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,customFonts,appColorScheme,windowSize); | ||||
| int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)'; | ||||
|   return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -46,7 +46,7 @@ abstract mixin class $AppSettingsCopyWith<$Res>  { | ||||
|   factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, String? customFonts, int? appColorScheme, Size? windowSize | ||||
|  bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -63,13 +63,15 @@ 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? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) { | ||||
| @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,}) { | ||||
|   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 | ||||
| as bool,soundEffects: null == soundEffects ? _self.soundEffects : soundEffects // ignore: cast_nullable_to_non_nullable | ||||
| as bool,aprilFoolFeatures: null == aprilFoolFeatures ? _self.aprilFoolFeatures : aprilFoolFeatures // ignore: cast_nullable_to_non_nullable | ||||
| as bool,enterToSend: null == enterToSend ? _self.enterToSend : enterToSend // ignore: cast_nullable_to_non_nullable | ||||
| as bool,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent : appBarTransparent // ignore: cast_nullable_to_non_nullable | ||||
| 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 | ||||
| @@ -155,10 +157,10 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool autoTranslate,  bool soundEffects,  bool aprilFoolFeatures,  bool enterToSend,  bool appBarTransparent,  String? customFonts,  int? appColorScheme,  Size? windowSize)?  $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,  Size? windowSize)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _AppSettings() when $default != null: | ||||
| return $default(_that.autoTranslate,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.customFonts,_that.appColorScheme,_that.windowSize);case _: | ||||
| return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);case _: | ||||
|   return orElse(); | ||||
|  | ||||
| } | ||||
| @@ -176,10 +178,10 @@ return $default(_that.autoTranslate,_that.soundEffects,_that.aprilFoolFeatures,_ | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool autoTranslate,  bool soundEffects,  bool aprilFoolFeatures,  bool enterToSend,  bool appBarTransparent,  String? customFonts,  int? appColorScheme,  Size? windowSize)  $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,  Size? windowSize)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _AppSettings(): | ||||
| return $default(_that.autoTranslate,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.customFonts,_that.appColorScheme,_that.windowSize);} | ||||
| return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);} | ||||
| } | ||||
| /// A variant of `when` that fallback to returning `null` | ||||
| /// | ||||
| @@ -193,10 +195,10 @@ return $default(_that.autoTranslate,_that.soundEffects,_that.aprilFoolFeatures,_ | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool autoTranslate,  bool soundEffects,  bool aprilFoolFeatures,  bool enterToSend,  bool appBarTransparent,  String? customFonts,  int? appColorScheme,  Size? windowSize)?  $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,  Size? windowSize)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _AppSettings() when $default != null: | ||||
| return $default(_that.autoTranslate,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.customFonts,_that.appColorScheme,_that.windowSize);case _: | ||||
| return $default(_that.autoTranslate,_that.dataSavingMode,_that.soundEffects,_that.aprilFoolFeatures,_that.enterToSend,_that.appBarTransparent,_that.showBackgroundImage,_that.customFonts,_that.appColorScheme,_that.windowSize);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| @@ -208,14 +210,16 @@ return $default(_that.autoTranslate,_that.soundEffects,_that.aprilFoolFeatures,_ | ||||
|  | ||||
|  | ||||
| class _AppSettings implements AppSettings { | ||||
|   const _AppSettings({required this.autoTranslate, required this.soundEffects, required this.aprilFoolFeatures, required this.enterToSend, required this.appBarTransparent, required this.customFonts, required this.appColorScheme, required this.windowSize}); | ||||
|   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}); | ||||
|    | ||||
|  | ||||
| @override final  bool autoTranslate; | ||||
| @override final  bool dataSavingMode; | ||||
| @override final  bool soundEffects; | ||||
| @override final  bool aprilFoolFeatures; | ||||
| @override final  bool enterToSend; | ||||
| @override final  bool appBarTransparent; | ||||
| @override final  bool showBackgroundImage; | ||||
| @override final  String? customFonts; | ||||
| @override final  int? appColorScheme; | ||||
| // The color stored via the int type | ||||
| @@ -231,16 +235,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.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.customFonts, customFonts) || other.customFonts == customFonts)&&(identical(other.appColorScheme, appColorScheme) || other.appColorScheme == appColorScheme)&&(identical(other.windowSize, windowSize) || other.windowSize == windowSize)); | ||||
|   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)); | ||||
| } | ||||
|  | ||||
|  | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,autoTranslate,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,customFonts,appColorScheme,windowSize); | ||||
| int get hashCode => Object.hash(runtimeType,autoTranslate,dataSavingMode,soundEffects,aprilFoolFeatures,enterToSend,appBarTransparent,showBackgroundImage,customFonts,appColorScheme,windowSize); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'AppSettings(autoTranslate: $autoTranslate, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)'; | ||||
|   return 'AppSettings(autoTranslate: $autoTranslate, dataSavingMode: $dataSavingMode, soundEffects: $soundEffects, aprilFoolFeatures: $aprilFoolFeatures, enterToSend: $enterToSend, appBarTransparent: $appBarTransparent, showBackgroundImage: $showBackgroundImage, customFonts: $customFonts, appColorScheme: $appColorScheme, windowSize: $windowSize)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -251,7 +255,7 @@ abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith | ||||
|   factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  bool autoTranslate, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, String? customFonts, int? appColorScheme, Size? windowSize | ||||
|  bool autoTranslate, bool dataSavingMode, bool soundEffects, bool aprilFoolFeatures, bool enterToSend, bool appBarTransparent, bool showBackgroundImage, String? customFonts, int? appColorScheme, Size? windowSize | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -268,13 +272,15 @@ 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? soundEffects = null,Object? aprilFoolFeatures = null,Object? enterToSend = null,Object? appBarTransparent = null,Object? customFonts = freezed,Object? appColorScheme = freezed,Object? windowSize = freezed,}) { | ||||
| @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,}) { | ||||
|   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 | ||||
| as bool,soundEffects: null == soundEffects ? _self.soundEffects : soundEffects // ignore: cast_nullable_to_non_nullable | ||||
| as bool,aprilFoolFeatures: null == aprilFoolFeatures ? _self.aprilFoolFeatures : aprilFoolFeatures // ignore: cast_nullable_to_non_nullable | ||||
| as bool,enterToSend: null == enterToSend ? _self.enterToSend : enterToSend // ignore: cast_nullable_to_non_nullable | ||||
| as bool,appBarTransparent: null == appBarTransparent ? _self.appBarTransparent : appBarTransparent // ignore: cast_nullable_to_non_nullable | ||||
| 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 | ||||
|   | ||||
| @@ -7,7 +7,7 @@ part of 'config.dart'; | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$appSettingsNotifierHash() => | ||||
|     r'c4f40a3bc4311c6360c2b5e44f8df5e5d7c1bd75'; | ||||
|     r'cd18bff2614a94e3523634e6c577cefad0367eba'; | ||||
|  | ||||
| /// See also [AppSettingsNotifier]. | ||||
| @ProviderFor(AppSettingsNotifier) | ||||
|   | ||||
| @@ -1,11 +1,9 @@ | ||||
| import 'dart:convert'; | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect; | ||||
|  | ||||
| part 'translate.freezed.dart'; | ||||
| part 'translate.g.dart'; | ||||
| @@ -29,10 +27,17 @@ Future<String> translateString(Ref ref, TranslateQuery query) async { | ||||
|  | ||||
| @riverpod | ||||
| String? detectStringLanguage(Ref ref, String text) { | ||||
|   try { | ||||
|     return langdetect.detectLangs(text).firstOrNull?.lang; | ||||
|   } catch (err) { | ||||
|     log('[Language] Unable to detect text\'s language: $text'); | ||||
|   bool isChinese(String text) { | ||||
|     final chineseRegex = RegExp(r'[\u4e00-\u9fff]'); | ||||
|     return chineseRegex.hasMatch(text); | ||||
|   } | ||||
|  | ||||
|   bool isEnglish(String text) { | ||||
|     final englishRegex = RegExp(r'[a-zA-Z]'); | ||||
|     return englishRegex.hasMatch(text) && !isChinese(text); | ||||
|   } | ||||
|  | ||||
|   if (isChinese(text)) return "zh"; | ||||
|   if (isEnglish(text)) return "en"; | ||||
|   return null; | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -149,7 +149,7 @@ class _TranslateStringProviderElement | ||||
| } | ||||
|  | ||||
| String _$detectStringLanguageHash() => | ||||
|     r'697b68464b3d00927cc43ccc1ba8ba93f2a470ed'; | ||||
|     r'24fbf52edbbffcc8dc4f09f7206f82d69728e703'; | ||||
|  | ||||
| /// See also [detectStringLanguage]. | ||||
| @ProviderFor(detectStringLanguage) | ||||
|   | ||||
| @@ -7,10 +7,13 @@ import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/screens/about.dart'; | ||||
| import 'package:island/screens/account/credits.dart'; | ||||
| 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_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'; | ||||
| @@ -35,6 +38,7 @@ import 'package:island/screens/chat/chat.dart'; | ||||
| import 'package:island/screens/chat/room.dart'; | ||||
| import 'package:island/screens/chat/room_detail.dart'; | ||||
| import 'package:island/screens/chat/call.dart'; | ||||
| import 'package:island/screens/chat/search_messages_screen.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'; | ||||
| @@ -347,11 +351,31 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|                           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) => EditBotScreen( | ||||
|                         (context, state) => NewBotScreen( | ||||
|                           publisherName: state.pathParameters['name']!, | ||||
|                           projectId: state.pathParameters['projectId']!, | ||||
|                         ), | ||||
| @@ -366,17 +390,6 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|                           id: state.pathParameters['id']!, | ||||
|                         ), | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     name: 'developerBotDetail', | ||||
|                     path: 'bots/:id/detail', | ||||
|                     builder: | ||||
|                         (context, state) => EditBotScreen( | ||||
|                           // Assuming EditBotScreen can also serve as a detail view | ||||
|                           publisherName: state.pathParameters['name']!, | ||||
|                           projectId: state.pathParameters['projectId']!, | ||||
|                           id: state.pathParameters['id']!, | ||||
|                         ), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ], | ||||
| @@ -543,6 +556,14 @@ final routerProvider = Provider<GoRouter>((ref) { | ||||
|                       return ChatDetailScreen(id: id); | ||||
|                     }, | ||||
|                   ), | ||||
|                   GoRoute( | ||||
|                     name: 'searchMessages', | ||||
|                     path: '/chat/:id/search', | ||||
|                     builder: (context, state) { | ||||
|                       final id = state.pathParameters['id']!; | ||||
|                       return SearchMessagesScreen(roomId: id); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|  | ||||
|   | ||||
| @@ -68,6 +68,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|       body: SingleChildScrollView( | ||||
|         padding: getTabbedPadding(context), | ||||
|         child: Column( | ||||
|           spacing: 4, | ||||
|           children: <Widget>[ | ||||
|             Card( | ||||
|               child: Column( | ||||
| @@ -112,20 +113,22 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                               crossAxisAlignment: CrossAxisAlignment.baseline, | ||||
|                               textBaseline: TextBaseline.alphabetic, | ||||
|                               children: [ | ||||
|                                 AccountName( | ||||
|                                 Flexible( | ||||
|                                   child: AccountName( | ||||
|                                     account: user.value!, | ||||
|                                     style: TextStyle( | ||||
|                                       fontSize: 16, | ||||
|                                       fontWeight: FontWeight.bold, | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                 Text('@${user.value!.name}'), | ||||
|                                 ), | ||||
|                                 Flexible(child: Text('@${user.value!.name}')), | ||||
|                               ], | ||||
|                             ), | ||||
|                             Text( | ||||
|                               (user.value!.profile.bio.isNotEmpty) | ||||
|                                   ? user.value!.profile.bio | ||||
|                                   : 'No description yet.', | ||||
|                                   : 'descriptionNone'.tr(), | ||||
|                               maxLines: 1, | ||||
|                               overflow: TextOverflow.ellipsis, | ||||
|                             ), | ||||
| @@ -158,8 +161,16 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                         children: [ | ||||
|                           Icon(Symbols.draw, size: 28).padding(bottom: 8), | ||||
|                           Text('creatorHub').tr().fontSize(16).bold(), | ||||
|                           Text('creatorHubDescription').tr(), | ||||
|                           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: () { | ||||
| @@ -176,8 +187,16 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                         children: [ | ||||
|                           Icon(Symbols.code, size: 28).padding(bottom: 8), | ||||
|                           Text('developerPortal').tr().fontSize(16).bold(), | ||||
|                           Text('developerPortalDescription').tr(), | ||||
|                           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: () { | ||||
|   | ||||
| @@ -95,8 +95,24 @@ class LevelingScreen extends HookConsumerWidget { | ||||
|           title: Text('levelingProgress'.tr()), | ||||
|           bottom: TabBar( | ||||
|             tabs: [ | ||||
|               Tab(text: 'leveling'.tr()), | ||||
|               Tab(text: 'stellarProgram'.tr()), | ||||
|               Tab( | ||||
|                 child: Text( | ||||
|                   'leveling'.tr(), | ||||
|                   textAlign: TextAlign.center, | ||||
|                   style: TextStyle( | ||||
|                     color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|               Tab( | ||||
|                 child: Text( | ||||
|                   'stellarProgram'.tr(), | ||||
|                   textAlign: TextAlign.center, | ||||
|                   style: TextStyle( | ||||
|                     color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|   | ||||
| @@ -14,7 +14,6 @@ import 'package:island/screens/account/me/settings_connections.dart'; | ||||
| import 'package:island/screens/account/me/settings_contacts.dart'; | ||||
| import 'package:island/screens/auth/captcha.dart'; | ||||
| import 'package:island/screens/auth/login.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/account/account_devices.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| @@ -57,7 +56,6 @@ class AccountSettingsScreen extends HookConsumerWidget { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final isDesktop = | ||||
|         !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux); | ||||
|     final isWide = isWideScreen(context); | ||||
|  | ||||
|     Future<void> requestAccountDeletion() async { | ||||
|       final confirm = await showConfirmAlert( | ||||
| @@ -440,37 +438,6 @@ class AccountSettingsScreen extends HookConsumerWidget { | ||||
|  | ||||
|     // Create a responsive layout based on screen width | ||||
|     Widget buildSettingsList() { | ||||
|       if (isWide) { | ||||
|         // Two-column layout for wide screens | ||||
|         return Row( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             Expanded( | ||||
|               child: Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   _SettingsSection( | ||||
|                     title: 'accountSecurityTitle', | ||||
|                     children: securitySettings, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|             Expanded( | ||||
|               child: Column( | ||||
|                 crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                 children: [ | ||||
|                   _SettingsSection( | ||||
|                     title: 'accountDangerZoneTitle', | ||||
|                     children: dangerZoneSettings, | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       } else { | ||||
|         // Single column layout for narrow screens | ||||
|       return Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
| @@ -485,7 +452,6 @@ class AccountSettingsScreen extends HookConsumerWidget { | ||||
|         ], | ||||
|       ); | ||||
|     } | ||||
|     } | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|   | ||||
| @@ -21,6 +21,7 @@ import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| const kServerSupportedLanguages = {'en-US': 'en-us', 'zh-CN': 'zh-hans'}; | ||||
| const kServerSupportedRegions = ['US', 'JP', 'CN']; | ||||
|  | ||||
| class UpdateProfileScreen extends HookConsumerWidget { | ||||
|   const UpdateProfileScreen({super.key}); | ||||
| @@ -97,6 +98,7 @@ class UpdateProfileScreen extends HookConsumerWidget { | ||||
|     final usernameController = useTextEditingController(text: user.value!.name); | ||||
|     final nicknameController = useTextEditingController(text: user.value!.nick); | ||||
|     final language = useState(user.value!.language); | ||||
|     final region = useState(user.value!.region); | ||||
|     final links = useState<List<ProfileLink>>(user.value!.profile.links); | ||||
|  | ||||
|     void updateBasicInfo() async { | ||||
| @@ -111,6 +113,7 @@ class UpdateProfileScreen extends HookConsumerWidget { | ||||
|             'name': usernameController.text, | ||||
|             'nick': nicknameController.text, | ||||
|             'language': language.value, | ||||
|             'region': region.value, | ||||
|           }, | ||||
|         ); | ||||
|         final userNotifier = ref.read(userInfoProvider.notifier); | ||||
| @@ -291,6 +294,32 @@ class UpdateProfileScreen extends HookConsumerWidget { | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|                   DropdownButtonFormField2<String>( | ||||
|                     decoration: InputDecoration( | ||||
|                       labelText: 'region'.tr(), | ||||
|                       helperText: 'accountRegionHint'.tr(), | ||||
|                     ), | ||||
|                     items: [ | ||||
|                       ...kServerSupportedRegions.map( | ||||
|                         (e) => DropdownMenuItem(value: e, child: Text(e)), | ||||
|                       ), | ||||
|                       if (!kServerSupportedRegions.contains(region.value)) | ||||
|                         DropdownMenuItem( | ||||
|                           value: region.value, | ||||
|                           child: Text(region.value), | ||||
|                         ), | ||||
|                     ], | ||||
|                     value: region.value, | ||||
|                     onChanged: (value) { | ||||
|                       region.value = value ?? region.value; | ||||
|                     }, | ||||
|                     customButton: Row( | ||||
|                       children: [ | ||||
|                         Expanded(child: Text(region.value)), | ||||
|                         Icon(Symbols.arrow_drop_down), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|                   Align( | ||||
|                     alignment: Alignment.centerRight, | ||||
|                     child: TextButton.icon( | ||||
|   | ||||
| @@ -1,14 +1,16 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:flutter_svg/flutter_svg.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/auth.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/screens/account/me/account_settings.dart'; | ||||
| import 'package:island/screens/auth/oidc.native.dart'; | ||||
| import 'package:island/services/text.dart'; | ||||
| import 'package:island/utils/text.dart'; | ||||
| import 'package:island/services/time.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| @@ -16,6 +18,7 @@ import 'package:island/widgets/response.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:sign_in_with_apple/sign_in_with_apple.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| // Helper function to get provider icon and localized name | ||||
| Widget getProviderIcon(String provider, {double size = 24, Color? color}) { | ||||
| @@ -165,9 +168,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget { | ||||
|               scopes: [AppleIDAuthorizationScopes.email], | ||||
|               webAuthenticationOptions: WebAuthenticationOptions( | ||||
|                 clientId: 'dev.solsynth.solarpass', | ||||
|                 redirectUri: Uri.parse( | ||||
|                   'https://id.solian.app/auth/callback/apple', | ||||
|                 ), | ||||
|                 redirectUri: Uri.parse('https://id.solian.app/auth/callback'), | ||||
|               ), | ||||
|             ); | ||||
|  | ||||
| @@ -195,6 +196,13 @@ class AccountConnectionNewSheet extends HookConsumerWidget { | ||||
|         case 'github': | ||||
|         case 'discord': | ||||
|         case 'afdian': | ||||
|           if (kIsWeb) { | ||||
|             final serverUrl = ref.watch(serverUrlProvider); | ||||
|             final accessToken = ref.watch(tokenProvider); | ||||
|             launchUrlString( | ||||
|               '$serverUrl/id/auth/login/${selectedProvider.value}?tk=${accessToken!.token}', | ||||
|             ); | ||||
|           } else { | ||||
|             await Navigator.of(context, rootNavigator: true).push( | ||||
|               MaterialPageRoute( | ||||
|                 builder: | ||||
| @@ -206,6 +214,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget { | ||||
|               ), | ||||
|             ); | ||||
|             if (context.mounted) Navigator.pop(context, true); | ||||
|           } | ||||
|           break; | ||||
|         default: | ||||
|           showSnackBar('accountConnectionAddError'.tr()); | ||||
|   | ||||
| @@ -2,11 +2,14 @@ import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/chat.dart'; | ||||
| import 'package:island/models/developer.dart'; | ||||
| import 'package:island/models/publisher.dart'; | ||||
| import 'package:island/models/relationship.dart'; | ||||
| import 'package:island/models/account.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
| @@ -15,7 +18,7 @@ import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/services/color.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/services/text.dart'; | ||||
| import 'package:island/utils/text.dart'; | ||||
| import 'package:island/services/time.dart'; | ||||
| import 'package:island/services/timezone/native.dart'; | ||||
| import 'package:island/widgets/account/account_name.dart'; | ||||
| @@ -37,166 +40,107 @@ import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| part 'profile.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| Future<SnAccount> account(Ref ref, String uname) async { | ||||
|   if (uname == 'me') { | ||||
|     final userInfo = ref.watch(userInfoProvider); | ||||
|     if (userInfo.hasValue && userInfo.value != null) { | ||||
|       return userInfo.value!; | ||||
|     } | ||||
|   } | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   final resp = await apiClient.get("/id/accounts/$uname"); | ||||
|   return SnAccount.fromJson(resp.data); | ||||
| } | ||||
| class _AccountBasicInfo extends StatelessWidget { | ||||
|   final SnAccount data; | ||||
|   final String uname; | ||||
|   final AsyncValue<SnDeveloper?> accountDeveloper; | ||||
|  | ||||
| @riverpod | ||||
| Future<List<SnAccountBadge>> accountBadges(Ref ref, String uname) async { | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   final resp = await apiClient.get("/id/accounts/$uname/badges"); | ||||
|   return List<SnAccountBadge>.from( | ||||
|     resp.data.map((x) => SnAccountBadge.fromJson(x)), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async { | ||||
|   try { | ||||
|     final account = await ref.watch(accountProvider(uname).future); | ||||
|     if (account.profile.background == null) return null; | ||||
|     final palette = await PaletteGenerator.fromImageProvider( | ||||
|       CloudImageWidget.provider( | ||||
|         fileId: account.profile.background!.id, | ||||
|         serverUrl: ref.watch(serverUrlProvider), | ||||
|       ), | ||||
|     ); | ||||
|     final dominantColor = palette.dominantColor?.color; | ||||
|     if (dominantColor == null) return null; | ||||
|     return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; | ||||
|   } catch (_) { | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async { | ||||
|   final userInfo = ref.watch(userInfoProvider); | ||||
|   if (userInfo.value == null) return null; | ||||
|   final account = await ref.watch(accountProvider(uname).future); | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   try { | ||||
|     final resp = await apiClient.get("/sphere/chat/direct/${account.id}"); | ||||
|     return SnChatRoom.fromJson(resp.data); | ||||
|   } catch (err) { | ||||
|     if (err is DioException && err.response?.statusCode == 404) { | ||||
|       return null; | ||||
|     } | ||||
|     rethrow; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<SnRelationship?> accountRelationship(Ref ref, String uname) async { | ||||
|   final userInfo = ref.watch(userInfoProvider); | ||||
|   if (userInfo.value == null) return null; | ||||
|   final account = await ref.watch(accountProvider(uname).future); | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   try { | ||||
|     final resp = await apiClient.get("/id/relationships/${account.id}"); | ||||
|     return SnRelationship.fromJson(resp.data); | ||||
|   } catch (err) { | ||||
|     if (err is DioException && err.response?.statusCode == 404) { | ||||
|       return null; | ||||
|     } | ||||
|     rethrow; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class AccountProfileScreen extends HookConsumerWidget { | ||||
|   final String name; | ||||
|   const AccountProfileScreen({super.key, required this.name}); | ||||
|   const _AccountBasicInfo({ | ||||
|     required this.data, | ||||
|     required this.uname, | ||||
|     required this.accountDeveloper, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final now = DateTime.now(); | ||||
|  | ||||
|     final account = ref.watch(accountProvider(name)); | ||||
|     final accountEvents = ref.watch( | ||||
|       eventCalendarProvider( | ||||
|         EventCalendarQuery(uname: name, year: now.year, month: now.month), | ||||
|   Widget build(BuildContext context) { | ||||
|     return Padding( | ||||
|       padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), | ||||
|       child: Row( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           ProfilePictureWidget(file: data.profile.picture, radius: 32), | ||||
|           const Gap(20), | ||||
|           Expanded( | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   children: [ | ||||
|                     AccountName(account: data, style: TextStyle(fontSize: 20)), | ||||
|                     const Gap(6), | ||||
|                     Flexible( | ||||
|                       child: Text( | ||||
|                         '@${data.name}', | ||||
|                         maxLines: 1, | ||||
|                         overflow: TextOverflow.ellipsis, | ||||
|                       ).fontSize(14).opacity(0.85), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 if (accountDeveloper.value != null) | ||||
|                   Row( | ||||
|                     spacing: 7, | ||||
|                     children: [ | ||||
|                       const Icon(Symbols.smart_toy, size: 18), | ||||
|                       Text( | ||||
|                         'botAutomatedBy'.tr( | ||||
|                           args: [accountDeveloper.value!.publisher!.nick], | ||||
|                         ), | ||||
|                       ).fontSize(13), | ||||
|                     ], | ||||
|                   ).opacity(0.75), | ||||
|                 const Gap(4), | ||||
|                 AccountStatusWidget(uname: uname, padding: EdgeInsets.zero), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|           IconButton( | ||||
|             onPressed: () { | ||||
|               SharePlus.instance.share( | ||||
|                 ShareParams( | ||||
|                   uri: Uri.parse('https://id.solian.app/@${data.name}'), | ||||
|                 ), | ||||
|               ); | ||||
|     final accountChat = ref.watch(accountDirectChatProvider(name)); | ||||
|     final accountRelationship = ref.watch(accountRelationshipProvider(name)); | ||||
|  | ||||
|     final appbarColor = ref.watch(accountAppbarForcegroundColorProvider(name)); | ||||
|  | ||||
|     final appbarShadow = Shadow( | ||||
|       color: appbarColor.value?.invert ?? Colors.transparent, | ||||
|       blurRadius: 5.0, | ||||
|       offset: Offset(1.0, 1.0), | ||||
|             }, | ||||
|             icon: const Icon(Symbols.share), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     Future<void> relationshipAction() async { | ||||
|       if (accountRelationship.value != null) return; | ||||
|       showLoadingModal(context); | ||||
|       try { | ||||
|         final client = ref.watch(apiClientProvider); | ||||
|         await client.post('/id/relationships/${account.value!.id}/friends'); | ||||
|         ref.invalidate(accountRelationshipProvider(name)); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|       } finally { | ||||
|         if (context.mounted) hideLoadingModal(context); | ||||
|   } | ||||
| } | ||||
|  | ||||
|     Future<void> blockAction() async { | ||||
|       showLoadingModal(context); | ||||
|       try { | ||||
|         final client = ref.watch(apiClientProvider); | ||||
|         if (accountRelationship.value == null) { | ||||
|           await client.post('/id/relationships/${account.value!.id}/block'); | ||||
|         } else { | ||||
|           await client.delete('/id/relationships/${account.value!.id}/block'); | ||||
|         } | ||||
|         ref.invalidate(accountRelationshipProvider(name)); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|       } finally { | ||||
|         if (context.mounted) hideLoadingModal(context); | ||||
|       } | ||||
|     } | ||||
| class _AccountProfileBio extends StatelessWidget { | ||||
|   final SnAccount data; | ||||
|  | ||||
|     Future<void> directMessageAction() async { | ||||
|       if (!account.hasValue) return; | ||||
|       if (accountChat.value != null) { | ||||
|         context.pushNamed( | ||||
|           'chatRoom', | ||||
|           pathParameters: {'id': accountChat.value!.id}, | ||||
|   const _AccountProfileBio({required this.data}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Card( | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Text('bio').tr().bold().fontSize(15).padding(bottom: 8), | ||||
|           if (data.profile.bio.isEmpty) | ||||
|             Text('descriptionNone').tr().italic() | ||||
|           else | ||||
|             MarkdownTextContent( | ||||
|               content: data.profile.bio, | ||||
|               linesMargin: EdgeInsets.zero, | ||||
|             ), | ||||
|         ], | ||||
|       ).padding(horizontal: 24, vertical: 20), | ||||
|     ); | ||||
|         return; | ||||
|       } | ||||
|       showLoadingModal(context); | ||||
|       try { | ||||
|         final client = ref.watch(apiClientProvider); | ||||
|         final resp = await client.post( | ||||
|           '/sphere/chat/direct', | ||||
|           data: {'related_user_id': account.value!.id}, | ||||
|         ); | ||||
|         final chat = SnChatRoom.fromJson(resp.data); | ||||
|         if (context.mounted) { | ||||
|           context.pushNamed('chatRoom', pathParameters: {'id': chat.id}); | ||||
|         } | ||||
|         ref.invalidate(accountDirectChatProvider(name)); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|       } finally { | ||||
|         if (context.mounted) hideLoadingModal(context); | ||||
|   } | ||||
| } | ||||
|  | ||||
|     List<Widget> buildSubcolumn(SnAccount data) { | ||||
| class _AccountProfileDetail extends StatelessWidget { | ||||
|   final SnAccount data; | ||||
|  | ||||
|   const _AccountProfileDetail({required this.data}); | ||||
|  | ||||
|   List<Widget> _buildSubcolumn() { | ||||
|     return [ | ||||
|       Row( | ||||
|         spacing: 6, | ||||
| @@ -252,90 +196,63 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|           spacing: 6, | ||||
|           children: [ | ||||
|             const Icon(Symbols.id_card, size: 17, fill: 1), | ||||
|               if (data.profile.firstName.isNotEmpty) | ||||
|                 Text(data.profile.firstName), | ||||
|             if (data.profile.firstName.isNotEmpty) Text(data.profile.firstName), | ||||
|             if (data.profile.middleName.isNotEmpty) | ||||
|               Text(data.profile.middleName), | ||||
|             if (data.profile.lastName.isNotEmpty) Text(data.profile.lastName), | ||||
|           ], | ||||
|         ), | ||||
|       Tooltip( | ||||
|         message: 'creditsStatus'.tr(), | ||||
|         child: Row( | ||||
|           spacing: 6, | ||||
|           children: [ | ||||
|             Icon(Symbols.star, size: 17, fill: 1).padding(right: 2), | ||||
|             Text('${data.profile.socialCredits.toStringAsFixed(2)} pts'), | ||||
|             Text('·').bold(), | ||||
|             switch (data.profile.socialCreditsLevel) { | ||||
|               -1 => Text('socialCreditsLevelPoor').tr(), | ||||
|               0 => Text('socialCreditsLevelNormal').tr(), | ||||
|               1 => Text('socialCreditsLevelGood').tr(), | ||||
|               2 => Text('socialCreditsLevelExcellent').tr(), | ||||
|               _ => Text('unknown').tr(), | ||||
|             }, | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|       InkWell( | ||||
|         child: Row( | ||||
|           spacing: 6, | ||||
|           children: [ | ||||
|             Icon(Symbols.fingerprint, size: 17, fill: 1).padding(right: 2), | ||||
|             Flexible( | ||||
|               child: Text( | ||||
|                 data.id, | ||||
|                 maxLines: 1, | ||||
|                 overflow: TextOverflow.ellipsis, | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|         onTap: () { | ||||
|           Clipboard.setData(ClipboardData(text: data.id)); | ||||
|         }, | ||||
|       ), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|     final user = ref.watch(userInfoProvider); | ||||
|     final isCurrentUser = useMemoized( | ||||
|       () => user.value?.id == account.value?.id, | ||||
|       [user, account], | ||||
|     ); | ||||
|  | ||||
|     Widget accountBasicInfo(SnAccount data) => Padding( | ||||
|       padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), | ||||
|       child: Row( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           ProfilePictureWidget(file: data.profile.picture, radius: 32), | ||||
|           const Gap(20), | ||||
|           Expanded( | ||||
|             child: Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   children: [ | ||||
|                     AccountName(account: data, style: TextStyle(fontSize: 20)), | ||||
|                     const Gap(6), | ||||
|                     Flexible( | ||||
|                       child: Text( | ||||
|                         '@${data.name}', | ||||
|                         maxLines: 1, | ||||
|                         overflow: TextOverflow.ellipsis, | ||||
|                       ).fontSize(14).opacity(0.85), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 AccountStatusWidget(uname: name, padding: EdgeInsets.zero), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|           IconButton( | ||||
|             onPressed: () { | ||||
|               SharePlus.instance.share( | ||||
|                 ShareParams( | ||||
|                   uri: Uri.parse('https://id.solian.app/@${data.name}'), | ||||
|                 ), | ||||
|               ); | ||||
|             }, | ||||
|             icon: const Icon(Symbols.share), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     Widget accountProfileBio(SnAccount data) => Card( | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Text('bio').tr().bold().fontSize(15).padding(bottom: 8), | ||||
|           if (data.profile.bio.isEmpty) | ||||
|             Text('descriptionNone').tr().italic() | ||||
|           else | ||||
|             MarkdownTextContent( | ||||
|               content: data.profile.bio, | ||||
|               linesMargin: EdgeInsets.zero, | ||||
|             ), | ||||
|         ], | ||||
|       ).padding(horizontal: 24, vertical: 20), | ||||
|     ); | ||||
|  | ||||
|     Widget accountProfileDetail(SnAccount data) => Card( | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Card( | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|         spacing: 24, | ||||
|         children: [ | ||||
|           if (buildSubcolumn(data).isNotEmpty) | ||||
|           if (_buildSubcolumn().isNotEmpty) | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               spacing: 2, | ||||
|               children: buildSubcolumn(data), | ||||
|               children: _buildSubcolumn(), | ||||
|             ), | ||||
|           if (data.profile.timeZone.isNotEmpty && !kIsWeb) | ||||
|             Column( | ||||
| @@ -366,8 +283,17 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|         ], | ||||
|       ).padding(horizontal: 24, vertical: 16), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
|     Widget accountProfileLinks(SnAccount data) => Card( | ||||
| class _AccountProfileLinks extends StatelessWidget { | ||||
|   final SnAccount data; | ||||
|  | ||||
|   const _AccountProfileLinks({required this.data}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Card( | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
| @@ -392,9 +318,83 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
|     Widget accountAction(SnAccount data) => Card( | ||||
| class _AccountPublisherList extends StatelessWidget { | ||||
|   final List<SnPublisher> publishers; | ||||
|  | ||||
|   const _AccountPublisherList({required this.publishers}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     if (publishers.isEmpty) return const SizedBox.shrink(); | ||||
|     return Card( | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Text( | ||||
|             'publishers', | ||||
|           ).tr().bold().padding(horizontal: 24, top: 12, bottom: 4), | ||||
|           for (final publisher in publishers) | ||||
|             ListTile( | ||||
|               title: Text(publisher.nick), | ||||
|               subtitle: Text( | ||||
|                 publisher.bio.isNotEmpty | ||||
|                     ? publisher.bio | ||||
|                         .split('\n') | ||||
|                         .where((line) => line.trim().isNotEmpty) | ||||
|                         .join('\n') | ||||
|                     : 'descriptionNone'.tr(), | ||||
|                 maxLines: 3, | ||||
|                 overflow: TextOverflow.ellipsis, | ||||
|               ), | ||||
|               leading: ProfilePictureWidget( | ||||
|                 file: publisher.picture, | ||||
|                 borderRadius: publisher.type == 1 ? 8 : null, | ||||
|               ), | ||||
|               isThreeLine: true, | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               shape: RoundedRectangleBorder( | ||||
|                 borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|               ), | ||||
|               onTap: () { | ||||
|                 Navigator.pop(context, true); | ||||
|                 context.pushNamed( | ||||
|                   'publisherProfile', | ||||
|                   pathParameters: {'name': publisher.name}, | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _AccountAction extends StatelessWidget { | ||||
|   final SnAccount data; | ||||
|   final AsyncValue<SnRelationship?> accountRelationship; | ||||
|   final AsyncValue<SnChatRoom?> accountChat; | ||||
|   final VoidCallback relationshipAction; | ||||
|   final VoidCallback blockAction; | ||||
|   final VoidCallback directMessageAction; | ||||
|  | ||||
|   const _AccountAction({ | ||||
|     required this.data, | ||||
|     required this.accountRelationship, | ||||
|     required this.accountChat, | ||||
|     required this.relationshipAction, | ||||
|     required this.blockAction, | ||||
|     required this.directMessageAction, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Card( | ||||
|       child: Column( | ||||
|         spacing: 8, | ||||
|         children: [ | ||||
|           Row( | ||||
|             spacing: 8, | ||||
| @@ -487,6 +487,7 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|                   color: Theme.of(context).colorScheme.onError, | ||||
|                 ), | ||||
|                 style: ButtonStyle( | ||||
|                   visualDensity: VisualDensity.compact, | ||||
|                   backgroundColor: WidgetStatePropertyAll( | ||||
|                     Theme.of(context).colorScheme.error, | ||||
|                   ), | ||||
| @@ -497,10 +498,211 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|         ], | ||||
|       ).padding(horizontal: 16, vertical: 12), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<SnAccount> account(Ref ref, String uname) async { | ||||
|   if (uname == 'me') { | ||||
|     final userInfo = ref.watch(userInfoProvider); | ||||
|     if (userInfo.hasValue && userInfo.value != null) { | ||||
|       return userInfo.value!; | ||||
|     } | ||||
|   } | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   final resp = await apiClient.get("/id/accounts/$uname"); | ||||
|   return SnAccount.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<List<SnAccountBadge>> accountBadges(Ref ref, String uname) async { | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   final resp = await apiClient.get("/id/accounts/$uname/badges"); | ||||
|   return List<SnAccountBadge>.from( | ||||
|     resp.data.map((x) => SnAccountBadge.fromJson(x)), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async { | ||||
|   try { | ||||
|     final account = await ref.watch(accountProvider(uname).future); | ||||
|     if (account.profile.background == null) return null; | ||||
|     final palette = await PaletteGenerator.fromImageProvider( | ||||
|       CloudImageWidget.provider( | ||||
|         fileId: account.profile.background!.id, | ||||
|         serverUrl: ref.watch(serverUrlProvider), | ||||
|       ), | ||||
|     ); | ||||
|     final dominantColor = palette.dominantColor?.color; | ||||
|     if (dominantColor == null) return null; | ||||
|     return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; | ||||
|   } catch (_) { | ||||
|     return null; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<SnChatRoom?> accountDirectChat(Ref ref, String uname) async { | ||||
|   final userInfo = ref.watch(userInfoProvider); | ||||
|   if (userInfo.value == null) return null; | ||||
|   final account = await ref.watch(accountProvider(uname).future); | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   try { | ||||
|     final resp = await apiClient.get("/sphere/chat/direct/${account.id}"); | ||||
|     return SnChatRoom.fromJson(resp.data); | ||||
|   } catch (err) { | ||||
|     if (err is DioException && err.response?.statusCode == 404) { | ||||
|       return null; | ||||
|     } | ||||
|     rethrow; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<SnRelationship?> accountRelationship(Ref ref, String uname) async { | ||||
|   final userInfo = ref.watch(userInfoProvider); | ||||
|   if (userInfo.value == null) return null; | ||||
|   final account = await ref.watch(accountProvider(uname).future); | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   try { | ||||
|     final resp = await apiClient.get("/id/relationships/${account.id}"); | ||||
|     return SnRelationship.fromJson(resp.data); | ||||
|   } catch (err) { | ||||
|     if (err is DioException && err.response?.statusCode == 404) { | ||||
|       return null; | ||||
|     } | ||||
|     rethrow; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<SnDeveloper?> accountBotDeveloper(Ref ref, String uname) async { | ||||
|   final account = await ref.watch(accountProvider(uname).future); | ||||
|   if (account.automatedId == null) return null; | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   try { | ||||
|     final resp = await apiClient.get( | ||||
|       "/develop/bots/${account.automatedId}/developer", | ||||
|     ); | ||||
|     return SnDeveloper.fromJson(resp.data); | ||||
|   } catch (err) { | ||||
|     if (err is DioException && err.response?.statusCode == 404) { | ||||
|       return null; | ||||
|     } | ||||
|     rethrow; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<List<SnPublisher>> accountPublishers(Ref ref, String id) async { | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   try { | ||||
|     final resp = await apiClient.get('/sphere/publishers/of/$id'); | ||||
|     return resp.data | ||||
|         .map((e) => SnPublisher.fromJson(e)) | ||||
|         .cast<SnPublisher>() | ||||
|         .toList(); | ||||
|   } catch (err) { | ||||
|     return []; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class AccountProfileScreen extends HookConsumerWidget { | ||||
|   final String name; | ||||
|   const AccountProfileScreen({super.key, required this.name}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final now = DateTime.now(); | ||||
|  | ||||
|     final account = ref.watch(accountProvider(name)); | ||||
|     final accountEvents = ref.watch( | ||||
|       eventCalendarProvider( | ||||
|         EventCalendarQuery(uname: name, year: now.year, month: now.month), | ||||
|       ), | ||||
|     ); | ||||
|     final accountChat = ref.watch(accountDirectChatProvider(name)); | ||||
|     final accountRelationship = ref.watch(accountRelationshipProvider(name)); | ||||
|     final accountDeveloper = ref.watch(accountBotDeveloperProvider(name)); | ||||
|  | ||||
|     final appbarColor = ref.watch(accountAppbarForcegroundColorProvider(name)); | ||||
|  | ||||
|     final appbarShadow = Shadow( | ||||
|       color: appbarColor.value?.invert ?? Colors.transparent, | ||||
|       blurRadius: 5.0, | ||||
|       offset: Offset(1.0, 1.0), | ||||
|     ); | ||||
|  | ||||
|     Future<void> relationshipAction() async { | ||||
|       if (accountRelationship.value != null) return; | ||||
|       showLoadingModal(context); | ||||
|       try { | ||||
|         final client = ref.watch(apiClientProvider); | ||||
|         await client.post('/id/relationships/${account.value!.id}/friends'); | ||||
|         ref.invalidate(accountRelationshipProvider(name)); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|       } finally { | ||||
|         if (context.mounted) hideLoadingModal(context); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     Future<void> blockAction() async { | ||||
|       showLoadingModal(context); | ||||
|       try { | ||||
|         final client = ref.watch(apiClientProvider); | ||||
|         if (accountRelationship.value == null) { | ||||
|           await client.post('/id/relationships/${account.value!.id}/block'); | ||||
|         } else { | ||||
|           await client.delete('/id/relationships/${account.value!.id}/block'); | ||||
|         } | ||||
|         ref.invalidate(accountRelationshipProvider(name)); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|       } finally { | ||||
|         if (context.mounted) hideLoadingModal(context); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     Future<void> directMessageAction() async { | ||||
|       if (!account.hasValue) return; | ||||
|       if (accountChat.value != null) { | ||||
|         context.pushNamed( | ||||
|           'chatRoom', | ||||
|           pathParameters: {'id': accountChat.value!.id}, | ||||
|         ); | ||||
|         return; | ||||
|       } | ||||
|       showLoadingModal(context); | ||||
|       try { | ||||
|         final client = ref.watch(apiClientProvider); | ||||
|         final resp = await client.post( | ||||
|           '/sphere/chat/direct', | ||||
|           data: {'related_user_id': account.value!.id}, | ||||
|         ); | ||||
|         final chat = SnChatRoom.fromJson(resp.data); | ||||
|         if (context.mounted) { | ||||
|           context.pushNamed('chatRoom', pathParameters: {'id': chat.id}); | ||||
|         } | ||||
|         ref.invalidate(accountDirectChatProvider(name)); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|       } finally { | ||||
|         if (context.mounted) hideLoadingModal(context); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     final user = ref.watch(userInfoProvider); | ||||
|     final isCurrentUser = useMemoized( | ||||
|       () => user.value?.id == account.value?.id, | ||||
|       [user, account], | ||||
|     ); | ||||
|  | ||||
|     return account.when( | ||||
|       data: | ||||
|           (data) => AppScaffold( | ||||
|       data: (data) { | ||||
|         final accountPublishers = ref.watch(accountPublishersProvider(data.id)); | ||||
|         return AppScaffold( | ||||
|           isNoBackground: false, | ||||
|           appBar: | ||||
|               isWideScreen(context) | ||||
| @@ -531,9 +733,7 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|                             style: TextStyle( | ||||
|                               color: | ||||
|                                   appbarColor.value ?? | ||||
|                                     Theme.of( | ||||
|                                       context, | ||||
|                                     ).appBarTheme.foregroundColor, | ||||
|                                   Theme.of(context).appBarTheme.foregroundColor, | ||||
|                               shadows: [appbarShadow], | ||||
|                             ), | ||||
|                           ), | ||||
| @@ -549,7 +749,13 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|                       Flexible( | ||||
|                         child: CustomScrollView( | ||||
|                           slivers: [ | ||||
|                               SliverToBoxAdapter(child: accountBasicInfo(data)), | ||||
|                             SliverToBoxAdapter( | ||||
|                               child: _AccountBasicInfo( | ||||
|                                 data: data, | ||||
|                                 uname: name, | ||||
|                                 accountDeveloper: accountDeveloper, | ||||
|                               ), | ||||
|                             ), | ||||
|                             if (data.badges.isNotEmpty) | ||||
|                               SliverToBoxAdapter( | ||||
|                                 child: Card( | ||||
| @@ -578,14 +784,16 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|                               ).padding(horizontal: 4, top: 8), | ||||
|                             ), | ||||
|                             SliverToBoxAdapter( | ||||
|                                 child: accountProfileBio(data).padding(top: 4), | ||||
|                               child: _AccountProfileBio( | ||||
|                                 data: data, | ||||
|                               ).padding(top: 4), | ||||
|                             ), | ||||
|                             if (data.profile.links.isNotEmpty) | ||||
|                               SliverToBoxAdapter( | ||||
|                                   child: accountProfileLinks(data), | ||||
|                                 child: _AccountProfileLinks(data: data), | ||||
|                               ), | ||||
|                             SliverToBoxAdapter( | ||||
|                                 child: accountProfileDetail(data), | ||||
|                               child: _AccountProfileDetail(data: data), | ||||
|                             ), | ||||
|                           ], | ||||
|                         ), | ||||
| @@ -594,8 +802,22 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|                         child: CustomScrollView( | ||||
|                           slivers: [ | ||||
|                             SliverGap(24), | ||||
|                             SliverToBoxAdapter( | ||||
|                               child: _AccountPublisherList( | ||||
|                                 publishers: accountPublishers.value ?? [], | ||||
|                               ), | ||||
|                             ), | ||||
|                             if (user.value != null && !isCurrentUser) | ||||
|                                 SliverToBoxAdapter(child: accountAction(data)), | ||||
|                               SliverToBoxAdapter( | ||||
|                                 child: _AccountAction( | ||||
|                                   data: data, | ||||
|                                   accountRelationship: accountRelationship, | ||||
|                                   accountChat: accountChat, | ||||
|                                   relationshipAction: relationshipAction, | ||||
|                                   blockAction: blockAction, | ||||
|                                   directMessageAction: directMessageAction, | ||||
|                                 ), | ||||
|                               ), | ||||
|                             SliverToBoxAdapter( | ||||
|                               child: Card( | ||||
|                                 child: FortuneGraphWidget( | ||||
| @@ -651,7 +873,13 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|                           ], | ||||
|                         ), | ||||
|                       ), | ||||
|                         SliverToBoxAdapter(child: accountBasicInfo(data)), | ||||
|                       SliverToBoxAdapter( | ||||
|                         child: _AccountBasicInfo( | ||||
|                           data: data, | ||||
|                           uname: name, | ||||
|                           accountDeveloper: accountDeveloper, | ||||
|                         ), | ||||
|                       ), | ||||
|                       if (data.badges.isNotEmpty) | ||||
|                         SliverToBoxAdapter( | ||||
|                           child: Card( | ||||
| @@ -678,22 +906,36 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|                         ), | ||||
|                       ), | ||||
|                       SliverToBoxAdapter( | ||||
|                           child: accountProfileBio(data).padding(horizontal: 4), | ||||
|                         child: _AccountProfileBio( | ||||
|                           data: data, | ||||
|                         ).padding(horizontal: 4), | ||||
|                       ), | ||||
|                       if (data.profile.links.isNotEmpty) | ||||
|                         SliverToBoxAdapter( | ||||
|                             child: accountProfileLinks( | ||||
|                               data, | ||||
|                           child: _AccountProfileLinks( | ||||
|                             data: data, | ||||
|                           ).padding(horizontal: 4), | ||||
|                         ), | ||||
|                       SliverToBoxAdapter( | ||||
|                           child: accountProfileDetail( | ||||
|                             data, | ||||
|                         child: _AccountPublisherList( | ||||
|                           publishers: accountPublishers.value ?? [], | ||||
|                         ).padding(horizontal: 4), | ||||
|                       ), | ||||
|                       SliverToBoxAdapter( | ||||
|                         child: _AccountProfileDetail( | ||||
|                           data: data, | ||||
|                         ).padding(horizontal: 4), | ||||
|                       ), | ||||
|                       if (user.value != null && !isCurrentUser) | ||||
|                         SliverToBoxAdapter( | ||||
|                             child: accountAction(data).padding(horizontal: 4), | ||||
|                           child: _AccountAction( | ||||
|                             data: data, | ||||
|                             accountRelationship: accountRelationship, | ||||
|                             accountChat: accountChat, | ||||
|                             relationshipAction: relationshipAction, | ||||
|                             blockAction: blockAction, | ||||
|                             directMessageAction: directMessageAction, | ||||
|                           ).padding(horizontal: 4), | ||||
|                         ), | ||||
|                       SliverToBoxAdapter( | ||||
|                         child: Card( | ||||
| @@ -705,7 +947,8 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|       error: | ||||
|           (error, stackTrace) => AppScaffold( | ||||
|             appBar: AppBar(leading: const PageBackButton()), | ||||
|   | ||||
| @@ -639,5 +639,250 @@ class _AccountRelationshipProviderElement | ||||
|   String get uname => (origin as AccountRelationshipProvider).uname; | ||||
| } | ||||
|  | ||||
| String _$accountBotDeveloperHash() => | ||||
|     r'673534770640a8cf1484ea0af0f4d0ef283ef157'; | ||||
|  | ||||
| /// See also [accountBotDeveloper]. | ||||
| @ProviderFor(accountBotDeveloper) | ||||
| const accountBotDeveloperProvider = AccountBotDeveloperFamily(); | ||||
|  | ||||
| /// See also [accountBotDeveloper]. | ||||
| class AccountBotDeveloperFamily extends Family<AsyncValue<SnDeveloper?>> { | ||||
|   /// See also [accountBotDeveloper]. | ||||
|   const AccountBotDeveloperFamily(); | ||||
|  | ||||
|   /// See also [accountBotDeveloper]. | ||||
|   AccountBotDeveloperProvider call(String uname) { | ||||
|     return AccountBotDeveloperProvider(uname); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AccountBotDeveloperProvider getProviderOverride( | ||||
|     covariant AccountBotDeveloperProvider 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'accountBotDeveloperProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [accountBotDeveloper]. | ||||
| class AccountBotDeveloperProvider | ||||
|     extends AutoDisposeFutureProvider<SnDeveloper?> { | ||||
|   /// See also [accountBotDeveloper]. | ||||
|   AccountBotDeveloperProvider(String uname) | ||||
|     : this._internal( | ||||
|         (ref) => accountBotDeveloper(ref as AccountBotDeveloperRef, uname), | ||||
|         from: accountBotDeveloperProvider, | ||||
|         name: r'accountBotDeveloperProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$accountBotDeveloperHash, | ||||
|         dependencies: AccountBotDeveloperFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             AccountBotDeveloperFamily._allTransitiveDependencies, | ||||
|         uname: uname, | ||||
|       ); | ||||
|  | ||||
|   AccountBotDeveloperProvider._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<SnDeveloper?> Function(AccountBotDeveloperRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: AccountBotDeveloperProvider._internal( | ||||
|         (ref) => create(ref as AccountBotDeveloperRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         uname: uname, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<SnDeveloper?> createElement() { | ||||
|     return _AccountBotDeveloperProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is AccountBotDeveloperProvider && 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 AccountBotDeveloperRef on AutoDisposeFutureProviderRef<SnDeveloper?> { | ||||
|   /// The parameter `uname` of this provider. | ||||
|   String get uname; | ||||
| } | ||||
|  | ||||
| class _AccountBotDeveloperProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<SnDeveloper?> | ||||
|     with AccountBotDeveloperRef { | ||||
|   _AccountBotDeveloperProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get uname => (origin as AccountBotDeveloperProvider).uname; | ||||
| } | ||||
|  | ||||
| String _$accountPublishersHash() => r'25f5695b4a5154163d77f1769876d826bf736609'; | ||||
|  | ||||
| /// See also [accountPublishers]. | ||||
| @ProviderFor(accountPublishers) | ||||
| const accountPublishersProvider = AccountPublishersFamily(); | ||||
|  | ||||
| /// See also [accountPublishers]. | ||||
| class AccountPublishersFamily extends Family<AsyncValue<List<SnPublisher>>> { | ||||
|   /// See also [accountPublishers]. | ||||
|   const AccountPublishersFamily(); | ||||
|  | ||||
|   /// See also [accountPublishers]. | ||||
|   AccountPublishersProvider call(String id) { | ||||
|     return AccountPublishersProvider(id); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AccountPublishersProvider getProviderOverride( | ||||
|     covariant AccountPublishersProvider provider, | ||||
|   ) { | ||||
|     return call(provider.id); | ||||
|   } | ||||
|  | ||||
|   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'accountPublishersProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [accountPublishers]. | ||||
| class AccountPublishersProvider | ||||
|     extends AutoDisposeFutureProvider<List<SnPublisher>> { | ||||
|   /// See also [accountPublishers]. | ||||
|   AccountPublishersProvider(String id) | ||||
|     : this._internal( | ||||
|         (ref) => accountPublishers(ref as AccountPublishersRef, id), | ||||
|         from: accountPublishersProvider, | ||||
|         name: r'accountPublishersProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$accountPublishersHash, | ||||
|         dependencies: AccountPublishersFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             AccountPublishersFamily._allTransitiveDependencies, | ||||
|         id: id, | ||||
|       ); | ||||
|  | ||||
|   AccountPublishersProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.id, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String id; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<List<SnPublisher>> Function(AccountPublishersRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: AccountPublishersProvider._internal( | ||||
|         (ref) => create(ref as AccountPublishersRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         id: id, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<List<SnPublisher>> createElement() { | ||||
|     return _AccountPublishersProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is AccountPublishersProvider && other.id == id; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, id.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin AccountPublishersRef on AutoDisposeFutureProviderRef<List<SnPublisher>> { | ||||
|   /// The parameter `id` of this provider. | ||||
|   String get id; | ||||
| } | ||||
|  | ||||
| class _AccountPublishersProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<List<SnPublisher>> | ||||
|     with AccountPublishersRef { | ||||
|   _AccountPublishersProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get id => (origin as AccountPublishersProvider).id; | ||||
| } | ||||
|  | ||||
| // 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 | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/widgets/account/account_pfc.dart'; | ||||
| import 'package:island/widgets/account/account_picker.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| @@ -99,7 +100,10 @@ class RelationshipListTile extends StatelessWidget { | ||||
|  | ||||
|     return ListTile( | ||||
|       contentPadding: const EdgeInsets.only(left: 16, right: 12), | ||||
|       leading: ProfilePictureWidget(fileId: account.profile.picture?.id), | ||||
|       leading: AccountPfcGestureDetector( | ||||
|         uname: account.name, | ||||
|         child: ProfilePictureWidget(fileId: account.profile.picture?.id), | ||||
|       ), | ||||
|       title: Row( | ||||
|         spacing: 6, | ||||
|         children: [ | ||||
|   | ||||
| @@ -700,6 +700,7 @@ class _LoginLookupScreen extends HookConsumerWidget { | ||||
|           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|           onSubmitted: isBusy.value ? null : (_) => performNewTicket(), | ||||
|         ).padding(horizontal: 7), | ||||
|         if (!kIsWeb) | ||||
|           Row( | ||||
|             spacing: 6, | ||||
|             crossAxisAlignment: CrossAxisAlignment.center, | ||||
| @@ -738,7 +739,9 @@ class _LoginLookupScreen extends HookConsumerWidget { | ||||
|                 tooltip: 'Apple Account', | ||||
|               ), | ||||
|             ], | ||||
|         ).padding(horizontal: 8, vertical: 8), | ||||
|           ).padding(horizontal: 8, vertical: 8) | ||||
|         else | ||||
|           const Gap(12), | ||||
|         Row( | ||||
|           mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|           children: [ | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import "dart:developer" as developer; | ||||
| import "dart:io"; | ||||
| import "package:dio/dio.dart"; | ||||
| import "package:easy_localization/easy_localization.dart"; | ||||
| import "package:file_picker/file_picker.dart"; | ||||
| import "package:flutter/foundation.dart"; | ||||
| import "package:flutter/material.dart"; | ||||
| import "package:go_router/go_router.dart"; | ||||
| @@ -72,6 +73,207 @@ class _AppLifecycleObserver extends WidgetsBindingObserver { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PublicRoomPreview extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   final SnChatRoom room; | ||||
|  | ||||
|   const _PublicRoomPreview({required this.id, required this.room}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final messages = ref.watch(messagesNotifierProvider(id)); | ||||
|     final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier); | ||||
|     final scrollController = useScrollController(); | ||||
|  | ||||
|     final listController = useMemoized(() => ListController(), []); | ||||
|  | ||||
|     var isLoading = false; | ||||
|  | ||||
|     // Add scroll listener for pagination | ||||
|     useEffect(() { | ||||
|       void onScroll() { | ||||
|         if (scrollController.position.pixels >= | ||||
|             scrollController.position.maxScrollExtent - 200) { | ||||
|           if (isLoading) return; | ||||
|           isLoading = true; | ||||
|           messagesNotifier.loadMore().then((_) => isLoading = false); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       scrollController.addListener(onScroll); | ||||
|       return () => scrollController.removeListener(onScroll); | ||||
|     }, [scrollController]); | ||||
|  | ||||
|     Widget chatMessageListWidget(List<LocalChatMessage> messageList) => | ||||
|         SuperListView.builder( | ||||
|           listController: listController, | ||||
|           padding: EdgeInsets.symmetric(vertical: 16), | ||||
|           controller: scrollController, | ||||
|           reverse: true, // Show newest messages at the bottom | ||||
|           itemCount: messageList.length, | ||||
|           findChildIndexCallback: (key) { | ||||
|             final valueKey = key as ValueKey; | ||||
|             final messageId = valueKey.value as String; | ||||
|             return messageList.indexWhere((m) => m.id == messageId); | ||||
|           }, | ||||
|           extentEstimation: (_, _) => 40, | ||||
|           itemBuilder: (context, index) { | ||||
|             final message = messageList[index]; | ||||
|             final nextMessage = | ||||
|                 index < messageList.length - 1 ? messageList[index + 1] : null; | ||||
|             final isLastInGroup = | ||||
|                 nextMessage == null || | ||||
|                 nextMessage.senderId != message.senderId || | ||||
|                 nextMessage.createdAt | ||||
|                         .difference(message.createdAt) | ||||
|                         .inMinutes | ||||
|                         .abs() > | ||||
|                     3; | ||||
|  | ||||
|             return MessageItem( | ||||
|               message: message, | ||||
|               isCurrentUser: false, // User is not a member, so not current user | ||||
|               onAction: null, // No actions allowed in preview mode | ||||
|               onJump: (_) {}, // No jump functionality in preview | ||||
|               progress: null, | ||||
|               showAvatar: isLastInGroup, | ||||
|             ); | ||||
|           }, | ||||
|         ); | ||||
|  | ||||
|     final compactHeader = isWideScreen(context); | ||||
|  | ||||
|     Widget comfortHeaderWidget() => Column( | ||||
|       spacing: 4, | ||||
|       mainAxisAlignment: MainAxisAlignment.center, | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: [ | ||||
|         SizedBox( | ||||
|           height: 26, | ||||
|           width: 26, | ||||
|           child: | ||||
|               (room.type == 1 && room.picture?.id == null) | ||||
|                   ? SplitAvatarWidget( | ||||
|                     filesId: | ||||
|                         room.members! | ||||
|                             .map((e) => e.account.profile.picture?.id) | ||||
|                             .toList(), | ||||
|                   ) | ||||
|                   : room.picture?.id != null | ||||
|                   ? ProfilePictureWidget( | ||||
|                     fileId: room.picture?.id, | ||||
|                     fallbackIcon: Symbols.chat, | ||||
|                   ) | ||||
|                   : CircleAvatar( | ||||
|                     child: Text( | ||||
|                       room.name![0].toUpperCase(), | ||||
|                       style: const TextStyle(fontSize: 12), | ||||
|                     ), | ||||
|                   ), | ||||
|         ), | ||||
|         Text( | ||||
|           (room.type == 1 && room.name == null) | ||||
|               ? room.members!.map((e) => e.account.nick).join(', ') | ||||
|               : room.name!, | ||||
|         ).fontSize(15), | ||||
|       ], | ||||
|     ); | ||||
|  | ||||
|     Widget compactHeaderWidget() => Row( | ||||
|       spacing: 8, | ||||
|       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|       children: [ | ||||
|         SizedBox( | ||||
|           height: 26, | ||||
|           width: 26, | ||||
|           child: | ||||
|               (room.type == 1 && room.picture?.id == null) | ||||
|                   ? SplitAvatarWidget( | ||||
|                     filesId: | ||||
|                         room.members! | ||||
|                             .map((e) => e.account.profile.picture?.id) | ||||
|                             .toList(), | ||||
|                   ) | ||||
|                   : room.picture?.id != null | ||||
|                   ? ProfilePictureWidget( | ||||
|                     fileId: room.picture?.id, | ||||
|                     fallbackIcon: Symbols.chat, | ||||
|                   ) | ||||
|                   : CircleAvatar( | ||||
|                     child: Text( | ||||
|                       room.name![0].toUpperCase(), | ||||
|                       style: const TextStyle(fontSize: 12), | ||||
|                     ), | ||||
|                   ), | ||||
|         ), | ||||
|         Text( | ||||
|           (room.type == 1 && room.name == null) | ||||
|               ? room.members!.map((e) => e.account.nick).join(', ') | ||||
|               : room.name!, | ||||
|         ).fontSize(19), | ||||
|       ], | ||||
|     ); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         leading: !compactHeader ? const Center(child: PageBackButton()) : null, | ||||
|         automaticallyImplyLeading: false, | ||||
|         toolbarHeight: compactHeader ? null : 64, | ||||
|         title: compactHeader ? compactHeaderWidget() : comfortHeaderWidget(), | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             icon: const Icon(Icons.more_vert), | ||||
|             onPressed: () { | ||||
|               context.pushNamed('chatDetail', pathParameters: {'id': id}); | ||||
|             }, | ||||
|           ), | ||||
|           const Gap(8), | ||||
|         ], | ||||
|       ), | ||||
|       body: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|         children: [ | ||||
|           Expanded( | ||||
|             child: messages.when( | ||||
|               data: | ||||
|                   (messageList) => | ||||
|                       messageList.isEmpty | ||||
|                           ? Center(child: Text('No messages yet'.tr())) | ||||
|                           : chatMessageListWidget(messageList), | ||||
|               loading: () => const Center(child: CircularProgressIndicator()), | ||||
|               error: | ||||
|                   (error, _) => ResponseErrorWidget( | ||||
|                     error: error, | ||||
|                     onRetry: () => messagesNotifier.loadInitial(), | ||||
|                   ), | ||||
|             ), | ||||
|           ), | ||||
|           // Join button at the bottom for public rooms | ||||
|           Container( | ||||
|             padding: const EdgeInsets.all(16), | ||||
|             child: FilledButton.tonalIcon( | ||||
|               onPressed: () async { | ||||
|                 try { | ||||
|                   showLoadingModal(context); | ||||
|                   final apiClient = ref.read(apiClientProvider); | ||||
|                   await apiClient.post('/sphere/chat/${room.id}/members/me'); | ||||
|                   ref.invalidate(chatroomIdentityProvider(id)); | ||||
|                 } catch (err) { | ||||
|                   showErrorAlert(err); | ||||
|                 } finally { | ||||
|                   if (context.mounted) hideLoadingModal(context); | ||||
|                 } | ||||
|               }, | ||||
|               label: Text('chatJoin').tr(), | ||||
|               icon: const Icon(Icons.add), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| class MessagesNotifier extends _$MessagesNotifier { | ||||
|   late final Dio _apiClient; | ||||
| @@ -82,6 +284,9 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|   final Map<String, LocalChatMessage> _pendingMessages = {}; | ||||
|   final Map<String, Map<int, double>> _fileUploadProgress = {}; | ||||
|   int? _totalCount; | ||||
|   String? _searchQuery; | ||||
|   bool? _withLinks; | ||||
|   bool? _withAttachments; | ||||
|  | ||||
|   late final String _roomId; | ||||
|   int _currentPage = 0; | ||||
| @@ -96,17 +301,24 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|     _database = ref.watch(databaseProvider); | ||||
|     final room = await ref.watch(chatroomProvider(roomId).future); | ||||
|     final identity = await ref.watch(chatroomIdentityProvider(roomId).future); | ||||
|     if (room == null || identity == null) { | ||||
|       throw Exception('Room or identity not found'); | ||||
|  | ||||
|     if (room == null) { | ||||
|       throw Exception('Room not found'); | ||||
|     } | ||||
|     _room = room; | ||||
|  | ||||
|     // Allow building even if identity is null for public rooms | ||||
|     if (identity != null) { | ||||
|       _identity = identity; | ||||
|     } | ||||
|  | ||||
|     developer.log( | ||||
|       'MessagesNotifier built for room $roomId', | ||||
|       name: 'MessagesNotifier', | ||||
|     ); | ||||
|  | ||||
|     // 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( | ||||
| @@ -116,8 +328,15 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|           syncMessages(); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     return await loadInitial(); | ||||
|     loadInitial(); | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   List<LocalChatMessage> _sortMessages(List<LocalChatMessage> messages) { | ||||
|     messages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); | ||||
|     return messages; | ||||
|   } | ||||
|  | ||||
|   Future<List<LocalChatMessage>> _getCachedMessages({ | ||||
| @@ -128,13 +347,32 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|       'Getting cached messages from offset $offset, take $take', | ||||
|       name: 'MessagesNotifier', | ||||
|     ); | ||||
|     final dbMessages = await _database.getMessagesForRoom( | ||||
|     final List<LocalChatMessage> dbMessages; | ||||
|     if (_searchQuery != null && _searchQuery!.isNotEmpty) { | ||||
|       dbMessages = await _database.searchMessages(_roomId, _searchQuery ?? ''); | ||||
|     } else { | ||||
|       final chatMessagesFromDb = await _database.getMessagesForRoom( | ||||
|         _roomId, | ||||
|         offset: offset, | ||||
|         limit: take, | ||||
|       ); | ||||
|     final dbLocalMessages = | ||||
|         dbMessages.map(_database.companionToMessage).toList(); | ||||
|       dbMessages = | ||||
|           chatMessagesFromDb.map(_database.companionToMessage).toList(); | ||||
|     } | ||||
|  | ||||
|     List<LocalChatMessage> filteredMessages = dbMessages; | ||||
|  | ||||
|     if (_withLinks == true) { | ||||
|       filteredMessages = | ||||
|           filteredMessages.where((msg) => _hasLink(msg)).toList(); | ||||
|     } | ||||
|  | ||||
|     if (_withAttachments == true) { | ||||
|       filteredMessages = | ||||
|           filteredMessages.where((msg) => _hasAttachment(msg)).toList(); | ||||
|     } | ||||
|  | ||||
|     final dbLocalMessages = filteredMessages; | ||||
|  | ||||
|     if (offset == 0) { | ||||
|       final pendingForRoom = | ||||
| @@ -143,7 +381,7 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|               .toList(); | ||||
|  | ||||
|       final allMessages = [...pendingForRoom, ...dbLocalMessages]; | ||||
|       allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); | ||||
|       _sortMessages(allMessages); // Use the helper function | ||||
|  | ||||
|       final uniqueMessages = <LocalChatMessage>[]; | ||||
|       final seenIds = <String>{}; | ||||
| @@ -218,7 +456,7 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|     _isSyncing = true; | ||||
|  | ||||
|     developer.log('Starting message sync', name: 'MessagesNotifier'); | ||||
|     ref.read(isSyncingProvider.notifier).state = true; | ||||
|     Future.microtask(() => ref.read(isSyncingProvider.notifier).state = true); | ||||
|     try { | ||||
|       final dbMessages = await _database.getMessagesForRoom( | ||||
|         _room.id, | ||||
| @@ -279,7 +517,9 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|       showErrorAlert(err); | ||||
|     } finally { | ||||
|       developer.log('Finished message sync', name: 'MessagesNotifier'); | ||||
|       ref.read(isSyncingProvider.notifier).state = false; | ||||
|       Future.microtask( | ||||
|         () => ref.read(isSyncingProvider.notifier).state = false, | ||||
|       ); | ||||
|       _isSyncing = false; | ||||
|     } | ||||
|   } | ||||
| @@ -290,7 +530,9 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|     bool synced = false, | ||||
|   }) async { | ||||
|     try { | ||||
|       if (offset == 0 && !synced) { | ||||
|       if (offset == 0 && | ||||
|           !synced && | ||||
|           (_searchQuery == null || _searchQuery!.isEmpty)) { | ||||
|         _fetchAndCacheMessages(offset: 0, take: take).catchError((_) { | ||||
|           return <LocalChatMessage>[]; | ||||
|         }); | ||||
| @@ -305,7 +547,11 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|         return localMessages; | ||||
|       } | ||||
|  | ||||
|       if (_searchQuery == null || _searchQuery!.isEmpty) { | ||||
|         return await _fetchAndCacheMessages(offset: offset, take: take); | ||||
|       } else { | ||||
|         return []; // If searching, and no local messages, don't fetch from network | ||||
|       } | ||||
|     } catch (e) { | ||||
|       final localMessages = await _getCachedMessages( | ||||
|         offset: offset, | ||||
| @@ -319,13 +565,15 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<List<LocalChatMessage>> loadInitial() async { | ||||
|   Future<void> loadInitial() async { | ||||
|     developer.log('Loading initial messages', name: 'MessagesNotifier'); | ||||
|     if (_searchQuery == null || _searchQuery!.isEmpty) { | ||||
|       syncMessages(); | ||||
|     } | ||||
|     final messages = await _getCachedMessages(offset: 0, take: 100); | ||||
|     _currentPage = 0; | ||||
|     _hasMore = messages.length == _pageSize; | ||||
|     return messages; | ||||
|     state = AsyncValue.data(messages); | ||||
|   } | ||||
|  | ||||
|   Future<void> loadMore() async { | ||||
| @@ -344,7 +592,9 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|         _hasMore = false; | ||||
|       } | ||||
|  | ||||
|       state = AsyncValue.data([...currentMessages, ...newMessages]); | ||||
|       state = AsyncValue.data( | ||||
|         _sortMessages([...currentMessages, ...newMessages]), | ||||
|       ); | ||||
|     } catch (err, stackTrace) { | ||||
|       developer.log( | ||||
|         'Error loading more messages', | ||||
| @@ -455,9 +705,12 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|  | ||||
|       final currentMessages = state.value ?? []; | ||||
|       if (editingTo != null) { | ||||
|         final newMessages = currentMessages | ||||
|         final newMessages = | ||||
|             currentMessages | ||||
|                 .where((m) => m.id != localMessage.id) // remove pending message | ||||
|             .map((m) => m.id == editingTo.id ? updatedMessage : m) // update original message | ||||
|                 .map( | ||||
|                   (m) => m.id == editingTo.id ? updatedMessage : m, | ||||
|                 ) // update original message | ||||
|                 .toList(); | ||||
|         state = AsyncValue.data(newMessages); | ||||
|       } else { | ||||
| @@ -566,7 +819,7 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|             } | ||||
|             return m; | ||||
|           }).toList(); | ||||
|       state = AsyncValue.data(newMessages); | ||||
|       state = AsyncValue.data(_sortMessages(newMessages)); | ||||
|       showErrorAlert(e); | ||||
|     } | ||||
|   } | ||||
| @@ -626,7 +879,7 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|     if (index >= 0) { | ||||
|       final newList = [...currentMessages]; | ||||
|       newList[index] = updatedMessage; | ||||
|       state = AsyncValue.data(newList); | ||||
|       state = AsyncValue.data(_sortMessages(newList)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -686,6 +939,20 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void searchMessages(String query, {bool? withLinks, bool? withAttachments}) { | ||||
|     _searchQuery = query.trim(); | ||||
|     _withLinks = withLinks; | ||||
|     _withAttachments = withAttachments; | ||||
|     loadInitial(); | ||||
|   } | ||||
|  | ||||
|   void clearSearch() { | ||||
|     _searchQuery = null; | ||||
|     _withLinks = null; | ||||
|     _withAttachments = null; | ||||
|     loadInitial(); | ||||
|   } | ||||
|  | ||||
|   Future<LocalChatMessage?> fetchMessageById(String messageId) async { | ||||
|     developer.log( | ||||
|       'Fetching message by id $messageId', | ||||
| @@ -715,6 +982,18 @@ class MessagesNotifier extends _$MessagesNotifier { | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   bool _hasLink(LocalChatMessage message) { | ||||
|     final content = message.toRemoteMessage().content; | ||||
|     if (content == null) return false; | ||||
|     final urlRegex = RegExp(r'https?://[^\s/$.?#].[^\s]*'); | ||||
|     return urlRegex.hasMatch(content); | ||||
|   } | ||||
|  | ||||
|   bool _hasAttachment(LocalChatMessage message) { | ||||
|     final remoteMessage = message.toRemoteMessage(); | ||||
|     return remoteMessage.attachments.isNotEmpty; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ChatRoomScreen extends HookConsumerWidget { | ||||
| @@ -734,6 +1013,13 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|       ); | ||||
|     } else if (chatIdentity.value == null) { | ||||
|       // Identity was not found, user was not joined | ||||
|       return chatRoom.when( | ||||
|         data: (room) { | ||||
|           if (room!.isPublic) { | ||||
|             // Show public room preview with messages but no input | ||||
|             return _PublicRoomPreview(id: id, room: room); | ||||
|           } else { | ||||
|             // Show regular "not joined" screen for private rooms | ||||
|             return AppScaffold( | ||||
|               appBar: AppBar(leading: const PageBackButton()), | ||||
|               body: Center( | ||||
| @@ -745,14 +1031,14 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|                         mainAxisAlignment: MainAxisAlignment.center, | ||||
|                         children: [ | ||||
|                           Icon( | ||||
|                       chatRoom.value?.isCommunity == true | ||||
|                             room.isCommunity == true | ||||
|                                 ? Symbols.person_add | ||||
|                                 : Symbols.person_remove, | ||||
|                             size: 36, | ||||
|                             fill: 1, | ||||
|                           ).padding(bottom: 4), | ||||
|                           Text('chatNotJoined').tr(), | ||||
|                     if (chatRoom.value?.isCommunity != true) | ||||
|                           if (room.isCommunity != true) | ||||
|                             Text( | ||||
|                               'chatUnableJoin', | ||||
|                               textAlign: TextAlign.center, | ||||
| @@ -763,19 +1049,16 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|                                 try { | ||||
|                                   showLoadingModal(context); | ||||
|                                   final apiClient = ref.read(apiClientProvider); | ||||
|                             if (chatRoom.value == null) { | ||||
|                               hideLoadingModal(context); | ||||
|                               return; | ||||
|                             } | ||||
|  | ||||
|                                   await apiClient.post( | ||||
|                               '/sphere/chat/${chatRoom.value!.id}/members/me', | ||||
|                                     '/sphere/chat/${room.id}/members/me', | ||||
|                                   ); | ||||
|                                   ref.invalidate(chatroomIdentityProvider(id)); | ||||
|                                 } catch (err) { | ||||
|                                   showErrorAlert(err); | ||||
|                                 } finally { | ||||
|                             if (context.mounted) hideLoadingModal(context); | ||||
|                                   if (context.mounted) { | ||||
|                                     hideLoadingModal(context); | ||||
|                                   } | ||||
|                                 } | ||||
|                               }, | ||||
|                               label: Text('chatJoin').tr(), | ||||
| @@ -787,6 +1070,22 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|               ), | ||||
|             ); | ||||
|           } | ||||
|         }, | ||||
|         loading: | ||||
|             () => AppScaffold( | ||||
|               appBar: AppBar(leading: const PageBackButton()), | ||||
|               body: CircularProgressIndicator().center(), | ||||
|             ), | ||||
|         error: | ||||
|             (error, _) => AppScaffold( | ||||
|               appBar: AppBar(leading: const PageBackButton()), | ||||
|               body: ResponseErrorWidget( | ||||
|                 error: error, | ||||
|                 onRetry: () => ref.refresh(chatroomProvider(id)), | ||||
|               ), | ||||
|             ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     final messages = ref.watch(messagesNotifierProvider(id)); | ||||
|     final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier); | ||||
| @@ -953,26 +1252,32 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|     }, [id]); | ||||
|  | ||||
|     Future<void> pickPhotoMedia() async { | ||||
|       final result = await ref | ||||
|           .watch(imagePickerProvider) | ||||
|           .pickMultiImage(requestFullMetadata: true); | ||||
|       if (result.isEmpty) return; | ||||
|       final result = await FilePicker.platform.pickFiles( | ||||
|         type: FileType.image, | ||||
|         allowMultiple: true, | ||||
|         allowCompression: false, | ||||
|       ); | ||||
|       if (result == null || result.count == 0) return; | ||||
|       attachments.value = [ | ||||
|         ...attachments.value, | ||||
|         ...result.map( | ||||
|           (e) => UniversalFile(data: e, type: UniversalFileType.image), | ||||
|         ...result.files.map( | ||||
|           (e) => UniversalFile(data: e.xFile, type: UniversalFileType.image), | ||||
|         ), | ||||
|       ]; | ||||
|     } | ||||
|  | ||||
|     Future<void> pickVideoMedia() async { | ||||
|       final result = await ref | ||||
|           .watch(imagePickerProvider) | ||||
|           .pickVideo(source: ImageSource.gallery); | ||||
|       if (result == null) return; | ||||
|       final result = await FilePicker.platform.pickFiles( | ||||
|         type: FileType.video, | ||||
|         allowMultiple: true, | ||||
|         allowCompression: false, | ||||
|       ); | ||||
|       if (result == null || result.count == 0) return; | ||||
|       attachments.value = [ | ||||
|         ...attachments.value, | ||||
|         UniversalFile(data: result, type: UniversalFileType.video), | ||||
|         ...result.files.map( | ||||
|           (e) => UniversalFile(data: e.xFile, type: UniversalFileType.video), | ||||
|         ), | ||||
|       ]; | ||||
|     } | ||||
|  | ||||
| @@ -1089,6 +1394,8 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|       ], | ||||
|     ); | ||||
|  | ||||
|     const messageKeyPrefix = 'message-'; | ||||
|  | ||||
|     Widget chatMessageListWidget(List<LocalChatMessage> messageList) => | ||||
|         SuperListView.builder( | ||||
|           listController: listController, | ||||
| @@ -1098,7 +1405,9 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|           itemCount: messageList.length, | ||||
|           findChildIndexCallback: (key) { | ||||
|             final valueKey = key as ValueKey; | ||||
|             final messageId = valueKey.value as String; | ||||
|             final messageId = (valueKey.value as String).substring( | ||||
|               messageKeyPrefix.length, | ||||
|             ); | ||||
|             return messageList.indexWhere((m) => m.id == messageId); | ||||
|           }, | ||||
|           extentEstimation: (_, _) => 40, | ||||
| @@ -1115,10 +1424,13 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|                         .abs() > | ||||
|                     3; | ||||
|  | ||||
|             final key = ValueKey('$messageKeyPrefix${message.id}'); | ||||
|  | ||||
|             return chatIdentity.when( | ||||
|               skipError: true, | ||||
|               data: | ||||
|                   (identity) => MessageItem( | ||||
|                     key: key, | ||||
|                     message: message, | ||||
|                     isCurrentUser: identity?.id == message.senderId, | ||||
|                     onAction: (action) { | ||||
| @@ -1161,6 +1473,7 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|                   ), | ||||
|               loading: | ||||
|                   () => MessageItem( | ||||
|                     key: key, | ||||
|                     message: message, | ||||
|                     isCurrentUser: false, | ||||
|                     onAction: null, | ||||
| @@ -1168,7 +1481,7 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|                     showAvatar: false, | ||||
|                     onJump: (_) {}, | ||||
|                   ), | ||||
|               error: (_, _) => const SizedBox.shrink(), | ||||
|               error: (_, _) => SizedBox.shrink(key: key), | ||||
|             ); | ||||
|           }, | ||||
|         ); | ||||
| @@ -1549,7 +1862,7 @@ class _ChatInput extends HookConsumerWidget { | ||||
|                   children: [ | ||||
|                     IconButton( | ||||
|                       tooltip: 'stickers'.tr(), | ||||
|                       icon: const Icon(Symbols.emoji_symbols), | ||||
|                       icon: const Icon(Symbols.add_reaction), | ||||
|                       onPressed: () { | ||||
|                         final size = MediaQuery.of(context).size; | ||||
|                         showStickerPickerPopover( | ||||
| @@ -1659,8 +1972,13 @@ class _ChatInput extends HookConsumerWidget { | ||||
|                           horizontal: 12, | ||||
|                           vertical: 4, | ||||
|                         ), | ||||
|                         counterText: | ||||
|                             messageController.text.length > 1024 | ||||
|                                 ? '${messageController.text.length}/4096' | ||||
|                                 : null, | ||||
|                       ), | ||||
|                       maxLines: null, | ||||
|                       maxLines: 3, | ||||
|                       minLines: 1, | ||||
|                       onTapOutside: | ||||
|                           (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     ), | ||||
|   | ||||
| @@ -6,7 +6,7 @@ part of 'room.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$messagesNotifierHash() => r'32afe6ea24086d869cc47bd3389c8fd734409ca0'; | ||||
| String _$messagesNotifierHash() => r'fc3b66dfb8dd3fc55d142dae5c5e7bdc67eca5d4'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/chat.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/screens/chat/chat.dart'; | ||||
| import 'package:island/widgets/account/account_pfc.dart'; | ||||
| import 'package:island/widgets/account/account_picker.dart'; | ||||
| import 'package:island/widgets/account/status.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| @@ -19,10 +20,17 @@ 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'; | ||||
| import 'package:island/pods/database.dart'; | ||||
|  | ||||
| part 'room_detail.freezed.dart'; | ||||
| part 'room_detail.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| Future<int> totalMessagesCount(Ref ref, String roomId) async { | ||||
|   final database = ref.watch(databaseProvider); | ||||
|   return database.getTotalMessagesForRoom(roomId); | ||||
| } | ||||
|  | ||||
| class ChatDetailScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const ChatDetailScreen({super.key, required this.id}); | ||||
| @@ -31,6 +39,7 @@ class ChatDetailScreen extends HookConsumerWidget { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final roomState = ref.watch(chatroomProvider(id)); | ||||
|     final roomIdentity = ref.watch(chatroomIdentityProvider(id)); | ||||
|     final totalMessages = ref.watch(totalMessagesCountProvider(id)); | ||||
|  | ||||
|     const kNotifyLevelText = [ | ||||
|       'chatNotifyLevelAll', | ||||
| @@ -131,7 +140,7 @@ class ChatDetailScreen extends HookConsumerWidget { | ||||
|                   const Text('chatBreakDescription').tr(), | ||||
|                   const Gap(16), | ||||
|                   ListTile( | ||||
|                     title: const Text('Clear').tr(), | ||||
|                     title: const Text('chatBreakClearButton').tr(), | ||||
|                     subtitle: const Text('chatBreakClear').tr(), | ||||
|                     leading: const Icon(Icons.notifications_active), | ||||
|                     onTap: () { | ||||
| @@ -143,8 +152,10 @@ class ChatDetailScreen extends HookConsumerWidget { | ||||
|                     }, | ||||
|                   ), | ||||
|                   ListTile( | ||||
|                     title: const Text('5m'), | ||||
|                     subtitle: const Text('chatBreakHour').tr(args: ['5m']), | ||||
|                     title: const Text('chatBreak5m').tr(), | ||||
|                     subtitle: const Text( | ||||
|                       'chatBreakHour', | ||||
|                     ).tr(args: ['chatBreak5m'.tr()]), | ||||
|                     leading: const Icon(Symbols.circle), | ||||
|                     onTap: () { | ||||
|                       setChatBreak(now.add(const Duration(minutes: 5))); | ||||
| @@ -155,8 +166,10 @@ class ChatDetailScreen extends HookConsumerWidget { | ||||
|                     }, | ||||
|                   ), | ||||
|                   ListTile( | ||||
|                     title: const Text('10m'), | ||||
|                     subtitle: const Text('chatBreakHour').tr(args: ['10m']), | ||||
|                     title: const Text('chatBreak10m').tr(), | ||||
|                     subtitle: const Text( | ||||
|                       'chatBreakHour', | ||||
|                     ).tr(args: ['chatBreak10m'.tr()]), | ||||
|                     leading: const Icon(Symbols.circle), | ||||
|                     onTap: () { | ||||
|                       setChatBreak(now.add(const Duration(minutes: 10))); | ||||
| @@ -167,8 +180,10 @@ class ChatDetailScreen extends HookConsumerWidget { | ||||
|                     }, | ||||
|                   ), | ||||
|                   ListTile( | ||||
|                     title: const Text('15m'), | ||||
|                     subtitle: const Text('chatBreakHour').tr(args: ['15m']), | ||||
|                     title: const Text('chatBreak15m').tr(), | ||||
|                     subtitle: const Text( | ||||
|                       'chatBreakHour', | ||||
|                     ).tr(args: ['chatBreak15m'.tr()]), | ||||
|                     leading: const Icon(Symbols.timer_3), | ||||
|                     onTap: () { | ||||
|                       setChatBreak(now.add(const Duration(minutes: 15))); | ||||
| @@ -179,8 +194,10 @@ class ChatDetailScreen extends HookConsumerWidget { | ||||
|                     }, | ||||
|                   ), | ||||
|                   ListTile( | ||||
|                     title: const Text('30m'), | ||||
|                     subtitle: const Text('chatBreakHour').tr(args: ['30m']), | ||||
|                     title: const Text('chatBreak30m').tr(), | ||||
|                     subtitle: const Text( | ||||
|                       'chatBreakHour', | ||||
|                     ).tr(args: ['chatBreak30m'.tr()]), | ||||
|                     leading: const Icon(Symbols.timer), | ||||
|                     onTap: () { | ||||
|                       setChatBreak(now.add(const Duration(minutes: 30))); | ||||
| @@ -194,8 +211,8 @@ class ChatDetailScreen extends HookConsumerWidget { | ||||
|                   TextField( | ||||
|                     controller: durationController, | ||||
|                     decoration: InputDecoration( | ||||
|                       labelText: 'Custom (minutes)'.tr(), | ||||
|                       hintText: 'Enter minutes'.tr(), | ||||
|                       labelText: 'chatBreakCustomMinutes'.tr(), | ||||
|                       hintText: 'chatBreakEnterMinutes'.tr(), | ||||
|                       border: const OutlineInputBorder(), | ||||
|                       suffixIcon: IconButton( | ||||
|                         icon: const Icon(Icons.check), | ||||
| @@ -238,7 +255,10 @@ class ChatDetailScreen extends HookConsumerWidget { | ||||
|     return AppScaffold( | ||||
|       body: roomState.when( | ||||
|         loading: () => const Center(child: CircularProgressIndicator()), | ||||
|         error: (error, _) => Center(child: Text('Error: $error')), | ||||
|         error: | ||||
|             (error, _) => Center( | ||||
|               child: Text('errorGeneric'.tr(args: [error.toString()])), | ||||
|             ), | ||||
|         data: | ||||
|             (currentRoom) => CustomScrollView( | ||||
|               slivers: [ | ||||
| @@ -358,6 +378,36 @@ class ChatDetailScreen extends HookConsumerWidget { | ||||
|                                           : const Text('chatBreakNone').tr(), | ||||
|                                   onTap: () => showChatBreakDialog(), | ||||
|                                 ), | ||||
|                                 ListTile( | ||||
|                                   contentPadding: EdgeInsets.symmetric( | ||||
|                                     horizontal: 24, | ||||
|                                   ), | ||||
|                                   leading: const Icon(Icons.search), | ||||
|                                   trailing: const Icon(Symbols.chevron_right), | ||||
|                                   title: const Text('searchMessages').tr(), | ||||
|                                   subtitle: totalMessages.when( | ||||
|                                     data: | ||||
|                                         (count) => Text( | ||||
|                                           'messagesCount'.tr( | ||||
|                                             args: [count.toString()], | ||||
|                                           ), | ||||
|                                         ), | ||||
|                                     loading: | ||||
|                                         () => const CircularProgressIndicator(), | ||||
|                                     error: | ||||
|                                         (err, stack) => Text( | ||||
|                                           'errorGeneric'.tr( | ||||
|                                             args: [err.toString()], | ||||
|                                           ), | ||||
|                                         ), | ||||
|                                   ), | ||||
|                                   onTap: () { | ||||
|                                     context.pushNamed( | ||||
|                                       'searchMessages', | ||||
|                                       pathParameters: {'id': id}, | ||||
|                                     ); | ||||
|                                   }, | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ), | ||||
|                         error: (_, _) => const SizedBox.shrink(), | ||||
| @@ -666,9 +716,12 @@ class _ChatMemberListSheet extends HookConsumerWidget { | ||||
|                     final member = data.items[index]; | ||||
|                     return ListTile( | ||||
|                       contentPadding: EdgeInsets.only(left: 16, right: 12), | ||||
|                       leading: ProfilePictureWidget( | ||||
|                       leading: AccountPfcGestureDetector( | ||||
|                         uname: member.account.name, | ||||
|                         child: ProfilePictureWidget( | ||||
|                           fileId: member.account.profile.picture?.id, | ||||
|                         ), | ||||
|                       ), | ||||
|                       title: Row( | ||||
|                         spacing: 6, | ||||
|                         children: [ | ||||
| @@ -848,7 +901,7 @@ class _ChatMemberRoleSheet extends HookConsumerWidget { | ||||
|                     try { | ||||
|                       final newRole = int.parse(roleController.text); | ||||
|                       if (newRole < 0 || newRole > 100) { | ||||
|                         throw 'Role must be between 0 and 100'; | ||||
|                         throw 'roleValidationHint'.tr(); | ||||
|                       } | ||||
|  | ||||
|                       final apiClient = ref.read(apiClientProvider); | ||||
|   | ||||
| @@ -6,8 +6,8 @@ part of 'room_detail.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$chatMemberListNotifierHash() => | ||||
|     r'3ea30150278523e9f6b23f9200ea9a9fbae9c973'; | ||||
| String _$totalMessagesCountHash() => | ||||
|     r'd55f1507aba2acdce5e468c1c2e15dba7640c571'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
| @@ -30,6 +30,128 @@ class _SystemHash { | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// See also [totalMessagesCount]. | ||||
| @ProviderFor(totalMessagesCount) | ||||
| const totalMessagesCountProvider = TotalMessagesCountFamily(); | ||||
|  | ||||
| /// See also [totalMessagesCount]. | ||||
| class TotalMessagesCountFamily extends Family<AsyncValue<int>> { | ||||
|   /// See also [totalMessagesCount]. | ||||
|   const TotalMessagesCountFamily(); | ||||
|  | ||||
|   /// See also [totalMessagesCount]. | ||||
|   TotalMessagesCountProvider call(String roomId) { | ||||
|     return TotalMessagesCountProvider(roomId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   TotalMessagesCountProvider getProviderOverride( | ||||
|     covariant TotalMessagesCountProvider 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'totalMessagesCountProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [totalMessagesCount]. | ||||
| class TotalMessagesCountProvider extends AutoDisposeFutureProvider<int> { | ||||
|   /// See also [totalMessagesCount]. | ||||
|   TotalMessagesCountProvider(String roomId) | ||||
|     : this._internal( | ||||
|         (ref) => totalMessagesCount(ref as TotalMessagesCountRef, roomId), | ||||
|         from: totalMessagesCountProvider, | ||||
|         name: r'totalMessagesCountProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$totalMessagesCountHash, | ||||
|         dependencies: TotalMessagesCountFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             TotalMessagesCountFamily._allTransitiveDependencies, | ||||
|         roomId: roomId, | ||||
|       ); | ||||
|  | ||||
|   TotalMessagesCountProvider._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 | ||||
|   Override overrideWith( | ||||
|     FutureOr<int> Function(TotalMessagesCountRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: TotalMessagesCountProvider._internal( | ||||
|         (ref) => create(ref as TotalMessagesCountRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         roomId: roomId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<int> createElement() { | ||||
|     return _TotalMessagesCountProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is TotalMessagesCountProvider && 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 TotalMessagesCountRef on AutoDisposeFutureProviderRef<int> { | ||||
|   /// The parameter `roomId` of this provider. | ||||
|   String get roomId; | ||||
| } | ||||
|  | ||||
| class _TotalMessagesCountProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<int> | ||||
|     with TotalMessagesCountRef { | ||||
|   _TotalMessagesCountProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get roomId => (origin as TotalMessagesCountProvider).roomId; | ||||
| } | ||||
|  | ||||
| String _$chatMemberListNotifierHash() => | ||||
|     r'3ea30150278523e9f6b23f9200ea9a9fbae9c973'; | ||||
|  | ||||
| abstract class _$ChatMemberListNotifier | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnChatMember>> { | ||||
|   late final String roomId; | ||||
|   | ||||
							
								
								
									
										139
									
								
								lib/screens/chat/search_messages_screen.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								lib/screens/chat/search_messages_screen.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| 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/screens/chat/room.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/chat/message_item.dart'; | ||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||
| import 'package:super_sliver_list/super_sliver_list.dart'; | ||||
|  | ||||
| class SearchMessagesScreen extends HookConsumerWidget { | ||||
|   final String roomId; | ||||
|  | ||||
|   const SearchMessagesScreen({super.key, required this.roomId}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final searchController = useTextEditingController(); | ||||
|     final withLinks = useState(false); | ||||
|     final withAttachments = useState(false); | ||||
|  | ||||
|     final messagesNotifier = ref.read( | ||||
|       messagesNotifierProvider(roomId).notifier, | ||||
|     ); | ||||
|     final messages = ref.watch(messagesNotifierProvider(roomId)); | ||||
|  | ||||
|     useEffect(() { | ||||
|       // Clear search when screen is disposed | ||||
|       return () { | ||||
|         messagesNotifier.clearSearch(); | ||||
|       }; | ||||
|     }, []); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: const Text('searchMessages').tr()), | ||||
|       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, | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|               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), | ||||
|           Expanded( | ||||
|             child: messages.when( | ||||
|               data: | ||||
|                   (messageList) => | ||||
|                       messageList.isEmpty | ||||
|                           ? Center(child: Text('noMessagesFound'.tr())) | ||||
|                           : SuperListView.builder( | ||||
|                             padding: const EdgeInsets.symmetric(vertical: 16), | ||||
|                             reverse: true, // Show newest messages at the bottom | ||||
|                             itemCount: messageList.length, | ||||
|                             itemBuilder: (context, index) { | ||||
|                               final message = messageList[index]; | ||||
|                               // Simplified MessageItem for search results, no grouping logic | ||||
|                               return MessageItem( | ||||
|                                 message: message, | ||||
|                                 isCurrentUser: | ||||
|                                     false, // Or determine based on actual user | ||||
|                                 onAction: null, | ||||
|                                 onJump: (_) {}, | ||||
|                                 progress: null, | ||||
|                                 showAvatar: true, | ||||
|                               ); | ||||
|                             }, | ||||
|                           ), | ||||
|               loading: () => const Center(child: CircularProgressIndicator()), | ||||
|               error: (error, _) => Center(child: Text('errorGeneric'.tr(args: [error.toString()]))), | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -11,7 +11,7 @@ import 'package:island/models/publisher.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/screens/creators/publishers.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/services/text.dart'; | ||||
| import 'package:island/utils/text.dart'; | ||||
| import 'package:island/widgets/account/account_picker.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import 'package:island/widgets/poll/poll_feedback.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:island/widgets/extended_refresh_indicator.dart'; | ||||
|  | ||||
| part 'poll_list.g.dart'; | ||||
|  | ||||
| @@ -86,7 +87,7 @@ class CreatorPollListScreen extends HookConsumerWidget { | ||||
|         onPressed: () => _createPoll(context), | ||||
|         child: const Icon(Icons.add), | ||||
|       ), | ||||
|       body: RefreshIndicator( | ||||
|       body: ExtendedRefreshIndicator( | ||||
|         onRefresh: () => ref.refresh(pollListNotifierProvider(pubName).future), | ||||
|         child: CustomScrollView( | ||||
|           slivers: [ | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; | ||||
| import 'package:island/pods/webfeed.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/empty_state.dart'; | ||||
| import 'package:island/widgets/extended_refresh_indicator.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
|  | ||||
| class WebFeedListScreen extends ConsumerWidget { | ||||
| @@ -20,7 +21,10 @@ class WebFeedListScreen extends ConsumerWidget { | ||||
|       floatingActionButton: FloatingActionButton( | ||||
|         child: const Icon(Symbols.add), | ||||
|         onPressed: () { | ||||
|           context.pushNamed('creatorFeedNew', pathParameters: {'name': pubName}); | ||||
|           context.pushNamed( | ||||
|             'creatorFeedNew', | ||||
|             pathParameters: {'name': pubName}, | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|       body: feedsAsync.when( | ||||
| @@ -32,7 +36,7 @@ class WebFeedListScreen extends ConsumerWidget { | ||||
|               description: 'Add a new web feed to get started', | ||||
|             ); | ||||
|           } | ||||
|           return RefreshIndicator( | ||||
|           return ExtendedRefreshIndicator( | ||||
|             onRefresh: () => ref.refresh(webFeedListProvider(pubName).future), | ||||
|             child: ListView.builder( | ||||
|               padding: EdgeInsets.only(top: 8), | ||||
| @@ -62,7 +66,10 @@ class WebFeedListScreen extends ConsumerWidget { | ||||
|                     ), | ||||
|                     trailing: const Icon(Symbols.chevron_right), | ||||
|                     onTap: () { | ||||
|                       context.pushNamed('creatorFeedEdit', pathParameters: {'name': pubName, 'feedId': feed.id}); | ||||
|                       context.pushNamed( | ||||
|                         'creatorFeedEdit', | ||||
|                         pathParameters: {'name': pubName, 'feedId': feed.id}, | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ); | ||||
|   | ||||
							
								
								
									
										156
									
								
								lib/screens/developers/app_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								lib/screens/developers/app_detail.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| import 'package:easy_localization/easy_localization.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:island/models/custom_app.dart'; | ||||
| import 'package:island/screens/developers/app_secrets.dart'; | ||||
| import 'package:island/screens/developers/apps.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/response.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| class AppDetailScreen extends HookConsumerWidget { | ||||
|   final String publisherName; | ||||
|   final String projectId; | ||||
|   final String appId; | ||||
|  | ||||
|   const AppDetailScreen({ | ||||
|     super.key, | ||||
|     required this.publisherName, | ||||
|     required this.projectId, | ||||
|     required this.appId, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final tabController = useTabController(initialLength: 2); | ||||
|     final appData = ref.watch( | ||||
|       customAppProvider(publisherName, projectId, appId), | ||||
|     ); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text(appData.value?.name ?? 'appDetails'.tr()), | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             icon: const Icon(Symbols.edit), | ||||
|             onPressed: | ||||
|                 appData.value == null | ||||
|                     ? null | ||||
|                     : () { | ||||
|                       context.pushNamed( | ||||
|                         'developerAppEdit', | ||||
|                         pathParameters: { | ||||
|                           'name': publisherName, | ||||
|                           'projectId': projectId, | ||||
|                           'id': appId, | ||||
|                         }, | ||||
|                       ); | ||||
|                     }, | ||||
|           ), | ||||
|         ], | ||||
|         bottom: TabBar( | ||||
|           controller: tabController, | ||||
|           tabs: [ | ||||
|             Tab( | ||||
|               child: Text( | ||||
|                 'overview'.tr(), | ||||
|                 textAlign: TextAlign.center, | ||||
|                 style: TextStyle( | ||||
|                   color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             Tab( | ||||
|               child: Text( | ||||
|                 'secrets'.tr(), | ||||
|                 textAlign: TextAlign.center, | ||||
|                 style: TextStyle( | ||||
|                   color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|       body: appData.when( | ||||
|         data: (app) { | ||||
|           return TabBarView( | ||||
|             controller: tabController, | ||||
|             physics: const NeverScrollableScrollPhysics(), | ||||
|             children: [ | ||||
|               _AppOverview(app: app), | ||||
|               AppSecretsScreen( | ||||
|                 publisherName: publisherName, | ||||
|                 projectId: projectId, | ||||
|                 appId: appId, | ||||
|               ), | ||||
|             ], | ||||
|           ); | ||||
|         }, | ||||
|         loading: () => const Center(child: CircularProgressIndicator()), | ||||
|         error: | ||||
|             (err, stack) => ResponseErrorWidget( | ||||
|               error: err, | ||||
|               onRetry: | ||||
|                   () => ref.invalidate( | ||||
|                     customAppProvider(publisherName, projectId, appId), | ||||
|                   ), | ||||
|             ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _AppOverview extends StatelessWidget { | ||||
|   final CustomApp app; | ||||
|   const _AppOverview({required this.app}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SingleChildScrollView( | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           AspectRatio( | ||||
|             aspectRatio: 16 / 7, | ||||
|             child: Stack( | ||||
|               clipBehavior: Clip.none, | ||||
|               fit: StackFit.expand, | ||||
|               children: [ | ||||
|                 Container( | ||||
|                   color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                   child: | ||||
|                       app.background != null | ||||
|                           ? CloudFileWidget( | ||||
|                             item: app.background!, | ||||
|                             fit: BoxFit.cover, | ||||
|                           ) | ||||
|                           : const SizedBox.shrink(), | ||||
|                 ), | ||||
|                 Positioned( | ||||
|                   left: 20, | ||||
|                   bottom: -32, | ||||
|                   child: ProfilePictureWidget( | ||||
|                     fileId: app.picture?.id, | ||||
|                     radius: 40, | ||||
|                     fallbackIcon: Symbols.apps, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ).padding(bottom: 32), | ||||
|           ListTile(title: Text('name'.tr()), subtitle: Text(app.name)), | ||||
|           ListTile(title: Text('slug'.tr()), subtitle: Text(app.slug)), | ||||
|           if (app.description?.isNotEmpty ?? false) | ||||
|             ListTile( | ||||
|               title: Text('description'.tr()), | ||||
|               subtitle: Text(app.description!), | ||||
|             ), | ||||
|         ], | ||||
|       ).padding(bottom: 24), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										252
									
								
								lib/screens/developers/app_secrets.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								lib/screens/developers/app_secrets.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,252 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/custom_app_secret.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/services/time.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:island/widgets/response.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
|  | ||||
| part 'app_secrets.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| Future<List<CustomAppSecret>> customAppSecrets( | ||||
|   Ref ref, | ||||
|   String publisherName, | ||||
|   String projectId, | ||||
|   String appId, | ||||
| ) async { | ||||
|   final client = ref.watch(apiClientProvider); | ||||
|   final resp = await client.get( | ||||
|     '/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets', | ||||
|   ); | ||||
|   return (resp.data as List) | ||||
|       .map((e) => CustomAppSecret.fromJson(e)) | ||||
|       .cast<CustomAppSecret>() | ||||
|       .toList(); | ||||
| } | ||||
|  | ||||
| class AppSecretsScreen extends HookConsumerWidget { | ||||
|   final String publisherName; | ||||
|   final String projectId; | ||||
|   final String appId; | ||||
|  | ||||
|   const AppSecretsScreen({ | ||||
|     super.key, | ||||
|     required this.publisherName, | ||||
|     required this.projectId, | ||||
|     required this.appId, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final secrets = ref.watch( | ||||
|       customAppSecretsProvider(publisherName, projectId, appId), | ||||
|     ); | ||||
|  | ||||
|     void showNewSecretSheet(String newSecret) { | ||||
|       showModalBottomSheet( | ||||
|         context: context, | ||||
|         isScrollControlled: true, | ||||
|         builder: | ||||
|             (context) => SheetScaffold( | ||||
|               titleText: 'newSecretGenerated'.tr(), | ||||
|               child: Padding( | ||||
|                 padding: const EdgeInsets.all(20.0), | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                   mainAxisSize: MainAxisSize.min, | ||||
|                   children: [ | ||||
|                     Text('copySecretHint'.tr()), | ||||
|                     const SizedBox(height: 16), | ||||
|                     Container( | ||||
|                       padding: const EdgeInsets.all(12), | ||||
|                       decoration: BoxDecoration( | ||||
|                         color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                         borderRadius: BorderRadius.circular(8), | ||||
|                       ), | ||||
|                       child: SelectableText(newSecret), | ||||
|                     ), | ||||
|                     const SizedBox(height: 20), | ||||
|                     FilledButton.icon( | ||||
|                       onPressed: () { | ||||
|                         Clipboard.setData(ClipboardData(text: newSecret)); | ||||
|                       }, | ||||
|                       icon: const Icon(Symbols.copy_all), | ||||
|                       label: Text('copy'.tr()), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|       ).whenComplete(() { | ||||
|         ref.invalidate( | ||||
|           customAppSecretsProvider(publisherName, projectId, appId), | ||||
|         ); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     void createSecret() { | ||||
|       showModalBottomSheet( | ||||
|         context: context, | ||||
|         isScrollControlled: true, | ||||
|         builder: (context) { | ||||
|           return HookBuilder( | ||||
|             builder: (context) { | ||||
|               final descriptionController = useTextEditingController(); | ||||
|               final expiresInController = useTextEditingController(); | ||||
|               final isOidc = useState(false); | ||||
|  | ||||
|               return SheetScaffold( | ||||
|                 titleText: 'generateSecret'.tr(), | ||||
|                 child: Padding( | ||||
|                   padding: const EdgeInsets.all(20.0), | ||||
|                   child: Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                     mainAxisSize: MainAxisSize.min, | ||||
|                     children: [ | ||||
|                       TextFormField( | ||||
|                         controller: descriptionController, | ||||
|                         decoration: InputDecoration( | ||||
|                           labelText: 'description'.tr(), | ||||
|                         ), | ||||
|                         autofocus: true, | ||||
|                       ), | ||||
|                       const SizedBox(height: 20), | ||||
|                       TextFormField( | ||||
|                         controller: expiresInController, | ||||
|                         decoration: InputDecoration( | ||||
|                           labelText: 'expiresIn'.tr(), | ||||
|                         ), | ||||
|                         keyboardType: TextInputType.number, | ||||
|                       ), | ||||
|                       const SizedBox(height: 20), | ||||
|                       SwitchListTile( | ||||
|                         title: Text('isOidc'.tr()), | ||||
|                         value: isOidc.value, | ||||
|                         onChanged: (value) => isOidc.value = value, | ||||
|                       ), | ||||
|                       const SizedBox(height: 20), | ||||
|                       FilledButton.icon( | ||||
|                         onPressed: () async { | ||||
|                           final description = descriptionController.text; | ||||
|                           final expiresIn = int.tryParse( | ||||
|                             expiresInController.text, | ||||
|                           ); | ||||
|                           Navigator.pop(context); // Close the sheet | ||||
|                           try { | ||||
|                             final client = ref.read(apiClientProvider); | ||||
|                             final resp = await client.post( | ||||
|                               '/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets', | ||||
|                               data: { | ||||
|                                 'description': description, | ||||
|                                 'expires_in': expiresIn, | ||||
|                                 'is_oidc': isOidc.value, | ||||
|                               }, | ||||
|                             ); | ||||
|                             final newSecret = CustomAppSecret.fromJson( | ||||
|                               resp.data, | ||||
|                             ); | ||||
|                             if (newSecret.secret != null) { | ||||
|                               showNewSecretSheet(newSecret.secret!); | ||||
|                             } | ||||
|                           } catch (e) { | ||||
|                             showErrorAlert(e.toString()); | ||||
|                           } | ||||
|                         }, | ||||
|                         icon: const Icon(Symbols.add), | ||||
|                         label: Text('create'.tr()), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|               ); | ||||
|             }, | ||||
|           ); | ||||
|         }, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return secrets.when( | ||||
|       data: (data) { | ||||
|         return RefreshIndicator( | ||||
|           onRefresh: | ||||
|               () => ref.refresh( | ||||
|                 customAppSecretsProvider( | ||||
|                   publisherName, | ||||
|                   projectId, | ||||
|                   appId, | ||||
|                 ).future, | ||||
|               ), | ||||
|           child: Column( | ||||
|             children: [ | ||||
|               ListTile( | ||||
|                 leading: const Icon(Symbols.add), | ||||
|                 title: Text('generateSecret'.tr()), | ||||
|                 onTap: createSecret, | ||||
|               ), | ||||
|               const Divider(height: 1), | ||||
|               Expanded( | ||||
|                 child: ListView.builder( | ||||
|                   padding: EdgeInsets.zero, | ||||
|                   itemCount: data.length, | ||||
|                   itemBuilder: (context, index) { | ||||
|                     final secret = data[index]; | ||||
|                     return ListTile( | ||||
|                       title: Text(secret.description ?? secret.id), | ||||
|                       subtitle: Text( | ||||
|                         'createdAt'.tr(args: [secret.createdAt.formatSystem()]), | ||||
|                       ), | ||||
|                       trailing: Row( | ||||
|                         mainAxisSize: MainAxisSize.min, | ||||
|                         children: [ | ||||
|                           IconButton( | ||||
|                             icon: const Icon(Symbols.delete, color: Colors.red), | ||||
|                             onPressed: () { | ||||
|                               showConfirmAlert( | ||||
|                                 'deleteSecretHint'.tr(), | ||||
|                                 'deleteSecret'.tr(), | ||||
|                               ).then((confirm) { | ||||
|                                 if (confirm) { | ||||
|                                   final client = ref.read(apiClientProvider); | ||||
|                                   client.delete( | ||||
|                                     '/develop/developers/$publisherName/projects/$projectId/apps/$appId/secrets/${secret.id}', | ||||
|                                   ); | ||||
|                                   ref.invalidate( | ||||
|                                     customAppSecretsProvider( | ||||
|                                       publisherName, | ||||
|                                       projectId, | ||||
|                                       appId, | ||||
|                                     ), | ||||
|                                   ); | ||||
|                                 } | ||||
|                               }); | ||||
|                             }, | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|       loading: () => const Center(child: CircularProgressIndicator()), | ||||
|       error: | ||||
|           (err, stack) => ResponseErrorWidget( | ||||
|             error: err, | ||||
|             onRetry: | ||||
|                 () => ref.invalidate( | ||||
|                   customAppSecretsProvider(publisherName, projectId, appId), | ||||
|                 ), | ||||
|           ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										188
									
								
								lib/screens/developers/app_secrets.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								lib/screens/developers/app_secrets.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,188 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'app_secrets.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$customAppSecretsHash() => r'1bc62ad812487883ce739793b22a76168d656752'; | ||||
|  | ||||
| /// 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 [customAppSecrets]. | ||||
| @ProviderFor(customAppSecrets) | ||||
| const customAppSecretsProvider = CustomAppSecretsFamily(); | ||||
|  | ||||
| /// See also [customAppSecrets]. | ||||
| class CustomAppSecretsFamily extends Family<AsyncValue<List<CustomAppSecret>>> { | ||||
|   /// See also [customAppSecrets]. | ||||
|   const CustomAppSecretsFamily(); | ||||
|  | ||||
|   /// See also [customAppSecrets]. | ||||
|   CustomAppSecretsProvider call( | ||||
|     String publisherName, | ||||
|     String projectId, | ||||
|     String appId, | ||||
|   ) { | ||||
|     return CustomAppSecretsProvider(publisherName, projectId, appId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   CustomAppSecretsProvider getProviderOverride( | ||||
|     covariant CustomAppSecretsProvider provider, | ||||
|   ) { | ||||
|     return call(provider.publisherName, provider.projectId, provider.appId); | ||||
|   } | ||||
|  | ||||
|   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'customAppSecretsProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [customAppSecrets]. | ||||
| class CustomAppSecretsProvider | ||||
|     extends AutoDisposeFutureProvider<List<CustomAppSecret>> { | ||||
|   /// See also [customAppSecrets]. | ||||
|   CustomAppSecretsProvider(String publisherName, String projectId, String appId) | ||||
|     : this._internal( | ||||
|         (ref) => customAppSecrets( | ||||
|           ref as CustomAppSecretsRef, | ||||
|           publisherName, | ||||
|           projectId, | ||||
|           appId, | ||||
|         ), | ||||
|         from: customAppSecretsProvider, | ||||
|         name: r'customAppSecretsProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$customAppSecretsHash, | ||||
|         dependencies: CustomAppSecretsFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             CustomAppSecretsFamily._allTransitiveDependencies, | ||||
|         publisherName: publisherName, | ||||
|         projectId: projectId, | ||||
|         appId: appId, | ||||
|       ); | ||||
|  | ||||
|   CustomAppSecretsProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.publisherName, | ||||
|     required this.projectId, | ||||
|     required this.appId, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String publisherName; | ||||
|   final String projectId; | ||||
|   final String appId; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<List<CustomAppSecret>> Function(CustomAppSecretsRef provider) | ||||
|     create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: CustomAppSecretsProvider._internal( | ||||
|         (ref) => create(ref as CustomAppSecretsRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         publisherName: publisherName, | ||||
|         projectId: projectId, | ||||
|         appId: appId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<List<CustomAppSecret>> createElement() { | ||||
|     return _CustomAppSecretsProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is CustomAppSecretsProvider && | ||||
|         other.publisherName == publisherName && | ||||
|         other.projectId == projectId && | ||||
|         other.appId == appId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, publisherName.hashCode); | ||||
|     hash = _SystemHash.combine(hash, projectId.hashCode); | ||||
|     hash = _SystemHash.combine(hash, appId.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin CustomAppSecretsRef | ||||
|     on AutoDisposeFutureProviderRef<List<CustomAppSecret>> { | ||||
|   /// The parameter `publisherName` of this provider. | ||||
|   String get publisherName; | ||||
|  | ||||
|   /// The parameter `projectId` of this provider. | ||||
|   String get projectId; | ||||
|  | ||||
|   /// The parameter `appId` of this provider. | ||||
|   String get appId; | ||||
| } | ||||
|  | ||||
| class _CustomAppSecretsProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<List<CustomAppSecret>> | ||||
|     with CustomAppSecretsRef { | ||||
|   _CustomAppSecretsProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get publisherName => | ||||
|       (origin as CustomAppSecretsProvider).publisherName; | ||||
|   @override | ||||
|   String get projectId => (origin as CustomAppSecretsProvider).projectId; | ||||
|   @override | ||||
|   String get appId => (origin as CustomAppSecretsProvider).appId; | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| @@ -14,6 +14,20 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| part 'apps.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| Future<CustomApp> customApp( | ||||
|   Ref ref, | ||||
|   String publisherName, | ||||
|   String projectId, | ||||
|   String appId, | ||||
| ) async { | ||||
|   final client = ref.watch(apiClientProvider); | ||||
|   final resp = await client.get( | ||||
|     '/develop/developers/$publisherName/projects/$projectId/apps/$appId', | ||||
|   ); | ||||
|   return CustomApp.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<List<CustomApp>> customApps( | ||||
|   Ref ref, | ||||
| @@ -81,6 +95,18 @@ class CustomAppsScreen extends HookConsumerWidget { | ||||
|               final app = data[index]; | ||||
|               return Card( | ||||
|                 margin: const EdgeInsets.all(8.0), | ||||
|                 clipBehavior: Clip.antiAlias, | ||||
|                 child: InkWell( | ||||
|                   onTap: () { | ||||
|                     context.pushNamed( | ||||
|                       'developerAppDetail', | ||||
|                       pathParameters: { | ||||
|                         'name': publisherName, | ||||
|                         'projectId': projectId, | ||||
|                         'appId': app.id, | ||||
|                       }, | ||||
|                     ); | ||||
|                   }, | ||||
|                   child: Column( | ||||
|                     children: [ | ||||
|                       SizedBox( | ||||
| @@ -164,7 +190,10 @@ class CustomAppsScreen extends HookConsumerWidget { | ||||
|                                     '/develop/developers/$publisherName/projects/$projectId/apps/${app.id}', | ||||
|                                   ); | ||||
|                                   ref.invalidate( | ||||
|                                   customAppsProvider(publisherName, projectId), | ||||
|                                     customAppsProvider( | ||||
|                                       publisherName, | ||||
|                                       projectId, | ||||
|                                     ), | ||||
|                                   ); | ||||
|                                 } | ||||
|                               }); | ||||
| @@ -174,6 +203,7 @@ class CustomAppsScreen extends HookConsumerWidget { | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|   | ||||
| @@ -6,7 +6,7 @@ part of 'apps.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$customAppsHash() => r'c36e5ee59f16a29220dc0e9fba65e579d341a28f'; | ||||
| String _$customAppHash() => r'be05431ba8bf06fd20ee988a61c3663a68e15fc9'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
| @@ -29,6 +29,148 @@ class _SystemHash { | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// See also [customApp]. | ||||
| @ProviderFor(customApp) | ||||
| const customAppProvider = CustomAppFamily(); | ||||
|  | ||||
| /// See also [customApp]. | ||||
| class CustomAppFamily extends Family<AsyncValue<CustomApp>> { | ||||
|   /// See also [customApp]. | ||||
|   const CustomAppFamily(); | ||||
|  | ||||
|   /// See also [customApp]. | ||||
|   CustomAppProvider call(String publisherName, String projectId, String appId) { | ||||
|     return CustomAppProvider(publisherName, projectId, appId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   CustomAppProvider getProviderOverride(covariant CustomAppProvider provider) { | ||||
|     return call(provider.publisherName, provider.projectId, provider.appId); | ||||
|   } | ||||
|  | ||||
|   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'customAppProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [customApp]. | ||||
| class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp> { | ||||
|   /// See also [customApp]. | ||||
|   CustomAppProvider(String publisherName, String projectId, String appId) | ||||
|     : this._internal( | ||||
|         (ref) => | ||||
|             customApp(ref as CustomAppRef, publisherName, projectId, appId), | ||||
|         from: customAppProvider, | ||||
|         name: r'customAppProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$customAppHash, | ||||
|         dependencies: CustomAppFamily._dependencies, | ||||
|         allTransitiveDependencies: CustomAppFamily._allTransitiveDependencies, | ||||
|         publisherName: publisherName, | ||||
|         projectId: projectId, | ||||
|         appId: appId, | ||||
|       ); | ||||
|  | ||||
|   CustomAppProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.publisherName, | ||||
|     required this.projectId, | ||||
|     required this.appId, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String publisherName; | ||||
|   final String projectId; | ||||
|   final String appId; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<CustomApp> Function(CustomAppRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: CustomAppProvider._internal( | ||||
|         (ref) => create(ref as CustomAppRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         publisherName: publisherName, | ||||
|         projectId: projectId, | ||||
|         appId: appId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<CustomApp> createElement() { | ||||
|     return _CustomAppProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is CustomAppProvider && | ||||
|         other.publisherName == publisherName && | ||||
|         other.projectId == projectId && | ||||
|         other.appId == appId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, publisherName.hashCode); | ||||
|     hash = _SystemHash.combine(hash, projectId.hashCode); | ||||
|     hash = _SystemHash.combine(hash, appId.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin CustomAppRef on AutoDisposeFutureProviderRef<CustomApp> { | ||||
|   /// The parameter `publisherName` of this provider. | ||||
|   String get publisherName; | ||||
|  | ||||
|   /// The parameter `projectId` of this provider. | ||||
|   String get projectId; | ||||
|  | ||||
|   /// The parameter `appId` of this provider. | ||||
|   String get appId; | ||||
| } | ||||
|  | ||||
| class _CustomAppProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<CustomApp> | ||||
|     with CustomAppRef { | ||||
|   _CustomAppProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get publisherName => (origin as CustomAppProvider).publisherName; | ||||
|   @override | ||||
|   String get projectId => (origin as CustomAppProvider).projectId; | ||||
|   @override | ||||
|   String get appId => (origin as CustomAppProvider).appId; | ||||
| } | ||||
|  | ||||
| String _$customAppsHash() => r'450bedaf4220b8963cb44afeb14d4c0e80f01b11'; | ||||
|  | ||||
| /// See also [customApps]. | ||||
| @ProviderFor(customApps) | ||||
| const customAppsProvider = CustomAppsFamily(); | ||||
|   | ||||
							
								
								
									
										161
									
								
								lib/screens/developers/bot_detail.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								lib/screens/developers/bot_detail.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| 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/bot.dart'; | ||||
| import 'package:island/screens/developers/bot_keys.dart'; | ||||
| import 'package:island/screens/developers/edit_bot.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/response.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| class BotDetailScreen extends HookConsumerWidget { | ||||
|   final String publisherName; | ||||
|   final String projectId; | ||||
|   final String botId; | ||||
|  | ||||
|   const BotDetailScreen({ | ||||
|     super.key, | ||||
|     required this.publisherName, | ||||
|     required this.projectId, | ||||
|     required this.botId, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final tabController = useTabController(initialLength: 2); | ||||
|     final botData = ref.watch(botProvider(publisherName, projectId, botId)); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text(botData.value?.account.nick ?? 'botDetails'.tr()), | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             icon: const Icon(Symbols.edit), | ||||
|             onPressed: | ||||
|                 botData.value == null | ||||
|                     ? null | ||||
|                     : () { | ||||
|                       context.pushNamed( | ||||
|                         'developerBotEdit', | ||||
|                         pathParameters: { | ||||
|                           'name': publisherName, | ||||
|                           'projectId': projectId, | ||||
|                           'id': botId, | ||||
|                         }, | ||||
|                       ); | ||||
|                     }, | ||||
|           ), | ||||
|         ], | ||||
|         bottom: TabBar( | ||||
|           controller: tabController, | ||||
|           tabs: [ | ||||
|             Tab( | ||||
|               child: Text( | ||||
|                 'overview'.tr(), | ||||
|                 textAlign: TextAlign.center, | ||||
|                 style: TextStyle( | ||||
|                   color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             Tab( | ||||
|               child: Text( | ||||
|                 'keys'.tr(), | ||||
|                 textAlign: TextAlign.center, | ||||
|                 style: TextStyle( | ||||
|                   color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|       body: botData.when( | ||||
|         data: (bot) { | ||||
|           if (bot == null) { | ||||
|             return Center(child: Text('botNotFound'.tr())); | ||||
|           } | ||||
|           return TabBarView( | ||||
|             controller: tabController, | ||||
|             physics: const NeverScrollableScrollPhysics(), | ||||
|             children: [ | ||||
|               _BotOverview(bot: bot), | ||||
|               BotKeysScreen( | ||||
|                 publisherName: publisherName, | ||||
|                 projectId: projectId, | ||||
|                 botId: botId, | ||||
|               ), | ||||
|             ], | ||||
|           ); | ||||
|         }, | ||||
|         loading: () => const Center(child: CircularProgressIndicator()), | ||||
|         error: | ||||
|             (err, stack) => ResponseErrorWidget( | ||||
|               error: err, | ||||
|               onRetry: | ||||
|                   () => ref.invalidate( | ||||
|                     botProvider(publisherName, projectId, botId), | ||||
|                   ), | ||||
|             ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _BotOverview extends StatelessWidget { | ||||
|   final Bot bot; | ||||
|   const _BotOverview({required this.bot}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return SingleChildScrollView( | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           AspectRatio( | ||||
|             aspectRatio: 16 / 7, | ||||
|             child: Stack( | ||||
|               clipBehavior: Clip.none, | ||||
|               fit: StackFit.expand, | ||||
|               children: [ | ||||
|                 Container( | ||||
|                   color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                   child: | ||||
|                       bot.account.profile.background != null | ||||
|                           ? CloudFileWidget( | ||||
|                             item: bot.account.profile.background!, | ||||
|                             fit: BoxFit.cover, | ||||
|                           ) | ||||
|                           : const SizedBox.shrink(), | ||||
|                 ), | ||||
|                 Positioned( | ||||
|                   left: 20, | ||||
|                   bottom: -32, | ||||
|                   child: ProfilePictureWidget( | ||||
|                     fileId: bot.account.profile.picture?.id, | ||||
|                     radius: 40, | ||||
|                     fallbackIcon: Symbols.smart_toy, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ).padding(bottom: 32), | ||||
|           ListTile(title: Text('name'.tr()), subtitle: Text(bot.account.name)), | ||||
|           ListTile( | ||||
|             title: Text('nickname'.tr()), | ||||
|             subtitle: Text(bot.account.nick), | ||||
|           ), | ||||
|           ListTile(title: Text('slug'.tr()), subtitle: Text(bot.slug)), | ||||
|           if (bot.account.profile.bio.isNotEmpty) | ||||
|             ListTile( | ||||
|               title: Text('bio'.tr()), | ||||
|               subtitle: Text(bot.account.profile.bio), | ||||
|             ), | ||||
|         ], | ||||
|       ).padding(bottom: 24), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										278
									
								
								lib/screens/developers/bot_keys.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								lib/screens/developers/bot_keys.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,278 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/bot_key.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/services/time.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:island/widgets/response.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
|  | ||||
| part 'bot_keys.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| Future<List<SnAccountApiKey>> botKeys( | ||||
|   Ref ref, | ||||
|   String publisherName, | ||||
|   String projectId, | ||||
|   String botId, | ||||
| ) async { | ||||
|   final client = ref.watch(apiClientProvider); | ||||
|   final resp = await client.get( | ||||
|     '/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys', | ||||
|   ); | ||||
|   return (resp.data as List).map((e) => SnAccountApiKey.fromJson(e)).toList(); | ||||
| } | ||||
|  | ||||
| class BotKeysScreen extends HookConsumerWidget { | ||||
|   final String publisherName; | ||||
|   final String projectId; | ||||
|   final String botId; | ||||
|  | ||||
|   const BotKeysScreen({ | ||||
|     super.key, | ||||
|     required this.publisherName, | ||||
|     required this.projectId, | ||||
|     required this.botId, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final keys = ref.watch(botKeysProvider(publisherName, projectId, botId)); | ||||
|     final keyNameController = useTextEditingController(); | ||||
|  | ||||
|     void showNewKeySheet(SnAccountApiKey newApiKey) { | ||||
|       final token = newApiKey.key; | ||||
|       if (token == null) return; | ||||
|  | ||||
|       showModalBottomSheet( | ||||
|         context: context, | ||||
|         isScrollControlled: true, | ||||
|         builder: | ||||
|             (context) => SheetScaffold( | ||||
|               titleText: 'newKeyGenerated'.tr(), | ||||
|               child: Padding( | ||||
|                 padding: const EdgeInsets.all(20.0), | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                   mainAxisSize: MainAxisSize.min, | ||||
|                   children: [ | ||||
|                     Text('copyKeyHint'.tr()), | ||||
|                     const SizedBox(height: 16), | ||||
|                     Container( | ||||
|                       padding: const EdgeInsets.all(12), | ||||
|                       decoration: BoxDecoration( | ||||
|                         color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                         borderRadius: BorderRadius.circular(8), | ||||
|                       ), | ||||
|                       child: SelectableText(token), | ||||
|                     ), | ||||
|                     const SizedBox(height: 20), | ||||
|                     FilledButton.icon( | ||||
|                       onPressed: () { | ||||
|                         Clipboard.setData(ClipboardData(text: token)); | ||||
|                       }, | ||||
|                       icon: const Icon(Symbols.copy_all), | ||||
|                       label: Text('copy'.tr()), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|       ).whenComplete(() { | ||||
|         ref.invalidate(botKeysProvider(publisherName, projectId, botId)); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     void createKey() { | ||||
|       keyNameController.clear(); | ||||
|       showModalBottomSheet( | ||||
|         context: context, | ||||
|         isScrollControlled: true, | ||||
|         builder: | ||||
|             (context) => SheetScaffold( | ||||
|               titleText: 'newBotKey'.tr(), | ||||
|               child: Padding( | ||||
|                 padding: const EdgeInsets.all(20.0), | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                   mainAxisSize: MainAxisSize.min, | ||||
|                   children: [ | ||||
|                     TextFormField( | ||||
|                       controller: keyNameController, | ||||
|                       decoration: InputDecoration(labelText: 'keyName'.tr()), | ||||
|                       autofocus: true, | ||||
|                     ), | ||||
|                     const SizedBox(height: 20), | ||||
|                     FilledButton.icon( | ||||
|                       onPressed: () async { | ||||
|                         if (keyNameController.text.isEmpty) return; | ||||
|                         final keyName = keyNameController.text; | ||||
|                         Navigator.pop(context); // Close the sheet | ||||
|                         try { | ||||
|                           final client = ref.read(apiClientProvider); | ||||
|                           final resp = await client.post( | ||||
|                             '/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys', | ||||
|                             data: {'label': keyName}, | ||||
|                           ); | ||||
|                           final newApiKey = SnAccountApiKey.fromJson(resp.data); | ||||
|                           showNewKeySheet(newApiKey); | ||||
|                         } catch (e) { | ||||
|                           showErrorAlert(e.toString()); | ||||
|                         } | ||||
|                       }, | ||||
|                       icon: const Icon(Symbols.add), | ||||
|                       label: Text('create'.tr()), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     void rotateKey(String keyId) { | ||||
|       showConfirmAlert('rotateBotKeyHint'.tr(), 'rotateBotKey'.tr()).then(( | ||||
|         confirm, | ||||
|       ) async { | ||||
|         if (confirm) { | ||||
|           try { | ||||
|             if (context.mounted) showLoadingModal(context); | ||||
|             final client = ref.read(apiClientProvider); | ||||
|             final resp = await client.post( | ||||
|               '/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys/$keyId/rotate', | ||||
|             ); | ||||
|             final rotatedApiKey = SnAccountApiKey.fromJson(resp.data); | ||||
|             showNewKeySheet(rotatedApiKey); | ||||
|           } catch (err) { | ||||
|             showErrorAlert(err.toString()); | ||||
|           } finally { | ||||
|             if (context.mounted) hideLoadingModal(context); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     void revokeKey(String keyId) { | ||||
|       showConfirmAlert('revokeBotKeyHint'.tr(), 'revokeBotKey'.tr()).then(( | ||||
|         confirm, | ||||
|       ) { | ||||
|         if (confirm) { | ||||
|           final client = ref.read(apiClientProvider); | ||||
|           client | ||||
|               .delete( | ||||
|                 '/develop/developers/$publisherName/projects/$projectId/bots/$botId/keys/$keyId', | ||||
|               ) | ||||
|               .then((_) { | ||||
|                 ref.invalidate( | ||||
|                   botKeysProvider(publisherName, projectId, botId), | ||||
|                 ); | ||||
|               }) | ||||
|               .catchError((err) { | ||||
|                 showErrorAlert(err.toString()); | ||||
|               }); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     return keys.when( | ||||
|       data: (data) { | ||||
|         return Column( | ||||
|           children: [ | ||||
|             ListTile( | ||||
|               leading: const Icon(Symbols.add), | ||||
|               title: Text('newBotKey'.tr()), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               onTap: createKey, | ||||
|             ), | ||||
|             const Divider(height: 1), | ||||
|             Expanded( | ||||
|               child: | ||||
|                   data.isEmpty | ||||
|                       ? Center(child: Text('noBotKeys'.tr())) | ||||
|                       : RefreshIndicator( | ||||
|                         onRefresh: | ||||
|                             () => ref.refresh( | ||||
|                               botKeysProvider( | ||||
|                                 publisherName, | ||||
|                                 projectId, | ||||
|                                 botId, | ||||
|                               ).future, | ||||
|                             ), | ||||
|                         child: ListView.builder( | ||||
|                           padding: EdgeInsets.zero, | ||||
|                           itemCount: data.length, | ||||
|                           itemBuilder: (context, index) { | ||||
|                             final apiKey = data[index]; | ||||
|                             return ListTile( | ||||
|                               title: Text(apiKey.label), | ||||
|                               subtitle: Text(apiKey.createdAt.formatSystem()), | ||||
|                               contentPadding: EdgeInsets.only( | ||||
|                                 left: 16, | ||||
|                                 right: 12, | ||||
|                               ), | ||||
|                               trailing: PopupMenuButton( | ||||
|                                 itemBuilder: | ||||
|                                     (context) => [ | ||||
|                                       PopupMenuItem( | ||||
|                                         value: 'rotate', | ||||
|                                         child: Row( | ||||
|                                           children: [ | ||||
|                                             const Icon(Symbols.refresh), | ||||
|                                             const Gap(12), | ||||
|                                             Text('rotateKey'.tr()), | ||||
|                                           ], | ||||
|                                         ), | ||||
|                                       ), | ||||
|                                       PopupMenuItem( | ||||
|                                         value: 'revoke', | ||||
|                                         child: Row( | ||||
|                                           children: [ | ||||
|                                             const Icon( | ||||
|                                               Symbols.delete, | ||||
|                                               color: Colors.red, | ||||
|                                             ), | ||||
|                                             const Gap(12), | ||||
|                                             Text( | ||||
|                                               'revoke'.tr(), | ||||
|                                               style: TextStyle( | ||||
|                                                 color: Colors.red, | ||||
|                                               ), | ||||
|                                             ), | ||||
|                                           ], | ||||
|                                         ), | ||||
|                                       ), | ||||
|                                     ], | ||||
|                                 onSelected: (value) { | ||||
|                                   if (value == 'rotate') { | ||||
|                                     rotateKey(apiKey.id); | ||||
|                                   } else if (value == 'revoke') { | ||||
|                                     revokeKey(apiKey.id); | ||||
|                                   } | ||||
|                                 }, | ||||
|                               ), | ||||
|                             ); | ||||
|                           }, | ||||
|                         ), | ||||
|                       ), | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       }, | ||||
|       loading: () => const Center(child: CircularProgressIndicator()), | ||||
|       error: | ||||
|           (err, stack) => ResponseErrorWidget( | ||||
|             error: err, | ||||
|             onRetry: | ||||
|                 () => ref.invalidate( | ||||
|                   botKeysProvider(publisherName, projectId, botId), | ||||
|                 ), | ||||
|           ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										172
									
								
								lib/screens/developers/bot_keys.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								lib/screens/developers/bot_keys.g.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| part of 'bot_keys.dart'; | ||||
|  | ||||
| // ************************************************************************** | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$botKeysHash() => r'f7d1121833dc3da0cbd84b6171c2b2539edeb785'; | ||||
|  | ||||
| /// 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 [botKeys]. | ||||
| @ProviderFor(botKeys) | ||||
| const botKeysProvider = BotKeysFamily(); | ||||
|  | ||||
| /// See also [botKeys]. | ||||
| class BotKeysFamily extends Family<AsyncValue<List<SnAccountApiKey>>> { | ||||
|   /// See also [botKeys]. | ||||
|   const BotKeysFamily(); | ||||
|  | ||||
|   /// See also [botKeys]. | ||||
|   BotKeysProvider call(String publisherName, String projectId, String botId) { | ||||
|     return BotKeysProvider(publisherName, projectId, botId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   BotKeysProvider getProviderOverride(covariant BotKeysProvider provider) { | ||||
|     return call(provider.publisherName, provider.projectId, provider.botId); | ||||
|   } | ||||
|  | ||||
|   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'botKeysProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [botKeys]. | ||||
| class BotKeysProvider extends AutoDisposeFutureProvider<List<SnAccountApiKey>> { | ||||
|   /// See also [botKeys]. | ||||
|   BotKeysProvider(String publisherName, String projectId, String botId) | ||||
|     : this._internal( | ||||
|         (ref) => botKeys(ref as BotKeysRef, publisherName, projectId, botId), | ||||
|         from: botKeysProvider, | ||||
|         name: r'botKeysProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$botKeysHash, | ||||
|         dependencies: BotKeysFamily._dependencies, | ||||
|         allTransitiveDependencies: BotKeysFamily._allTransitiveDependencies, | ||||
|         publisherName: publisherName, | ||||
|         projectId: projectId, | ||||
|         botId: botId, | ||||
|       ); | ||||
|  | ||||
|   BotKeysProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.publisherName, | ||||
|     required this.projectId, | ||||
|     required this.botId, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String publisherName; | ||||
|   final String projectId; | ||||
|   final String botId; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<List<SnAccountApiKey>> Function(BotKeysRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: BotKeysProvider._internal( | ||||
|         (ref) => create(ref as BotKeysRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         publisherName: publisherName, | ||||
|         projectId: projectId, | ||||
|         botId: botId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<List<SnAccountApiKey>> createElement() { | ||||
|     return _BotKeysProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is BotKeysProvider && | ||||
|         other.publisherName == publisherName && | ||||
|         other.projectId == projectId && | ||||
|         other.botId == botId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, publisherName.hashCode); | ||||
|     hash = _SystemHash.combine(hash, projectId.hashCode); | ||||
|     hash = _SystemHash.combine(hash, botId.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin BotKeysRef on AutoDisposeFutureProviderRef<List<SnAccountApiKey>> { | ||||
|   /// The parameter `publisherName` of this provider. | ||||
|   String get publisherName; | ||||
|  | ||||
|   /// The parameter `projectId` of this provider. | ||||
|   String get projectId; | ||||
|  | ||||
|   /// The parameter `botId` of this provider. | ||||
|   String get botId; | ||||
| } | ||||
|  | ||||
| class _BotKeysProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<List<SnAccountApiKey>> | ||||
|     with BotKeysRef { | ||||
|   _BotKeysProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get publisherName => (origin as BotKeysProvider).publisherName; | ||||
|   @override | ||||
|   String get projectId => (origin as BotKeysProvider).projectId; | ||||
|   @override | ||||
|   String get botId => (origin as BotKeysProvider).botId; | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| @@ -9,6 +9,7 @@ import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/response.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:island/widgets/extended_refresh_indicator.dart'; | ||||
|  | ||||
| part 'bots.g.dart'; | ||||
|  | ||||
| @@ -60,7 +61,7 @@ class BotsScreen extends HookConsumerWidget { | ||||
|             ), | ||||
|           ); | ||||
|         } | ||||
|         return RefreshIndicator( | ||||
|         return ExtendedRefreshIndicator( | ||||
|           onRefresh: | ||||
|               () => ref.refresh(botsProvider(publisherName, projectId).future), | ||||
|           child: ListView.builder( | ||||
| @@ -71,14 +72,19 @@ class BotsScreen extends HookConsumerWidget { | ||||
|               return Card( | ||||
|                 margin: const EdgeInsets.all(8.0), | ||||
|                 child: ListTile( | ||||
|                   shape: const RoundedRectangleBorder( | ||||
|                     borderRadius: BorderRadius.all(Radius.circular(8.0)), | ||||
|                   ), | ||||
|                   leading: CircleAvatar( | ||||
|                     child: | ||||
|                         bot.picture != null | ||||
|                             ? CloudFileWidget(item: bot.picture!) | ||||
|                         bot.account.profile.picture != null | ||||
|                             ? ProfilePictureWidget( | ||||
|                               file: bot.account.profile.picture!, | ||||
|                             ) | ||||
|                             : const Icon(Symbols.smart_toy), | ||||
|                   ), | ||||
|                   title: Text(bot.name), | ||||
|                   subtitle: Text(bot.description ?? ''), | ||||
|                   title: Text(bot.account.nick), | ||||
|                   subtitle: Text(bot.account.name), | ||||
|                   trailing: PopupMenuButton( | ||||
|                     itemBuilder: | ||||
|                         (context) => [ | ||||
| @@ -140,7 +146,7 @@ class BotsScreen extends HookConsumerWidget { | ||||
|                       pathParameters: { | ||||
|                         'name': publisherName, | ||||
|                         'projectId': projectId, | ||||
|                         'id': bot.id, | ||||
|                         'botId': bot.id, | ||||
|                       }, | ||||
|                     ); | ||||
|                   }, | ||||
|   | ||||
| @@ -6,7 +6,7 @@ part of 'bots.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$botsHash() => r'a54c8b4df23f94754398706779044903fcca6eea'; | ||||
| String _$botsHash() => r'15cefd5781350eb68208a342e85fcb0b9e0e3269'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
| @@ -297,6 +297,8 @@ class EditAppScreen extends HookConsumerWidget { | ||||
|                 } | ||||
|                 : null, | ||||
|       }; | ||||
|       try { | ||||
|         showLoadingModal(context); | ||||
|         if (isNew) { | ||||
|           await client.post( | ||||
|             '/develop/developers/$publisherName/projects/$projectId/apps', | ||||
| @@ -308,6 +310,12 @@ class EditAppScreen extends HookConsumerWidget { | ||||
|             data: data, | ||||
|           ); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|         return; | ||||
|       } finally { | ||||
|         if (context.mounted) hideLoadingModal(context); | ||||
|       } | ||||
|       ref.invalidate(customAppsProvider(publisherName, projectId)); | ||||
|       if (context.mounted) { | ||||
|         Navigator.pop(context); | ||||
|   | ||||
| @@ -6,7 +6,7 @@ part of 'edit_app.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$customAppHash() => r'17b3d1385e59bc5ee7f13fb0f11c56cf8a9ba41f'; | ||||
| String _$customAppHash() => r'8e1b38f3dc9b04fad362ee1141fcbfc53f008c09'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
| @@ -55,31 +55,44 @@ class EditBotScreen extends HookConsumerWidget { | ||||
|     final submitting = useState(false); | ||||
|  | ||||
|     final nameController = useTextEditingController(); | ||||
|     final nickController = useTextEditingController(); | ||||
|     final slugController = useTextEditingController(); | ||||
|     final descriptionController = useTextEditingController(); | ||||
|     final picture = useState<SnCloudFile?>(null); | ||||
|     final websiteController = useTextEditingController(); | ||||
|     final documentationController = useTextEditingController(); | ||||
|  | ||||
|     final isPublic = useState(false); | ||||
|     final isInteractive = useState(false); | ||||
|     final firstNameController = useTextEditingController(); | ||||
|     final middleNameController = useTextEditingController(); | ||||
|     final lastNameController = useTextEditingController(); | ||||
|     final genderController = useTextEditingController(); | ||||
|     final pronounsController = useTextEditingController(); | ||||
|     final locationController = useTextEditingController(); | ||||
|     final timeZoneController = useTextEditingController(); | ||||
|     final bioController = useTextEditingController(); | ||||
|     final birthday = useState<DateTime?>(null); | ||||
|     final background = useState<SnCloudFile?>(null); | ||||
|  | ||||
|     useEffect(() { | ||||
|       if (botData?.value != null) { | ||||
|         nameController.text = botData!.value!.name; | ||||
|         nameController.text = botData!.value!.account.name; | ||||
|         nickController.text = botData.value!.account.nick; | ||||
|         slugController.text = botData.value!.slug; | ||||
|         descriptionController.text = botData.value!.description ?? ''; | ||||
|         picture.value = botData.value!.picture; | ||||
|         websiteController.text = botData.value!.links?.website ?? ''; | ||||
|         documentationController.text = | ||||
|             botData.value!.links?.documentation ?? ''; | ||||
|         isPublic.value = botData.value!.config?.isPublic ?? false; | ||||
|         isInteractive.value = botData.value!.config?.isInteractive ?? false; | ||||
|         picture.value = botData.value!.account.profile.picture; | ||||
|         background.value = botData.value!.account.profile.background; | ||||
|  | ||||
|         // Populate from botData.value.account.profile | ||||
|         firstNameController.text = botData.value!.account.profile.firstName; | ||||
|         middleNameController.text = botData.value!.account.profile.middleName; | ||||
|         lastNameController.text = botData.value!.account.profile.lastName; | ||||
|         genderController.text = botData.value!.account.profile.gender; | ||||
|         pronounsController.text = botData.value!.account.profile.pronouns; | ||||
|         locationController.text = botData.value!.account.profile.location; | ||||
|         timeZoneController.text = botData.value!.account.profile.timeZone; | ||||
|         bioController.text = botData.value!.account.profile.bio; | ||||
|         birthday.value = botData.value!.account.profile.birthday?.toLocal(); | ||||
|       } | ||||
|       return null; | ||||
|     }, [botData]); | ||||
|  | ||||
|     void setPicture() async { | ||||
|     void setPicture(String position) async { | ||||
|       showLoadingModal(context); | ||||
|       var result = await ref | ||||
|           .read(imagePickerProvider) | ||||
| @@ -94,7 +107,12 @@ class EditBotScreen extends HookConsumerWidget { | ||||
|       result = await cropImage( | ||||
|         context, | ||||
|         image: result, | ||||
|         allowedAspectRatios: [const CropAspectRatio(height: 1, width: 1)], | ||||
|         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); | ||||
| @@ -122,7 +140,12 @@ class EditBotScreen extends HookConsumerWidget { | ||||
|         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 { | ||||
| @@ -135,23 +158,23 @@ class EditBotScreen extends HookConsumerWidget { | ||||
|       final client = ref.read(apiClientProvider); | ||||
|       final data = { | ||||
|         'name': nameController.text, | ||||
|         'nick': nickController.text, | ||||
|         'slug': slugController.text, | ||||
|         'description': descriptionController.text, | ||||
|         'picture_id': picture.value?.id, | ||||
|         'config': { | ||||
|           'is_public': isPublic.value, | ||||
|           'is_interactive': isInteractive.value, | ||||
|         }, | ||||
|         'links': { | ||||
|           'website': | ||||
|               websiteController.text.isNotEmpty ? websiteController.text : null, | ||||
|           'documentation': | ||||
|               documentationController.text.isNotEmpty | ||||
|                   ? documentationController.text | ||||
|                   : null, | ||||
|         }, | ||||
|         'background_id': background.value?.id, | ||||
|         'first_name': firstNameController.text, | ||||
|         'middle_name': middleNameController.text, | ||||
|         'last_name': lastNameController.text, | ||||
|         'gender': genderController.text, | ||||
|         'pronouns': pronounsController.text, | ||||
|         'location': locationController.text, | ||||
|         'time_zone': timeZoneController.text, | ||||
|         'bio': bioController.text, | ||||
|         'birthday': birthday.value?.toUtc().toIso8601String(), | ||||
|       }; | ||||
|  | ||||
|       try { | ||||
|         showLoadingModal(context); | ||||
|         if (isNew) { | ||||
|           await client.post( | ||||
|             '/develop/developers/$publisherName/projects/$projectId/bots', | ||||
| @@ -167,6 +190,11 @@ class EditBotScreen extends HookConsumerWidget { | ||||
|         if (context.mounted) { | ||||
|           context.pop(); | ||||
|         } | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
|       } finally { | ||||
|         if (context.mounted) hideLoadingModal(context); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return AppScaffold( | ||||
| @@ -186,22 +214,44 @@ class EditBotScreen extends HookConsumerWidget { | ||||
|                 child: Column( | ||||
|                   children: [ | ||||
|                     AspectRatio( | ||||
|                       aspectRatio: 1, | ||||
|                       child: GestureDetector( | ||||
|                         onTap: setPicture, | ||||
|                       aspectRatio: 16 / 7, | ||||
|                       child: Stack( | ||||
|                         clipBehavior: Clip.none, | ||||
|                         fit: StackFit.expand, | ||||
|                         children: [ | ||||
|                           GestureDetector( | ||||
|                             child: Container( | ||||
|                               color: | ||||
|                                   Theme.of( | ||||
|                                     context, | ||||
|                                   ).colorScheme.surfaceContainerHigh, | ||||
|                               child: | ||||
|                               picture.value != null | ||||
|                                   background.value != null | ||||
|                                       ? CloudFileWidget( | ||||
|                                     item: picture.value!, | ||||
|                                         item: background.value!, | ||||
|                                         fit: BoxFit.cover, | ||||
|                                       ) | ||||
|                                   : const Icon(Symbols.smart_toy, size: 48), | ||||
|                                       : const SizedBox.shrink(), | ||||
|                             ), | ||||
|                             onTap: () { | ||||
|                               setPicture('background'); | ||||
|                             }, | ||||
|                           ), | ||||
|                           Positioned( | ||||
|                             left: 20, | ||||
|                             bottom: -32, | ||||
|                             child: GestureDetector( | ||||
|                               child: ProfilePictureWidget( | ||||
|                                 fileId: picture.value?.id, | ||||
|                                 radius: 40, | ||||
|                                 fallbackIcon: Symbols.smart_toy, | ||||
|                               ), | ||||
|                               onTap: () { | ||||
|                                 setPicture('picture'); | ||||
|                               }, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ).padding(bottom: 32), | ||||
|                     Form( | ||||
| @@ -213,6 +263,14 @@ class EditBotScreen extends HookConsumerWidget { | ||||
|                             decoration: InputDecoration(labelText: 'name'.tr()), | ||||
|                           ), | ||||
|                           const SizedBox(height: 16), | ||||
|                           TextFormField( | ||||
|                             controller: nickController, | ||||
|                             decoration: InputDecoration( | ||||
|                               labelText: 'nickname'.tr(), | ||||
|                               alignLabelWithHint: true, | ||||
|                             ), | ||||
|                           ), | ||||
|                           const SizedBox(height: 16), | ||||
|                           TextFormField( | ||||
|                             controller: slugController, | ||||
|                             decoration: InputDecoration( | ||||
| @@ -222,41 +280,129 @@ class EditBotScreen extends HookConsumerWidget { | ||||
|                           ), | ||||
|                           const SizedBox(height: 16), | ||||
|                           TextFormField( | ||||
|                             controller: descriptionController, | ||||
|                             controller: bioController, | ||||
|                             decoration: InputDecoration( | ||||
|                               labelText: 'description'.tr(), | ||||
|                               labelText: 'bio'.tr(), | ||||
|                               alignLabelWithHint: true, | ||||
|                             ), | ||||
|                             maxLines: 3, | ||||
|                           ), | ||||
|                           const SizedBox(height: 16), | ||||
|                           TextFormField( | ||||
|                             controller: websiteController, | ||||
|                           Row( | ||||
|                             spacing: 16, | ||||
|                             children: [ | ||||
|                               Expanded( | ||||
|                                 child: TextFormField( | ||||
|                                   controller: firstNameController, | ||||
|                                   decoration: InputDecoration( | ||||
|                               labelText: 'websiteUrl'.tr(), | ||||
|                               hintText: 'https://example.com', | ||||
|                                     labelText: 'firstName'.tr(), | ||||
|                                   ), | ||||
|                             keyboardType: TextInputType.url, | ||||
|                                 ), | ||||
|                               ), | ||||
|                               Expanded( | ||||
|                                 child: TextFormField( | ||||
|                                   controller: middleNameController, | ||||
|                                   decoration: InputDecoration( | ||||
|                                     labelText: 'middleName'.tr(), | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                               Expanded( | ||||
|                                 child: TextFormField( | ||||
|                                   controller: lastNameController, | ||||
|                                   decoration: InputDecoration( | ||||
|                                     labelText: 'lastName'.tr(), | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                           const SizedBox(height: 16), | ||||
|                           TextFormField( | ||||
|                             controller: documentationController, | ||||
|                           Row( | ||||
|                             spacing: 16, | ||||
|                             children: [ | ||||
|                               Expanded( | ||||
|                                 child: TextFormField( | ||||
|                                   controller: genderController, | ||||
|                                   decoration: InputDecoration( | ||||
|                               labelText: 'documentationUrl'.tr(), | ||||
|                               hintText: 'https://example.com/docs', | ||||
|                                     labelText: 'gender'.tr(), | ||||
|                                   ), | ||||
|                             keyboardType: TextInputType.url, | ||||
|                                 ), | ||||
|                               ), | ||||
|                               Expanded( | ||||
|                                 child: TextFormField( | ||||
|                                   controller: pronounsController, | ||||
|                                   decoration: InputDecoration( | ||||
|                                     labelText: 'pronouns'.tr(), | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                           const SizedBox(height: 16), | ||||
|                           SwitchListTile( | ||||
|                             title: Text('isPublic').tr(), | ||||
|                             value: isPublic.value, | ||||
|                             onChanged: (value) => isPublic.value = value, | ||||
|                           Row( | ||||
|                             spacing: 16, | ||||
|                             children: [ | ||||
|                               Expanded( | ||||
|                                 child: TextFormField( | ||||
|                                   controller: locationController, | ||||
|                                   decoration: InputDecoration( | ||||
|                                     labelText: 'location'.tr(), | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                               Expanded( | ||||
|                                 child: TextFormField( | ||||
|                                   controller: timeZoneController, | ||||
|                                   decoration: InputDecoration( | ||||
|                                     labelText: 'timeZone'.tr(), | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                           const SizedBox(height: 16), | ||||
|                           GestureDetector( | ||||
|                             onTap: () async { | ||||
|                               final date = await showDatePicker( | ||||
|                                 context: context, | ||||
|                                 initialDate: birthday.value ?? DateTime.now(), | ||||
|                                 firstDate: DateTime(1900), | ||||
|                                 lastDate: DateTime.now(), | ||||
|                               ); | ||||
|                               if (date != null) { | ||||
|                                 birthday.value = date; | ||||
|                               } | ||||
|                             }, | ||||
|                             child: Container( | ||||
|                               padding: const EdgeInsets.symmetric(vertical: 8), | ||||
|                               decoration: BoxDecoration( | ||||
|                                 border: Border( | ||||
|                                   bottom: BorderSide( | ||||
|                                     color: Theme.of(context).dividerColor, | ||||
|                                     width: 1, | ||||
|                                   ), | ||||
|                                 ), | ||||
|                               ), | ||||
|                               child: Column( | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                                 children: [ | ||||
|                                   Text( | ||||
|                                     'birthday'.tr(), | ||||
|                                     style: TextStyle( | ||||
|                                       color: Theme.of(context).hintColor, | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                   Text( | ||||
|                                     birthday.value != null | ||||
|                                         ? DateFormat.yMMMd().format( | ||||
|                                           birthday.value!, | ||||
|                                         ) | ||||
|                                         : 'Select a date'.tr(), | ||||
|                                   ), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
|                           SwitchListTile( | ||||
|                             title: Text('isInteractive').tr(), | ||||
|                             value: isInteractive.value, | ||||
|                             onChanged: (value) => isInteractive.value = value, | ||||
|                           ), | ||||
|                           const SizedBox(height: 16), | ||||
|                           Align( | ||||
| @@ -264,7 +410,7 @@ class EditBotScreen extends HookConsumerWidget { | ||||
|                             child: TextButton.icon( | ||||
|                               onPressed: | ||||
|                                   submitting.value ? null : performAction, | ||||
|                               label: Text('saveChanges'.tr()), | ||||
|                               label: Text('saveChanges').tr(), | ||||
|                               icon: const Icon(Symbols.save), | ||||
|                             ), | ||||
|                           ), | ||||
|   | ||||
| @@ -6,7 +6,7 @@ part of 'edit_bot.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$botHash() => r'a3e412ed575c513434bc718b7920db1d017111f4'; | ||||
| String _$botHash() => r'7bec47bb2a4061a5babc6d6d19c3d4c320c91188'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
| @@ -6,7 +6,7 @@ part of 'edit_project.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$devProjectHash() => r'fc68254c6e598e3fa05c86c36f1469c0b689bc43'; | ||||
| String _$devProjectHash() => r'd92be3f5cdc510c2a377615ed5c70622a6842bf2'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
							
								
								
									
										14
									
								
								lib/screens/developers/new_bot.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								lib/screens/developers/new_bot.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:island/screens/developers/edit_bot.dart'; | ||||
|  | ||||
| class NewBotScreen extends StatelessWidget { | ||||
|   final String publisherName; | ||||
|   final String projectId; | ||||
|   const NewBotScreen({super.key, required this.publisherName, required this.projectId}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return EditBotScreen(publisherName: publisherName, projectId: projectId); | ||||
|   } | ||||
| } | ||||
| @@ -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/screens/developers/apps.dart'; | ||||
| @@ -20,9 +21,9 @@ class ProjectDetailScreen extends HookConsumerWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return DefaultTabController( | ||||
|       length: 2, | ||||
|       child: AppScaffold( | ||||
|     final tabController = useTabController(initialLength: 2); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text('projectDetails').tr(), | ||||
|         actions: [ | ||||
| @@ -30,42 +31,62 @@ class ProjectDetailScreen extends HookConsumerWidget { | ||||
|             icon: const Icon(Symbols.add), | ||||
|             onPressed: () { | ||||
|               // Get current tab index | ||||
|                 final tabController = DefaultTabController.of(context); | ||||
|               final index = tabController.index; | ||||
|                 if (index == 0) { | ||||
|               switch (index) { | ||||
|                 case 0: | ||||
|                   context.pushNamed( | ||||
|                     'developerAppNew', | ||||
|                     pathParameters: { | ||||
|                       'name': publisherName, | ||||
|                       'projectId': projectId | ||||
|                       'projectId': projectId, | ||||
|                     }, | ||||
|                   ); | ||||
|                 } else { | ||||
|                   break; | ||||
|                 case 1: | ||||
|                   context.pushNamed( | ||||
|                     'developerBotNew', | ||||
|                     pathParameters: { | ||||
|                       'name': publisherName, | ||||
|                       'projectId': projectId | ||||
|                       'projectId': projectId, | ||||
|                     }, | ||||
|                   ); | ||||
|                   break; | ||||
|               } | ||||
|             }, | ||||
|           ), | ||||
|           const Gap(8), | ||||
|         ], | ||||
|         bottom: TabBar( | ||||
|           controller: tabController, | ||||
|           tabs: [ | ||||
|               Tab(text: 'customApps'.tr()), | ||||
|               Tab(text: 'bots'.tr()), | ||||
|             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), | ||||
|         ], | ||||
|       ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| 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'; | ||||
| @@ -43,6 +44,7 @@ class DevProjectsScreen extends HookConsumerWidget { | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|           const Gap(8), | ||||
|         ], | ||||
|       ), | ||||
|       body: projects.when( | ||||
| @@ -61,6 +63,10 @@ class DevProjectsScreen extends HookConsumerWidget { | ||||
|                 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( | ||||
|   | ||||
| @@ -6,7 +6,7 @@ part of 'projects.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$devProjectsHash() => r'4c86ea5c3c02185514dbfa32804f1529f68d56c7'; | ||||
| String _$devProjectsHash() => r'87fdcab47cd7d79ab019a5625617abeb1ffa1f39'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
| @@ -101,7 +101,7 @@ class SliverArticlesList extends ConsumerWidget { | ||||
|             publisherId: publisherId, | ||||
|           ).notifier, | ||||
|       contentBuilder: | ||||
|           (data, widgetCount, endItemView) => SliverList.builder( | ||||
|           (data, widgetCount, endItemView) => SliverList.separated( | ||||
|             itemCount: widgetCount, | ||||
|             itemBuilder: (context, index) { | ||||
|               if (index == widgetCount - 1) { | ||||
| @@ -111,37 +111,115 @@ class SliverArticlesList extends ConsumerWidget { | ||||
|               final article = data.items[index]; | ||||
|               return WebArticleCard(article: article, showDetails: true); | ||||
|             }, | ||||
|             separatorBuilder: (context, index) => const SizedBox(height: 12), | ||||
|           ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class ArticlesScreen extends ConsumerWidget { | ||||
|   final String? feedId; | ||||
|   final String? publisherId; | ||||
|   final String? title; | ||||
| @riverpod | ||||
| Future<List<SnWebFeed>> subscribedFeeds(Ref ref) async { | ||||
|   final client = ref.watch(apiClientProvider); | ||||
|   final response = await client.get('/sphere/feeds/subscribed'); | ||||
|   final data = response.data as List<dynamic>; | ||||
|   return data.map((json) => SnWebFeed.fromJson(json)).toList(); | ||||
| } | ||||
|  | ||||
|   const ArticlesScreen({super.key, this.feedId, this.publisherId, this.title}); | ||||
| class ArticlesScreen extends ConsumerWidget { | ||||
|   const ArticlesScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: Text(title ?? 'Articles')), | ||||
|       body: Center( | ||||
|         child: ConstrainedBox( | ||||
|           constraints: const BoxConstraints(maxWidth: 560), | ||||
|           child: CustomScrollView( | ||||
|             slivers: [ | ||||
|               SliverPadding( | ||||
|                 padding: const EdgeInsets.only(top: 8, left: 8, right: 8), | ||||
|                 sliver: SliverArticlesList( | ||||
|                   feedId: feedId, | ||||
|                   publisherId: publisherId, | ||||
|     final subscribedFeedsAsync = ref.watch(subscribedFeedsProvider); | ||||
|  | ||||
|     return subscribedFeedsAsync.when( | ||||
|       data: (feeds) { | ||||
|         return DefaultTabController( | ||||
|           length: feeds.length + 1, | ||||
|           child: AppScaffold( | ||||
|             isNoBackground: false, | ||||
|             appBar: AppBar( | ||||
|               title: const Text('Articles'), | ||||
|               bottom: TabBar( | ||||
|                 isScrollable: true, | ||||
|                 tabs: [ | ||||
|                   Tab( | ||||
|                     child: Text( | ||||
|                       'All', | ||||
|                       textAlign: TextAlign.center, | ||||
|                       style: TextStyle( | ||||
|                         color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                   ...feeds.map( | ||||
|                     (feed) => Tab( | ||||
|                       child: Text( | ||||
|                         feed.title, | ||||
|                         textAlign: TextAlign.center, | ||||
|                         style: TextStyle( | ||||
|                           color: Theme.of(context).appBarTheme.foregroundColor!, | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|             body: TabBarView( | ||||
|               children: [ | ||||
|                 Center( | ||||
|                   child: ConstrainedBox( | ||||
|                     constraints: const BoxConstraints(maxWidth: 560), | ||||
|                     child: CustomScrollView( | ||||
|                       slivers: [ | ||||
|                         SliverPadding( | ||||
|                           padding: const EdgeInsets.only( | ||||
|                             top: 12, | ||||
|                             left: 8, | ||||
|                             right: 8, | ||||
|                           ), | ||||
|                           sliver: SliverArticlesList(), | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|                 ...feeds.map((feed) { | ||||
|                   return Center( | ||||
|                     child: ConstrainedBox( | ||||
|                       constraints: const BoxConstraints(maxWidth: 560), | ||||
|                       child: CustomScrollView( | ||||
|                         slivers: [ | ||||
|                           SliverPadding( | ||||
|                             padding: const EdgeInsets.only( | ||||
|                               top: 8, | ||||
|                               left: 8, | ||||
|                               right: 8, | ||||
|                             ), | ||||
|                             sliver: SliverArticlesList(feedId: feed.id), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                   ); | ||||
|                 }), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ); | ||||
|       }, | ||||
|       loading: | ||||
|           () => AppScaffold( | ||||
|             isNoBackground: false, | ||||
|             appBar: AppBar(title: const Text('Articles')), | ||||
|             body: const Center(child: CircularProgressIndicator()), | ||||
|           ), | ||||
|       error: | ||||
|           (err, stack) => AppScaffold( | ||||
|             isNoBackground: false, | ||||
|             appBar: AppBar(title: const Text('Articles')), | ||||
|             body: Center(child: Text('Error: $err')), | ||||
|           ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -6,6 +6,25 @@ part of 'articles.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$subscribedFeedsHash() => r'5c0c8c30c5f543f6ea1d39786a6778f77ba5b3df'; | ||||
|  | ||||
| /// See also [subscribedFeeds]. | ||||
| @ProviderFor(subscribedFeeds) | ||||
| final subscribedFeedsProvider = | ||||
|     AutoDisposeFutureProvider<List<SnWebFeed>>.internal( | ||||
|       subscribedFeeds, | ||||
|       name: r'subscribedFeedsProvider', | ||||
|       debugGetCreateSourceHash: | ||||
|           const bool.fromEnvironment('dart.vm.product') | ||||
|               ? null | ||||
|               : _$subscribedFeedsHash, | ||||
|       dependencies: null, | ||||
|       allTransitiveDependencies: null, | ||||
|     ); | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| typedef SubscribedFeedsRef = AutoDisposeFutureProviderRef<List<SnWebFeed>>; | ||||
| String _$articlesListNotifierHash() => | ||||
|     r'579741af4d90c7c81f2e2697e57c4895b7a9dabc'; | ||||
|  | ||||
|   | ||||
| @@ -1,27 +1,75 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/webfeed.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/web_article_card.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 'feed_detail.g.dart'; | ||||
|  | ||||
| @riverpod | ||||
| Future<SnWebFeed> marketplaceWebFeed(Ref ref, String feedId) async { | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   final resp = await apiClient.get('/sphere/feeds/$feedId'); | ||||
|   return SnWebFeed.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| @riverpod | ||||
| Future<List<SnWebArticle>> marketplaceWebFeedContent( | ||||
|   Ref ref, { | ||||
|   required String feedId, | ||||
| class MarketplaceWebFeedContentNotifier | ||||
|     extends _$MarketplaceWebFeedContentNotifier | ||||
|     with CursorPagingNotifierMixin<SnWebArticle> { | ||||
|   static const int _pageSize = 20; | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnWebArticle>> build(String feedId) async { | ||||
|     _feedId = feedId; | ||||
|     return fetch(cursor: null); | ||||
|   } | ||||
|  | ||||
|   late final String _feedId; | ||||
|   ValueNotifier<int> totalCount = ValueNotifier(0); | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnWebArticle>> fetch({ | ||||
|     required String? cursor, | ||||
|   }) async { | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   final resp = await apiClient.get('/sphere/feeds/$feedId/articles'); | ||||
|   return (resp.data as List).map((e) => SnWebArticle.fromJson(e)).toList(); | ||||
|     final client = ref.read(apiClientProvider); | ||||
|     final offset = cursor == null ? 0 : int.parse(cursor); | ||||
|  | ||||
|     final queryParams = {'offset': offset, 'take': _pageSize}; | ||||
|  | ||||
|     final response = await client.get( | ||||
|       '/sphere/feeds/$_feedId/articles', | ||||
|       queryParameters: queryParams, | ||||
|     ); | ||||
|     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||
|     totalCount.value = total; | ||||
|     final List<dynamic> data = response.data; | ||||
|     final articles = data.map((json) => SnWebArticle.fromJson(json)).toList(); | ||||
|  | ||||
|     final hasMore = offset + articles.length < total; | ||||
|     final nextCursor = hasMore ? (offset + articles.length).toString() : null; | ||||
|  | ||||
|     return CursorPagingData( | ||||
|       items: articles, | ||||
|       hasMore: hasMore, | ||||
|       nextCursor: nextCursor, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void dispose() { | ||||
|     totalCount.dispose(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Provider for web feed subscription status | ||||
| @@ -49,11 +97,7 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     // TODO: Need to create a web feed provider similar to stickerPackProvider | ||||
|     // For now, we'll fetch the feed directly | ||||
|     final feedContent = ref.watch( | ||||
|       marketplaceWebFeedContentProvider(feedId: id), | ||||
|     ); | ||||
|     final feed = ref.watch(marketplaceWebFeedProvider(id)); | ||||
|     final subscribed = ref.watch( | ||||
|       marketplaceWebFeedSubscriptionProvider(feedId: id), | ||||
|     ); | ||||
| @@ -65,7 +109,7 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { | ||||
|       HapticFeedback.selectionClick(); | ||||
|       ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id)); | ||||
|       if (!context.mounted) return; | ||||
|       showSnackBar('feedSubscribed'.tr()); | ||||
|       showSnackBar('webFeedSubscribed'.tr()); | ||||
|     } | ||||
|  | ||||
|     // Unsubscribe from web feed | ||||
| @@ -75,86 +119,94 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { | ||||
|       HapticFeedback.selectionClick(); | ||||
|       ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id)); | ||||
|       if (!context.mounted) return; | ||||
|       showSnackBar('feedUnsubscribed'.tr()); | ||||
|       showSnackBar('webFeedUnsubscribed'.tr()); | ||||
|     } | ||||
|  | ||||
|     // TODO: Replace with actual feed data provider once created | ||||
|     final dummyFeed = SnWebFeed( | ||||
|       id: id, | ||||
|       url: 'https://example.com', | ||||
|       title: 'Loading...', | ||||
|       publisherId: 'publisher-id', | ||||
|       createdAt: DateTime.now(), | ||||
|       updatedAt: DateTime.now(), | ||||
|     final feedNotifier = ref.watch( | ||||
|       marketplaceWebFeedContentNotifierProvider(id).notifier, | ||||
|     ); | ||||
|  | ||||
|     useEffect(() { | ||||
|       return feedNotifier.dispose; | ||||
|     }, []); | ||||
|  | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: Text(dummyFeed.title)), | ||||
|       appBar: AppBar(title: Text(feed.value?.title ?? 'loading'.tr())), | ||||
|       body: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|         children: [ | ||||
|           // Feed meta | ||||
|           Column( | ||||
|           feed | ||||
|               .when( | ||||
|                 data: | ||||
|                     (data) => Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                       children: [ | ||||
|               Text(dummyFeed.description ?? ''), | ||||
|                         Text(data.description ?? 'descriptionNone'.tr()), | ||||
|                         Row( | ||||
|                           spacing: 4, | ||||
|                           children: [ | ||||
|                             const Icon(Symbols.rss_feed, size: 16), | ||||
|                   Text('${feedContent.value?.length ?? 0} articles'), | ||||
|                             ListenableBuilder( | ||||
|                               listenable: feedNotifier.totalCount, | ||||
|                               builder: | ||||
|                                   (context, _) => Text( | ||||
|                                     'webFeedArticleCount'.plural( | ||||
|                                       feedNotifier.totalCount.value, | ||||
|                                     ), | ||||
|                                   ), | ||||
|                             ), | ||||
|                           ], | ||||
|                         ).opacity(0.85), | ||||
|                         Row( | ||||
|                           spacing: 4, | ||||
|                           children: [ | ||||
|                             const Icon(Symbols.link, size: 16), | ||||
|                   SelectableText(dummyFeed.url), | ||||
|                             SelectableText(data.url), | ||||
|                           ], | ||||
|                         ).opacity(0.85), | ||||
|                       ], | ||||
|           ).padding(horizontal: 24, vertical: 24), | ||||
|                     ), | ||||
|                 error: (err, _) => Text(err.toString()), | ||||
|                 loading: () => CircularProgressIndicator().center(), | ||||
|               ) | ||||
|               .padding(horizontal: 24, vertical: 24), | ||||
|           const Divider(height: 1), | ||||
|           // Articles list | ||||
|           Expanded( | ||||
|             child: feedContent.when( | ||||
|               data: | ||||
|                   (articles) => RefreshIndicator( | ||||
|                     onRefresh: | ||||
|                         () => ref.refresh( | ||||
|                           marketplaceWebFeedContentProvider(feedId: id).future, | ||||
|                         ), | ||||
|                     child: ListView.builder( | ||||
|             child: PagingHelperView( | ||||
|               provider: marketplaceWebFeedContentNotifierProvider(id), | ||||
|               futureRefreshable: | ||||
|                   marketplaceWebFeedContentNotifierProvider(id).future, | ||||
|               notifierRefreshable: | ||||
|                   marketplaceWebFeedContentNotifierProvider(id).notifier, | ||||
|               contentBuilder: | ||||
|                   (data, widgetCount, endItemView) => ListView.separated( | ||||
|                     padding: const EdgeInsets.symmetric( | ||||
|                       horizontal: 24, | ||||
|                       vertical: 20, | ||||
|                     ), | ||||
|                       itemCount: articles.length, | ||||
|                     itemCount: widgetCount, | ||||
|                     itemBuilder: (context, index) { | ||||
|                         final article = articles[index]; | ||||
|                         return Card( | ||||
|                           child: ListTile( | ||||
|                             title: Text(article.title), | ||||
|                             subtitle: Text(article.author ?? ''), | ||||
|                             trailing: const Icon(Symbols.open_in_new), | ||||
|                             onTap: () { | ||||
|                               // TODO: Navigate to article detail or open URL | ||||
|                             }, | ||||
|                           ), | ||||
|                         ); | ||||
|                       if (index == widgetCount - 1) { | ||||
|                         return endItemView; | ||||
|                       } | ||||
|  | ||||
|                       final article = data.items[index]; | ||||
|                       return WebArticleCard(article: article); | ||||
|                     }, | ||||
|                     separatorBuilder: (context, index) => const Gap(12), | ||||
|                   ), | ||||
|             ), | ||||
|               error: | ||||
|                   (err, _) => | ||||
|                       Text( | ||||
|                         'Error: $err', | ||||
|                       ).textAlignment(TextAlign.center).center(), | ||||
|               loading: () => const CircularProgressIndicator().center(), | ||||
|           ), | ||||
|           Container( | ||||
|             padding: EdgeInsets.only( | ||||
|               bottom: 16 + MediaQuery.of(context).padding.bottom, | ||||
|               left: 24, | ||||
|               right: 24, | ||||
|               top: 16, | ||||
|             ), | ||||
|           Padding( | ||||
|             padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8), | ||||
|             color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|             child: subscribed.when( | ||||
|               data: | ||||
|                   (isSubscribed) => FilledButton.icon( | ||||
| @@ -181,7 +233,6 @@ class MarketplaceWebFeedDetailScreen extends HookConsumerWidget { | ||||
|                   ), | ||||
|             ), | ||||
|           ), | ||||
|           Gap(MediaQuery.of(context).padding.bottom), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -6,8 +6,8 @@ part of 'feed_detail.dart'; | ||||
| // RiverpodGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$marketplaceWebFeedContentHash() => | ||||
|     r'4e65350bff4055302e15ec14266cdebb1cd89bbe'; | ||||
| String _$marketplaceWebFeedHash() => | ||||
|     r'8383f94f1bc272b903c341b8d95000313b69d14c'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
| @@ -30,34 +30,25 @@ class _SystemHash { | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedContent]. | ||||
| @ProviderFor(marketplaceWebFeedContent) | ||||
| const marketplaceWebFeedContentProvider = MarketplaceWebFeedContentFamily(); | ||||
| /// See also [marketplaceWebFeed]. | ||||
| @ProviderFor(marketplaceWebFeed) | ||||
| const marketplaceWebFeedProvider = MarketplaceWebFeedFamily(); | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedContent]. | ||||
| class MarketplaceWebFeedContentFamily | ||||
|     extends Family<AsyncValue<List<SnWebArticle>>> { | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedContent]. | ||||
|   const MarketplaceWebFeedContentFamily(); | ||||
| /// See also [marketplaceWebFeed]. | ||||
| class MarketplaceWebFeedFamily extends Family<AsyncValue<SnWebFeed>> { | ||||
|   /// See also [marketplaceWebFeed]. | ||||
|   const MarketplaceWebFeedFamily(); | ||||
|  | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedContent]. | ||||
|   MarketplaceWebFeedContentProvider call({required String feedId}) { | ||||
|     return MarketplaceWebFeedContentProvider(feedId: feedId); | ||||
|   /// See also [marketplaceWebFeed]. | ||||
|   MarketplaceWebFeedProvider call(String feedId) { | ||||
|     return MarketplaceWebFeedProvider(feedId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MarketplaceWebFeedContentProvider getProviderOverride( | ||||
|     covariant MarketplaceWebFeedContentProvider provider, | ||||
|   MarketplaceWebFeedProvider getProviderOverride( | ||||
|     covariant MarketplaceWebFeedProvider provider, | ||||
|   ) { | ||||
|     return call(feedId: provider.feedId); | ||||
|     return call(provider.feedId); | ||||
|   } | ||||
|  | ||||
|   static const Iterable<ProviderOrFamily>? _dependencies = null; | ||||
| @@ -72,36 +63,28 @@ class MarketplaceWebFeedContentFamily | ||||
|       _allTransitiveDependencies; | ||||
|  | ||||
|   @override | ||||
|   String? get name => r'marketplaceWebFeedContentProvider'; | ||||
|   String? get name => r'marketplaceWebFeedProvider'; | ||||
| } | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [marketplaceWebFeedContent]. | ||||
| class MarketplaceWebFeedContentProvider | ||||
|     extends AutoDisposeFutureProvider<List<SnWebArticle>> { | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [marketplaceWebFeedContent]. | ||||
|   MarketplaceWebFeedContentProvider({required String feedId}) | ||||
| /// See also [marketplaceWebFeed]. | ||||
| class MarketplaceWebFeedProvider extends AutoDisposeFutureProvider<SnWebFeed> { | ||||
|   /// See also [marketplaceWebFeed]. | ||||
|   MarketplaceWebFeedProvider(String feedId) | ||||
|     : this._internal( | ||||
|         (ref) => marketplaceWebFeedContent( | ||||
|           ref as MarketplaceWebFeedContentRef, | ||||
|           feedId: feedId, | ||||
|         ), | ||||
|         from: marketplaceWebFeedContentProvider, | ||||
|         name: r'marketplaceWebFeedContentProvider', | ||||
|         (ref) => marketplaceWebFeed(ref as MarketplaceWebFeedRef, feedId), | ||||
|         from: marketplaceWebFeedProvider, | ||||
|         name: r'marketplaceWebFeedProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$marketplaceWebFeedContentHash, | ||||
|         dependencies: MarketplaceWebFeedContentFamily._dependencies, | ||||
|                 : _$marketplaceWebFeedHash, | ||||
|         dependencies: MarketplaceWebFeedFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             MarketplaceWebFeedContentFamily._allTransitiveDependencies, | ||||
|             MarketplaceWebFeedFamily._allTransitiveDependencies, | ||||
|         feedId: feedId, | ||||
|       ); | ||||
|  | ||||
|   MarketplaceWebFeedContentProvider._internal( | ||||
|   MarketplaceWebFeedProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
| @@ -115,13 +98,12 @@ class MarketplaceWebFeedContentProvider | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<List<SnWebArticle>> Function(MarketplaceWebFeedContentRef provider) | ||||
|     create, | ||||
|     FutureOr<SnWebFeed> Function(MarketplaceWebFeedRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: MarketplaceWebFeedContentProvider._internal( | ||||
|         (ref) => create(ref as MarketplaceWebFeedContentRef), | ||||
|       override: MarketplaceWebFeedProvider._internal( | ||||
|         (ref) => create(ref as MarketplaceWebFeedRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
| @@ -133,13 +115,13 @@ class MarketplaceWebFeedContentProvider | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<List<SnWebArticle>> createElement() { | ||||
|     return _MarketplaceWebFeedContentProviderElement(this); | ||||
|   AutoDisposeFutureProviderElement<SnWebFeed> createElement() { | ||||
|     return _MarketplaceWebFeedProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is MarketplaceWebFeedContentProvider && other.feedId == feedId; | ||||
|     return other is MarketplaceWebFeedProvider && other.feedId == feedId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -153,19 +135,18 @@ class MarketplaceWebFeedContentProvider | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin MarketplaceWebFeedContentRef | ||||
|     on AutoDisposeFutureProviderRef<List<SnWebArticle>> { | ||||
| mixin MarketplaceWebFeedRef on AutoDisposeFutureProviderRef<SnWebFeed> { | ||||
|   /// The parameter `feedId` of this provider. | ||||
|   String get feedId; | ||||
| } | ||||
|  | ||||
| class _MarketplaceWebFeedContentProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<List<SnWebArticle>> | ||||
|     with MarketplaceWebFeedContentRef { | ||||
|   _MarketplaceWebFeedContentProviderElement(super.provider); | ||||
| class _MarketplaceWebFeedProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<SnWebFeed> | ||||
|     with MarketplaceWebFeedRef { | ||||
|   _MarketplaceWebFeedProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get feedId => (origin as MarketplaceWebFeedContentProvider).feedId; | ||||
|   String get feedId => (origin as MarketplaceWebFeedProvider).feedId; | ||||
| } | ||||
|  | ||||
| String _$marketplaceWebFeedSubscriptionHash() => | ||||
| @@ -309,5 +290,169 @@ class _MarketplaceWebFeedSubscriptionProviderElement | ||||
|       (origin as MarketplaceWebFeedSubscriptionProvider).feedId; | ||||
| } | ||||
|  | ||||
| String _$marketplaceWebFeedContentNotifierHash() => | ||||
|     r'25688082884cb824eeff300888ba38c9748295dc'; | ||||
|  | ||||
| abstract class _$MarketplaceWebFeedContentNotifier | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnWebArticle>> { | ||||
|   late final String feedId; | ||||
|  | ||||
|   FutureOr<CursorPagingData<SnWebArticle>> build(String feedId); | ||||
| } | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [MarketplaceWebFeedContentNotifier]. | ||||
| @ProviderFor(MarketplaceWebFeedContentNotifier) | ||||
| const marketplaceWebFeedContentNotifierProvider = | ||||
|     MarketplaceWebFeedContentNotifierFamily(); | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [MarketplaceWebFeedContentNotifier]. | ||||
| class MarketplaceWebFeedContentNotifierFamily | ||||
|     extends Family<AsyncValue<CursorPagingData<SnWebArticle>>> { | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [MarketplaceWebFeedContentNotifier]. | ||||
|   const MarketplaceWebFeedContentNotifierFamily(); | ||||
|  | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [MarketplaceWebFeedContentNotifier]. | ||||
|   MarketplaceWebFeedContentNotifierProvider call(String feedId) { | ||||
|     return MarketplaceWebFeedContentNotifierProvider(feedId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   MarketplaceWebFeedContentNotifierProvider getProviderOverride( | ||||
|     covariant MarketplaceWebFeedContentNotifierProvider provider, | ||||
|   ) { | ||||
|     return call(provider.feedId); | ||||
|   } | ||||
|  | ||||
|   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'marketplaceWebFeedContentNotifierProvider'; | ||||
| } | ||||
|  | ||||
| /// Provider for web feed articles content | ||||
| /// | ||||
| /// Copied from [MarketplaceWebFeedContentNotifier]. | ||||
| class MarketplaceWebFeedContentNotifierProvider | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderImpl< | ||||
|           MarketplaceWebFeedContentNotifier, | ||||
|           CursorPagingData<SnWebArticle> | ||||
|         > { | ||||
|   /// Provider for web feed articles content | ||||
|   /// | ||||
|   /// Copied from [MarketplaceWebFeedContentNotifier]. | ||||
|   MarketplaceWebFeedContentNotifierProvider(String feedId) | ||||
|     : this._internal( | ||||
|         () => MarketplaceWebFeedContentNotifier()..feedId = feedId, | ||||
|         from: marketplaceWebFeedContentNotifierProvider, | ||||
|         name: r'marketplaceWebFeedContentNotifierProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$marketplaceWebFeedContentNotifierHash, | ||||
|         dependencies: MarketplaceWebFeedContentNotifierFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             MarketplaceWebFeedContentNotifierFamily._allTransitiveDependencies, | ||||
|         feedId: feedId, | ||||
|       ); | ||||
|  | ||||
|   MarketplaceWebFeedContentNotifierProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.feedId, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String feedId; | ||||
|  | ||||
|   @override | ||||
|   FutureOr<CursorPagingData<SnWebArticle>> runNotifierBuild( | ||||
|     covariant MarketplaceWebFeedContentNotifier notifier, | ||||
|   ) { | ||||
|     return notifier.build(feedId); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith(MarketplaceWebFeedContentNotifier Function() create) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: MarketplaceWebFeedContentNotifierProvider._internal( | ||||
|         () => create()..feedId = feedId, | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         feedId: feedId, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeAsyncNotifierProviderElement< | ||||
|     MarketplaceWebFeedContentNotifier, | ||||
|     CursorPagingData<SnWebArticle> | ||||
|   > | ||||
|   createElement() { | ||||
|     return _MarketplaceWebFeedContentNotifierProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is MarketplaceWebFeedContentNotifierProvider && | ||||
|         other.feedId == feedId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, feedId.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin MarketplaceWebFeedContentNotifierRef | ||||
|     on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnWebArticle>> { | ||||
|   /// The parameter `feedId` of this provider. | ||||
|   String get feedId; | ||||
| } | ||||
|  | ||||
| class _MarketplaceWebFeedContentNotifierProviderElement | ||||
|     extends | ||||
|         AutoDisposeAsyncNotifierProviderElement< | ||||
|           MarketplaceWebFeedContentNotifier, | ||||
|           CursorPagingData<SnWebArticle> | ||||
|         > | ||||
|     with MarketplaceWebFeedContentNotifierRef { | ||||
|   _MarketplaceWebFeedContentNotifierProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get feedId => | ||||
|       (origin as MarketplaceWebFeedContentNotifierProvider).feedId; | ||||
| } | ||||
|  | ||||
| // 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 | ||||
|   | ||||
| @@ -32,7 +32,7 @@ class MarketplaceWebFeedsNotifier extends _$MarketplaceWebFeedsNotifier | ||||
|     final offset = cursor == null ? 0 : int.parse(cursor); | ||||
|  | ||||
|     final response = await client.get( | ||||
|       '/sphere/feeds', | ||||
|       '/sphere/feeds/explore', | ||||
|       queryParameters: { | ||||
|         'offset': offset, | ||||
|         'take': 20, | ||||
| @@ -72,7 +72,7 @@ class MarketplaceWebFeedsScreen extends HookConsumerWidget { | ||||
|         searchController.clear(); | ||||
|       } | ||||
|       return null; | ||||
|     }, [query.value]); | ||||
|     }, [query]); | ||||
|  | ||||
|     // Clean up timer on dispose | ||||
|     useEffect(() { | ||||
|   | ||||
| @@ -7,7 +7,7 @@ part of 'feed_marketplace.dart'; | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$marketplaceWebFeedsNotifierHash() => | ||||
|     r'dbf885d95570ca9c2259a58998975db813b18cbb'; | ||||
|     r'774b2985f2f7d61fe958f534f84e39f814327c4e'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
| @@ -27,6 +27,7 @@ import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/realm/realm_card.dart'; | ||||
| import 'package:island/widgets/publisher/publisher_card.dart'; | ||||
| import 'package:island/widgets/web_article_card.dart'; | ||||
| import 'package:island/widgets/extended_refresh_indicator.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| part 'explore.g.dart'; | ||||
| @@ -368,7 +369,7 @@ class ExploreScreen extends HookConsumerWidget { | ||||
|  | ||||
|     final isWide = isWideScreen(context); | ||||
|  | ||||
|     return RefreshIndicator( | ||||
|     return ExtendedRefreshIndicator( | ||||
|       onRefresh: () => Future.sync(activitiesNotifier.forceRefresh), | ||||
|       child: PagingHelperView( | ||||
|         provider: activityListNotifierProvider(filter), | ||||
| @@ -399,39 +400,48 @@ class _DiscoveryActivityItem extends StatelessWidget { | ||||
|     final items = data['items'] as List; | ||||
|     final type = items.firstOrNull?['type'] ?? 'unknown'; | ||||
|  | ||||
|     return Card( | ||||
|       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Row( | ||||
|             crossAxisAlignment: CrossAxisAlignment.center, | ||||
|             children: [ | ||||
|               const Icon(Symbols.explore, size: 19), | ||||
|               const Gap(8), | ||||
|               Text( | ||||
|                 (switch (type) { | ||||
|                   'realm' => 'discoverRealms', | ||||
|                   'publisher' => 'discoverPublishers', | ||||
|                   'article' => 'discoverWebArticles', | ||||
|                   _ => 'unknown', | ||||
|                 }).tr(), | ||||
|                 style: Theme.of(context).textTheme.titleMedium, | ||||
|               ).padding(top: 1), | ||||
|             ], | ||||
|           ).padding(horizontal: 20, top: 8, bottom: 4), | ||||
|           SizedBox( | ||||
|             height: 180, | ||||
|             child: ConstrainedBox( | ||||
|               constraints: const BoxConstraints(maxHeight: 200), | ||||
|               child: CarouselView.weighted( | ||||
|                 flexWeights: | ||||
|                     isWideScreen(context) ? <int>[3, 2, 1] : <int>[4, 1], | ||||
|     var flexWeights = isWideScreen(context) ? <int>[3, 2, 1] : <int>[4, 1]; | ||||
|     if (type == 'post') flexWeights = <int>[3, 2]; | ||||
|  | ||||
|     final height = type == 'post' ? 280.0 : 180.0; | ||||
|  | ||||
|     final contentWidget = switch (type) { | ||||
|       'post' => ListView.separated( | ||||
|         scrollDirection: Axis.horizontal, | ||||
|         itemCount: items.length, | ||||
|         separatorBuilder: (context, index) => const Gap(12), | ||||
|         padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|         itemBuilder: (context, index) { | ||||
|           final item = items[index]; | ||||
|           return Container( | ||||
|             width: 320, | ||||
|             decoration: BoxDecoration( | ||||
|               border: Border.all( | ||||
|                 width: 1 / MediaQuery.of(context).devicePixelRatio, | ||||
|                 color: Theme.of(context).dividerColor.withOpacity(0.5), | ||||
|               ), | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|             ), | ||||
|             child: ClipRRect( | ||||
|               borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|               child: SingleChildScrollView( | ||||
|                 child: PostActionableItem( | ||||
|                   item: SnPost.fromJson(item['data']), | ||||
|                   isCompact: true, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|       _ => CarouselView.weighted( | ||||
|         flexWeights: flexWeights, | ||||
|         consumeMaxWeight: false, | ||||
|         enableSplash: false, | ||||
|                 shape: RoundedRectangleBorder( | ||||
|         shape: const RoundedRectangleBorder( | ||||
|           borderRadius: BorderRadius.all(Radius.circular(8)), | ||||
|         ), | ||||
|         itemSnapping: false, | ||||
|         children: [ | ||||
|           for (final item in items) | ||||
|             switch (type) { | ||||
| @@ -447,11 +457,43 @@ class _DiscoveryActivityItem extends StatelessWidget { | ||||
|                 article: SnWebArticle.fromJson(item['data']), | ||||
|                 maxWidth: 280, | ||||
|               ), | ||||
|                       _ => Placeholder(), | ||||
|               _ => const Placeholder(), | ||||
|             }, | ||||
|         ], | ||||
|       ), | ||||
|             ), | ||||
|     }; | ||||
|  | ||||
|     return Card( | ||||
|       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Row( | ||||
|             crossAxisAlignment: CrossAxisAlignment.center, | ||||
|             children: [ | ||||
|               Icon(switch (type) { | ||||
|                 'realm' => Symbols.public, | ||||
|                 'publisher' => Symbols.account_circle, | ||||
|                 'article' => Symbols.auto_stories, | ||||
|                 'post' => Symbols.shuffle, | ||||
|                 _ => Symbols.explore, | ||||
|               }, size: 19), | ||||
|               const Gap(8), | ||||
|               Text( | ||||
|                 (switch (type) { | ||||
|                   'realm' => 'discoverRealms', | ||||
|                   'publisher' => 'discoverPublishers', | ||||
|                   'article' => 'discoverWebArticles', | ||||
|                   'post' => 'discoverShuffledPost', | ||||
|                   _ => 'unknown', | ||||
|                 }).tr(), | ||||
|                 style: Theme.of(context).textTheme.titleMedium, | ||||
|               ).padding(top: 1), | ||||
|             ], | ||||
|           ).padding(horizontal: 20, top: 8, bottom: 4), | ||||
|           SizedBox( | ||||
|             height: height, | ||||
|             child: contentWidget, | ||||
|           ).padding(bottom: 8, horizontal: 8), | ||||
|         ], | ||||
|       ), | ||||
| @@ -569,7 +611,8 @@ class ActivityListNotifier extends _$ActivityListNotifier | ||||
|       if (cursor != null) 'cursor': cursor, | ||||
|       'take': take, | ||||
|       if (filter != null) 'filter': filter, | ||||
|       if (kDebugMode) 'debugInclude': 'realms,publishers,articles', | ||||
|       if (kDebugMode) | ||||
|         'debugInclude': 'realms,publishers,articles,shuffledPosts', | ||||
|     }; | ||||
|  | ||||
|     final response = await client.get( | ||||
| @@ -584,12 +627,13 @@ class ActivityListNotifier extends _$ActivityListNotifier | ||||
|  | ||||
|     final hasMore = (items.firstOrNull?.type ?? 'empty') != 'empty'; | ||||
|     final nextCursor = | ||||
|         items | ||||
|         items.isNotEmpty | ||||
|             ? items | ||||
|                 .map((x) => x.createdAt) | ||||
|             .lastOrNull | ||||
|             ?.toUtc() | ||||
|                 .reduce((a, b) => a.isBefore(b) ? a : b) | ||||
|                 .toUtc() | ||||
|                 .toIso8601String() | ||||
|             .toString(); | ||||
|             : null; | ||||
|  | ||||
|     return CursorPagingData( | ||||
|       items: items, | ||||
|   | ||||
| @@ -7,7 +7,7 @@ part of 'explore.dart'; | ||||
| // ************************************************************************** | ||||
|  | ||||
| String _$activityListNotifierHash() => | ||||
|     r'b75fd5c08d5f84ca433e16b7387d317ea72b91c9'; | ||||
|     r'167021cada54da7c8d8437eef1ffb387a92ea2e3'; | ||||
|  | ||||
| /// Copied from Dart SDK | ||||
| class _SystemHash { | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/post/post_list.dart'; | ||||
| import 'package:island/widgets/response.dart'; | ||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| @@ -27,6 +28,49 @@ Future<SnPostTag> postTag(Ref ref, String slug) async { | ||||
|   return SnPostTag.fromJson(resp.data); | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<bool> postCategorySubscriptionStatus( | ||||
|   Ref ref, | ||||
|   String slug, | ||||
|   bool isCategory, | ||||
| ) async { | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   try { | ||||
|     final resp = await apiClient.get( | ||||
|       '/sphere/posts/${isCategory ? 'categories' : 'tags'}/$slug/subscription', | ||||
|     ); | ||||
|     return resp.statusCode == 200; | ||||
|   } catch (_) { | ||||
|     return false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| Future<void> _subscribeToCategoryOrTag( | ||||
|   WidgetRef ref, { | ||||
|   required String slug, | ||||
|   required bool isCategory, | ||||
| }) async { | ||||
|   final apiClient = ref.read(apiClientProvider); | ||||
|   await apiClient.post( | ||||
|     '/sphere/posts/${isCategory ? 'categories' : 'tags'}/$slug/subscribe', | ||||
|   ); | ||||
|   // Invalidate the subscription status to refresh it | ||||
|   ref.invalidate(postCategorySubscriptionStatusProvider(slug, isCategory)); | ||||
| } | ||||
|  | ||||
| Future<void> _unsubscribeFromCategoryOrTag( | ||||
|   WidgetRef ref, { | ||||
|   required String slug, | ||||
|   required bool isCategory, | ||||
| }) async { | ||||
|   final apiClient = ref.read(apiClientProvider); | ||||
|   await apiClient.post( | ||||
|     '/sphere/posts/${isCategory ? 'categories' : 'tags'}/$slug/unsubscribe', | ||||
|   ); | ||||
|   // Invalidate the subscription status to refresh it | ||||
|   ref.invalidate(postCategorySubscriptionStatusProvider(slug, isCategory)); | ||||
| } | ||||
|  | ||||
| class PostCategoryDetailScreen extends HookConsumerWidget { | ||||
|   final String slug; | ||||
|   final bool isCategory; | ||||
| @@ -41,6 +85,9 @@ class PostCategoryDetailScreen extends HookConsumerWidget { | ||||
|     final postCategory = | ||||
|         isCategory ? ref.watch(postCategoryProvider(slug)) : null; | ||||
|     final postTag = isCategory ? null : ref.watch(postTagProvider(slug)); | ||||
|     final subscriptionStatus = ref.watch( | ||||
|       postCategorySubscriptionStatusProvider(slug, isCategory), | ||||
|     ); | ||||
|  | ||||
|     final postFilterTitle = | ||||
|         isCategory | ||||
| @@ -50,58 +97,159 @@ class PostCategoryDetailScreen extends HookConsumerWidget { | ||||
|     return AppScaffold( | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar(title: Text(postFilterTitle).tr()), | ||||
|       body: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           if (isCategory) | ||||
|             postCategory!.when( | ||||
|               data: | ||||
|                   (category) => Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       Text(category.categoryDisplayTitle).bold().fontSize(15), | ||||
|                       Text('A category'), | ||||
|                     ], | ||||
|                   ).padding(horizontal: 24, vertical: 16), | ||||
|               error: | ||||
|                   (error, _) => ResponseErrorWidget( | ||||
|                     error: error, | ||||
|                     onRetry: () => ref.invalidate(postCategoryProvider(slug)), | ||||
|                   ), | ||||
|               loading: () => ResponseLoadingWidget(), | ||||
|             ) | ||||
|           else | ||||
|             postTag!.when( | ||||
|               data: | ||||
|                   (tag) => Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       Text(tag.name ?? '#${tag.slug}').bold().fontSize(15), | ||||
|                       Text('A tag'), | ||||
|                     ], | ||||
|                   ).padding(horizontal: 24, vertical: 16), | ||||
|               error: | ||||
|                   (error, _) => ResponseErrorWidget( | ||||
|                     error: error, | ||||
|                     onRetry: () => ref.invalidate(postTagProvider(slug)), | ||||
|                   ), | ||||
|               loading: () => ResponseLoadingWidget(), | ||||
|             ), | ||||
|           const Divider(height: 1), | ||||
|           Expanded( | ||||
|       body: Expanded( | ||||
|         child: CustomScrollView( | ||||
|           slivers: [ | ||||
|             if (isCategory) | ||||
|               SliverToBoxAdapter( | ||||
|                 child: Center( | ||||
|                   child: ConstrainedBox( | ||||
|                     constraints: const BoxConstraints(maxWidth: 540), | ||||
|                     child: Card( | ||||
|                       margin: EdgeInsets.only(top: 8), | ||||
|                       child: postCategory!.when( | ||||
|                         data: | ||||
|                             (category) => Column( | ||||
|                               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                               children: [ | ||||
|                                 Text( | ||||
|                                   category.categoryDisplayTitle, | ||||
|                                 ).bold().fontSize(15), | ||||
|                                 Text('A category'), | ||||
|                                 const Gap(8), | ||||
|                                 subscriptionStatus.when( | ||||
|                                   data: | ||||
|                                       (isSubscribed) => | ||||
|                                           isSubscribed | ||||
|                                               ? FilledButton.icon( | ||||
|                                                 onPressed: () async { | ||||
|                                                   await _unsubscribeFromCategoryOrTag( | ||||
|                                                     ref, | ||||
|                                                     slug: slug, | ||||
|                                                     isCategory: isCategory, | ||||
|                                                   ); | ||||
|                                                 }, | ||||
|                                                 icon: const Icon( | ||||
|                                                   Symbols.remove_circle, | ||||
|                                                 ), | ||||
|                                                 label: Text('unsubscribe'.tr()), | ||||
|                                               ) | ||||
|                                               : FilledButton.icon( | ||||
|                                                 onPressed: () async { | ||||
|                                                   await _subscribeToCategoryOrTag( | ||||
|                                                     ref, | ||||
|                                                     slug: slug, | ||||
|                                                     isCategory: isCategory, | ||||
|                                                   ); | ||||
|                                                 }, | ||||
|                                                 icon: const Icon( | ||||
|                                                   Symbols.add_circle, | ||||
|                                                 ), | ||||
|                                                 label: Text('subscribe'.tr()), | ||||
|                                               ), | ||||
|                                   error: | ||||
|                                       (error, _) => Text( | ||||
|                                         'Error loading subscription status', | ||||
|                                       ), | ||||
|                                   loading: | ||||
|                                       () => | ||||
|                                           CircularProgressIndicator().center(), | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ).padding(horizontal: 24, vertical: 16), | ||||
|                         error: | ||||
|                             (error, _) => ResponseErrorWidget( | ||||
|                               error: error, | ||||
|                               onRetry: | ||||
|                                   () => ref.invalidate( | ||||
|                                     postCategoryProvider(slug), | ||||
|                                   ), | ||||
|                             ), | ||||
|                         loading: () => ResponseLoadingWidget(), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ) | ||||
|             else | ||||
|               SliverToBoxAdapter( | ||||
|                 child: Center( | ||||
|                   child: ConstrainedBox( | ||||
|                     constraints: const BoxConstraints(maxWidth: 540), | ||||
|                     child: Card( | ||||
|                       margin: EdgeInsets.only(top: 8), | ||||
|                       child: postTag!.when( | ||||
|                         data: | ||||
|                             (tag) => Column( | ||||
|                               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                               children: [ | ||||
|                                 Text( | ||||
|                                   tag.name ?? '#${tag.slug}', | ||||
|                                 ).bold().fontSize(15), | ||||
|                                 Text('A tag'), | ||||
|                                 const Gap(8), | ||||
|                                 subscriptionStatus.when( | ||||
|                                   data: | ||||
|                                       (isSubscribed) => | ||||
|                                           isSubscribed | ||||
|                                               ? FilledButton.icon( | ||||
|                                                 onPressed: () async { | ||||
|                                                   await _unsubscribeFromCategoryOrTag( | ||||
|                                                     ref, | ||||
|                                                     slug: slug, | ||||
|                                                     isCategory: isCategory, | ||||
|                                                   ); | ||||
|                                                 }, | ||||
|                                                 icon: const Icon( | ||||
|                                                   Symbols.add_circle, | ||||
|                                                 ), | ||||
|                                                 label: Text('unsubscribe'.tr()), | ||||
|                                               ) | ||||
|                                               : FilledButton.icon( | ||||
|                                                 onPressed: () async { | ||||
|                                                   await _subscribeToCategoryOrTag( | ||||
|                                                     ref, | ||||
|                                                     slug: slug, | ||||
|                                                     isCategory: isCategory, | ||||
|                                                   ); | ||||
|                                                 }, | ||||
|                                                 icon: const Icon( | ||||
|                                                   Symbols.remove_circle, | ||||
|                                                 ), | ||||
|                                                 label: Text('subscribe'.tr()), | ||||
|                                               ), | ||||
|                                   error: | ||||
|                                       (error, _) => Text( | ||||
|                                         'Error loading subscription status', | ||||
|                                       ), | ||||
|                                   loading: | ||||
|                                       () => | ||||
|                                           CircularProgressIndicator().center(), | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ).padding(horizontal: 24, vertical: 16), | ||||
|                         error: | ||||
|                             (error, _) => ResponseErrorWidget( | ||||
|                               error: error, | ||||
|                               onRetry: | ||||
|                                   () => ref.invalidate(postTagProvider(slug)), | ||||
|                             ), | ||||
|                         loading: () => ResponseLoadingWidget(), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             const SliverGap(4), | ||||
|             SliverPostList( | ||||
|               categories: isCategory ? [slug] : null, | ||||
|               tags: isCategory ? null : [slug], | ||||
|               maxWidth: 540 + 16, | ||||
|             ), | ||||
|             SliverGap(MediaQuery.of(context).padding.bottom + 8), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -266,5 +266,146 @@ class _PostTagProviderElement | ||||
|   String get slug => (origin as PostTagProvider).slug; | ||||
| } | ||||
|  | ||||
| String _$postCategorySubscriptionStatusHash() => | ||||
|     r'407dc7fcaeffc461b591b4ee2418811aa4f0a63f'; | ||||
|  | ||||
| /// See also [postCategorySubscriptionStatus]. | ||||
| @ProviderFor(postCategorySubscriptionStatus) | ||||
| const postCategorySubscriptionStatusProvider = | ||||
|     PostCategorySubscriptionStatusFamily(); | ||||
|  | ||||
| /// See also [postCategorySubscriptionStatus]. | ||||
| class PostCategorySubscriptionStatusFamily extends Family<AsyncValue<bool>> { | ||||
|   /// See also [postCategorySubscriptionStatus]. | ||||
|   const PostCategorySubscriptionStatusFamily(); | ||||
|  | ||||
|   /// See also [postCategorySubscriptionStatus]. | ||||
|   PostCategorySubscriptionStatusProvider call(String slug, bool isCategory) { | ||||
|     return PostCategorySubscriptionStatusProvider(slug, isCategory); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   PostCategorySubscriptionStatusProvider getProviderOverride( | ||||
|     covariant PostCategorySubscriptionStatusProvider provider, | ||||
|   ) { | ||||
|     return call(provider.slug, provider.isCategory); | ||||
|   } | ||||
|  | ||||
|   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'postCategorySubscriptionStatusProvider'; | ||||
| } | ||||
|  | ||||
| /// See also [postCategorySubscriptionStatus]. | ||||
| class PostCategorySubscriptionStatusProvider | ||||
|     extends AutoDisposeFutureProvider<bool> { | ||||
|   /// See also [postCategorySubscriptionStatus]. | ||||
|   PostCategorySubscriptionStatusProvider(String slug, bool isCategory) | ||||
|     : this._internal( | ||||
|         (ref) => postCategorySubscriptionStatus( | ||||
|           ref as PostCategorySubscriptionStatusRef, | ||||
|           slug, | ||||
|           isCategory, | ||||
|         ), | ||||
|         from: postCategorySubscriptionStatusProvider, | ||||
|         name: r'postCategorySubscriptionStatusProvider', | ||||
|         debugGetCreateSourceHash: | ||||
|             const bool.fromEnvironment('dart.vm.product') | ||||
|                 ? null | ||||
|                 : _$postCategorySubscriptionStatusHash, | ||||
|         dependencies: PostCategorySubscriptionStatusFamily._dependencies, | ||||
|         allTransitiveDependencies: | ||||
|             PostCategorySubscriptionStatusFamily._allTransitiveDependencies, | ||||
|         slug: slug, | ||||
|         isCategory: isCategory, | ||||
|       ); | ||||
|  | ||||
|   PostCategorySubscriptionStatusProvider._internal( | ||||
|     super._createNotifier, { | ||||
|     required super.name, | ||||
|     required super.dependencies, | ||||
|     required super.allTransitiveDependencies, | ||||
|     required super.debugGetCreateSourceHash, | ||||
|     required super.from, | ||||
|     required this.slug, | ||||
|     required this.isCategory, | ||||
|   }) : super.internal(); | ||||
|  | ||||
|   final String slug; | ||||
|   final bool isCategory; | ||||
|  | ||||
|   @override | ||||
|   Override overrideWith( | ||||
|     FutureOr<bool> Function(PostCategorySubscriptionStatusRef provider) create, | ||||
|   ) { | ||||
|     return ProviderOverride( | ||||
|       origin: this, | ||||
|       override: PostCategorySubscriptionStatusProvider._internal( | ||||
|         (ref) => create(ref as PostCategorySubscriptionStatusRef), | ||||
|         from: from, | ||||
|         name: null, | ||||
|         dependencies: null, | ||||
|         allTransitiveDependencies: null, | ||||
|         debugGetCreateSourceHash: null, | ||||
|         slug: slug, | ||||
|         isCategory: isCategory, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   AutoDisposeFutureProviderElement<bool> createElement() { | ||||
|     return _PostCategorySubscriptionStatusProviderElement(this); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     return other is PostCategorySubscriptionStatusProvider && | ||||
|         other.slug == slug && | ||||
|         other.isCategory == isCategory; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     var hash = _SystemHash.combine(0, runtimeType.hashCode); | ||||
|     hash = _SystemHash.combine(hash, slug.hashCode); | ||||
|     hash = _SystemHash.combine(hash, isCategory.hashCode); | ||||
|  | ||||
|     return _SystemHash.finish(hash); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| mixin PostCategorySubscriptionStatusRef on AutoDisposeFutureProviderRef<bool> { | ||||
|   /// The parameter `slug` of this provider. | ||||
|   String get slug; | ||||
|  | ||||
|   /// The parameter `isCategory` of this provider. | ||||
|   bool get isCategory; | ||||
| } | ||||
|  | ||||
| class _PostCategorySubscriptionStatusProviderElement | ||||
|     extends AutoDisposeFutureProviderElement<bool> | ||||
|     with PostCategorySubscriptionStatusRef { | ||||
|   _PostCategorySubscriptionStatusProviderElement(super.provider); | ||||
|  | ||||
|   @override | ||||
|   String get slug => (origin as PostCategorySubscriptionStatusProvider).slug; | ||||
|   @override | ||||
|   bool get isCategory => | ||||
|       (origin as PostCategorySubscriptionStatusProvider).isCategory; | ||||
| } | ||||
|  | ||||
| // 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,15 +1,26 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| 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_item.dart'; | ||||
| import 'package:island/widgets/post/post_pin_sheet.dart'; | ||||
| import 'package:island/widgets/post/post_quick_reply.dart'; | ||||
| import 'package:island/widgets/post/post_replies.dart'; | ||||
| import 'package:island/widgets/response.dart'; | ||||
| import 'package:island/utils/share_utils.dart'; | ||||
| import 'package:island/widgets/safety/abuse_report_helper.dart'; | ||||
| import 'package:island/widgets/share/share_sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| @@ -46,6 +57,321 @@ class PostState extends StateNotifier<AsyncValue<SnPost?>> { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PostActionButtons extends HookConsumerWidget { | ||||
|   final SnPost post; | ||||
|   final EdgeInsets renderingPadding; | ||||
|   final VoidCallback? onRefresh; | ||||
|   final Function(SnPost)? onUpdate; | ||||
|  | ||||
|   const PostActionButtons({ | ||||
|     super.key, | ||||
|     required this.post, | ||||
|     this.renderingPadding = EdgeInsets.zero, | ||||
|     this.onRefresh, | ||||
|     this.onUpdate, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final user = ref.watch(userInfoProvider); | ||||
|     final isAuthor = | ||||
|         user.value != null && user.value?.id == post.publisher.accountId; | ||||
|  | ||||
|     final actions = <Widget>[]; | ||||
|  | ||||
|     const kButtonHeight = 40.0; | ||||
|     const kButtonRadius = 20.0; | ||||
|  | ||||
|     // 1. Author-only actions first | ||||
|     if (isAuthor) { | ||||
|       // Combined edit/delete actions using custom segmented-style buttons | ||||
|       final editButtons = <Widget>[ | ||||
|         FilledButton.tonal( | ||||
|           onPressed: () { | ||||
|             context.pushNamed('postEdit', pathParameters: {'id': post.id}).then( | ||||
|               (value) { | ||||
|                 if (value != null) { | ||||
|                   onRefresh?.call(); | ||||
|                 } | ||||
|               }, | ||||
|             ); | ||||
|           }, | ||||
|           style: FilledButton.styleFrom( | ||||
|             shape: const RoundedRectangleBorder( | ||||
|               borderRadius: BorderRadius.only( | ||||
|                 topLeft: Radius.circular(kButtonRadius), | ||||
|                 bottomLeft: Radius.circular(kButtonRadius), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           child: Row( | ||||
|             mainAxisSize: MainAxisSize.min, | ||||
|             children: [ | ||||
|               const Icon(Symbols.edit, size: 18), | ||||
|               const Gap(4), | ||||
|               Text('edit'.tr()), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|         Tooltip( | ||||
|           message: 'delete'.tr(), | ||||
|           child: FilledButton.tonal( | ||||
|             onPressed: () { | ||||
|               showConfirmAlert('deletePostHint'.tr(), 'deletePost'.tr()).then(( | ||||
|                 confirm, | ||||
|               ) { | ||||
|                 if (confirm) { | ||||
|                   final client = ref.watch(apiClientProvider); | ||||
|                   client | ||||
|                       .delete('/sphere/posts/${post.id}') | ||||
|                       .catchError((err) { | ||||
|                         showErrorAlert(err); | ||||
|                         return err; | ||||
|                       }) | ||||
|                       .then((_) { | ||||
|                         onRefresh?.call(); | ||||
|                       }); | ||||
|                 } | ||||
|               }); | ||||
|             }, | ||||
|             style: FilledButton.styleFrom( | ||||
|               shape: const RoundedRectangleBorder( | ||||
|                 borderRadius: BorderRadius.only( | ||||
|                   topRight: Radius.circular(kButtonRadius), | ||||
|                   bottomRight: Radius.circular(kButtonRadius), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             child: const Icon(Symbols.delete, size: 18), | ||||
|           ), | ||||
|         ), | ||||
|       ]; | ||||
|  | ||||
|       actions.add( | ||||
|         Row( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: | ||||
|               editButtons | ||||
|                   .map((e) => SizedBox(height: kButtonHeight, child: e)) | ||||
|                   .expand((widget) => [widget, const VerticalDivider(width: 1)]) | ||||
|                   .toList() | ||||
|                 ..removeLast(), | ||||
|         ), | ||||
|       ); | ||||
|  | ||||
|       // Pin/Unpin actions (also author-only) | ||||
|       if (post.pinMode == null) { | ||||
|         actions.add( | ||||
|           FilledButton.tonalIcon( | ||||
|             onPressed: () { | ||||
|               showModalBottomSheet( | ||||
|                 context: context, | ||||
|                 isScrollControlled: true, | ||||
|                 builder: (context) => PostPinSheet(post: post), | ||||
|               ).then((value) { | ||||
|                 if (value is int) { | ||||
|                   onUpdate?.call(post.copyWith(pinMode: value)); | ||||
|                 } | ||||
|               }); | ||||
|             }, | ||||
|             icon: const Icon(Symbols.keep), | ||||
|             label: Text('pinPost'.tr()), | ||||
|           ), | ||||
|         ); | ||||
|       } else { | ||||
|         actions.add( | ||||
|           FilledButton.tonalIcon( | ||||
|             onPressed: () { | ||||
|               showConfirmAlert('unpinPostHint'.tr(), 'unpinPost'.tr()).then(( | ||||
|                 confirm, | ||||
|               ) async { | ||||
|                 if (confirm) { | ||||
|                   final client = ref.watch(apiClientProvider); | ||||
|                   try { | ||||
|                     if (context.mounted) showLoadingModal(context); | ||||
|                     await client.delete('/sphere/posts/${post.id}/pin'); | ||||
|                     onUpdate?.call(post.copyWith(pinMode: null)); | ||||
|                   } catch (err) { | ||||
|                     showErrorAlert(err); | ||||
|                   } finally { | ||||
|                     if (context.mounted) hideLoadingModal(context); | ||||
|                   } | ||||
|                 } | ||||
|               }); | ||||
|             }, | ||||
|             icon: const Icon(Symbols.keep_off), | ||||
|             label: Text('unpinPost'.tr()), | ||||
|           ), | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // 2. Replies and forwards | ||||
|     final replyButtons = <Widget>[ | ||||
|       FilledButton.tonal( | ||||
|         onPressed: () { | ||||
|           context.pushNamed( | ||||
|             'postCompose', | ||||
|             extra: PostComposeInitialState(replyingTo: post), | ||||
|           ); | ||||
|         }, | ||||
|         style: FilledButton.styleFrom( | ||||
|           shape: const RoundedRectangleBorder( | ||||
|             borderRadius: BorderRadius.only( | ||||
|               topLeft: Radius.circular(kButtonRadius), | ||||
|               bottomLeft: Radius.circular(kButtonRadius), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|         child: Row( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: [ | ||||
|             const Icon(Symbols.reply, size: 18), | ||||
|             const Gap(4), | ||||
|             Text('reply'.tr()), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|       Tooltip( | ||||
|         message: 'forward'.tr(), | ||||
|         child: FilledButton.tonal( | ||||
|           onPressed: () { | ||||
|             context.pushNamed( | ||||
|               'postCompose', | ||||
|               extra: PostComposeInitialState(forwardingTo: post), | ||||
|             ); | ||||
|           }, | ||||
|           style: FilledButton.styleFrom( | ||||
|             shape: const RoundedRectangleBorder( | ||||
|               borderRadius: BorderRadius.only( | ||||
|                 topRight: Radius.circular(kButtonRadius), | ||||
|                 bottomRight: Radius.circular(kButtonRadius), | ||||
|               ), | ||||
|             ), | ||||
|           ), | ||||
|           child: const Icon(Symbols.forward, size: 18), | ||||
|         ), | ||||
|       ), | ||||
|     ]; | ||||
|  | ||||
|     actions.add( | ||||
|       Row( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: | ||||
|             replyButtons | ||||
|                 .map((e) => SizedBox(height: kButtonHeight, child: e)) | ||||
|                 .toList(), | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     // 3. Share, copy link, and report | ||||
|     final shareButtons = <Widget>[ | ||||
|       FilledButton.tonal( | ||||
|         onPressed: () { | ||||
|           showShareSheetLink( | ||||
|             context: context, | ||||
|             link: 'https://solian.app/posts/${post.id}', | ||||
|             title: 'sharePost'.tr(), | ||||
|             toSystem: true, | ||||
|           ); | ||||
|         }, | ||||
|         style: FilledButton.styleFrom( | ||||
|           shape: const RoundedRectangleBorder( | ||||
|             borderRadius: BorderRadius.only( | ||||
|               topLeft: Radius.circular(kButtonRadius), | ||||
|               bottomLeft: Radius.circular(kButtonRadius), | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|         child: Row( | ||||
|           mainAxisSize: MainAxisSize.min, | ||||
|           children: [ | ||||
|             const Icon(Symbols.share, size: 18), | ||||
|             const Gap(4), | ||||
|             Text('share'.tr()), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ]; | ||||
|  | ||||
|     if (!kIsWeb) { | ||||
|       shareButtons.add( | ||||
|         Tooltip( | ||||
|           message: 'sharePostPhoto'.tr(), | ||||
|           child: FilledButton.tonal( | ||||
|             onPressed: () => sharePostAsScreenshot(context, ref, post), | ||||
|             style: FilledButton.styleFrom( | ||||
|               shape: const RoundedRectangleBorder( | ||||
|                 borderRadius: BorderRadius.only( | ||||
|                   topRight: Radius.circular(kButtonRadius), | ||||
|                   bottomRight: Radius.circular(kButtonRadius), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             child: const Icon(Symbols.share_reviews, size: 18), | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     actions.add( | ||||
|       Row( | ||||
|         mainAxisSize: MainAxisSize.min, | ||||
|         children: | ||||
|             shareButtons | ||||
|                 .map((e) => SizedBox(height: kButtonHeight, child: e)) | ||||
|                 .expand((widget) => [widget, const VerticalDivider(width: 1)]) | ||||
|                 .toList() | ||||
|               ..removeLast(), | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     actions.add( | ||||
|       FilledButton.tonalIcon( | ||||
|         onPressed: () { | ||||
|           Clipboard.setData( | ||||
|             ClipboardData(text: 'https://solian.app/posts/${post.id}'), | ||||
|           ); | ||||
|         }, | ||||
|         icon: const Icon(Symbols.link), | ||||
|         label: Text('copyLink'.tr()), | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     actions.add( | ||||
|       FilledButton.tonalIcon( | ||||
|         onPressed: () { | ||||
|           showAbuseReportSheet(context, resourceIdentifier: 'post/${post.id}'); | ||||
|         }, | ||||
|         icon: const Icon(Symbols.flag), | ||||
|         label: Text('abuseReport'.tr()), | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     // Add gaps between actions (excluding first one) using FP style | ||||
|     final children = | ||||
|         actions.asMap().entries.expand((entry) { | ||||
|           final index = entry.key; | ||||
|           final action = entry.value; | ||||
|           if (index == 0) { | ||||
|             return [action]; | ||||
|           } else { | ||||
|             return [const Gap(8), action]; | ||||
|           } | ||||
|         }).toList(); | ||||
|  | ||||
|     return Container( | ||||
|       height: kButtonHeight, | ||||
|       margin: const EdgeInsets.only(bottom: 12), | ||||
|       child: ListView( | ||||
|         scrollDirection: Axis.horizontal, | ||||
|         padding: EdgeInsets.symmetric(horizontal: renderingPadding.horizontal), | ||||
|         children: children, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PostDetailScreen extends HookConsumerWidget { | ||||
|   final String id; | ||||
|   const PostDetailScreen({super.key, required this.id}); | ||||
| @@ -66,7 +392,13 @@ class PostDetailScreen extends HookConsumerWidget { | ||||
|           return Stack( | ||||
|             fit: StackFit.expand, | ||||
|             children: [ | ||||
|               CustomScrollView( | ||||
|               ExtendedRefreshIndicator( | ||||
|                 onRefresh: () async { | ||||
|                   ref.invalidate(postProvider(id)); | ||||
|                   ref.invalidate(postRepliesNotifierProvider(id)); | ||||
|                 }, | ||||
|                 child: CustomScrollView( | ||||
|                   physics: const AlwaysScrollableScrollPhysics(), | ||||
|                   slivers: [ | ||||
|                     SliverToBoxAdapter( | ||||
|                       child: Center( | ||||
| @@ -86,10 +418,33 @@ class PostDetailScreen extends HookConsumerWidget { | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                     SliverToBoxAdapter( | ||||
|                       child: Center( | ||||
|                         child: ConstrainedBox( | ||||
|                           constraints: BoxConstraints(maxWidth: 600), | ||||
|                           child: PostActionButtons( | ||||
|                             post: post, | ||||
|                             renderingPadding: const EdgeInsets.symmetric( | ||||
|                               horizontal: 8, | ||||
|                             ), | ||||
|                             onRefresh: () { | ||||
|                               ref.invalidate(postProvider(id)); | ||||
|                               ref.invalidate(postRepliesNotifierProvider(id)); | ||||
|                             }, | ||||
|                             onUpdate: (newItem) { | ||||
|                               ref | ||||
|                                   .read(postStateProvider(id).notifier) | ||||
|                                   .updatePost(newItem); | ||||
|                             }, | ||||
|                           ), | ||||
|                         ), | ||||
|                       ), | ||||
|                     ), | ||||
|                     PostRepliesList(postId: id, maxWidth: 600), | ||||
|                     SliverGap(MediaQuery.of(context).padding.bottom + 80), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|               if (user.value != null) | ||||
|                 Positioned( | ||||
|                   bottom: 0, | ||||
| @@ -126,7 +481,7 @@ class PostDetailScreen extends HookConsumerWidget { | ||||
|         error: | ||||
|             (e, _) => ResponseErrorWidget( | ||||
|               error: e, | ||||
|               onRetry: () => ref.invalidate(postStateProvider(id)), | ||||
|               onRetry: () => ref.invalidate(postProvider(id)), | ||||
|             ), | ||||
|       ), | ||||
|     ); | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import 'dart:async'; | ||||
| 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/post.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| @@ -7,6 +9,7 @@ import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/post/post_item.dart'; | ||||
| import 'package:island/widgets/response.dart'; | ||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| final postSearchNotifierProvider = StateNotifierProvider.autoDispose< | ||||
|   PostSearchNotifier, | ||||
| @@ -18,6 +21,13 @@ class PostSearchNotifier | ||||
|   final AutoDisposeRef ref; | ||||
|   static const int _pageSize = 20; | ||||
|   String _currentQuery = ''; | ||||
|   String? _pubName; | ||||
|   String? _realm; | ||||
|   int? _type; | ||||
|   List<String>? _categories; | ||||
|   List<String>? _tags; | ||||
|   bool _shuffle = false; | ||||
|   bool? _pinned; | ||||
|   bool _isLoading = false; | ||||
|  | ||||
|   PostSearchNotifier(this.ref) : super(const AsyncValue.loading()) { | ||||
| @@ -26,11 +36,38 @@ class PostSearchNotifier | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<void> search(String query) async { | ||||
|   Future<void> search( | ||||
|     String query, { | ||||
|     String? pubName, | ||||
|     String? realm, | ||||
|     int? type, | ||||
|     List<String>? categories, | ||||
|     List<String>? tags, | ||||
|     bool shuffle = false, | ||||
|     bool? pinned, | ||||
|   }) async { | ||||
|     if (_isLoading) return; | ||||
|  | ||||
|     _currentQuery = query.trim(); | ||||
|     if (_currentQuery.isEmpty) { | ||||
|     _pubName = pubName; | ||||
|     _realm = realm; | ||||
|     _type = type; | ||||
|     _categories = categories; | ||||
|     _tags = tags; | ||||
|     _shuffle = shuffle; | ||||
|     _pinned = pinned; | ||||
|  | ||||
|     // Allow search even with empty query if any filters are applied | ||||
|     final hasFilters = | ||||
|         pubName != null || | ||||
|         realm != null || | ||||
|         type != null || | ||||
|         categories != null || | ||||
|         tags != null || | ||||
|         shuffle || | ||||
|         pinned != null; | ||||
|  | ||||
|     if (_currentQuery.isEmpty && !hasFilters) { | ||||
|       state = AsyncValue.data( | ||||
|         CursorPagingData(items: [], hasMore: false, nextCursor: null), | ||||
|       ); | ||||
| @@ -57,6 +94,13 @@ class PostSearchNotifier | ||||
|           'offset': offset, | ||||
|           'take': _pageSize, | ||||
|           'vector': false, | ||||
|           if (_pubName != null) 'pub': _pubName, | ||||
|           if (_realm != null) 'realm': _realm, | ||||
|           if (_type != null) 'type': _type, | ||||
|           if (_tags != null) 'tags': _tags, | ||||
|           if (_categories != null) 'categories': _categories, | ||||
|           if (_shuffle) 'shuffle': true, | ||||
|           if (_pinned != null) 'pinned': _pinned, | ||||
|         }, | ||||
|       ); | ||||
|  | ||||
| @@ -80,42 +124,177 @@ class PostSearchNotifier | ||||
|   } | ||||
| } | ||||
|  | ||||
| class PostSearchScreen extends ConsumerStatefulWidget { | ||||
| class PostSearchScreen extends HookConsumerWidget { | ||||
|   const PostSearchScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   ConsumerState<PostSearchScreen> createState() => _PostSearchScreenState(); | ||||
| } | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final searchController = useTextEditingController(); | ||||
|     final debounce = useMemoized(() => Duration(milliseconds: 500)); | ||||
|     final debounceTimer = useRef<Timer?>(null); | ||||
|     final showFilters = useState(false); | ||||
|     final pubNameController = useTextEditingController(); | ||||
|     final realmController = useTextEditingController(); | ||||
|     final typeValue = useState<int?>(null); | ||||
|     final selectedCategories = useState<List<String>>([]); | ||||
|     final selectedTags = useState<List<String>>([]); | ||||
|     final shuffleValue = useState(false); | ||||
|     final pinnedValue = useState<bool?>(null); | ||||
|  | ||||
| class _PostSearchScreenState extends ConsumerState<PostSearchScreen> { | ||||
|   final _searchController = TextEditingController(); | ||||
|   final _debounce = Duration(milliseconds: 500); | ||||
|   Timer? _debounceTimer; | ||||
|     useEffect(() { | ||||
|       return () { | ||||
|         searchController.dispose(); | ||||
|         pubNameController.dispose(); | ||||
|         realmController.dispose(); | ||||
|         debounceTimer.value?.cancel(); | ||||
|       }; | ||||
|     }, []); | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _searchController.dispose(); | ||||
|     _debounceTimer?.cancel(); | ||||
|     super.dispose(); | ||||
|   } | ||||
|     void onSearchChanged(String query) { | ||||
|       if (debounceTimer.value?.isActive ?? false) debounceTimer.value!.cancel(); | ||||
|  | ||||
|   void _onSearchChanged(String query) { | ||||
|     if (_debounceTimer?.isActive ?? false) _debounceTimer!.cancel(); | ||||
|  | ||||
|     _debounceTimer = Timer(_debounce, () { | ||||
|       debounceTimer.value = Timer(debounce, () { | ||||
|         ref.read(postSearchNotifierProvider.notifier).search(query); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     void onSearchWithFilters(String query) { | ||||
|       if (debounceTimer.value?.isActive ?? false) debounceTimer.value!.cancel(); | ||||
|  | ||||
|       debounceTimer.value = Timer(debounce, () { | ||||
|         ref | ||||
|             .read(postSearchNotifierProvider.notifier) | ||||
|             .search( | ||||
|               query, | ||||
|               pubName: | ||||
|                   pubNameController.text.isNotEmpty | ||||
|                       ? pubNameController.text | ||||
|                       : null, | ||||
|               realm: | ||||
|                   realmController.text.isNotEmpty ? realmController.text : null, | ||||
|               type: typeValue.value, | ||||
|               categories: | ||||
|                   selectedCategories.value.isNotEmpty | ||||
|                       ? selectedCategories.value | ||||
|                       : null, | ||||
|               tags: selectedTags.value.isNotEmpty ? selectedTags.value : null, | ||||
|               shuffle: shuffleValue.value, | ||||
|               pinned: pinnedValue.value, | ||||
|             ); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     void toggleFilters() { | ||||
|       showFilters.value = !showFilters.value; | ||||
|     } | ||||
|  | ||||
|     void applyFilters() { | ||||
|       onSearchWithFilters(searchController.text); | ||||
|     } | ||||
|  | ||||
|     void clearFilters() { | ||||
|       pubNameController.clear(); | ||||
|       realmController.clear(); | ||||
|       typeValue.value = null; | ||||
|       selectedCategories.value = []; | ||||
|       selectedTags.value = []; | ||||
|       shuffleValue.value = false; | ||||
|       pinnedValue.value = null; | ||||
|       onSearchChanged(searchController.text); | ||||
|     } | ||||
|  | ||||
|     Widget buildFilterPanel() { | ||||
|       return Card( | ||||
|         margin: EdgeInsets.symmetric(vertical: 8, horizontal: 8), | ||||
|         child: Padding( | ||||
|           padding: EdgeInsets.all(16), | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|             children: [ | ||||
|               Row( | ||||
|                 mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                 children: [ | ||||
|                   Text( | ||||
|                     'filters'.tr(), | ||||
|                     style: Theme.of(context).textTheme.titleMedium, | ||||
|                   ).padding(left: 4), | ||||
|                   Row( | ||||
|                     children: [ | ||||
|                       TextButton( | ||||
|                         onPressed: applyFilters, | ||||
|                         child: Text('apply'.tr()), | ||||
|                       ), | ||||
|                       TextButton( | ||||
|                         onPressed: clearFilters, | ||||
|                         child: Text('clear'.tr()), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|               SizedBox(height: 16), | ||||
|               TextField( | ||||
|                 controller: pubNameController, | ||||
|                 decoration: InputDecoration( | ||||
|                   labelText: 'pubName'.tr(), | ||||
|                   border: OutlineInputBorder(), | ||||
|                 ), | ||||
|                 onChanged: | ||||
|                     (value) => onSearchWithFilters(searchController.text), | ||||
|               ), | ||||
|               SizedBox(height: 8), | ||||
|               TextField( | ||||
|                 controller: realmController, | ||||
|                 decoration: InputDecoration( | ||||
|                   labelText: 'realm'.tr(), | ||||
|                   border: OutlineInputBorder(), | ||||
|                 ), | ||||
|                 onChanged: | ||||
|                     (value) => onSearchWithFilters(searchController.text), | ||||
|               ), | ||||
|               SizedBox(height: 8), | ||||
|               Row( | ||||
|                 children: [ | ||||
|                   Checkbox( | ||||
|                     value: shuffleValue.value, | ||||
|                     onChanged: (value) { | ||||
|                       shuffleValue.value = value ?? false; | ||||
|                       onSearchWithFilters(searchController.text); | ||||
|                     }, | ||||
|                   ), | ||||
|                   Text('shuffle'.tr()), | ||||
|                 ], | ||||
|               ), | ||||
|               Row( | ||||
|                 children: [ | ||||
|                   Checkbox( | ||||
|                     value: pinnedValue.value ?? false, | ||||
|                     onChanged: (value) { | ||||
|                       pinnedValue.value = value; | ||||
|                       onSearchWithFilters(searchController.text); | ||||
|                     }, | ||||
|                   ), | ||||
|                   Text('pinned'.tr()), | ||||
|                 ], | ||||
|               ), | ||||
|               // TODO: Add dropdown for type selection | ||||
|               // TODO: Add multi-select for categories and tags | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     return AppScaffold( | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar( | ||||
|         title: TextField( | ||||
|           controller: _searchController, | ||||
|         title: Row( | ||||
|           children: [ | ||||
|             Expanded( | ||||
|               child: TextField( | ||||
|                 controller: searchController, | ||||
|                 decoration: InputDecoration( | ||||
|             hintText: 'Search posts...', | ||||
|                   hintText: 'search'.tr(), | ||||
|                   border: InputBorder.none, | ||||
|                   hintStyle: TextStyle( | ||||
|                     color: Theme.of(context).appBarTheme.foregroundColor, | ||||
| @@ -124,32 +303,55 @@ class _PostSearchScreenState extends ConsumerState<PostSearchScreen> { | ||||
|                 style: TextStyle( | ||||
|                   color: Theme.of(context).appBarTheme.foregroundColor, | ||||
|                 ), | ||||
|           onChanged: _onSearchChanged, | ||||
|                 onChanged: onSearchChanged, | ||||
|                 onSubmitted: (value) { | ||||
|             ref.read(postSearchNotifierProvider.notifier).search(value); | ||||
|                   onSearchWithFilters(value); | ||||
|                 }, | ||||
|                 autofocus: true, | ||||
|               ), | ||||
|             ), | ||||
|             IconButton( | ||||
|               icon: Icon( | ||||
|                 showFilters.value | ||||
|                     ? Icons.filter_alt | ||||
|                     : Icons.filter_alt_outlined, | ||||
|               ), | ||||
|               onPressed: toggleFilters, | ||||
|               tooltip: 'toggleFilters'.tr(), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|       body: Consumer( | ||||
|         builder: (context, ref, child) { | ||||
|           final searchState = ref.watch(postSearchNotifierProvider); | ||||
|  | ||||
|           return searchState.when( | ||||
|           return CustomScrollView( | ||||
|             slivers: [ | ||||
|               if (showFilters.value) | ||||
|                 SliverToBoxAdapter( | ||||
|                   child: Center( | ||||
|                     child: ConstrainedBox( | ||||
|                       constraints: const BoxConstraints(maxWidth: 600), | ||||
|                       child: buildFilterPanel(), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               searchState.when( | ||||
|                 data: (data) { | ||||
|               if (data.items.isEmpty && _searchController.text.isNotEmpty) { | ||||
|                 return const Center(child: Text('No results found')); | ||||
|                   if (data.items.isEmpty && searchController.text.isNotEmpty) { | ||||
|                     return SliverFillRemaining( | ||||
|                       child: Center(child: Text('noResultsFound'.tr())), | ||||
|                     ); | ||||
|                   } | ||||
|  | ||||
|               return ListView.builder( | ||||
|                 padding: EdgeInsets.zero, | ||||
|                 itemCount: data.items.length + (data.hasMore ? 1 : 0), | ||||
|                 itemBuilder: (context, index) { | ||||
|                   return SliverList( | ||||
|                     delegate: SliverChildBuilderDelegate((context, index) { | ||||
|                       if (index >= data.items.length) { | ||||
|                         ref | ||||
|                             .read(postSearchNotifierProvider.notifier) | ||||
|                             .fetch(cursor: data.nextCursor); | ||||
|                     return const Center(child: CircularProgressIndicator()); | ||||
|                         return Center(child: CircularProgressIndicator()); | ||||
|                       } | ||||
|  | ||||
|                       final post = data.items[index]; | ||||
| @@ -161,19 +363,30 @@ class _PostSearchScreenState extends ConsumerState<PostSearchScreen> { | ||||
|                               horizontal: 8, | ||||
|                               vertical: 4, | ||||
|                             ), | ||||
|                         child: PostActionableItem(item: post, borderRadius: 8), | ||||
|                             child: PostActionableItem( | ||||
|                               item: post, | ||||
|                               borderRadius: 8, | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                       ); | ||||
|                 }, | ||||
|                     }, childCount: data.items.length + (data.hasMore ? 1 : 0)), | ||||
|                   ); | ||||
|                 }, | ||||
|             loading: () => const Center(child: CircularProgressIndicator()), | ||||
|                 loading: | ||||
|                     () => SliverFillRemaining( | ||||
|                       child: Center(child: CircularProgressIndicator()), | ||||
|                     ), | ||||
|                 error: | ||||
|                 (error, stack) => ResponseErrorWidget( | ||||
|                     (error, stack) => SliverFillRemaining( | ||||
|                       child: ResponseErrorWidget( | ||||
|                         error: error, | ||||
|                   onRetry: () => ref.invalidate(postSearchNotifierProvider), | ||||
|                         onRetry: | ||||
|                             () => ref.invalidate(postSearchNotifierProvider), | ||||
|                       ), | ||||
|                     ), | ||||
|               ), | ||||
|             ], | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|   | ||||
| @@ -27,6 +27,224 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| part 'pub_profile.g.dart'; | ||||
|  | ||||
| class _PublisherBasisWidget extends StatelessWidget { | ||||
|   final SnPublisher data; | ||||
|   final AsyncValue<SnSubscriptionStatus> subStatus; | ||||
|   final ValueNotifier<bool> subscribing; | ||||
|   final VoidCallback subscribe; | ||||
|   final VoidCallback unsubscribe; | ||||
|  | ||||
|   const _PublisherBasisWidget({ | ||||
|     required this.data, | ||||
|     required this.subStatus, | ||||
|     required this.subscribing, | ||||
|     required this.subscribe, | ||||
|     required this.unsubscribe, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Row( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       spacing: 20, | ||||
|       children: [ | ||||
|         GestureDetector( | ||||
|           child: Badge( | ||||
|             isLabelVisible: data.type == 0, | ||||
|             padding: EdgeInsets.all(4), | ||||
|             label: Icon( | ||||
|               Symbols.launch, | ||||
|               size: 16, | ||||
|               color: Theme.of(context).colorScheme.onPrimary, | ||||
|             ), | ||||
|             backgroundColor: Theme.of(context).colorScheme.primary, | ||||
|             offset: Offset(0, 48), | ||||
|             child: ProfilePictureWidget( | ||||
|               file: data.picture, | ||||
|               radius: 32, | ||||
|               borderRadius: data.type == 0 ? null : 12, | ||||
|             ), | ||||
|           ), | ||||
|           onTap: () { | ||||
|             if (data.account?.name != null) { | ||||
|               Navigator.pop(context, true); | ||||
|               context.pushNamed( | ||||
|                 'accountProfile', | ||||
|                 pathParameters: {'name': data.account!.name}, | ||||
|               ); | ||||
|             } | ||||
|           }, | ||||
|         ), | ||||
|         Expanded( | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|             children: [ | ||||
|               Row( | ||||
|                 spacing: 6, | ||||
|                 children: [ | ||||
|                   Text(data.nick).fontSize(20), | ||||
|                   if (data.verification != null) | ||||
|                     VerificationMark(mark: data.verification!), | ||||
|                   Expanded( | ||||
|                     child: Text( | ||||
|                       '@${data.name}', | ||||
|                       maxLines: 1, | ||||
|                       overflow: TextOverflow.ellipsis, | ||||
|                     ).fontSize(14).opacity(0.85), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|               if (data.type == 0 && data.account != null) | ||||
|                 Row( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   spacing: 6, | ||||
|                   children: [ | ||||
|                     Icon( | ||||
|                       data.type == 0 ? Symbols.person : Symbols.workspaces, | ||||
|                       fill: 1, | ||||
|                       size: 17, | ||||
|                     ), | ||||
|                     Text( | ||||
|                       'publisherBelongsTo'.tr(args: ['@${data.account!.name}']), | ||||
|                     ).fontSize(14), | ||||
|                   ], | ||||
|                 ).opacity(0.85), | ||||
|               const Gap(4), | ||||
|               if (data.type == 0 && data.account != null) | ||||
|                 AccountStatusWidget( | ||||
|                   uname: data.account!.name, | ||||
|                   padding: EdgeInsets.zero, | ||||
|                 ), | ||||
|               subStatus | ||||
|                   .when( | ||||
|                     data: | ||||
|                         (status) => FilledButton.icon( | ||||
|                           onPressed: | ||||
|                               subscribing.value | ||||
|                                   ? null | ||||
|                                   : (status.isSubscribed | ||||
|                                       ? unsubscribe | ||||
|                                       : subscribe), | ||||
|                           icon: Icon( | ||||
|                             status.isSubscribed | ||||
|                                 ? Symbols.remove_circle | ||||
|                                 : Symbols.add_circle, | ||||
|                           ), | ||||
|                           label: | ||||
|                               Text( | ||||
|                                 status.isSubscribed | ||||
|                                     ? 'unsubscribe' | ||||
|                                     : 'subscribe', | ||||
|                               ).tr(), | ||||
|                           style: ButtonStyle( | ||||
|                             visualDensity: VisualDensity(vertical: -2), | ||||
|                           ), | ||||
|                         ), | ||||
|                     error: (_, _) => const SizedBox(), | ||||
|                     loading: | ||||
|                         () => const SizedBox( | ||||
|                           height: 36, | ||||
|                           child: Center( | ||||
|                             child: SizedBox( | ||||
|                               width: 20, | ||||
|                               height: 20, | ||||
|                               child: CircularProgressIndicator(strokeWidth: 2), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                   ) | ||||
|                   .padding(top: 8), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ).padding(horizontal: 24, top: 24); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PublisherBadgesWidget extends StatelessWidget { | ||||
|   final SnPublisher data; | ||||
|   final AsyncValue<List<SnAccountBadge>> badges; | ||||
|  | ||||
|   const _PublisherBadgesWidget({required this.data, required this.badges}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return (badges.value?.isNotEmpty ?? false) | ||||
|         ? Card( | ||||
|           child: BadgeList( | ||||
|             badges: badges.value!, | ||||
|           ).padding(horizontal: 26, vertical: 20), | ||||
|         ).padding(horizontal: 4) | ||||
|         : const SizedBox.shrink(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PublisherVerificationWidget extends StatelessWidget { | ||||
|   final SnPublisher data; | ||||
|  | ||||
|   const _PublisherVerificationWidget({required this.data}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return (data.verification != null) | ||||
|         ? Card( | ||||
|           margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|           child: VerificationStatusCard(mark: data.verification!), | ||||
|         ) | ||||
|         : const SizedBox.shrink(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PublisherBioWidget extends StatelessWidget { | ||||
|   final SnPublisher data; | ||||
|  | ||||
|   const _PublisherBioWidget({required this.data}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Card( | ||||
|       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|         children: [ | ||||
|           Text('bio').tr().bold().fontSize(15).padding(bottom: 8), | ||||
|           if (data.bio.isEmpty) | ||||
|             Text('descriptionNone').tr().italic() | ||||
|           else | ||||
|             MarkdownTextContent( | ||||
|               content: data.bio, | ||||
|               linesMargin: EdgeInsets.zero, | ||||
|             ), | ||||
|         ], | ||||
|       ).padding(horizontal: 20, vertical: 16), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _PublisherCategoryTabWidget extends StatelessWidget { | ||||
|   final TabController categoryTabController; | ||||
|  | ||||
|   const _PublisherCategoryTabWidget({required this.categoryTabController}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Card( | ||||
|       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|       child: TabBar( | ||||
|         controller: categoryTabController, | ||||
|         dividerColor: Colors.transparent, | ||||
|         splashBorderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|         tabs: [ | ||||
|           Tab(text: 'all'.tr()), | ||||
|           Tab(text: 'postTypePost'.tr()), | ||||
|           Tab(text: 'postArticle'.tr()), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<SnPublisher> publisher(Ref ref, String uname) async { | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
| @@ -132,165 +350,6 @@ class PublisherProfileScreen extends HookConsumerWidget { | ||||
|       offset: Offset(1.0, 1.0), | ||||
|     ); | ||||
|  | ||||
|     Widget publisherBasisWidget(SnPublisher data) => Row( | ||||
|       crossAxisAlignment: CrossAxisAlignment.start, | ||||
|       spacing: 20, | ||||
|       children: [ | ||||
|         GestureDetector( | ||||
|           child: Badge( | ||||
|             isLabelVisible: data.type == 0, | ||||
|             padding: EdgeInsets.all(4), | ||||
|             label: Icon( | ||||
|               Symbols.launch, | ||||
|               size: 16, | ||||
|               color: Theme.of(context).colorScheme.onPrimary, | ||||
|             ), | ||||
|             backgroundColor: Theme.of(context).colorScheme.primary, | ||||
|             offset: Offset(0, 48), | ||||
|             child: ProfilePictureWidget( | ||||
|               file: data.picture, | ||||
|               radius: 32, | ||||
|               borderRadius: data.type == 0 ? null : 12, | ||||
|             ), | ||||
|           ), | ||||
|           onTap: () { | ||||
|             Navigator.pop(context, true); | ||||
|             if (data.account?.name != null) { | ||||
|               context.pushNamed( | ||||
|                 'accountProfile', | ||||
|                 pathParameters: {'name': data.account!.name}, | ||||
|               ); | ||||
|             } | ||||
|           }, | ||||
|         ), | ||||
|         Expanded( | ||||
|           child: Column( | ||||
|             crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|             children: [ | ||||
|               Row( | ||||
|                 spacing: 6, | ||||
|                 children: [ | ||||
|                   Text(data.nick).fontSize(20), | ||||
|                   if (data.verification != null) | ||||
|                     VerificationMark(mark: data.verification!), | ||||
|                   Expanded( | ||||
|                     child: Text( | ||||
|                       '@${data.name}', | ||||
|                       maxLines: 1, | ||||
|                       overflow: TextOverflow.ellipsis, | ||||
|                     ).fontSize(14).opacity(0.85), | ||||
|                   ), | ||||
|                 ], | ||||
|               ), | ||||
|               if (data.type == 0 && data.account != null) | ||||
|                 Row( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   spacing: 6, | ||||
|                   children: [ | ||||
|                     Icon( | ||||
|                       data.type == 0 ? Symbols.person : Symbols.workspaces, | ||||
|                       fill: 1, | ||||
|                       size: 17, | ||||
|                     ), | ||||
|                     Text( | ||||
|                       'publisherBelongsTo'.tr(args: ['@${data.account!.name}']), | ||||
|                     ).fontSize(14), | ||||
|                   ], | ||||
|                 ).opacity(0.85).padding(bottom: 6), | ||||
|               if (data.type == 0 && data.account != null) | ||||
|                 AccountStatusWidget( | ||||
|                   uname: data.account!.name, | ||||
|                   padding: EdgeInsets.zero, | ||||
|                 ), | ||||
|               subStatus | ||||
|                   .when( | ||||
|                     data: | ||||
|                         (status) => FilledButton.icon( | ||||
|                           onPressed: | ||||
|                               subscribing.value | ||||
|                                   ? null | ||||
|                                   : (status.isSubscribed | ||||
|                                       ? unsubscribe | ||||
|                                       : subscribe), | ||||
|                           icon: Icon( | ||||
|                             status.isSubscribed | ||||
|                                 ? Symbols.remove_circle | ||||
|                                 : Symbols.add_circle, | ||||
|                           ), | ||||
|                           label: | ||||
|                               Text( | ||||
|                                 status.isSubscribed | ||||
|                                     ? 'unsubscribe' | ||||
|                                     : 'subscribe', | ||||
|                               ).tr(), | ||||
|                           style: ButtonStyle( | ||||
|                             visualDensity: VisualDensity(vertical: -2), | ||||
|                           ), | ||||
|                         ), | ||||
|                     error: (_, _) => const SizedBox(), | ||||
|                     loading: | ||||
|                         () => const SizedBox( | ||||
|                           height: 36, | ||||
|                           child: Center( | ||||
|                             child: SizedBox( | ||||
|                               width: 20, | ||||
|                               height: 20, | ||||
|                               child: CircularProgressIndicator(strokeWidth: 2), | ||||
|                             ), | ||||
|                           ), | ||||
|                         ), | ||||
|                   ) | ||||
|                   .padding(top: 8), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ).padding(horizontal: 24, top: 24); | ||||
|  | ||||
|     Widget publisherBadgesWidget(SnPublisher data) => | ||||
|         (badges.value?.isNotEmpty ?? false) | ||||
|             ? Card( | ||||
|               child: BadgeList( | ||||
|                 badges: badges.value!, | ||||
|               ).padding(horizontal: 26, vertical: 20), | ||||
|             ).padding(horizontal: 4) | ||||
|             : const SizedBox.shrink(); | ||||
|  | ||||
|     Widget publisherVerificationWidget(SnPublisher data) => | ||||
|         (data.verification != null) | ||||
|             ? Card( | ||||
|               margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|               child: VerificationStatusCard(mark: data.verification!), | ||||
|             ) | ||||
|             : const SizedBox.shrink(); | ||||
|  | ||||
|     Widget publisherBioWidget(SnPublisher data) => Card( | ||||
|       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|         children: [ | ||||
|           Text('bio').tr().bold().fontSize(15).padding(bottom: 8), | ||||
|           if (data.bio.isEmpty) | ||||
|             Text('descriptionNone').tr().italic() | ||||
|           else | ||||
|             MarkdownTextContent( | ||||
|               content: data.bio, | ||||
|               linesMargin: EdgeInsets.zero, | ||||
|             ), | ||||
|         ], | ||||
|       ).padding(horizontal: 20, vertical: 16), | ||||
|     ); | ||||
|  | ||||
|     Widget publisherCategoryTabWidget() => Card( | ||||
|       margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|       child: TabBar( | ||||
|         controller: categoryTabController, | ||||
|         dividerColor: Colors.transparent, | ||||
|         splashBorderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|         tabs: [Tab(text: 'All'), Tab(text: 'Posts'), Tab(text: 'Articles')], | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     return publisher.when( | ||||
|       data: | ||||
|           (data) => AppScaffold( | ||||
| @@ -344,12 +403,16 @@ class PublisherProfileScreen extends HookConsumerWidget { | ||||
|                           child: CustomScrollView( | ||||
|                             slivers: [ | ||||
|                               SliverGap(16), | ||||
|                               SliverPostList(pubName: name, pinned: true), | ||||
|                               SliverToBoxAdapter( | ||||
|                                 child: publisherCategoryTabWidget(), | ||||
|                                 child: _PublisherCategoryTabWidget( | ||||
|                                   categoryTabController: categoryTabController, | ||||
|                                 ), | ||||
|                               ), | ||||
|                               SliverPostList( | ||||
|                                 key: ValueKey(categoryTab.value), | ||||
|                                 pubName: name, | ||||
|                                 pinned: false, | ||||
|                                 type: switch (categoryTab.value) { | ||||
|                                   1 => 0, | ||||
|                                   2 => 1, | ||||
| @@ -370,10 +433,19 @@ class PublisherProfileScreen extends HookConsumerWidget { | ||||
|                               child: Column( | ||||
|                                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                                 children: [ | ||||
|                                   publisherBasisWidget(data).padding(bottom: 8), | ||||
|                                   publisherBadgesWidget(data), | ||||
|                                   publisherVerificationWidget(data), | ||||
|                                   publisherBioWidget(data), | ||||
|                                   _PublisherBasisWidget( | ||||
|                                     data: data, | ||||
|                                     subStatus: subStatus, | ||||
|                                     subscribing: subscribing, | ||||
|                                     subscribe: subscribe, | ||||
|                                     unsubscribe: unsubscribe, | ||||
|                                   ).padding(bottom: 8), | ||||
|                                   _PublisherBadgesWidget( | ||||
|                                     data: data, | ||||
|                                     badges: badges, | ||||
|                                   ), | ||||
|                                   _PublisherVerificationWidget(data: data), | ||||
|                                   _PublisherBioWidget(data: data), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ), | ||||
| @@ -425,17 +497,36 @@ class PublisherProfileScreen extends HookConsumerWidget { | ||||
|                           ), | ||||
|                         ), | ||||
|                         SliverToBoxAdapter( | ||||
|                           child: publisherBasisWidget(data).padding(bottom: 8), | ||||
|                           child: _PublisherBasisWidget( | ||||
|                             data: data, | ||||
|                             subStatus: subStatus, | ||||
|                             subscribing: subscribing, | ||||
|                             subscribe: subscribe, | ||||
|                             unsubscribe: unsubscribe, | ||||
|                           ).padding(bottom: 8), | ||||
|                         ), | ||||
|                         SliverToBoxAdapter(child: publisherBadgesWidget(data)), | ||||
|                         SliverToBoxAdapter( | ||||
|                           child: publisherVerificationWidget(data), | ||||
|                           child: _PublisherBadgesWidget( | ||||
|                             data: data, | ||||
|                             badges: badges, | ||||
|                           ), | ||||
|                         ), | ||||
|                         SliverToBoxAdapter( | ||||
|                           child: _PublisherVerificationWidget(data: data), | ||||
|                         ), | ||||
|                         SliverToBoxAdapter( | ||||
|                           child: _PublisherBioWidget(data: data), | ||||
|                         ), | ||||
|                         SliverPostList(pubName: name, pinned: true), | ||||
|                         SliverToBoxAdapter( | ||||
|                           child: _PublisherCategoryTabWidget( | ||||
|                             categoryTabController: categoryTabController, | ||||
|                           ), | ||||
|                         ), | ||||
|                         SliverToBoxAdapter(child: publisherBioWidget(data)), | ||||
|                         SliverToBoxAdapter(child: publisherCategoryTabWidget()), | ||||
|                         SliverPostList( | ||||
|                           key: ValueKey(categoryTab.value), | ||||
|                           pubName: name, | ||||
|                           pinned: false, | ||||
|                           type: switch (categoryTab.value) { | ||||
|                             1 => 0, | ||||
|                             2 => 1, | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:island/models/chat.dart'; | ||||
| import 'package:island/services/color.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/widgets/account/account_pfc.dart'; | ||||
| import 'package:island/widgets/account/status.dart'; | ||||
| import 'package:island/widgets/post/post_list.dart'; | ||||
| import 'package:palette_generator/palette_generator.dart'; | ||||
| @@ -244,7 +245,10 @@ class RealmDetailScreen extends HookConsumerWidget { | ||||
|                         Flexible( | ||||
|                           flex: 3, | ||||
|                           child: CustomScrollView( | ||||
|                             slivers: [SliverPostList(realm: slug)], | ||||
|                             slivers: [ | ||||
|                               SliverPostList(realm: slug, pinned: true), | ||||
|                               SliverPostList(realm: slug, pinned: false), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|                         Flexible( | ||||
| @@ -359,7 +363,8 @@ class RealmDetailScreen extends HookConsumerWidget { | ||||
|                         SliverToBoxAdapter( | ||||
|                           child: realmChatRoomListWidget(realm), | ||||
|                         ), | ||||
|                         SliverPostList(realm: slug), | ||||
|                         SliverPostList(realm: slug, pinned: true), | ||||
|                         SliverPostList(realm: slug, pinned: false), | ||||
|                       ], | ||||
|                     ), | ||||
|       ), | ||||
| @@ -520,9 +525,11 @@ class _RealmActionMenu extends HookConsumerWidget { | ||||
| class RealmMemberListNotifier extends _$RealmMemberListNotifier | ||||
|     with CursorPagingNotifierMixin<SnRealmMember> { | ||||
|   static const int _pageSize = 20; | ||||
|   ValueNotifier<int> totalCount = ValueNotifier(0); | ||||
|  | ||||
|   @override | ||||
|   Future<CursorPagingData<SnRealmMember>> build(String realmSlug) async { | ||||
|     totalCount.value = 0; | ||||
|     return fetch(); | ||||
|   } | ||||
|  | ||||
| @@ -541,6 +548,7 @@ class RealmMemberListNotifier extends _$RealmMemberListNotifier | ||||
|     ); | ||||
|  | ||||
|     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||
|     totalCount.value = total; | ||||
|     final List<dynamic> data = response.data; | ||||
|     final members = data.map((e) => SnRealmMember.fromJson(e)).toList(); | ||||
|  | ||||
| @@ -553,52 +561,9 @@ class RealmMemberListNotifier extends _$RealmMemberListNotifier | ||||
|       nextCursor: nextCursor, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Keep the old provider for backward compatibility | ||||
| final realmMemberStateProvider = | ||||
|     StateNotifierProvider.family<RealmMemberNotifier, RealmMemberState, String>( | ||||
|       (ref, realmSlug) { | ||||
|         final apiClient = ref.watch(apiClientProvider); | ||||
|         return RealmMemberNotifier(apiClient, realmSlug); | ||||
|       }, | ||||
|     ); | ||||
|  | ||||
| class RealmMemberNotifier extends StateNotifier<RealmMemberState> { | ||||
|   final String realmSlug; | ||||
|   final Dio _apiClient; | ||||
|  | ||||
|   RealmMemberNotifier(this._apiClient, this.realmSlug) | ||||
|     : super(const RealmMemberState(members: [], isLoading: false, total: 0)); | ||||
|  | ||||
|   Future<void> loadMore({int offset = 0, int take = 20}) async { | ||||
|     if (state.isLoading) return; | ||||
|     if (state.total > 0 && state.members.length >= state.total) return; | ||||
|  | ||||
|     state = state.copyWith(isLoading: true, error: null); | ||||
|  | ||||
|     try { | ||||
|       final response = await _apiClient.get( | ||||
|         '/sphere/realms/$realmSlug/members', | ||||
|         queryParameters: {'offset': offset, 'take': take, 'withStatus': true}, | ||||
|       ); | ||||
|  | ||||
|       final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||
|       final List<dynamic> data = response.data; | ||||
|       final members = data.map((e) => SnRealmMember.fromJson(e)).toList(); | ||||
|  | ||||
|       state = state.copyWith( | ||||
|         members: [...state.members, ...members], | ||||
|         total: total, | ||||
|         isLoading: false, | ||||
|       ); | ||||
|     } catch (e) { | ||||
|       state = state.copyWith(error: e.toString(), isLoading: false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void reset() { | ||||
|     state = const RealmMemberState(members: [], isLoading: false, total: 0); | ||||
|   void dispose() { | ||||
|     totalCount.dispose(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -610,18 +575,10 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final realmIdentity = ref.watch(realmIdentityProvider(realmSlug)); | ||||
|     final memberListProvider = realmMemberListNotifierProvider(realmSlug); | ||||
|  | ||||
|     // For backward compatibility and to show total count in the header | ||||
|     final memberState = ref.watch(realmMemberStateProvider(realmSlug)); | ||||
|     final memberNotifier = ref.read( | ||||
|       realmMemberStateProvider(realmSlug).notifier, | ||||
|     ); | ||||
|     final memberListNotifier = ref.watch(memberListProvider.notifier); | ||||
|  | ||||
|     useEffect(() { | ||||
|       Future(() { | ||||
|         memberNotifier.loadMore(); | ||||
|       }); | ||||
|       return null; | ||||
|       return memberListNotifier.dispose; | ||||
|     }, []); | ||||
|  | ||||
|     Future<void> invitePerson() async { | ||||
| @@ -638,9 +595,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|           '/sphere/realms/invites/$realmSlug', | ||||
|           data: {'related_user_id': result.id, 'role': 0}, | ||||
|         ); | ||||
|         // Refresh both providers | ||||
|         memberNotifier.reset(); | ||||
|         await memberNotifier.loadMore(); | ||||
|         // Refresh the provider | ||||
|         ref.invalidate(memberListProvider); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
| @@ -652,13 +607,18 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|         padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12), | ||||
|         child: Row( | ||||
|           children: [ | ||||
|             Text( | ||||
|               'members'.plural(memberState.total), | ||||
|             ListenableBuilder( | ||||
|               listenable: memberListNotifier.totalCount, | ||||
|               builder: | ||||
|                   (context, _) => Text( | ||||
|                     'members'.plural(memberListNotifier.totalCount.value), | ||||
|                     key: ValueKey(memberListNotifier), | ||||
|                     style: Theme.of(context).textTheme.headlineSmall?.copyWith( | ||||
|                       fontWeight: FontWeight.w600, | ||||
|                       letterSpacing: -0.5, | ||||
|                     ), | ||||
|                   ), | ||||
|             ), | ||||
|             const Spacer(), | ||||
|             IconButton( | ||||
|               icon: const Icon(Symbols.person_add), | ||||
| @@ -668,9 +628,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|             IconButton( | ||||
|               icon: const Icon(Symbols.refresh), | ||||
|               onPressed: () { | ||||
|                 // Refresh both providers | ||||
|                 memberNotifier.reset(); | ||||
|                 memberNotifier.loadMore(); | ||||
|                 // Refresh the provider | ||||
|                 ref.invalidate(memberListProvider); | ||||
|               }, | ||||
|             ), | ||||
| @@ -701,9 +659,12 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|                 final member = data.items[index]; | ||||
|                 return ListTile( | ||||
|                   contentPadding: EdgeInsets.only(left: 16, right: 12), | ||||
|                   leading: ProfilePictureWidget( | ||||
|                   leading: AccountPfcGestureDetector( | ||||
|                     uname: member.account!.name, | ||||
|                     child: ProfilePictureWidget( | ||||
|                       fileId: member.account!.profile.picture?.id, | ||||
|                     ), | ||||
|                   ), | ||||
|                   title: Row( | ||||
|                     spacing: 6, | ||||
|                     children: [ | ||||
| @@ -744,9 +705,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|                                   ), | ||||
|                             ).then((value) { | ||||
|                               if (value != null) { | ||||
|                                 // Refresh both providers | ||||
|                                 memberNotifier.reset(); | ||||
|                                 memberNotifier.loadMore(); | ||||
|                                 // Refresh the provider | ||||
|                                 ref.invalidate(memberListProvider); | ||||
|                               } | ||||
|                             }); | ||||
| @@ -766,9 +725,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|                                 await apiClient.delete( | ||||
|                                   '/sphere/realms/$realmSlug/members/${member.accountId}', | ||||
|                                 ); | ||||
|                                 // Refresh both providers | ||||
|                                 memberNotifier.reset(); | ||||
|                                 memberNotifier.loadMore(); | ||||
|                                 // Refresh the provider | ||||
|                                 ref.invalidate(memberListProvider); | ||||
|                               } catch (err) { | ||||
|                                 showErrorAlert(err); | ||||
| @@ -801,34 +758,6 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class RealmMemberState { | ||||
|   final List<SnRealmMember> members; | ||||
|   final bool isLoading; | ||||
|   final int total; | ||||
|   final String? error; | ||||
|  | ||||
|   const RealmMemberState({ | ||||
|     required this.members, | ||||
|     required this.isLoading, | ||||
|     required this.total, | ||||
|     this.error, | ||||
|   }); | ||||
|  | ||||
|   RealmMemberState copyWith({ | ||||
|     List<SnRealmMember>? members, | ||||
|     bool? isLoading, | ||||
|     int? total, | ||||
|     String? error, | ||||
|   }) { | ||||
|     return RealmMemberState( | ||||
|       members: members ?? this.members, | ||||
|       isLoading: isLoading ?? this.isLoading, | ||||
|       total: total ?? this.total, | ||||
|       error: error ?? this.error, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _RealmMemberRoleSheet extends HookConsumerWidget { | ||||
|   final String realmSlug; | ||||
|   final SnRealmMember member; | ||||
|   | ||||
| @@ -399,7 +399,7 @@ class _RealmChatRoomsProviderElement | ||||
| } | ||||
|  | ||||
| String _$realmMemberListNotifierHash() => | ||||
|     r'2f88f803b2e61e7287ed8a43025173e56ff6ca3b'; | ||||
|     r'db1fd8a6741dfb3d5bb921d5d965f0cfdc0e7bcc'; | ||||
|  | ||||
| abstract class _$RealmMemberListNotifier | ||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnRealmMember>> { | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:island/widgets/realm/realm_list_tile.dart'; | ||||
| import 'package:island/widgets/extended_refresh_indicator.dart'; | ||||
|  | ||||
| part 'realms.g.dart'; | ||||
|  | ||||
| @@ -90,7 +91,7 @@ class RealmListScreen extends HookConsumerWidget { | ||||
|         }, | ||||
|       ), | ||||
|       floatingActionButtonLocation: TabbedFabLocation(context), | ||||
|       body: RefreshIndicator( | ||||
|       body: ExtendedRefreshIndicator( | ||||
|         child: realms.when( | ||||
|           data: | ||||
|               (value) => Column( | ||||
|   | ||||
| @@ -219,6 +219,33 @@ class SettingsScreen extends HookConsumerWidget { | ||||
|           }, | ||||
|         ), | ||||
|  | ||||
|       // Background image enabled | ||||
|       if (!kIsWeb && docBasepath.value != null) | ||||
|         FutureBuilder<bool>( | ||||
|           future: | ||||
|               File('${docBasepath.value}/$kAppBackgroundImagePath').exists(), | ||||
|           builder: (context, snapshot) { | ||||
|             if (!snapshot.hasData || !snapshot.data!) { | ||||
|               return const SizedBox.shrink(); | ||||
|             } | ||||
|  | ||||
|             return ListTile( | ||||
|               minLeadingWidth: 48, | ||||
|               title: Text('settingsBackgroundImageEnable').tr(), | ||||
|               contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|               leading: const Icon(Symbols.image), | ||||
|               trailing: Switch( | ||||
|                 value: settings.showBackgroundImage, | ||||
|                 onChanged: (value) { | ||||
|                   ref | ||||
|                       .read(appSettingsNotifierProvider.notifier) | ||||
|                       .setShowBackgroundImage(value); | ||||
|                 }, | ||||
|               ), | ||||
|             ); | ||||
|           }, | ||||
|         ), | ||||
|  | ||||
|       // Clear background image option | ||||
|       if (!kIsWeb && docBasepath.value != null) | ||||
|         FutureBuilder<bool>( | ||||
| @@ -423,66 +450,25 @@ class SettingsScreen extends HookConsumerWidget { | ||||
|           }, | ||||
|         ), | ||||
|       ), | ||||
|       ListTile( | ||||
|         minLeadingWidth: 48, | ||||
|         title: Text('settingsDataSavingMode').tr(), | ||||
|         contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|         leading: const Icon(Symbols.data_saver_on_rounded), | ||||
|         trailing: Switch( | ||||
|           value: settings.dataSavingMode, | ||||
|           onChanged: (value) { | ||||
|             ref | ||||
|                 .read(appSettingsNotifierProvider.notifier) | ||||
|                 .setDataSavingMode(value); | ||||
|           }, | ||||
|         ), | ||||
|       ), | ||||
|     ]; | ||||
|  | ||||
|     // Desktop-specific settings | ||||
|     final desktopSettings = | ||||
|         !isDesktop | ||||
|             ? <Widget>[] | ||||
|             : <Widget>[ | ||||
|               ListTile( | ||||
|                 minLeadingWidth: 48, | ||||
|                 title: Text('settingsKeyboardShortcuts').tr(), | ||||
|                 contentPadding: const EdgeInsets.only(left: 24, right: 17), | ||||
|                 leading: const Icon(Symbols.keyboard), | ||||
|                 onTap: () { | ||||
|                   showDialog( | ||||
|                     context: context, | ||||
|                     builder: | ||||
|                         (context) => AlertDialog( | ||||
|                           title: Text('settingsKeyboardShortcuts').tr(), | ||||
|                           content: SingleChildScrollView( | ||||
|                             child: Column( | ||||
|                               mainAxisSize: MainAxisSize.min, | ||||
|                               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                               children: [ | ||||
|                                 _ShortcutRow( | ||||
|                                   shortcut: 'Ctrl+F', | ||||
|                                   description: | ||||
|                                       'settingsKeyboardShortcutSearch'.tr(), | ||||
|                                 ), | ||||
|                                 _ShortcutRow( | ||||
|                                   shortcut: 'Ctrl+,', | ||||
|                                   description: | ||||
|                                       'settingsKeyboardShortcutSettings'.tr(), | ||||
|                                 ), | ||||
|                                 _ShortcutRow( | ||||
|                                   shortcut: 'Ctrl+N', | ||||
|                                   description: | ||||
|                                       'settingsKeyboardShortcutNewMessage'.tr(), | ||||
|                                 ), | ||||
|                                 _ShortcutRow( | ||||
|                                   shortcut: 'Esc', | ||||
|                                   description: | ||||
|                                       'settingsKeyboardShortcutCloseDialog' | ||||
|                                           .tr(), | ||||
|                                 ), | ||||
|                                 // Add more shortcuts as needed | ||||
|                               ], | ||||
|                             ), | ||||
|                           ), | ||||
|                           actions: [ | ||||
|                             TextButton( | ||||
|                               onPressed: () => Navigator.of(context).pop(), | ||||
|                               child: Text('close').tr(), | ||||
|                             ), | ||||
|                           ], | ||||
|                         ), | ||||
|                   ); | ||||
|                 }, | ||||
|                 trailing: const Icon(Symbols.chevron_right), | ||||
|               ), | ||||
|             ]; | ||||
|     // But nothing for now | ||||
|     final desktopSettings = !isDesktop ? <Widget>[] : <Widget>[]; | ||||
|  | ||||
|     // Create a responsive layout based on screen width | ||||
|     Widget buildSettingsList() { | ||||
| @@ -553,34 +539,7 @@ class SettingsScreen extends HookConsumerWidget { | ||||
|  | ||||
|     return AppScaffold( | ||||
|       isNoBackground: false, | ||||
|       appBar: AppBar( | ||||
|         title: Text('settings').tr(), | ||||
|         actions: | ||||
|             isDesktop | ||||
|                 ? [ | ||||
|                   IconButton( | ||||
|                     icon: const Icon(Symbols.help_outline), | ||||
|                     onPressed: () { | ||||
|                       // Show help dialog | ||||
|                       showDialog( | ||||
|                         context: context, | ||||
|                         builder: | ||||
|                             (context) => AlertDialog( | ||||
|                               title: Text('settingsHelp').tr(), | ||||
|                               content: Text('settingsHelpContent').tr(), | ||||
|                               actions: [ | ||||
|                                 TextButton( | ||||
|                                   onPressed: () => Navigator.of(context).pop(), | ||||
|                                   child: Text('close').tr(), | ||||
|                                 ), | ||||
|                               ], | ||||
|                             ), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ] | ||||
|                 : null, | ||||
|       ), | ||||
|       appBar: AppBar(title: Text('settings').tr()), | ||||
|       body: Focus( | ||||
|         autofocus: true, | ||||
|         onKeyEvent: (node, event) { | ||||
| @@ -630,35 +589,3 @@ class _SettingsSection extends StatelessWidget { | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Helper widget for displaying keyboard shortcuts | ||||
| class _ShortcutRow extends StatelessWidget { | ||||
|   final String shortcut; | ||||
|   final String description; | ||||
|  | ||||
|   const _ShortcutRow({required this.shortcut, required this.description}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Padding( | ||||
|       padding: const EdgeInsets.symmetric(vertical: 8.0), | ||||
|       child: Row( | ||||
|         children: [ | ||||
|           Container( | ||||
|             padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), | ||||
|             decoration: BoxDecoration( | ||||
|               color: Theme.of(context).colorScheme.surfaceVariant, | ||||
|               borderRadius: BorderRadius.circular(4), | ||||
|               border: Border.all( | ||||
|                 color: Theme.of(context).colorScheme.outline.withOpacity(0.5), | ||||
|               ), | ||||
|             ), | ||||
|             child: Text(shortcut, style: TextStyle(fontFamily: 'monospace')), | ||||
|           ), | ||||
|           SizedBox(width: 16), | ||||
|           Text(description), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -216,7 +216,7 @@ class MarketplaceStickerPackDetailScreen extends HookConsumerWidget { | ||||
|                       ), | ||||
|                 ), | ||||
|               ), | ||||
|               Gap(MediaQuery.of(context).padding.bottom), | ||||
|               Gap(MediaQuery.of(context).padding.bottom + 16), | ||||
|             ], | ||||
|           ); | ||||
|         }, | ||||
|   | ||||
| @@ -77,7 +77,7 @@ class MarketplaceStickersScreen extends HookConsumerWidget { | ||||
|         searchController.clear(); | ||||
|       } | ||||
|       return null; | ||||
|     }, [query.value]); | ||||
|     }, [query]); | ||||
|  | ||||
|     // Clean up timer on dispose | ||||
|     useEffect(() { | ||||
|   | ||||
							
								
								
									
										62
									
								
								lib/screens/tray_manager.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								lib/screens/tray_manager.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:bitsdojo_window/bitsdojo_window.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:tray_manager/tray_manager.dart'; | ||||
|  | ||||
| class TrayService { | ||||
|   TrayService._(); | ||||
|  | ||||
|   static final TrayService _instance = TrayService._(); | ||||
|  | ||||
|   static TrayService get instance => _instance; | ||||
|  | ||||
|   bool _checkPlatformAvalability() { | ||||
|     if (kIsWeb) return false; | ||||
|     if (Platform.isAndroid || Platform.isIOS) return false; | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   Future<void> initialize(TrayListener listener) async { | ||||
|     if (!_checkPlatformAvalability()) return; | ||||
|  | ||||
|     await trayManager.setIcon( | ||||
|       Platform.isWindows | ||||
|           ? 'assets/icons/icon.ico' | ||||
|           : 'assets/icons/icon-outline.svg', | ||||
|     ); | ||||
|  | ||||
|     final menu = Menu( | ||||
|       items: [ | ||||
|         MenuItem(key: 'show_window', label: 'Show Window'), | ||||
|         MenuItem.separator(), | ||||
|         MenuItem(key: 'exit_app', label: 'Exit App'), | ||||
|       ], | ||||
|     ); | ||||
|     await trayManager.setContextMenu(menu); | ||||
|  | ||||
|     trayManager.addListener(listener); | ||||
|   } | ||||
|  | ||||
|   Future<void> dispose(TrayListener listener) async { | ||||
|     if (!_checkPlatformAvalability()) return; | ||||
|  | ||||
|     trayManager.removeListener(listener); | ||||
|     await trayManager.destroy(); | ||||
|   } | ||||
|  | ||||
|   void handleAction(MenuItem item) { | ||||
|     switch (item.key) { | ||||
|       case 'show_window': | ||||
|         if (appWindow.isVisible) { | ||||
|           appWindow.restore(); | ||||
|         } else { | ||||
|           appWindow.show(); | ||||
|         } | ||||
|         break; | ||||
|       case 'exit_app': | ||||
|         appWindow.close(); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -6,6 +6,7 @@ import 'package:dio/dio.dart'; | ||||
| import 'package:firebase_messaging/firebase_messaging.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_local_notifications/flutter_local_notifications.dart'; | ||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:island/main.dart'; | ||||
| @@ -16,14 +17,88 @@ import 'package:island/widgets/app_notification.dart'; | ||||
| import 'package:top_snackbar_flutter/top_snack_bar.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = | ||||
|     FlutterLocalNotificationsPlugin(); | ||||
|  | ||||
| AppLifecycleState _appLifecycleState = AppLifecycleState.resumed; | ||||
|  | ||||
| void _onAppLifecycleChanged(AppLifecycleState state) { | ||||
|   _appLifecycleState = state; | ||||
| } | ||||
|  | ||||
| Future<void> initializeLocalNotifications() async { | ||||
|   const AndroidInitializationSettings initializationSettingsAndroid = | ||||
|       AndroidInitializationSettings('@mipmap/ic_launcher'); | ||||
|  | ||||
|   const DarwinInitializationSettings initializationSettingsIOS = | ||||
|       DarwinInitializationSettings(); | ||||
|  | ||||
|   const DarwinInitializationSettings initializationSettingsMacOS = | ||||
|       DarwinInitializationSettings(); | ||||
|  | ||||
|   const LinuxInitializationSettings initializationSettingsLinux = | ||||
|       LinuxInitializationSettings(defaultActionName: 'Open notification'); | ||||
|  | ||||
|   const WindowsInitializationSettings initializationSettingsWindows = | ||||
|       WindowsInitializationSettings( | ||||
|         appName: 'Island', | ||||
|         appUserModelId: 'dev.solsynth.solian', | ||||
|         guid: 'dev.solsynth.solian', | ||||
|       ); | ||||
|  | ||||
|   const InitializationSettings initializationSettings = InitializationSettings( | ||||
|     android: initializationSettingsAndroid, | ||||
|     iOS: initializationSettingsIOS, | ||||
|     macOS: initializationSettingsMacOS, | ||||
|     linux: initializationSettingsLinux, | ||||
|     windows: initializationSettingsWindows, | ||||
|   ); | ||||
|  | ||||
|   await flutterLocalNotificationsPlugin.initialize( | ||||
|     initializationSettings, | ||||
|     onDidReceiveNotificationResponse: (NotificationResponse response) async { | ||||
|       final payload = response.payload; | ||||
|       if (payload != null) { | ||||
|         if (payload.startsWith('/')) { | ||||
|           // In-app routes | ||||
|           rootNavigatorKey.currentContext?.push(payload); | ||||
|         } else { | ||||
|           // External URLs | ||||
|           launchUrlString(payload); | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|   ); | ||||
|  | ||||
|   WidgetsBinding.instance.addObserver( | ||||
|     LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class LifecycleEventHandler extends WidgetsBindingObserver { | ||||
|   final void Function(AppLifecycleState) onAppLifecycleChanged; | ||||
|  | ||||
|   LifecycleEventHandler({required this.onAppLifecycleChanged}); | ||||
|  | ||||
|   @override | ||||
|   void didChangeAppLifecycleState(AppLifecycleState state) { | ||||
|     onAppLifecycleChanged(state); | ||||
|   } | ||||
| } | ||||
|  | ||||
| StreamSubscription<WebSocketPacket> setupNotificationListener( | ||||
|   BuildContext context, | ||||
|   WidgetRef ref, | ||||
| ) { | ||||
|   final ws = ref.watch(websocketProvider); | ||||
|   return ws.dataStream.listen((pkt) { | ||||
|   return ws.dataStream.listen((pkt) async { | ||||
|     if (pkt.type == "notifications.new") { | ||||
|       final notification = SnNotification.fromJson(pkt.data!); | ||||
|       if (_appLifecycleState == AppLifecycleState.resumed) { | ||||
|         // App is focused, show in-app notification | ||||
|         log( | ||||
|           '[Notification] Showing in-app notification: ${notification.title}', | ||||
|         ); | ||||
|         showTopSnackBar( | ||||
|           globalOverlay.currentState!, | ||||
|           Center( | ||||
| @@ -64,6 +139,37 @@ StreamSubscription<WebSocketPacket> setupNotificationListener( | ||||
|             bottom: 16, | ||||
|           ), | ||||
|         ); | ||||
|       } else { | ||||
|         // App is in background, show system notification (only on supported platforms) | ||||
|         if (!kIsWeb && !Platform.isIOS) { | ||||
|           log( | ||||
|             '[Notification] Showing system notification: ${notification.title}', | ||||
|           ); | ||||
|           const AndroidNotificationDetails androidNotificationDetails = | ||||
|               AndroidNotificationDetails( | ||||
|                 'channel_id', | ||||
|                 'channel_name', | ||||
|                 channelDescription: 'channel_description', | ||||
|                 importance: Importance.max, | ||||
|                 priority: Priority.high, | ||||
|                 ticker: 'ticker', | ||||
|               ); | ||||
|           const NotificationDetails notificationDetails = NotificationDetails( | ||||
|             android: androidNotificationDetails, | ||||
|           ); | ||||
|           await flutterLocalNotificationsPlugin.show( | ||||
|             0, | ||||
|             notification.title, | ||||
|             notification.content, | ||||
|             notificationDetails, | ||||
|             payload: notification.meta['action_uri'] as String?, | ||||
|           ); | ||||
|         } else { | ||||
|           log( | ||||
|             '[Notification] Skipping system notification for unsupported platform: ${notification.title}', | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| @@ -72,7 +178,7 @@ Future<void> subscribePushNotification( | ||||
|   Dio apiClient, { | ||||
|   bool detailedErrors = false, | ||||
| }) async { | ||||
|   if (Platform.isLinux) { | ||||
|   if (!kIsWeb && Platform.isLinux) { | ||||
|     return; | ||||
|   } | ||||
|   await FirebaseMessaging.instance.requestPermission( | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user