Compare commits
	
		
			43 Commits
		
	
	
		
			3.2.0+124
			...
			6c48aa2356
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6c48aa2356 | |||
| 466e354679 | |||
| 5d4b896f70 | |||
| a04dffdfe8 | |||
| ff871943cf | |||
| 1a892ab227 | |||
| af1b303211 | |||
| 6fd702eba8 | |||
| d220d43cd2 | |||
| 6892afb974 | |||
| 007b46b080 | |||
| 67d130dc34 | |||
| 7e923c77fe | |||
| a593b52812 | |||
|  | 520dc80303 | ||
| 001897bbcd | |||
|  | bab29c23e3 | ||
| 76b39f2df3 | |||
| 509b3e145b | |||
| 2b80ebc2d0 | |||
| 0ab908dd2a | |||
| 6007467e7a | |||
| 3745157c42 | |||
| 94481ec7bd | |||
| fbfe8cbdee | |||
| fbbab0a981 | |||
| ae2fb3b303 | |||
| 3d7a4666ed | |||
| 5d3e0fb800 | |||
| 85ff52a661 | |||
| da7fd64a43 | |||
| 3902633217 | |||
| f478ea8b84 | |||
| 0f481aff5b | |||
| 7a31663310 | |||
| 0239c53c04 | |||
| 16987c758e | |||
| 3a36915140 | |||
| 4bde708878 | |||
| 2f0cf560f8 | |||
| cf355a95fd | |||
| 2f43073172 | |||
| 8236d31ecc | 
							
								
								
									
										9
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -41,6 +41,15 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           name: build-output-windows |           name: build-output-windows | ||||||
|           path: build/windows/x64/runner/Release |           path: build/windows/x64/runner/Release | ||||||
|  |       - name: Compile Installer | ||||||
|  |         uses: Minionguyjpro/Inno-Setup-Action@v1.2.2 | ||||||
|  |         with: | ||||||
|  |           path: setup.iss | ||||||
|  |       - name: Archive installer artifacts | ||||||
|  |         uses: actions/upload-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           name: build-output-windows-installer | ||||||
|  |           path: Installer/windows-x86_64-setup.exe | ||||||
|   build-linux: |   build-linux: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -12,6 +12,9 @@ | |||||||
| .swiftpm/ | .swiftpm/ | ||||||
| migrate_working_dir/ | migrate_working_dir/ | ||||||
|  |  | ||||||
|  | # Inno Setup | ||||||
|  | Installer/ | ||||||
|  |  | ||||||
| # IntelliJ related | # IntelliJ related | ||||||
| *.iml | *.iml | ||||||
| *.ipr | *.ipr | ||||||
|   | |||||||
| @@ -334,6 +334,7 @@ | |||||||
|   "walletCreate": "Create a Wallet", |   "walletCreate": "Create a Wallet", | ||||||
|   "settingsServerUrl": "Server URL", |   "settingsServerUrl": "Server URL", | ||||||
|   "settingsApplied": "The settings has been applied.", |   "settingsApplied": "The settings has been applied.", | ||||||
|  |   "settingsCustomFontsHelper": "Use comma to seprate.", | ||||||
|   "notifications": "Notifications", |   "notifications": "Notifications", | ||||||
|   "posts": "Posts", |   "posts": "Posts", | ||||||
|   "settingsBackgroundImage": "Background Image", |   "settingsBackgroundImage": "Background Image", | ||||||
| @@ -573,6 +574,7 @@ | |||||||
|   "keyboardShortcuts": "Keyboard Shortcuts", |   "keyboardShortcuts": "Keyboard Shortcuts", | ||||||
|   "share": "Share", |   "share": "Share", | ||||||
|   "sharePost": "Share Post", |   "sharePost": "Share Post", | ||||||
|  |   "sharePostPhoto": "Share Post as Photo", | ||||||
|   "quickActions": "Quick Actions", |   "quickActions": "Quick Actions", | ||||||
|   "post": "Post", |   "post": "Post", | ||||||
|   "copy": "Copy", |   "copy": "Copy", | ||||||
| @@ -760,6 +762,7 @@ | |||||||
|   "pollsRecent": "Recent Polls", |   "pollsRecent": "Recent Polls", | ||||||
|   "pollCreateNew": "Create New", |   "pollCreateNew": "Create New", | ||||||
|   "pollCreateNewHint": "Create a new poll for your post. Pick a publisher and continue.", |   "pollCreateNewHint": "Create a new poll for your post. Pick a publisher and continue.", | ||||||
|  |   "pollQuestions": "Questions", | ||||||
|   "publisher": "Publisher", |   "publisher": "Publisher", | ||||||
|   "publisherHint": "Enter the publisher name", |   "publisherHint": "Enter the publisher name", | ||||||
|   "publisherCannotBeEmpty": "Publisher cannot be empty", |   "publisherCannotBeEmpty": "Publisher cannot be empty", | ||||||
| @@ -792,5 +795,54 @@ | |||||||
|   "joinedAt": "Joined at {}", |   "joinedAt": "Joined at {}", | ||||||
|   "searchAccounts": "Search accounts...", |   "searchAccounts": "Search accounts...", | ||||||
|   "webFeeds": "Web Feeds", |   "webFeeds": "Web Feeds", | ||||||
|   "polls": "Polls" |   "polls": "Polls", | ||||||
|  |   "sharePostSlogan": "Explore more on the Solar Network", | ||||||
|  |   "filesListAdditional": { | ||||||
|  |     "one": "+{} file remaining", | ||||||
|  |     "other": "+{} files remaining" | ||||||
|  |   }, | ||||||
|  |   "pollAnswerSubmitted": "Poll answer has been submitted.", | ||||||
|  |   "modifyAnswers": "Modify Answers", | ||||||
|  |   "back": "Back", | ||||||
|  |   "submit": "Submit", | ||||||
|  |   "pollOptionDefaultLabel": "Option 1", | ||||||
|  |   "pollUpdated": "Poll updated.", | ||||||
|  |   "pollCreated": "Poll created.", | ||||||
|  |   "pollCreate": "Create Poll", | ||||||
|  |   "pollEdit": "Edit Poll", | ||||||
|  |   "pollPreviewJsonDebug": "Debug Preview", | ||||||
|  |   "pollTitleRequired": "Title is required", | ||||||
|  |   "pollEndDateOptional": "End date & time (optional)", | ||||||
|  |   "notSet": "Not set", | ||||||
|  |   "pick": "Pick", | ||||||
|  |   "clear": "Clear", | ||||||
|  |   "questions": "Questions", | ||||||
|  |   "pollAddQuestion": "Add question", | ||||||
|  |   "pollQuestionTypeSingleChoice": "Single choice", | ||||||
|  |   "pollQuestionTypeMultipleChoice": "Multiple choice", | ||||||
|  |   "pollQuestionTypeFreeText": "Free text", | ||||||
|  |   "pollQuestionTypeYesNo": "Yes / No", | ||||||
|  |   "pollQuestionTypeRating": "Rating", | ||||||
|  |   "pollNoQuestionsYet": "No questions yet", | ||||||
|  |   "pollNoQuestionsHint": "Use \"Add question\" to start building your poll.", | ||||||
|  |   "pollDebugPreview": "Debug Preview", | ||||||
|  |   "pollUntitledQuestion": "Untitled question", | ||||||
|  |   "moveUp": "Move up", | ||||||
|  |   "moveDown": "Move down", | ||||||
|  |   "required": "Required", | ||||||
|  |   "pollQuestionTitle": "Question title", | ||||||
|  |   "pollQuestionTitleRequired": "Question title is required", | ||||||
|  |   "pollQuestionDescriptionOptional": "Question description (optional)", | ||||||
|  |   "options": "Options", | ||||||
|  |   "pollAddOption": "Add option", | ||||||
|  |   "pollOptionLabel": "Option label", | ||||||
|  |   "pollLongTextAnswerPreview": "Long text answer (preview)", | ||||||
|  |   "pollShortTextAnswerPreview": "Short text answer (preview)", | ||||||
|  |   "messageJumpNotLoaded": "The referenced message was not loaded, unable to jump to it.", | ||||||
|  |   "postUnlinkRealm": "No linked realm", | ||||||
|  |   "postSlug": "Slug", | ||||||
|  |   "postSlugHint": "The slug can be used to access your post via URL in the webpage, it should be publisher-wide unique.", | ||||||
|  |   "attachmentOnDevice": "On-device", | ||||||
|  |   "attachmentOnCloud": "On-cloud", | ||||||
|  |   "attachments": "Attachments" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -46,7 +46,6 @@ | |||||||
|   "delete": "删除", |   "delete": "删除", | ||||||
|   "deletePublisher": "删除发布者", |   "deletePublisher": "删除发布者", | ||||||
|   "deletePublisherHint": "确定要删除此发布者吗?这也会删除此发布者下的所有帖子和收藏。", |   "deletePublisherHint": "确定要删除此发布者吗?这也会删除此发布者下的所有帖子和收藏。", | ||||||
|   "somethingWentWrong": "发生了一些错误……", |  | ||||||
|   "deletePost": "删除帖子", |   "deletePost": "删除帖子", | ||||||
|   "deletePostHint": "确定要删除这篇帖子吗?", |   "deletePostHint": "确定要删除这篇帖子吗?", | ||||||
|   "copyLink": "复制链接", |   "copyLink": "复制链接", | ||||||
| @@ -301,6 +300,7 @@ | |||||||
|   "walletCreate": "创建钱包", |   "walletCreate": "创建钱包", | ||||||
|   "settingsServerUrl": "服务器 URL", |   "settingsServerUrl": "服务器 URL", | ||||||
|   "settingsApplied": "设置已应用。", |   "settingsApplied": "设置已应用。", | ||||||
|  |   "settingsCustomFontsHelper": "用逗号分隔。", | ||||||
|   "notifications": "通知", |   "notifications": "通知", | ||||||
|   "posts": "帖子", |   "posts": "帖子", | ||||||
|   "settingsBackgroundImage": "背景图片", |   "settingsBackgroundImage": "背景图片", | ||||||
| @@ -349,7 +349,6 @@ | |||||||
|   "postContent": "内容", |   "postContent": "内容", | ||||||
|   "postSettings": "设置", |   "postSettings": "设置", | ||||||
|   "postPublisherUnselected": "未指定发布者", |   "postPublisherUnselected": "未指定发布者", | ||||||
|   "postVisibility": "可见性", |  | ||||||
|   "postVisibilityPublic": "公开", |   "postVisibilityPublic": "公开", | ||||||
|   "postVisibilityFriends": "仅好友可见", |   "postVisibilityFriends": "仅好友可见", | ||||||
|   "postVisibilityUnlisted": "不公开", |   "postVisibilityUnlisted": "不公开", | ||||||
| @@ -501,9 +500,6 @@ | |||||||
|   "membershipTierNova": "新星", |   "membershipTierNova": "新星", | ||||||
|   "membershipTierSupernova": "超新星", |   "membershipTierSupernova": "超新星", | ||||||
|   "membershipTierUnknown": "未知", |   "membershipTierUnknown": "未知", | ||||||
|   "membershipPriceStellar": "每月 10 金点", |  | ||||||
|   "membershipPriceNova": "每月 20 金点", |  | ||||||
|   "membershipPriceSupernova": "每月 30 金点", |  | ||||||
|   "membershipFeatureBasic": "基础功能", |   "membershipFeatureBasic": "基础功能", | ||||||
|   "membershipFeaturePrioritySupport": "优先支持", |   "membershipFeaturePrioritySupport": "优先支持", | ||||||
|   "membershipFeatureAdFree": "无广告", |   "membershipFeatureAdFree": "无广告", | ||||||
| @@ -573,37 +569,36 @@ | |||||||
|   "share": "分享", |   "share": "分享", | ||||||
|   "sharePost": "分享帖子", |   "sharePost": "分享帖子", | ||||||
|   "quickActions": "快捷操作", |   "quickActions": "快捷操作", | ||||||
|   "post": "帖子", |   "post": "发帖", | ||||||
|   "copy": "复制", |   "copy": "复制", | ||||||
|   "sendToChat": "发送到聊天", |   "sendToChat": "发送到聊天", | ||||||
|   "failedToShareToPost": "分享到帖子失败:{}", |   "failedToShareToPost": "分享到帖子失败:{}", | ||||||
|   "shareToChatComingSoon": "分享到聊天的功能即将到来", |   "shareToChatComingSoon": "分享到聊天功能即将推出", | ||||||
|   "failedToShareToChat": "分享到聊天失败:{}", |   "failedToShareToChat": "分享到聊天失败:{}", | ||||||
|   "shareToSpecificChatComingSoon": "分享到 {} 的功能即将到来", |   "shareToSpecificChatComingSoon": "分享到 {} 功能即将推出", | ||||||
|   "directChat": "私信", |   "directChat": "私信", | ||||||
|   "systemShareComingSoon": "系统分享功能即将到来", |   "systemShareComingSoon": "系统分享功能即将推出", | ||||||
|   "failedToShareToSystem": "分享到系统失败:{}", |   "failedToShareToSystem": "分享到系统失败:{}", | ||||||
|   "failedToCopy": "复制失败:{}", |   "failedToCopy": "复制失败:{}", | ||||||
|   "noChatRoomsAvailable": "没有聊天室可用", |   "noChatRoomsAvailable": "无可用聊天室", | ||||||
|   "failedToLoadChats": "加载聊天室失败", |   "failedToLoadChats": "加载聊天失败", | ||||||
|   "contentToShare": "要分享的内容:", |   "contentToShare": "分享内容:", | ||||||
|   "unknownChat": "未知聊天室", |   "unknownChat": "未知聊天", | ||||||
|   "addAdditionalMessage": "添加额外消息……", |   "addAdditionalMessage": "添加附加消息……", | ||||||
|   "uploadingFiles": "上传文件中……", |   "uploadingFiles": "上传文件中……", | ||||||
|   "sharedSuccessfully": "分享成功!", |   "sharedSuccessfully": "分享成功!", | ||||||
|   "shareSuccess": "分享成功!", |   "shareSuccess": "分享成功!", | ||||||
|   "shareToSpecificChatSuccess": "分享到 {} 成功!", |   "shareToSpecificChatSuccess": "成功分享至 {}!", | ||||||
|   "wouldYouLikeToGoToChat": "你想要前往聊天页面吗?", |   "wouldYouLikeToGoToChat": "是否前往该聊天?", | ||||||
|   "no": "是", |   "no": "否", | ||||||
|   "yes": "否", |   "yes": "是", | ||||||
|   "navigateToChat": "前往聊天室", |   "navigateToChat": "前往聊天", | ||||||
|   "wouldYouLikeToNavigateToChat": "你想要前往聊天页面吗?", |  | ||||||
|   "abuseReport": "举报", |   "abuseReport": "举报", | ||||||
|   "abuseReportTitle": "举报内容", |   "abuseReportTitle": "举报内容", | ||||||
|   "abuseReportDescription": "通过举报不合适的内容和行为来帮助我们维护社区的健康稳定发展。", |   "abuseReportDescription": "举报不当内容或行为,协助维护社区安全。", | ||||||
|   "abuseReportType": "举报类型", |   "abuseReportType": "举报类型", | ||||||
|   "abuseReportReason": "额外细节", |   "abuseReportReason": "补充详情", | ||||||
|   "abuseReportReasonHint": "请提供更多关于此的细节……", |   "abuseReportReasonHint": "请提供更多详情……", | ||||||
|   "abuseReportSubmit": "提交举报", |   "abuseReportSubmit": "提交举报", | ||||||
|   "abuseReportSuccess": "举报提交成功,感谢你为社区维护作出贡献。", |   "abuseReportSuccess": "举报提交成功,感谢你为社区维护作出贡献。", | ||||||
|   "abuseReportError": "无法提交举报,请稍后再试。", |   "abuseReportError": "无法提交举报,请稍后再试。", | ||||||
| @@ -629,8 +624,6 @@ | |||||||
|   "chatJoin": "加入聊天", |   "chatJoin": "加入聊天", | ||||||
|   "realmJoin": "加入领域", |   "realmJoin": "加入领域", | ||||||
|   "realmJoinSuccess": "成功加入领域。", |   "realmJoinSuccess": "成功加入领域。", | ||||||
|   "discoverRealms": "发现领域", |  | ||||||
|   "discoverPublishers": "发现发布者", |  | ||||||
|   "search": "搜索", |   "search": "搜索", | ||||||
|   "publisherMembers": "合作者", |   "publisherMembers": "合作者", | ||||||
|   "developerHub": "开发者中心", |   "developerHub": "开发者中心", | ||||||
| @@ -679,10 +672,32 @@ | |||||||
|   "discoverWebArticles": "来自站外的文章", |   "discoverWebArticles": "来自站外的文章", | ||||||
|   "webArticlesStand": "文章亭", |   "webArticlesStand": "文章亭", | ||||||
|   "about": "关于", |   "about": "关于", | ||||||
|   "membershipCancel": "取消会员资格", |   "somethingWentWrong": "发生了一些错误", | ||||||
|   "membershipCancelConfirm": "你确定要取消会员资格吗?", |   "editedAt": "编辑于 {}", | ||||||
|   "membershipCancelHint": "你确定要取消会员资格吗?你将不会再次被扣费。你的会员资格将在当前计费周期结束前保持有效。并且你将无法重新订阅,直到当前订阅结束。", |   "addAudio": "添加音频", | ||||||
|   "membershipCancelSuccess": "你的会员资格已成功取消。", |   "recordAudio": "录制音频", | ||||||
|  |   "linkAttachment": "链接附件", | ||||||
|  |   "fileIdCannotBeEmpty": "文件 ID 不能为空", | ||||||
|  |   "fileIdLinkHint": "还没有上传到 Solar Network?点击此处打开 Solar Network Drive,自定义您的上传内容。", | ||||||
|  |   "failedToFetchFile": "获取文件失败:{}", | ||||||
|  |   "callLeave": "离开", | ||||||
|  |   "callEnd": "挂断通话", | ||||||
|  |   "postType": "帖子类型", | ||||||
|  |   "articleAttachmentHint": "附件必须上传并插入到文章主体中才能显示出来。", | ||||||
|  |   "postVisibility": "可见性", | ||||||
|  |   "currentMembershipMember": "恒星计划成员 · {}", | ||||||
|  |   "membershipPriceStellar": "需要用户等级 3+,每月价格 1200 NSP", | ||||||
|  |   "membershipPriceNova": "需要用户等级 6+,每月价格 2400 NSP", | ||||||
|  |   "membershipPriceSupernova": "需要用户等级 9+,每月价格 3600 NSP", | ||||||
|  |   "sharePostPhoto": "通过图片分享帖子", | ||||||
|  |   "wouldYouLikeToNavigateToChat": "你想要前往聊天页面吗?", | ||||||
|  |   "abuseReports": "举报", | ||||||
|  |   "discoverRealms": "发现领域", | ||||||
|  |   "discoverPublishers": "发现发布者", | ||||||
|  |   "membershipCancel": "取消会员订阅", | ||||||
|  |   "membershipCancelConfirm": "你确定要取消会员订阅吗?", | ||||||
|  |   "membershipCancelHint": "你确定要取消会员订阅吗?你将不会再次被扣费。你的会员资格将在当前计费周期结束前保持有效。并且你将无法重新订阅,直到当前订阅结束。", | ||||||
|  |   "membershipCancelSuccess": "你的会员订阅已成功取消。", | ||||||
|   "aboutScreenTitle": "关于", |   "aboutScreenTitle": "关于", | ||||||
|   "aboutScreenVersionInfo": "版本 {} ({})", |   "aboutScreenVersionInfo": "版本 {} ({})", | ||||||
|   "aboutScreenAppInfoSectionTitle": "应用信息", |   "aboutScreenAppInfoSectionTitle": "应用信息", | ||||||
| @@ -696,7 +711,7 @@ | |||||||
|   "aboutScreenDeveloperSectionTitle": "开发者", |   "aboutScreenDeveloperSectionTitle": "开发者", | ||||||
|   "aboutScreenContactUsTitle": "联系我们", |   "aboutScreenContactUsTitle": "联系我们", | ||||||
|   "aboutScreenLicenseTitle": "许可", |   "aboutScreenLicenseTitle": "许可", | ||||||
|   "aboutScreenLicenseContent": "GNU Affero General Public License v3.0", |   "aboutScreenLicenseContent": "无法翻译", | ||||||
|   "aboutScreenCopyright": "版权所有 © Solsynth {}", |   "aboutScreenCopyright": "版权所有 © Solsynth {}", | ||||||
|   "aboutScreenMadeWith": "由 Solar Network 团队用 ❤︎️ 制作", |   "aboutScreenMadeWith": "由 Solar Network 团队用 ❤︎️ 制作", | ||||||
|   "aboutScreenFailedToLoadPackageInfo": "无法加载包信息:{error}", |   "aboutScreenFailedToLoadPackageInfo": "无法加载包信息:{error}", | ||||||
| @@ -710,13 +725,13 @@ | |||||||
|   "aboutDeviceName": "设备名称", |   "aboutDeviceName": "设备名称", | ||||||
|   "aboutDeviceIdentifier": "设备标识符", |   "aboutDeviceIdentifier": "设备标识符", | ||||||
|   "donate": "捐赠", |   "donate": "捐赠", | ||||||
|   "donateDescription": "支持我们继续开发 Solar Network,并保持服务器运行。", |   "donateDescription": "支持我们继续开发 Solar Network,并维持服务器运行。", | ||||||
|   "fileId": "文件ID", |   "fileId": "文件 ID", | ||||||
|   "fileIdHint": "文件ID是你通过 Solar Network Drive 上传文件后获得的ID。", |   "fileIdHint": "文件 ID 是你通过 Solar Network Drive 上传文件后获得的 ID。", | ||||||
|   "translate": "翻译", |   "translate": "翻译", | ||||||
|   "translating": "正在翻译", |   "translating": "正在翻译", | ||||||
|   "translated": "已翻译", |   "translated": "已翻译", | ||||||
|   "reactionThumbUp": "点赞", |   "reactionThumbUp": "赞", | ||||||
|   "reactionThumbDown": "踩", |   "reactionThumbDown": "踩", | ||||||
|   "reactionJustOkay": "还行", |   "reactionJustOkay": "还行", | ||||||
|   "reactionCry": "哭", |   "reactionCry": "哭", | ||||||
| @@ -726,7 +741,7 @@ | |||||||
|   "reactionAngry": "生气", |   "reactionAngry": "生气", | ||||||
|   "reactionParty": "派对", |   "reactionParty": "派对", | ||||||
|   "reactionPray": "祈祷", |   "reactionPray": "祈祷", | ||||||
|   "reactionHeart": "心", |   "reactionHeart": "爱心", | ||||||
|   "selectMicrophone": "选择麦克风", |   "selectMicrophone": "选择麦克风", | ||||||
|   "selectCamera": "选择摄像头", |   "selectCamera": "选择摄像头", | ||||||
|   "switchedTo": "已切换到 {}", |   "switchedTo": "已切换到 {}", | ||||||
| @@ -741,19 +756,21 @@ | |||||||
|   "rename": "重命名", |   "rename": "重命名", | ||||||
|   "markAsSensitive": "标记为敏感", |   "markAsSensitive": "标记为敏感", | ||||||
|   "fileName": "文件名", |   "fileName": "文件名", | ||||||
|   "sensitiveCategories.language": "语言", |   "sensitiveCategories": { | ||||||
|   "sensitiveCategories.sexualContent": "色情内容", |     "language": "语言", | ||||||
|   "sensitiveCategories.violence": "暴力", |     "sexualContent": "色情内容", | ||||||
|   "sensitiveCategories.profanity": "亵渎", |     "violence": "暴力", | ||||||
|   "sensitiveCategories.hateSpeech": "仇恨言论", |     "profanity": "亵渎", | ||||||
|   "sensitiveCategories.racism": "种族主义", |     "hateSpeech": "仇恨言论", | ||||||
|   "sensitiveCategories.adultContent": "成人内容", |     "racism": "种族主义", | ||||||
|   "sensitiveCategories.drugAbuse": "药物滥用", |     "adultContent": "成人内容", | ||||||
|   "sensitiveCategories.alcoholAbuse": "酗酒", |     "drugAbuse": "药物滥用", | ||||||
|   "sensitiveCategories.gambling": "赌博", |     "alcoholAbuse": "酗酒", | ||||||
|   "sensitiveCategories.selfHarm": "自残", |     "gambling": "赌博", | ||||||
|   "sensitiveCategories.childAbuse": "虐待儿童", |     "selfHarm": "自残", | ||||||
|   "sensitiveCategories.other": "其他", |     "childAbuse": "虐待儿童", | ||||||
|  |     "other": "其他" | ||||||
|  |   }, | ||||||
|   "poll": "投票", |   "poll": "投票", | ||||||
|   "pollsRecent": "最近投票", |   "pollsRecent": "最近投票", | ||||||
|   "pollCreateNew": "创建新投票", |   "pollCreateNew": "创建新投票", | ||||||
| @@ -785,10 +802,21 @@ | |||||||
|   "links": "链接", |   "links": "链接", | ||||||
|   "addLink": "添加链接", |   "addLink": "添加链接", | ||||||
|   "linkKey": "链接名称", |   "linkKey": "链接名称", | ||||||
|   "linkValue": "URL", |   "linkValue": "链接", | ||||||
|   "debugOptions": "调试选项", |   "debugOptions": "调试选项", | ||||||
|   "joinedAt": "加入于 {}", |   "joinedAt": "加入于 {}", | ||||||
|   "searchAccounts": "搜索帐号……", |   "searchAccounts": "搜索帐号……", | ||||||
|   "webFeeds": "订阅源", |   "webFeeds": "订阅源", | ||||||
|   "polls": "投票" |   "polls": "投票", | ||||||
|  |   "sharePostSlogan": "加入 Solar Network 以便探索更多", | ||||||
|  |   "filesListAdditional": { | ||||||
|  |     "one": "+{} 个文件被折叠", | ||||||
|  |     "other": "+{} 个文件被折叠" | ||||||
|  |   }, | ||||||
|  |   "messageJumpNotLoaded": "引用的消息没有被加载,无法跳转。", | ||||||
|  |   "postUnlinkRealm": "不关联领域", | ||||||
|  |   "postSlug": "别名", | ||||||
|  |   "postSlugHint": "这个别名可以用于在网页通过 URL 浏览到你的帖子,它应该在同一发布者中是唯一。", | ||||||
|  |   "attachmentOnDevice": "离线", | ||||||
|  |   "attachmentOnCloud": "在线" | ||||||
| } | } | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								assets/icons/icon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/icons/icon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 108 KiB | 
| @@ -245,7 +245,7 @@ PODS: | |||||||
|     - PromisesObjC (= 2.4.0) |     - PromisesObjC (= 2.4.0) | ||||||
|   - receive_sharing_intent (1.8.1): |   - receive_sharing_intent (1.8.1): | ||||||
|     - Flutter |     - Flutter | ||||||
|   - record_ios (1.0.0): |   - record_ios (1.1.0): | ||||||
|     - Flutter |     - Flutter | ||||||
|   - SAMKeychain (1.5.3) |   - SAMKeychain (1.5.3) | ||||||
|   - SDWebImage (5.21.1): |   - SDWebImage (5.21.1): | ||||||
| @@ -510,7 +510,7 @@ SPEC CHECKSUMS: | |||||||
|   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 |   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 | ||||||
|   PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 |   PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 | ||||||
|   receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 |   receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 | ||||||
|   record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b |   record_ios: f75fa1d57f840012775c0e93a38a7f3ceea1a374 | ||||||
|   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c |   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c | ||||||
|   SDWebImage: f29024626962457f3470184232766516dee8dfea |   SDWebImage: f29024626962457f3470184232766516dee8dfea | ||||||
|   share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a |   share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a | ||||||
|   | |||||||
| @@ -1,484 +0,0 @@ | |||||||
| import 'package:dio/dio.dart'; |  | ||||||
| import 'package:island/database/drift_db.dart'; |  | ||||||
| import 'package:island/database/message.dart'; |  | ||||||
| import 'package:island/models/chat.dart'; |  | ||||||
| import 'package:island/models/file.dart'; |  | ||||||
| import 'package:island/services/file.dart'; |  | ||||||
| import 'package:island/widgets/alert.dart'; |  | ||||||
| import 'package:uuid/uuid.dart'; |  | ||||||
|  |  | ||||||
| class MessageRepository { |  | ||||||
|   final SnChatRoom room; |  | ||||||
|   final SnChatMember identity; |  | ||||||
|   final Dio _apiClient; |  | ||||||
|   final AppDatabase _database; |  | ||||||
|  |  | ||||||
|   final Map<String, LocalChatMessage> pendingMessages = {}; |  | ||||||
|   final Map<String, Map<int, double>> fileUploadProgress = {}; |  | ||||||
|   int? _totalCount; |  | ||||||
|  |  | ||||||
|   MessageRepository(this.room, this.identity, this._apiClient, this._database); |  | ||||||
|  |  | ||||||
|   Future<LocalChatMessage?> getLastMessages() async { |  | ||||||
|     final dbMessages = await _database.getMessagesForRoom( |  | ||||||
|       room.id, |  | ||||||
|       offset: 0, |  | ||||||
|       limit: 1, |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     if (dbMessages.isEmpty) { |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return _database.companionToMessage(dbMessages.first); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<bool> syncMessages() async { |  | ||||||
|     final lastMessage = await getLastMessages(); |  | ||||||
|     if (lastMessage == null) return false; |  | ||||||
|     try { |  | ||||||
|       final resp = await _apiClient.post( |  | ||||||
|         '/sphere/chat/${room.id}/sync', |  | ||||||
|         data: { |  | ||||||
|           'last_sync_timestamp': |  | ||||||
|               lastMessage.toRemoteMessage().updatedAt.millisecondsSinceEpoch, |  | ||||||
|         }, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       final response = MessageSyncResponse.fromJson(resp.data); |  | ||||||
|       for (final change in response.changes) { |  | ||||||
|         switch (change.action) { |  | ||||||
|           case MessageChangeAction.create: |  | ||||||
|             await receiveMessage(change.message!); |  | ||||||
|             break; |  | ||||||
|           case MessageChangeAction.update: |  | ||||||
|             await receiveMessageUpdate(change.message!); |  | ||||||
|             break; |  | ||||||
|           case MessageChangeAction.delete: |  | ||||||
|             await receiveMessageDeletion(change.messageId.toString()); |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } catch (err) { |  | ||||||
|       showErrorAlert(err); |  | ||||||
|     } |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<List<LocalChatMessage>> listMessages({ |  | ||||||
|     int offset = 0, |  | ||||||
|     int take = 20, |  | ||||||
|     bool synced = false, |  | ||||||
|   }) async { |  | ||||||
|     try { |  | ||||||
|       // For initial load, fetch latest messages in the background to sync. |  | ||||||
|       if (offset == 0 && !synced) { |  | ||||||
|         // Not awaiting this is intentional, for a quicker UI response. |  | ||||||
|         // The UI should rely on a stream from the database to get updates. |  | ||||||
|         _fetchAndCacheMessages(room.id, offset: 0, take: take).catchError((_) { |  | ||||||
|           // Best effort, errors will be handled by later fetches. |  | ||||||
|           return <LocalChatMessage>[]; |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       final localMessages = await _getCachedMessages( |  | ||||||
|         room.id, |  | ||||||
|         offset: offset, |  | ||||||
|         take: take, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       // If local cache has messages, return them. This is the common case for scrolling up. |  | ||||||
|       if (localMessages.isNotEmpty) { |  | ||||||
|         return localMessages; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // If local cache is empty, we've probably reached the end of cached history. |  | ||||||
|       // Fetch from remote. This will also be hit on first load if cache is empty. |  | ||||||
|       return await _fetchAndCacheMessages(room.id, offset: offset, take: take); |  | ||||||
|     } catch (e) { |  | ||||||
|       // Final fallback to cache in case of network errors during fetch. |  | ||||||
|       final localMessages = await _getCachedMessages( |  | ||||||
|         room.id, |  | ||||||
|         offset: offset, |  | ||||||
|         take: take, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       if (localMessages.isNotEmpty) { |  | ||||||
|         return localMessages; |  | ||||||
|       } |  | ||||||
|       rethrow; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<List<LocalChatMessage>> _getCachedMessages( |  | ||||||
|     String roomId, { |  | ||||||
|     int offset = 0, |  | ||||||
|     int take = 20, |  | ||||||
|   }) async { |  | ||||||
|     // Get messages from local database |  | ||||||
|     final dbMessages = await _database.getMessagesForRoom( |  | ||||||
|       roomId, |  | ||||||
|       offset: offset, |  | ||||||
|       limit: take, |  | ||||||
|     ); |  | ||||||
|     final dbLocalMessages = |  | ||||||
|         dbMessages.map(_database.companionToMessage).toList(); |  | ||||||
|  |  | ||||||
|     // Combine with pending messages for the first page |  | ||||||
|     if (offset == 0) { |  | ||||||
|       final pendingForRoom = |  | ||||||
|           pendingMessages.values.where((msg) => msg.roomId == roomId).toList(); |  | ||||||
|  |  | ||||||
|       final allMessages = [...pendingForRoom, ...dbLocalMessages]; |  | ||||||
|       allMessages.sort((a, b) => b.createdAt.compareTo(a.createdAt)); |  | ||||||
|  |  | ||||||
|       // Remove duplicates by ID, preserving the order |  | ||||||
|       final uniqueMessages = <LocalChatMessage>[]; |  | ||||||
|       final seenIds = <String>{}; |  | ||||||
|       for (final message in allMessages) { |  | ||||||
|         if (seenIds.add(message.id)) { |  | ||||||
|           uniqueMessages.add(message); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       return uniqueMessages; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return dbLocalMessages; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<List<LocalChatMessage>> _fetchAndCacheMessages( |  | ||||||
|     String roomId, { |  | ||||||
|     int offset = 0, |  | ||||||
|     int take = 20, |  | ||||||
|   }) async { |  | ||||||
|     // Use cached total count if available, otherwise fetch it |  | ||||||
|     if (_totalCount == null) { |  | ||||||
|       final response = await _apiClient.get( |  | ||||||
|         '/sphere/chat/$roomId/messages', |  | ||||||
|         queryParameters: {'offset': 0, 'take': 1}, |  | ||||||
|       ); |  | ||||||
|       _totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (offset >= _totalCount!) { |  | ||||||
|       return []; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     final response = await _apiClient.get( |  | ||||||
|       '/sphere/chat/$roomId/messages', |  | ||||||
|       queryParameters: {'offset': offset, 'take': take}, |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     final List<dynamic> data = response.data; |  | ||||||
|     // Update total count from response headers |  | ||||||
|     _totalCount = int.parse(response.headers['x-total']?.firstOrNull ?? '0'); |  | ||||||
|  |  | ||||||
|     final messages = |  | ||||||
|         data.map((json) { |  | ||||||
|           final remoteMessage = SnChatMessage.fromJson(json); |  | ||||||
|           return LocalChatMessage.fromRemoteMessage( |  | ||||||
|             remoteMessage, |  | ||||||
|             MessageStatus.sent, |  | ||||||
|           ); |  | ||||||
|         }).toList(); |  | ||||||
|  |  | ||||||
|     for (final message in messages) { |  | ||||||
|       await _database.saveMessage(_database.messageToCompanion(message)); |  | ||||||
|       if (message.nonce != null) { |  | ||||||
|         pendingMessages.removeWhere( |  | ||||||
|           (_, pendingMsg) => pendingMsg.nonce == message.nonce, |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return messages; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<LocalChatMessage> sendMessage( |  | ||||||
|     String token, |  | ||||||
|     String baseUrl, |  | ||||||
|     String roomId, |  | ||||||
|     String content, |  | ||||||
|     String nonce, { |  | ||||||
|     required List<UniversalFile> attachments, |  | ||||||
|     Map<String, dynamic>? meta, |  | ||||||
|     SnChatMessage? replyingTo, |  | ||||||
|     SnChatMessage? forwardingTo, |  | ||||||
|     SnChatMessage? editingTo, |  | ||||||
|     Function(LocalChatMessage)? onPending, |  | ||||||
|     Function(String, Map<int, double>)? onProgress, |  | ||||||
|   }) async { |  | ||||||
|     // Generate a unique nonce for this message |  | ||||||
|     final nonce = const Uuid().v4(); |  | ||||||
|  |  | ||||||
|     // Create a local message with pending status |  | ||||||
|     final mockMessage = SnChatMessage( |  | ||||||
|       id: 'pending_$nonce', |  | ||||||
|       chatRoomId: roomId, |  | ||||||
|       senderId: identity.id, |  | ||||||
|       content: content, |  | ||||||
|       createdAt: DateTime.now(), |  | ||||||
|       updatedAt: DateTime.now(), |  | ||||||
|       nonce: nonce, |  | ||||||
|       sender: identity, |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     final localMessage = LocalChatMessage.fromRemoteMessage( |  | ||||||
|       mockMessage, |  | ||||||
|       MessageStatus.pending, |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     // Store in memory and database |  | ||||||
|     pendingMessages[localMessage.id] = localMessage; |  | ||||||
|     fileUploadProgress[localMessage.id] = {}; |  | ||||||
|     await _database.saveMessage(_database.messageToCompanion(localMessage)); |  | ||||||
|     onPending?.call(localMessage); |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       var cloudAttachments = List.empty(growable: true); |  | ||||||
|       // Upload files |  | ||||||
|       for (var idx = 0; idx < attachments.length; idx++) { |  | ||||||
|         final cloudFile = |  | ||||||
|             await putMediaToCloud( |  | ||||||
|               fileData: attachments[idx], |  | ||||||
|               atk: token, |  | ||||||
|               baseUrl: baseUrl, |  | ||||||
|               filename: attachments[idx].data.name ?? 'Post media', |  | ||||||
|               mimetype: |  | ||||||
|                   attachments[idx].data.mimeType ?? |  | ||||||
|                   switch (attachments[idx].type) { |  | ||||||
|                     UniversalFileType.image => 'image/unknown', |  | ||||||
|                     UniversalFileType.video => 'video/unknown', |  | ||||||
|                     UniversalFileType.audio => 'audio/unknown', |  | ||||||
|                     UniversalFileType.file => 'application/octet-stream', |  | ||||||
|                   }, |  | ||||||
|               onProgress: (progress, _) { |  | ||||||
|                 fileUploadProgress[localMessage.id]?[idx] = progress; |  | ||||||
|                 onProgress?.call( |  | ||||||
|                   localMessage.id, |  | ||||||
|                   fileUploadProgress[localMessage.id] ?? {}, |  | ||||||
|                 ); |  | ||||||
|               }, |  | ||||||
|             ).future; |  | ||||||
|         if (cloudFile == null) { |  | ||||||
|           throw ArgumentError('Failed to upload the file...'); |  | ||||||
|         } |  | ||||||
|         cloudAttachments.add(cloudFile); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // Send to server |  | ||||||
|       final response = await _apiClient.request( |  | ||||||
|         editingTo == null |  | ||||||
|             ? '/sphere/chat/$roomId/messages' |  | ||||||
|             : '/sphere/chat/$roomId/messages/${editingTo.id}', |  | ||||||
|         data: { |  | ||||||
|           'content': content, |  | ||||||
|           'attachments_id': cloudAttachments.map((e) => e.id).toList(), |  | ||||||
|           'replied_message_id': replyingTo?.id, |  | ||||||
|           'forwarded_message_id': forwardingTo?.id, |  | ||||||
|           'meta': meta, |  | ||||||
|           'nonce': nonce, |  | ||||||
|         }, |  | ||||||
|         options: Options(method: editingTo == null ? 'POST' : 'PATCH'), |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       // Update with server response |  | ||||||
|       final remoteMessage = SnChatMessage.fromJson(response.data); |  | ||||||
|       final updatedMessage = LocalChatMessage.fromRemoteMessage( |  | ||||||
|         remoteMessage, |  | ||||||
|         MessageStatus.sent, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       // Remove from pending and update in database |  | ||||||
|       pendingMessages.remove(localMessage.id); |  | ||||||
|       await _database.deleteMessage(localMessage.id); |  | ||||||
|       await _database.saveMessage(_database.messageToCompanion(updatedMessage)); |  | ||||||
|  |  | ||||||
|       return updatedMessage; |  | ||||||
|     } catch (e) { |  | ||||||
|       // Update status to failed |  | ||||||
|       localMessage.status = MessageStatus.failed; |  | ||||||
|       pendingMessages[localMessage.id] = localMessage; |  | ||||||
|       await _database.updateMessageStatus( |  | ||||||
|         localMessage.id, |  | ||||||
|         MessageStatus.failed, |  | ||||||
|       ); |  | ||||||
|       rethrow; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<LocalChatMessage> retryMessage(String pendingMessageId) async { |  | ||||||
|     final message = await getMessageById(pendingMessageId); |  | ||||||
|     if (message == null) { |  | ||||||
|       throw Exception('Message not found'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Update status back to pending |  | ||||||
|     message.status = MessageStatus.pending; |  | ||||||
|     pendingMessages[pendingMessageId] = message; |  | ||||||
|     await _database.updateMessageStatus( |  | ||||||
|       pendingMessageId, |  | ||||||
|       MessageStatus.pending, |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       // Send to server |  | ||||||
|       var remoteMessage = message.toRemoteMessage(); |  | ||||||
|       final response = await _apiClient.post( |  | ||||||
|         '/sphere/chat/${message.roomId}/messages', |  | ||||||
|         data: { |  | ||||||
|           'content': remoteMessage.content, |  | ||||||
|           'attachments_id': remoteMessage.attachments, |  | ||||||
|           'meta': remoteMessage.meta, |  | ||||||
|           'nonce': message.nonce, |  | ||||||
|         }, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       // Update with server response |  | ||||||
|       remoteMessage = SnChatMessage.fromJson(response.data); |  | ||||||
|       final updatedMessage = LocalChatMessage.fromRemoteMessage( |  | ||||||
|         remoteMessage, |  | ||||||
|         MessageStatus.sent, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       // Remove from pending and update in database |  | ||||||
|       pendingMessages.remove(pendingMessageId); |  | ||||||
|       await _database.deleteMessage(pendingMessageId); |  | ||||||
|       await _database.saveMessage(_database.messageToCompanion(updatedMessage)); |  | ||||||
|  |  | ||||||
|       return updatedMessage; |  | ||||||
|     } catch (e) { |  | ||||||
|       // Update status to failed |  | ||||||
|       message.status = MessageStatus.failed; |  | ||||||
|       pendingMessages[pendingMessageId] = message; |  | ||||||
|       await _database.updateMessageStatus( |  | ||||||
|         pendingMessageId, |  | ||||||
|         MessageStatus.failed, |  | ||||||
|       ); |  | ||||||
|       rethrow; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<LocalChatMessage> receiveMessage(SnChatMessage remoteMessage) async { |  | ||||||
|     final localMessage = LocalChatMessage.fromRemoteMessage( |  | ||||||
|       remoteMessage, |  | ||||||
|       MessageStatus.sent, |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     if (remoteMessage.nonce != null) { |  | ||||||
|       pendingMessages.removeWhere( |  | ||||||
|         (_, pendingMsg) => pendingMsg.nonce == remoteMessage.nonce, |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     await _database.saveMessage(_database.messageToCompanion(localMessage)); |  | ||||||
|     return localMessage; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<LocalChatMessage> receiveMessageUpdate( |  | ||||||
|     SnChatMessage remoteMessage, |  | ||||||
|   ) async { |  | ||||||
|     final localMessage = LocalChatMessage.fromRemoteMessage( |  | ||||||
|       remoteMessage, |  | ||||||
|       MessageStatus.sent, |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     await _database.updateMessage(_database.messageToCompanion(localMessage)); |  | ||||||
|     return localMessage; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<void> receiveMessageDeletion(String messageId) async { |  | ||||||
|     // Remove from pending messages if exists |  | ||||||
|     pendingMessages.remove(messageId); |  | ||||||
|  |  | ||||||
|     // Delete from local database |  | ||||||
|     await _database.deleteMessage(messageId); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<LocalChatMessage> updateMessage( |  | ||||||
|     String messageId, |  | ||||||
|     String content, { |  | ||||||
|     List<SnCloudFile>? attachments, |  | ||||||
|     Map<String, dynamic>? meta, |  | ||||||
|   }) async { |  | ||||||
|     final message = pendingMessages[messageId]; |  | ||||||
|     if (message != null) { |  | ||||||
|       // Update pending message |  | ||||||
|       final rmMessage = message.toRemoteMessage(); |  | ||||||
|       final updatedRemoteMessage = rmMessage.copyWith( |  | ||||||
|         content: content, |  | ||||||
|         meta: meta ?? rmMessage.meta, |  | ||||||
|       ); |  | ||||||
|       final updatedLocalMessage = LocalChatMessage.fromRemoteMessage( |  | ||||||
|         updatedRemoteMessage, |  | ||||||
|         MessageStatus.pending, |  | ||||||
|       ); |  | ||||||
|       pendingMessages[messageId] = updatedLocalMessage; |  | ||||||
|       await _database.updateMessage( |  | ||||||
|         _database.messageToCompanion(updatedLocalMessage), |  | ||||||
|       ); |  | ||||||
|       return message; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     try { |  | ||||||
|       // Update on server |  | ||||||
|       final response = await _apiClient.put( |  | ||||||
|         '/sphere/chat/${room.id}/messages/$messageId', |  | ||||||
|         data: {'content': content, 'attachments': attachments, 'meta': meta}, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       // Update local copy |  | ||||||
|       final remoteMessage = SnChatMessage.fromJson(response.data); |  | ||||||
|       final updatedMessage = LocalChatMessage.fromRemoteMessage( |  | ||||||
|         remoteMessage, |  | ||||||
|         MessageStatus.sent, |  | ||||||
|       ); |  | ||||||
|       await _database.updateMessage( |  | ||||||
|         _database.messageToCompanion(updatedMessage), |  | ||||||
|       ); |  | ||||||
|       return updatedMessage; |  | ||||||
|     } catch (e) { |  | ||||||
|       rethrow; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<void> deleteMessage(String messageId) async { |  | ||||||
|     try { |  | ||||||
|       await _apiClient.delete('/sphere/chat/${room.id}/messages/$messageId'); |  | ||||||
|       pendingMessages.remove(messageId); |  | ||||||
|       await _database.deleteMessage(messageId); |  | ||||||
|     } catch (e) { |  | ||||||
|       rethrow; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Future<LocalChatMessage?> getMessageById(String messageId) async { |  | ||||||
|     try { |  | ||||||
|       // Attempt to get the message from the local database |  | ||||||
|       final localMessage = |  | ||||||
|           await (_database.select(_database.chatMessages) |  | ||||||
|             ..where((tbl) => tbl.id.equals(messageId))).getSingleOrNull(); |  | ||||||
|       if (localMessage != null) { |  | ||||||
|         return _database.companionToMessage(localMessage); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       // If not found locally, fetch from the server |  | ||||||
|       final response = await _apiClient.get( |  | ||||||
|         '/sphere/chat/${room.id}/messages/$messageId', |  | ||||||
|       ); |  | ||||||
|       final remoteMessage = SnChatMessage.fromJson(response.data); |  | ||||||
|       final message = LocalChatMessage.fromRemoteMessage( |  | ||||||
|         remoteMessage, |  | ||||||
|         MessageStatus.sent, |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       // Save the fetched message to the local database |  | ||||||
|       await _database.saveMessage(_database.messageToCompanion(message)); |  | ||||||
|       return message; |  | ||||||
|     } catch (e) { |  | ||||||
|       if (e is DioException) return null; |  | ||||||
|       // Handle errors |  | ||||||
|       rethrow; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -62,12 +62,17 @@ void main() async { | |||||||
|       FirebaseMessaging.onBackgroundMessage( |       FirebaseMessaging.onBackgroundMessage( | ||||||
|         _firebaseMessagingBackgroundHandler, |         _firebaseMessagingBackgroundHandler, | ||||||
|       ); |       ); | ||||||
|       FlutterError.onError = |       // Although previous if case checked this. Still check is web or not | ||||||
|           FirebaseCrashlytics.instance.recordFlutterFatalError; |       // Otherwise the web platform will broke due to there is no Platform api on the web | ||||||
|       PlatformDispatcher.instance.onError = (error, stack) { |       // Skip crashlytics setup on debug mode to prevent unexpected report to firebase | ||||||
|         FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); |       if ((kIsWeb || !Platform.isWindows) && !kDebugMode) { | ||||||
|         return true; |         FlutterError.onError = | ||||||
|       }; |             FirebaseCrashlytics.instance.recordFlutterFatalError; | ||||||
|  |         PlatformDispatcher.instance.onError = (error, stack) { | ||||||
|  |           FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); | ||||||
|  |           return true; | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     log("[SplashScreen] Firebase is ready!"); |     log("[SplashScreen] Firebase is ready!"); | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | import 'package:freezed_annotation/freezed_annotation.dart'; | ||||||
|  | import 'package:island/models/auth.dart'; | ||||||
| import 'package:island/models/file.dart'; | import 'package:island/models/file.dart'; | ||||||
| import 'package:island/models/wallet.dart'; | import 'package:island/models/wallet.dart'; | ||||||
| 
 | 
 | ||||||
| part 'user.freezed.dart'; | part 'account.freezed.dart'; | ||||||
| part 'user.g.dart'; | part 'account.g.dart'; | ||||||
| 
 | 
 | ||||||
| @freezed | @freezed | ||||||
| sealed class SnAccount with _$SnAccount { | sealed class SnAccount with _$SnAccount { | ||||||
| @@ -174,3 +175,36 @@ sealed class SnVerificationMark with _$SnVerificationMark { | |||||||
|   factory SnVerificationMark.fromJson(Map<String, dynamic> json) => |   factory SnVerificationMark.fromJson(Map<String, dynamic> json) => | ||||||
|       _$SnVerificationMarkFromJson(json); |       _$SnVerificationMarkFromJson(json); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | @freezed | ||||||
|  | sealed class SnAuthDevice with _$SnAuthDevice { | ||||||
|  |   const factory SnAuthDevice({ | ||||||
|  |     required String id, | ||||||
|  |     required String deviceId, | ||||||
|  |     required String deviceName, | ||||||
|  |     required String? deviceLabel, | ||||||
|  |     required String accountId, | ||||||
|  |     required int platform, | ||||||
|  |     @Default(false) bool isCurrent, | ||||||
|  |   }) = _SnAuthDevice; | ||||||
|  | 
 | ||||||
|  |   factory SnAuthDevice.fromJson(Map<String, dynamic> json) => | ||||||
|  |       _$SnAuthDeviceFromJson(json); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @freezed | ||||||
|  | sealed class SnAuthDeviceWithChallenge with _$SnAuthDeviceWithChallenge { | ||||||
|  |   const factory SnAuthDeviceWithChallenge({ | ||||||
|  |     required String id, | ||||||
|  |     required String deviceId, | ||||||
|  |     required String deviceName, | ||||||
|  |     required String? deviceLabel, | ||||||
|  |     required String accountId, | ||||||
|  |     required int platform, | ||||||
|  |     required List<SnAuthChallenge> challenges, | ||||||
|  |     @Default(false) bool isCurrent, | ||||||
|  |   }) = _SnAuthDeviceWithChallengee; | ||||||
|  | 
 | ||||||
|  |   factory SnAuthDeviceWithChallenge.fromJson(Map<String, dynamic> json) => | ||||||
|  |       _$SnAuthDeviceWithChallengeFromJson(json); | ||||||
|  | } | ||||||
| @@ -3,7 +3,7 @@ | |||||||
| // ignore_for_file: type=lint | // 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 | // 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 'user.dart'; | part of 'account.dart'; | ||||||
| 
 | 
 | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
| // FreezedGenerator | // FreezedGenerator | ||||||
| @@ -2452,6 +2452,572 @@ as String?, | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// @nodoc | ||||||
|  | mixin _$SnAuthDevice { | ||||||
|  | 
 | ||||||
|  |  String get id; String get deviceId; String get deviceName; String? get deviceLabel; String get accountId; int get platform; bool get isCurrent; | ||||||
|  | /// Create a copy of SnAuthDevice | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | $SnAuthDeviceCopyWith<SnAuthDevice> get copyWith => _$SnAuthDeviceCopyWithImpl<SnAuthDevice>(this as SnAuthDevice, _$identity); | ||||||
|  | 
 | ||||||
|  |   /// Serializes this SnAuthDevice to a JSON map. | ||||||
|  |   Map<String, dynamic> toJson(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @override | ||||||
|  | bool operator ==(Object other) { | ||||||
|  |   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthDevice&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @override | ||||||
|  | int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,isCurrent); | ||||||
|  | 
 | ||||||
|  | @override | ||||||
|  | String toString() { | ||||||
|  |   return 'SnAuthDevice(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, isCurrent: $isCurrent)'; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// @nodoc | ||||||
|  | abstract mixin class $SnAuthDeviceCopyWith<$Res>  { | ||||||
|  |   factory $SnAuthDeviceCopyWith(SnAuthDevice value, $Res Function(SnAuthDevice) _then) = _$SnAuthDeviceCopyWithImpl; | ||||||
|  | @useResult | ||||||
|  | $Res call({ | ||||||
|  |  String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, bool isCurrent | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | /// @nodoc | ||||||
|  | class _$SnAuthDeviceCopyWithImpl<$Res> | ||||||
|  |     implements $SnAuthDeviceCopyWith<$Res> { | ||||||
|  |   _$SnAuthDeviceCopyWithImpl(this._self, this._then); | ||||||
|  | 
 | ||||||
|  |   final SnAuthDevice _self; | ||||||
|  |   final $Res Function(SnAuthDevice) _then; | ||||||
|  | 
 | ||||||
|  | /// Create a copy of SnAuthDevice | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? isCurrent = null,}) { | ||||||
|  |   return _then(_self.copyWith( | ||||||
|  | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable | ||||||
|  | as int,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable | ||||||
|  | as bool, | ||||||
|  |   )); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// Adds pattern-matching-related methods to [SnAuthDevice]. | ||||||
|  | extension SnAuthDevicePatterns on SnAuthDevice { | ||||||
|  | /// 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( _SnAuthDevice value)?  $default,{required TResult orElse(),}){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnAuthDevice() 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( _SnAuthDevice value)  $default,){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnAuthDevice(): | ||||||
|  | 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( _SnAuthDevice value)?  $default,){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnAuthDevice() 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 deviceId,  String deviceName,  String? deviceLabel,  String accountId,  int platform,  bool isCurrent)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnAuthDevice() when $default != null: | ||||||
|  | return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.isCurrent);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 deviceId,  String deviceName,  String? deviceLabel,  String accountId,  int platform,  bool isCurrent)  $default,) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnAuthDevice(): | ||||||
|  | return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.isCurrent);} | ||||||
|  | } | ||||||
|  | /// 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 deviceId,  String deviceName,  String? deviceLabel,  String accountId,  int platform,  bool isCurrent)?  $default,) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnAuthDevice() when $default != null: | ||||||
|  | return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.isCurrent);case _: | ||||||
|  |   return null; | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// @nodoc | ||||||
|  | @JsonSerializable() | ||||||
|  | 
 | ||||||
|  | class _SnAuthDevice implements SnAuthDevice { | ||||||
|  |   const _SnAuthDevice({required this.id, required this.deviceId, required this.deviceName, required this.deviceLabel, required this.accountId, required this.platform, this.isCurrent = false}); | ||||||
|  |   factory _SnAuthDevice.fromJson(Map<String, dynamic> json) => _$SnAuthDeviceFromJson(json); | ||||||
|  | 
 | ||||||
|  | @override final  String id; | ||||||
|  | @override final  String deviceId; | ||||||
|  | @override final  String deviceName; | ||||||
|  | @override final  String? deviceLabel; | ||||||
|  | @override final  String accountId; | ||||||
|  | @override final  int platform; | ||||||
|  | @override@JsonKey() final  bool isCurrent; | ||||||
|  | 
 | ||||||
|  | /// Create a copy of SnAuthDevice | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | _$SnAuthDeviceCopyWith<_SnAuthDevice> get copyWith => __$SnAuthDeviceCopyWithImpl<_SnAuthDevice>(this, _$identity); | ||||||
|  | 
 | ||||||
|  | @override | ||||||
|  | Map<String, dynamic> toJson() { | ||||||
|  |   return _$SnAuthDeviceToJson(this, ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @override | ||||||
|  | bool operator ==(Object other) { | ||||||
|  |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthDevice&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @override | ||||||
|  | int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,isCurrent); | ||||||
|  | 
 | ||||||
|  | @override | ||||||
|  | String toString() { | ||||||
|  |   return 'SnAuthDevice(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, isCurrent: $isCurrent)'; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// @nodoc | ||||||
|  | abstract mixin class _$SnAuthDeviceCopyWith<$Res> implements $SnAuthDeviceCopyWith<$Res> { | ||||||
|  |   factory _$SnAuthDeviceCopyWith(_SnAuthDevice value, $Res Function(_SnAuthDevice) _then) = __$SnAuthDeviceCopyWithImpl; | ||||||
|  | @override @useResult | ||||||
|  | $Res call({ | ||||||
|  |  String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, bool isCurrent | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | /// @nodoc | ||||||
|  | class __$SnAuthDeviceCopyWithImpl<$Res> | ||||||
|  |     implements _$SnAuthDeviceCopyWith<$Res> { | ||||||
|  |   __$SnAuthDeviceCopyWithImpl(this._self, this._then); | ||||||
|  | 
 | ||||||
|  |   final _SnAuthDevice _self; | ||||||
|  |   final $Res Function(_SnAuthDevice) _then; | ||||||
|  | 
 | ||||||
|  | /// Create a copy of SnAuthDevice | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? isCurrent = null,}) { | ||||||
|  |   return _then(_SnAuthDevice( | ||||||
|  | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable | ||||||
|  | as int,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable | ||||||
|  | as bool, | ||||||
|  |   )); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | SnAuthDeviceWithChallenge _$SnAuthDeviceWithChallengeFromJson( | ||||||
|  |   Map<String, dynamic> json | ||||||
|  | ) { | ||||||
|  |     return _SnAuthDeviceWithChallengee.fromJson( | ||||||
|  |       json | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// @nodoc | ||||||
|  | mixin _$SnAuthDeviceWithChallenge { | ||||||
|  | 
 | ||||||
|  |  String get id; String get deviceId; String get deviceName; String? get deviceLabel; String get accountId; int get platform; List<SnAuthChallenge> get challenges; bool get isCurrent; | ||||||
|  | /// Create a copy of SnAuthDeviceWithChallenge | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | $SnAuthDeviceWithChallengeCopyWith<SnAuthDeviceWithChallenge> get copyWith => _$SnAuthDeviceWithChallengeCopyWithImpl<SnAuthDeviceWithChallenge>(this as SnAuthDeviceWithChallenge, _$identity); | ||||||
|  | 
 | ||||||
|  |   /// Serializes this SnAuthDeviceWithChallenge to a JSON map. | ||||||
|  |   Map<String, dynamic> toJson(); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @override | ||||||
|  | bool operator ==(Object other) { | ||||||
|  |   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthDeviceWithChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other.challenges, challenges)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @override | ||||||
|  | int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,const DeepCollectionEquality().hash(challenges),isCurrent); | ||||||
|  | 
 | ||||||
|  | @override | ||||||
|  | String toString() { | ||||||
|  |   return 'SnAuthDeviceWithChallenge(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, challenges: $challenges, isCurrent: $isCurrent)'; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// @nodoc | ||||||
|  | abstract mixin class $SnAuthDeviceWithChallengeCopyWith<$Res>  { | ||||||
|  |   factory $SnAuthDeviceWithChallengeCopyWith(SnAuthDeviceWithChallenge value, $Res Function(SnAuthDeviceWithChallenge) _then) = _$SnAuthDeviceWithChallengeCopyWithImpl; | ||||||
|  | @useResult | ||||||
|  | $Res call({ | ||||||
|  |  String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | /// @nodoc | ||||||
|  | class _$SnAuthDeviceWithChallengeCopyWithImpl<$Res> | ||||||
|  |     implements $SnAuthDeviceWithChallengeCopyWith<$Res> { | ||||||
|  |   _$SnAuthDeviceWithChallengeCopyWithImpl(this._self, this._then); | ||||||
|  | 
 | ||||||
|  |   final SnAuthDeviceWithChallenge _self; | ||||||
|  |   final $Res Function(SnAuthDeviceWithChallenge) _then; | ||||||
|  | 
 | ||||||
|  | /// Create a copy of SnAuthDeviceWithChallenge | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? challenges = null,Object? isCurrent = null,}) { | ||||||
|  |   return _then(_self.copyWith( | ||||||
|  | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable | ||||||
|  | as int,challenges: null == challenges ? _self.challenges : challenges // ignore: cast_nullable_to_non_nullable | ||||||
|  | as List<SnAuthChallenge>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable | ||||||
|  | as bool, | ||||||
|  |   )); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /// Adds pattern-matching-related methods to [SnAuthDeviceWithChallenge]. | ||||||
|  | extension SnAuthDeviceWithChallengePatterns on SnAuthDeviceWithChallenge { | ||||||
|  | /// 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( _SnAuthDeviceWithChallengee value)?  $default,{required TResult orElse(),}){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnAuthDeviceWithChallengee() 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( _SnAuthDeviceWithChallengee value)  $default,){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnAuthDeviceWithChallengee(): | ||||||
|  | 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( _SnAuthDeviceWithChallengee value)?  $default,){ | ||||||
|  | final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnAuthDeviceWithChallengee() 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 deviceId,  String deviceName,  String? deviceLabel,  String accountId,  int platform,  List<SnAuthChallenge> challenges,  bool isCurrent)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnAuthDeviceWithChallengee() when $default != null: | ||||||
|  | return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.challenges,_that.isCurrent);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 deviceId,  String deviceName,  String? deviceLabel,  String accountId,  int platform,  List<SnAuthChallenge> challenges,  bool isCurrent)  $default,) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnAuthDeviceWithChallengee(): | ||||||
|  | return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.challenges,_that.isCurrent);} | ||||||
|  | } | ||||||
|  | /// 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 deviceId,  String deviceName,  String? deviceLabel,  String accountId,  int platform,  List<SnAuthChallenge> challenges,  bool isCurrent)?  $default,) {final _that = this; | ||||||
|  | switch (_that) { | ||||||
|  | case _SnAuthDeviceWithChallengee() when $default != null: | ||||||
|  | return $default(_that.id,_that.deviceId,_that.deviceName,_that.deviceLabel,_that.accountId,_that.platform,_that.challenges,_that.isCurrent);case _: | ||||||
|  |   return null; | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// @nodoc | ||||||
|  | @JsonSerializable() | ||||||
|  | 
 | ||||||
|  | class _SnAuthDeviceWithChallengee implements SnAuthDeviceWithChallenge { | ||||||
|  |   const _SnAuthDeviceWithChallengee({required this.id, required this.deviceId, required this.deviceName, required this.deviceLabel, required this.accountId, required this.platform, required final  List<SnAuthChallenge> challenges, this.isCurrent = false}): _challenges = challenges; | ||||||
|  |   factory _SnAuthDeviceWithChallengee.fromJson(Map<String, dynamic> json) => _$SnAuthDeviceWithChallengeeFromJson(json); | ||||||
|  | 
 | ||||||
|  | @override final  String id; | ||||||
|  | @override final  String deviceId; | ||||||
|  | @override final  String deviceName; | ||||||
|  | @override final  String? deviceLabel; | ||||||
|  | @override final  String accountId; | ||||||
|  | @override final  int platform; | ||||||
|  |  final  List<SnAuthChallenge> _challenges; | ||||||
|  | @override List<SnAuthChallenge> get challenges { | ||||||
|  |   if (_challenges is EqualUnmodifiableListView) return _challenges; | ||||||
|  |   // ignore: implicit_dynamic_type | ||||||
|  |   return EqualUnmodifiableListView(_challenges); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @override@JsonKey() final  bool isCurrent; | ||||||
|  | 
 | ||||||
|  | /// Create a copy of SnAuthDeviceWithChallenge | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
|  | _$SnAuthDeviceWithChallengeeCopyWith<_SnAuthDeviceWithChallengee> get copyWith => __$SnAuthDeviceWithChallengeeCopyWithImpl<_SnAuthDeviceWithChallengee>(this, _$identity); | ||||||
|  | 
 | ||||||
|  | @override | ||||||
|  | Map<String, dynamic> toJson() { | ||||||
|  |   return _$SnAuthDeviceWithChallengeeToJson(this, ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @override | ||||||
|  | bool operator ==(Object other) { | ||||||
|  |   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthDeviceWithChallengee&&(identical(other.id, id) || other.id == id)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)&&(identical(other.deviceLabel, deviceLabel) || other.deviceLabel == deviceLabel)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other._challenges, _challenges)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
|  | @override | ||||||
|  | int get hashCode => Object.hash(runtimeType,id,deviceId,deviceName,deviceLabel,accountId,platform,const DeepCollectionEquality().hash(_challenges),isCurrent); | ||||||
|  | 
 | ||||||
|  | @override | ||||||
|  | String toString() { | ||||||
|  |   return 'SnAuthDeviceWithChallenge(id: $id, deviceId: $deviceId, deviceName: $deviceName, deviceLabel: $deviceLabel, accountId: $accountId, platform: $platform, challenges: $challenges, isCurrent: $isCurrent)'; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// @nodoc | ||||||
|  | abstract mixin class _$SnAuthDeviceWithChallengeeCopyWith<$Res> implements $SnAuthDeviceWithChallengeCopyWith<$Res> { | ||||||
|  |   factory _$SnAuthDeviceWithChallengeeCopyWith(_SnAuthDeviceWithChallengee value, $Res Function(_SnAuthDeviceWithChallengee) _then) = __$SnAuthDeviceWithChallengeeCopyWithImpl; | ||||||
|  | @override @useResult | ||||||
|  | $Res call({ | ||||||
|  |  String id, String deviceId, String deviceName, String? deviceLabel, String accountId, int platform, List<SnAuthChallenge> challenges, bool isCurrent | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | /// @nodoc | ||||||
|  | class __$SnAuthDeviceWithChallengeeCopyWithImpl<$Res> | ||||||
|  |     implements _$SnAuthDeviceWithChallengeeCopyWith<$Res> { | ||||||
|  |   __$SnAuthDeviceWithChallengeeCopyWithImpl(this._self, this._then); | ||||||
|  | 
 | ||||||
|  |   final _SnAuthDeviceWithChallengee _self; | ||||||
|  |   final $Res Function(_SnAuthDeviceWithChallengee) _then; | ||||||
|  | 
 | ||||||
|  | /// Create a copy of SnAuthDeviceWithChallenge | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? deviceId = null,Object? deviceName = null,Object? deviceLabel = freezed,Object? accountId = null,Object? platform = null,Object? challenges = null,Object? isCurrent = null,}) { | ||||||
|  |   return _then(_SnAuthDeviceWithChallengee( | ||||||
|  | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,deviceName: null == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,deviceLabel: freezed == deviceLabel ? _self.deviceLabel : deviceLabel // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable | ||||||
|  | as int,challenges: null == challenges ? _self._challenges : challenges // ignore: cast_nullable_to_non_nullable | ||||||
|  | as List<SnAuthChallenge>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable | ||||||
|  | as bool, | ||||||
|  |   )); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // dart format on | // dart format on | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
| 
 | 
 | ||||||
| part of 'user.dart'; | part of 'account.dart'; | ||||||
| 
 | 
 | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
| // JsonSerializableGenerator | // JsonSerializableGenerator | ||||||
| @@ -297,3 +297,54 @@ Map<String, dynamic> _$SnVerificationMarkToJson(_SnVerificationMark instance) => | |||||||
|       'description': instance.description, |       'description': instance.description, | ||||||
|       'verified_by': instance.verifiedBy, |       'verified_by': instance.verifiedBy, | ||||||
|     }; |     }; | ||||||
|  | 
 | ||||||
|  | _SnAuthDevice _$SnAuthDeviceFromJson(Map<String, dynamic> json) => | ||||||
|  |     _SnAuthDevice( | ||||||
|  |       id: json['id'] as String, | ||||||
|  |       deviceId: json['device_id'] as String, | ||||||
|  |       deviceName: json['device_name'] as String, | ||||||
|  |       deviceLabel: json['device_label'] as String?, | ||||||
|  |       accountId: json['account_id'] as String, | ||||||
|  |       platform: (json['platform'] as num).toInt(), | ||||||
|  |       isCurrent: json['is_current'] as bool? ?? false, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  | Map<String, dynamic> _$SnAuthDeviceToJson(_SnAuthDevice instance) => | ||||||
|  |     <String, dynamic>{ | ||||||
|  |       'id': instance.id, | ||||||
|  |       'device_id': instance.deviceId, | ||||||
|  |       'device_name': instance.deviceName, | ||||||
|  |       'device_label': instance.deviceLabel, | ||||||
|  |       'account_id': instance.accountId, | ||||||
|  |       'platform': instance.platform, | ||||||
|  |       'is_current': instance.isCurrent, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  | _SnAuthDeviceWithChallengee _$SnAuthDeviceWithChallengeeFromJson( | ||||||
|  |   Map<String, dynamic> json, | ||||||
|  | ) => _SnAuthDeviceWithChallengee( | ||||||
|  |   id: json['id'] as String, | ||||||
|  |   deviceId: json['device_id'] as String, | ||||||
|  |   deviceName: json['device_name'] as String, | ||||||
|  |   deviceLabel: json['device_label'] as String?, | ||||||
|  |   accountId: json['account_id'] as String, | ||||||
|  |   platform: (json['platform'] as num).toInt(), | ||||||
|  |   challenges: | ||||||
|  |       (json['challenges'] as List<dynamic>) | ||||||
|  |           .map((e) => SnAuthChallenge.fromJson(e as Map<String, dynamic>)) | ||||||
|  |           .toList(), | ||||||
|  |   isCurrent: json['is_current'] as bool? ?? false, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | Map<String, dynamic> _$SnAuthDeviceWithChallengeeToJson( | ||||||
|  |   _SnAuthDeviceWithChallengee instance, | ||||||
|  | ) => <String, dynamic>{ | ||||||
|  |   'id': instance.id, | ||||||
|  |   'device_id': instance.deviceId, | ||||||
|  |   'device_name': instance.deviceName, | ||||||
|  |   'device_label': instance.deviceLabel, | ||||||
|  |   'account_id': instance.accountId, | ||||||
|  |   'platform': instance.platform, | ||||||
|  |   'challenges': instance.challenges.map((e) => e.toJson()).toList(), | ||||||
|  |   'is_current': instance.isCurrent, | ||||||
|  | }; | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | import 'package:freezed_annotation/freezed_annotation.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
|  |  | ||||||
| part 'activity.freezed.dart'; | part 'activity.freezed.dart'; | ||||||
| part 'activity.g.dart'; | part 'activity.g.dart'; | ||||||
|   | |||||||
| @@ -19,14 +19,12 @@ sealed class SnAuthChallenge with _$SnAuthChallenge { | |||||||
|     required int stepRemain, |     required int stepRemain, | ||||||
|     required int stepTotal, |     required int stepTotal, | ||||||
|     required int failedAttempts, |     required int failedAttempts, | ||||||
|     required int platform, |  | ||||||
|     required int type, |     required int type, | ||||||
|     required List<String> blacklistFactors, |     required List<String> blacklistFactors, | ||||||
|     required List<dynamic> audiences, |     required List<dynamic> audiences, | ||||||
|     required List<dynamic> scopes, |     required List<dynamic> scopes, | ||||||
|     required String ipAddress, |     required String ipAddress, | ||||||
|     required String userAgent, |     required String userAgent, | ||||||
|     required String deviceId, |  | ||||||
|     required String? nonce, |     required String? nonce, | ||||||
|     required String? location, |     required String? location, | ||||||
|     required String accountId, |     required String accountId, | ||||||
| @@ -76,22 +74,6 @@ sealed class SnAuthFactor with _$SnAuthFactor { | |||||||
|       _$SnAuthFactorFromJson(json); |       _$SnAuthFactorFromJson(json); | ||||||
| } | } | ||||||
|  |  | ||||||
| @freezed |  | ||||||
| sealed class SnAuthDevice with _$SnAuthDevice { |  | ||||||
|   const factory SnAuthDevice({ |  | ||||||
|     required dynamic label, |  | ||||||
|     required String userAgent, |  | ||||||
|     required String deviceId, |  | ||||||
|     required int platform, |  | ||||||
|     required List<SnAuthSession> sessions, |  | ||||||
|     // Not from backend, used for UI |  | ||||||
|     @Default(false) bool isCurrent, |  | ||||||
|   }) = _SnAuthDevice; |  | ||||||
|  |  | ||||||
|   factory SnAuthDevice.fromJson(Map<String, dynamic> json) => |  | ||||||
|       _$SnAuthDeviceFromJson(json); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @freezed | @freezed | ||||||
| sealed class SnAccountConnection with _$SnAccountConnection { | sealed class SnAccountConnection with _$SnAccountConnection { | ||||||
|   const factory SnAccountConnection({ |   const factory SnAccountConnection({ | ||||||
|   | |||||||
| @@ -272,7 +272,7 @@ as String, | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnAuthChallenge { | mixin _$SnAuthChallenge { | ||||||
|  |  | ||||||
|  String get id; DateTime get expiredAt; int get stepRemain; int get stepTotal; int get failedAttempts; int get platform; int get type; List<String> get blacklistFactors; List<dynamic> get audiences; List<dynamic> get scopes; String get ipAddress; String get userAgent; String get deviceId; 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; String? get location; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||||
| /// Create a copy of SnAuthChallenge | /// Create a copy of SnAuthChallenge | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @@ -285,16 +285,16 @@ $SnAuthChallengeCopyWith<SnAuthChallenge> get copyWith => _$SnAuthChallengeCopyW | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.blacklistFactors, blacklistFactors)&&const DeepCollectionEquality().equals(other.audiences, audiences)&&const DeepCollectionEquality().equals(other.scopes, scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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 SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.blacklistFactors, blacklistFactors)&&const DeepCollectionEquality().equals(other.audiences, audiences)&&const DeepCollectionEquality().equals(other.scopes, scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hashAll([runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,platform,type,const DeepCollectionEquality().hash(blacklistFactors),const DeepCollectionEquality().hash(audiences),const DeepCollectionEquality().hash(scopes),ipAddress,userAgent,deviceId,nonce,location,accountId,createdAt,updatedAt,deletedAt]); | int get hashCode => Object.hash(runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,type,const DeepCollectionEquality().hash(blacklistFactors),const DeepCollectionEquality().hash(audiences),const DeepCollectionEquality().hash(scopes),ipAddress,userAgent,nonce,location,accountId,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, platform: $platform, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, deviceId: $deviceId, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -305,7 +305,7 @@ abstract mixin class $SnAuthChallengeCopyWith<$Res>  { | |||||||
|   factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl; |   factory $SnAuthChallengeCopyWith(SnAuthChallenge value, $Res Function(SnAuthChallenge) _then) = _$SnAuthChallengeCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int platform, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String deviceId, 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, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -322,21 +322,19 @@ class _$SnAuthChallengeCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnAuthChallenge | /// Create a copy of SnAuthChallenge | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? platform = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? deviceId = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable | as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable | as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable | ||||||
| as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable | as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable | ||||||
| as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable | as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable | ||||||
| as int,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable |  | ||||||
| as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||||
| as int,blacklistFactors: null == blacklistFactors ? _self.blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable | as int,blacklistFactors: null == blacklistFactors ? _self.blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable | ||||||
| as List<String>,audiences: null == audiences ? _self.audiences : audiences // ignore: cast_nullable_to_non_nullable | as List<String>,audiences: null == audiences ? _self.audiences : audiences // ignore: cast_nullable_to_non_nullable | ||||||
| as List<dynamic>,scopes: null == scopes ? _self.scopes : scopes // ignore: cast_nullable_to_non_nullable | as List<dynamic>,scopes: null == scopes ? _self.scopes : scopes // ignore: cast_nullable_to_non_nullable | ||||||
| as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable | as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable | ||||||
| as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable | as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable | ||||||
| as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable |  | ||||||
| as String,nonce: freezed == nonce ? _self.nonce : nonce // 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?,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 String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -425,10 +423,10 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  DateTime expiredAt,  int stepRemain,  int stepTotal,  int failedAttempts,  int platform,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String deviceId,  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,  String? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAuthChallenge() when $default != null: | case _SnAuthChallenge() when $default != null: | ||||||
| return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.platform,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.deviceId,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | 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 _: | ||||||
|   return orElse(); |   return orElse(); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -446,10 +444,10 @@ 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 platform,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String deviceId,  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,  String? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAuthChallenge(): | case _SnAuthChallenge(): | ||||||
| return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.platform,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.deviceId,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);} | 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);} | ||||||
| } | } | ||||||
| /// A variant of `when` that fallback to returning `null` | /// A variant of `when` that fallback to returning `null` | ||||||
| /// | /// | ||||||
| @@ -463,10 +461,10 @@ 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 platform,  int type,  List<String> blacklistFactors,  List<dynamic> audiences,  List<dynamic> scopes,  String ipAddress,  String userAgent,  String deviceId,  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,  String? location,  String accountId,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnAuthChallenge() when $default != null: | case _SnAuthChallenge() when $default != null: | ||||||
| return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that.failedAttempts,_that.platform,_that.type,_that.blacklistFactors,_that.audiences,_that.scopes,_that.ipAddress,_that.userAgent,_that.deviceId,_that.nonce,_that.location,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | 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 _: | ||||||
|   return null; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -478,7 +476,7 @@ return $default(_that.id,_that.expiredAt,_that.stepRemain,_that.stepTotal,_that. | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnAuthChallenge implements SnAuthChallenge { | class _SnAuthChallenge implements SnAuthChallenge { | ||||||
|   const _SnAuthChallenge({required this.id, required this.expiredAt, required this.stepRemain, required this.stepTotal, required this.failedAttempts, required this.platform, required this.type, required final  List<String> blacklistFactors, required final  List<dynamic> audiences, required final  List<dynamic> scopes, required this.ipAddress, required this.userAgent, required this.deviceId, required this.nonce, required this.location, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _blacklistFactors = blacklistFactors,_audiences = audiences,_scopes = scopes; |   const _SnAuthChallenge({required this.id, required this.expiredAt, required this.stepRemain, required this.stepTotal, required this.failedAttempts, required this.type, required final  List<String> blacklistFactors, required final  List<dynamic> audiences, required final  List<dynamic> scopes, required this.ipAddress, required this.userAgent, required this.nonce, required this.location, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt}): _blacklistFactors = blacklistFactors,_audiences = audiences,_scopes = scopes; | ||||||
|   factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json); |   factory _SnAuthChallenge.fromJson(Map<String, dynamic> json) => _$SnAuthChallengeFromJson(json); | ||||||
|  |  | ||||||
| @override final  String id; | @override final  String id; | ||||||
| @@ -486,7 +484,6 @@ class _SnAuthChallenge implements SnAuthChallenge { | |||||||
| @override final  int stepRemain; | @override final  int stepRemain; | ||||||
| @override final  int stepTotal; | @override final  int stepTotal; | ||||||
| @override final  int failedAttempts; | @override final  int failedAttempts; | ||||||
| @override final  int platform; |  | ||||||
| @override final  int type; | @override final  int type; | ||||||
|  final  List<String> _blacklistFactors; |  final  List<String> _blacklistFactors; | ||||||
| @override List<String> get blacklistFactors { | @override List<String> get blacklistFactors { | ||||||
| @@ -511,7 +508,6 @@ class _SnAuthChallenge implements SnAuthChallenge { | |||||||
|  |  | ||||||
| @override final  String ipAddress; | @override final  String ipAddress; | ||||||
| @override final  String userAgent; | @override final  String userAgent; | ||||||
| @override final  String deviceId; |  | ||||||
| @override final  String? nonce; | @override final  String? nonce; | ||||||
| @override final  String? location; | @override final  String? location; | ||||||
| @override final  String accountId; | @override final  String accountId; | ||||||
| @@ -532,16 +528,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | bool operator ==(Object other) { | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.platform, platform) || other.platform == platform)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._blacklistFactors, _blacklistFactors)&&const DeepCollectionEquality().equals(other._audiences, _audiences)&&const DeepCollectionEquality().equals(other._scopes, _scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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 _SnAuthChallenge&&(identical(other.id, id) || other.id == id)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.stepRemain, stepRemain) || other.stepRemain == stepRemain)&&(identical(other.stepTotal, stepTotal) || other.stepTotal == stepTotal)&&(identical(other.failedAttempts, failedAttempts) || other.failedAttempts == failedAttempts)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._blacklistFactors, _blacklistFactors)&&const DeepCollectionEquality().equals(other._audiences, _audiences)&&const DeepCollectionEquality().equals(other._scopes, _scopes)&&(identical(other.ipAddress, ipAddress) || other.ipAddress == ipAddress)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.nonce, nonce) || other.nonce == nonce)&&(identical(other.location, location) || other.location == location)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hashAll([runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,platform,type,const DeepCollectionEquality().hash(_blacklistFactors),const DeepCollectionEquality().hash(_audiences),const DeepCollectionEquality().hash(_scopes),ipAddress,userAgent,deviceId,nonce,location,accountId,createdAt,updatedAt,deletedAt]); | int get hashCode => Object.hash(runtimeType,id,expiredAt,stepRemain,stepTotal,failedAttempts,type,const DeepCollectionEquality().hash(_blacklistFactors),const DeepCollectionEquality().hash(_audiences),const DeepCollectionEquality().hash(_scopes),ipAddress,userAgent,nonce,location,accountId,createdAt,updatedAt,deletedAt); | ||||||
|  |  | ||||||
| @override | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, platform: $platform, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, deviceId: $deviceId, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; |   return 'SnAuthChallenge(id: $id, expiredAt: $expiredAt, stepRemain: $stepRemain, stepTotal: $stepTotal, failedAttempts: $failedAttempts, type: $type, blacklistFactors: $blacklistFactors, audiences: $audiences, scopes: $scopes, ipAddress: $ipAddress, userAgent: $userAgent, nonce: $nonce, location: $location, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -552,7 +548,7 @@ abstract mixin class _$SnAuthChallengeCopyWith<$Res> implements $SnAuthChallenge | |||||||
|   factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl; |   factory _$SnAuthChallengeCopyWith(_SnAuthChallenge value, $Res Function(_SnAuthChallenge) _then) = __$SnAuthChallengeCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, DateTime expiredAt, int stepRemain, int stepTotal, int failedAttempts, int platform, int type, List<String> blacklistFactors, List<dynamic> audiences, List<dynamic> scopes, String ipAddress, String userAgent, String deviceId, 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, String? location, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -569,21 +565,19 @@ class __$SnAuthChallengeCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnAuthChallenge | /// Create a copy of SnAuthChallenge | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? platform = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? deviceId = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? expiredAt = null,Object? stepRemain = null,Object? stepTotal = null,Object? failedAttempts = null,Object? type = null,Object? blacklistFactors = null,Object? audiences = null,Object? scopes = null,Object? ipAddress = null,Object? userAgent = null,Object? nonce = freezed,Object? location = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) { | ||||||
|   return _then(_SnAuthChallenge( |   return _then(_SnAuthChallenge( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||||
| as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable | as String,expiredAt: null == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable | as DateTime,stepRemain: null == stepRemain ? _self.stepRemain : stepRemain // ignore: cast_nullable_to_non_nullable | ||||||
| as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable | as int,stepTotal: null == stepTotal ? _self.stepTotal : stepTotal // ignore: cast_nullable_to_non_nullable | ||||||
| as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable | as int,failedAttempts: null == failedAttempts ? _self.failedAttempts : failedAttempts // ignore: cast_nullable_to_non_nullable | ||||||
| as int,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable |  | ||||||
| as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||||
| as int,blacklistFactors: null == blacklistFactors ? _self._blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable | as int,blacklistFactors: null == blacklistFactors ? _self._blacklistFactors : blacklistFactors // ignore: cast_nullable_to_non_nullable | ||||||
| as List<String>,audiences: null == audiences ? _self._audiences : audiences // ignore: cast_nullable_to_non_nullable | as List<String>,audiences: null == audiences ? _self._audiences : audiences // ignore: cast_nullable_to_non_nullable | ||||||
| as List<dynamic>,scopes: null == scopes ? _self._scopes : scopes // ignore: cast_nullable_to_non_nullable | as List<dynamic>,scopes: null == scopes ? _self._scopes : scopes // ignore: cast_nullable_to_non_nullable | ||||||
| as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable | as List<dynamic>,ipAddress: null == ipAddress ? _self.ipAddress : ipAddress // ignore: cast_nullable_to_non_nullable | ||||||
| as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable | as String,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable | ||||||
| as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable |  | ||||||
| as String,nonce: freezed == nonce ? _self.nonce : nonce // 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?,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 String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -1189,286 +1183,6 @@ as Map<String, dynamic>?, | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| /// @nodoc |  | ||||||
| mixin _$SnAuthDevice { |  | ||||||
|  |  | ||||||
|  dynamic get label; String get userAgent; String get deviceId; int get platform; List<SnAuthSession> get sessions;// Not from backend, used for UI |  | ||||||
|  bool get isCurrent; |  | ||||||
| /// Create a copy of SnAuthDevice |  | ||||||
| /// with the given fields replaced by the non-null parameter values. |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) |  | ||||||
| @pragma('vm:prefer-inline') |  | ||||||
| $SnAuthDeviceCopyWith<SnAuthDevice> get copyWith => _$SnAuthDeviceCopyWithImpl<SnAuthDevice>(this as SnAuthDevice, _$identity); |  | ||||||
|  |  | ||||||
|   /// Serializes this SnAuthDevice to a JSON map. |  | ||||||
|   Map<String, dynamic> toJson(); |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @override |  | ||||||
| bool operator ==(Object other) { |  | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnAuthDevice&&const DeepCollectionEquality().equals(other.label, label)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other.sessions, sessions)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) |  | ||||||
| @override |  | ||||||
| int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(label),userAgent,deviceId,platform,const DeepCollectionEquality().hash(sessions),isCurrent); |  | ||||||
|  |  | ||||||
| @override |  | ||||||
| String toString() { |  | ||||||
|   return 'SnAuthDevice(label: $label, userAgent: $userAgent, deviceId: $deviceId, platform: $platform, sessions: $sessions, isCurrent: $isCurrent)'; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// @nodoc |  | ||||||
| abstract mixin class $SnAuthDeviceCopyWith<$Res>  { |  | ||||||
|   factory $SnAuthDeviceCopyWith(SnAuthDevice value, $Res Function(SnAuthDevice) _then) = _$SnAuthDeviceCopyWithImpl; |  | ||||||
| @useResult |  | ||||||
| $Res call({ |  | ||||||
|  dynamic label, String userAgent, String deviceId, int platform, List<SnAuthSession> sessions, bool isCurrent |  | ||||||
| }); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| } |  | ||||||
| /// @nodoc |  | ||||||
| class _$SnAuthDeviceCopyWithImpl<$Res> |  | ||||||
|     implements $SnAuthDeviceCopyWith<$Res> { |  | ||||||
|   _$SnAuthDeviceCopyWithImpl(this._self, this._then); |  | ||||||
|  |  | ||||||
|   final SnAuthDevice _self; |  | ||||||
|   final $Res Function(SnAuthDevice) _then; |  | ||||||
|  |  | ||||||
| /// Create a copy of SnAuthDevice |  | ||||||
| /// with the given fields replaced by the non-null parameter values. |  | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? label = freezed,Object? userAgent = null,Object? deviceId = null,Object? platform = null,Object? sessions = null,Object? isCurrent = null,}) { |  | ||||||
|   return _then(_self.copyWith( |  | ||||||
| label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable |  | ||||||
| as dynamic,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable |  | ||||||
| as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable |  | ||||||
| as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable |  | ||||||
| as int,sessions: null == sessions ? _self.sessions : sessions // ignore: cast_nullable_to_non_nullable |  | ||||||
| as List<SnAuthSession>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable |  | ||||||
| as bool, |  | ||||||
|   )); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /// Adds pattern-matching-related methods to [SnAuthDevice]. |  | ||||||
| extension SnAuthDevicePatterns on SnAuthDevice { |  | ||||||
| /// 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( _SnAuthDevice value)?  $default,{required TResult orElse(),}){ |  | ||||||
| final _that = this; |  | ||||||
| switch (_that) { |  | ||||||
| case _SnAuthDevice() 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( _SnAuthDevice value)  $default,){ |  | ||||||
| final _that = this; |  | ||||||
| switch (_that) { |  | ||||||
| case _SnAuthDevice(): |  | ||||||
| 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( _SnAuthDevice value)?  $default,){ |  | ||||||
| final _that = this; |  | ||||||
| switch (_that) { |  | ||||||
| case _SnAuthDevice() 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( dynamic label,  String userAgent,  String deviceId,  int platform,  List<SnAuthSession> sessions,  bool isCurrent)?  $default,{required TResult orElse(),}) {final _that = this; |  | ||||||
| switch (_that) { |  | ||||||
| case _SnAuthDevice() when $default != null: |  | ||||||
| return $default(_that.label,_that.userAgent,_that.deviceId,_that.platform,_that.sessions,_that.isCurrent);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( dynamic label,  String userAgent,  String deviceId,  int platform,  List<SnAuthSession> sessions,  bool isCurrent)  $default,) {final _that = this; |  | ||||||
| switch (_that) { |  | ||||||
| case _SnAuthDevice(): |  | ||||||
| return $default(_that.label,_that.userAgent,_that.deviceId,_that.platform,_that.sessions,_that.isCurrent);} |  | ||||||
| } |  | ||||||
| /// 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( dynamic label,  String userAgent,  String deviceId,  int platform,  List<SnAuthSession> sessions,  bool isCurrent)?  $default,) {final _that = this; |  | ||||||
| switch (_that) { |  | ||||||
| case _SnAuthDevice() when $default != null: |  | ||||||
| return $default(_that.label,_that.userAgent,_that.deviceId,_that.platform,_that.sessions,_that.isCurrent);case _: |  | ||||||
|   return null; |  | ||||||
|  |  | ||||||
| } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// @nodoc |  | ||||||
| @JsonSerializable() |  | ||||||
|  |  | ||||||
| class _SnAuthDevice implements SnAuthDevice { |  | ||||||
|   const _SnAuthDevice({required this.label, required this.userAgent, required this.deviceId, required this.platform, required final  List<SnAuthSession> sessions, this.isCurrent = false}): _sessions = sessions; |  | ||||||
|   factory _SnAuthDevice.fromJson(Map<String, dynamic> json) => _$SnAuthDeviceFromJson(json); |  | ||||||
|  |  | ||||||
| @override final  dynamic label; |  | ||||||
| @override final  String userAgent; |  | ||||||
| @override final  String deviceId; |  | ||||||
| @override final  int platform; |  | ||||||
|  final  List<SnAuthSession> _sessions; |  | ||||||
| @override List<SnAuthSession> get sessions { |  | ||||||
|   if (_sessions is EqualUnmodifiableListView) return _sessions; |  | ||||||
|   // ignore: implicit_dynamic_type |  | ||||||
|   return EqualUnmodifiableListView(_sessions); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Not from backend, used for UI |  | ||||||
| @override@JsonKey() final  bool isCurrent; |  | ||||||
|  |  | ||||||
| /// Create a copy of SnAuthDevice |  | ||||||
| /// with the given fields replaced by the non-null parameter values. |  | ||||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) |  | ||||||
| @pragma('vm:prefer-inline') |  | ||||||
| _$SnAuthDeviceCopyWith<_SnAuthDevice> get copyWith => __$SnAuthDeviceCopyWithImpl<_SnAuthDevice>(this, _$identity); |  | ||||||
|  |  | ||||||
| @override |  | ||||||
| Map<String, dynamic> toJson() { |  | ||||||
|   return _$SnAuthDeviceToJson(this, ); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @override |  | ||||||
| bool operator ==(Object other) { |  | ||||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnAuthDevice&&const DeepCollectionEquality().equals(other.label, label)&&(identical(other.userAgent, userAgent) || other.userAgent == userAgent)&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.platform, platform) || other.platform == platform)&&const DeepCollectionEquality().equals(other._sessions, _sessions)&&(identical(other.isCurrent, isCurrent) || other.isCurrent == isCurrent)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) |  | ||||||
| @override |  | ||||||
| int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(label),userAgent,deviceId,platform,const DeepCollectionEquality().hash(_sessions),isCurrent); |  | ||||||
|  |  | ||||||
| @override |  | ||||||
| String toString() { |  | ||||||
|   return 'SnAuthDevice(label: $label, userAgent: $userAgent, deviceId: $deviceId, platform: $platform, sessions: $sessions, isCurrent: $isCurrent)'; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// @nodoc |  | ||||||
| abstract mixin class _$SnAuthDeviceCopyWith<$Res> implements $SnAuthDeviceCopyWith<$Res> { |  | ||||||
|   factory _$SnAuthDeviceCopyWith(_SnAuthDevice value, $Res Function(_SnAuthDevice) _then) = __$SnAuthDeviceCopyWithImpl; |  | ||||||
| @override @useResult |  | ||||||
| $Res call({ |  | ||||||
|  dynamic label, String userAgent, String deviceId, int platform, List<SnAuthSession> sessions, bool isCurrent |  | ||||||
| }); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| } |  | ||||||
| /// @nodoc |  | ||||||
| class __$SnAuthDeviceCopyWithImpl<$Res> |  | ||||||
|     implements _$SnAuthDeviceCopyWith<$Res> { |  | ||||||
|   __$SnAuthDeviceCopyWithImpl(this._self, this._then); |  | ||||||
|  |  | ||||||
|   final _SnAuthDevice _self; |  | ||||||
|   final $Res Function(_SnAuthDevice) _then; |  | ||||||
|  |  | ||||||
| /// Create a copy of SnAuthDevice |  | ||||||
| /// with the given fields replaced by the non-null parameter values. |  | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? label = freezed,Object? userAgent = null,Object? deviceId = null,Object? platform = null,Object? sessions = null,Object? isCurrent = null,}) { |  | ||||||
|   return _then(_SnAuthDevice( |  | ||||||
| label: freezed == label ? _self.label : label // ignore: cast_nullable_to_non_nullable |  | ||||||
| as dynamic,userAgent: null == userAgent ? _self.userAgent : userAgent // ignore: cast_nullable_to_non_nullable |  | ||||||
| as String,deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable |  | ||||||
| as String,platform: null == platform ? _self.platform : platform // ignore: cast_nullable_to_non_nullable |  | ||||||
| as int,sessions: null == sessions ? _self._sessions : sessions // ignore: cast_nullable_to_non_nullable |  | ||||||
| as List<SnAuthSession>,isCurrent: null == isCurrent ? _self.isCurrent : isCurrent // ignore: cast_nullable_to_non_nullable |  | ||||||
| as bool, |  | ||||||
|   )); |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnAccountConnection { | mixin _$SnAccountConnection { | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,7 +20,6 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) => | |||||||
|       stepRemain: (json['step_remain'] as num).toInt(), |       stepRemain: (json['step_remain'] as num).toInt(), | ||||||
|       stepTotal: (json['step_total'] as num).toInt(), |       stepTotal: (json['step_total'] as num).toInt(), | ||||||
|       failedAttempts: (json['failed_attempts'] as num).toInt(), |       failedAttempts: (json['failed_attempts'] as num).toInt(), | ||||||
|       platform: (json['platform'] as num).toInt(), |  | ||||||
|       type: (json['type'] as num).toInt(), |       type: (json['type'] as num).toInt(), | ||||||
|       blacklistFactors: |       blacklistFactors: | ||||||
|           (json['blacklist_factors'] as List<dynamic>) |           (json['blacklist_factors'] as List<dynamic>) | ||||||
| @@ -30,7 +29,6 @@ _SnAuthChallenge _$SnAuthChallengeFromJson(Map<String, dynamic> json) => | |||||||
|       scopes: json['scopes'] as List<dynamic>, |       scopes: json['scopes'] as List<dynamic>, | ||||||
|       ipAddress: json['ip_address'] as String, |       ipAddress: json['ip_address'] as String, | ||||||
|       userAgent: json['user_agent'] as String, |       userAgent: json['user_agent'] as String, | ||||||
|       deviceId: json['device_id'] as String, |  | ||||||
|       nonce: json['nonce'] as String?, |       nonce: json['nonce'] as String?, | ||||||
|       location: json['location'] as String?, |       location: json['location'] as String?, | ||||||
|       accountId: json['account_id'] as String, |       accountId: json['account_id'] as String, | ||||||
| @@ -49,14 +47,12 @@ Map<String, dynamic> _$SnAuthChallengeToJson(_SnAuthChallenge instance) => | |||||||
|       'step_remain': instance.stepRemain, |       'step_remain': instance.stepRemain, | ||||||
|       'step_total': instance.stepTotal, |       'step_total': instance.stepTotal, | ||||||
|       'failed_attempts': instance.failedAttempts, |       'failed_attempts': instance.failedAttempts, | ||||||
|       'platform': instance.platform, |  | ||||||
|       'type': instance.type, |       'type': instance.type, | ||||||
|       'blacklist_factors': instance.blacklistFactors, |       'blacklist_factors': instance.blacklistFactors, | ||||||
|       'audiences': instance.audiences, |       'audiences': instance.audiences, | ||||||
|       'scopes': instance.scopes, |       'scopes': instance.scopes, | ||||||
|       'ip_address': instance.ipAddress, |       'ip_address': instance.ipAddress, | ||||||
|       'user_agent': instance.userAgent, |       'user_agent': instance.userAgent, | ||||||
|       'device_id': instance.deviceId, |  | ||||||
|       'nonce': instance.nonce, |       'nonce': instance.nonce, | ||||||
|       'location': instance.location, |       'location': instance.location, | ||||||
|       'account_id': instance.accountId, |       'account_id': instance.accountId, | ||||||
| @@ -133,29 +129,6 @@ Map<String, dynamic> _$SnAuthFactorToJson(_SnAuthFactor instance) => | |||||||
|       'created_response': instance.createdResponse, |       'created_response': instance.createdResponse, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| _SnAuthDevice _$SnAuthDeviceFromJson(Map<String, dynamic> json) => |  | ||||||
|     _SnAuthDevice( |  | ||||||
|       label: json['label'], |  | ||||||
|       userAgent: json['user_agent'] as String, |  | ||||||
|       deviceId: json['device_id'] as String, |  | ||||||
|       platform: (json['platform'] as num).toInt(), |  | ||||||
|       sessions: |  | ||||||
|           (json['sessions'] as List<dynamic>) |  | ||||||
|               .map((e) => SnAuthSession.fromJson(e as Map<String, dynamic>)) |  | ||||||
|               .toList(), |  | ||||||
|       isCurrent: json['is_current'] as bool? ?? false, |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
| Map<String, dynamic> _$SnAuthDeviceToJson(_SnAuthDevice instance) => |  | ||||||
|     <String, dynamic>{ |  | ||||||
|       'label': instance.label, |  | ||||||
|       'user_agent': instance.userAgent, |  | ||||||
|       'device_id': instance.deviceId, |  | ||||||
|       'platform': instance.platform, |  | ||||||
|       'sessions': instance.sessions.map((e) => e.toJson()).toList(), |  | ||||||
|       'is_current': instance.isCurrent, |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
| _SnAccountConnection _$SnAccountConnectionFromJson(Map<String, dynamic> json) => | _SnAccountConnection _$SnAccountConnectionFromJson(Map<String, dynamic> json) => | ||||||
|     _SnAccountConnection( |     _SnAccountConnection( | ||||||
|       id: json['id'] as String, |       id: json['id'] as String, | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | import 'package:freezed_annotation/freezed_annotation.dart'; | ||||||
| import 'package:island/models/file.dart'; | import 'package:island/models/file.dart'; | ||||||
| import 'package:island/models/realm.dart'; | import 'package:island/models/realm.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
|  |  | ||||||
| part 'chat.freezed.dart'; | part 'chat.freezed.dart'; | ||||||
| part 'chat.g.dart'; | part 'chat.g.dart'; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | import 'package:freezed_annotation/freezed_annotation.dart'; | ||||||
| import 'package:island/models/file.dart'; | import 'package:island/models/file.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
|  |  | ||||||
| part 'custom_app.freezed.dart'; | part 'custom_app.freezed.dart'; | ||||||
| part 'custom_app.g.dart'; | part 'custom_app.g.dart'; | ||||||
|   | |||||||
| @@ -11,8 +11,8 @@ sealed class SnScrappedLink with _$SnScrappedLink { | |||||||
|     required String title, |     required String title, | ||||||
|     required String? description, |     required String? description, | ||||||
|     required String? imageUrl, |     required String? imageUrl, | ||||||
|     required String faviconUrl, |     required String? faviconUrl, | ||||||
|     required String siteName, |     required String? siteName, | ||||||
|     required String? contentType, |     required String? contentType, | ||||||
|     required String? author, |     required String? author, | ||||||
|     required DateTime? publishedDate, |     required DateTime? publishedDate, | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ T _$identity<T>(T value) => value; | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnScrappedLink { | mixin _$SnScrappedLink { | ||||||
|  |  | ||||||
|  String get type; String get url; String get title; String? get description; String? get imageUrl; String get faviconUrl; String get siteName; String? get contentType; String? get author; DateTime? get publishedDate; |  String get type; String get url; String get title; String? get description; String? get imageUrl; String? get faviconUrl; String? get siteName; String? get contentType; String? get author; DateTime? get publishedDate; | ||||||
| /// Create a copy of SnScrappedLink | /// Create a copy of SnScrappedLink | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @@ -48,7 +48,7 @@ abstract mixin class $SnScrappedLinkCopyWith<$Res>  { | |||||||
|   factory $SnScrappedLinkCopyWith(SnScrappedLink value, $Res Function(SnScrappedLink) _then) = _$SnScrappedLinkCopyWithImpl; |   factory $SnScrappedLinkCopyWith(SnScrappedLink value, $Res Function(SnScrappedLink) _then) = _$SnScrappedLinkCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String type, String url, String title, String? description, String? imageUrl, String faviconUrl, String siteName, String? contentType, String? author, DateTime? publishedDate |  String type, String url, String title, String? description, String? imageUrl, String? faviconUrl, String? siteName, String? contentType, String? author, DateTime? publishedDate | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -65,16 +65,16 @@ class _$SnScrappedLinkCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnScrappedLink | /// Create a copy of SnScrappedLink | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) { | @pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = freezed,Object? siteName = freezed,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) { | ||||||
|   return _then(_self.copyWith( |   return _then(_self.copyWith( | ||||||
| type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||||
| as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable | as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable | ||||||
| as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||||
| as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable | as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable | as String?,faviconUrl: freezed == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable | ||||||
| as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable | as String?,siteName: freezed == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable | ||||||
| as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable | as String?,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable | as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable | as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?, | as DateTime?, | ||||||
| @@ -159,7 +159,7 @@ return $default(_that);case _: | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type,  String url,  String title,  String? description,  String? imageUrl,  String faviconUrl,  String siteName,  String? contentType,  String? author,  DateTime? publishedDate)?  $default,{required TResult orElse(),}) {final _that = this; | @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type,  String url,  String title,  String? description,  String? imageUrl,  String? faviconUrl,  String? siteName,  String? contentType,  String? author,  DateTime? publishedDate)?  $default,{required TResult orElse(),}) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnScrappedLink() when $default != null: | case _SnScrappedLink() when $default != null: | ||||||
| return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _: | return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _: | ||||||
| @@ -180,7 +180,7 @@ return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUr | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type,  String url,  String title,  String? description,  String? imageUrl,  String faviconUrl,  String siteName,  String? contentType,  String? author,  DateTime? publishedDate)  $default,) {final _that = this; | @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type,  String url,  String title,  String? description,  String? imageUrl,  String? faviconUrl,  String? siteName,  String? contentType,  String? author,  DateTime? publishedDate)  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnScrappedLink(): | case _SnScrappedLink(): | ||||||
| return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);} | return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);} | ||||||
| @@ -197,7 +197,7 @@ return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUr | |||||||
| /// } | /// } | ||||||
| /// ``` | /// ``` | ||||||
|  |  | ||||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type,  String url,  String title,  String? description,  String? imageUrl,  String faviconUrl,  String siteName,  String? contentType,  String? author,  DateTime? publishedDate)?  $default,) {final _that = this; | @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type,  String url,  String title,  String? description,  String? imageUrl,  String? faviconUrl,  String? siteName,  String? contentType,  String? author,  DateTime? publishedDate)?  $default,) {final _that = this; | ||||||
| switch (_that) { | switch (_that) { | ||||||
| case _SnScrappedLink() when $default != null: | case _SnScrappedLink() when $default != null: | ||||||
| return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _: | return $default(_that.type,_that.url,_that.title,_that.description,_that.imageUrl,_that.faviconUrl,_that.siteName,_that.contentType,_that.author,_that.publishedDate);case _: | ||||||
| @@ -220,8 +220,8 @@ class _SnScrappedLink implements SnScrappedLink { | |||||||
| @override final  String title; | @override final  String title; | ||||||
| @override final  String? description; | @override final  String? description; | ||||||
| @override final  String? imageUrl; | @override final  String? imageUrl; | ||||||
| @override final  String faviconUrl; | @override final  String? faviconUrl; | ||||||
| @override final  String siteName; | @override final  String? siteName; | ||||||
| @override final  String? contentType; | @override final  String? contentType; | ||||||
| @override final  String? author; | @override final  String? author; | ||||||
| @override final  DateTime? publishedDate; | @override final  DateTime? publishedDate; | ||||||
| @@ -259,7 +259,7 @@ abstract mixin class _$SnScrappedLinkCopyWith<$Res> implements $SnScrappedLinkCo | |||||||
|   factory _$SnScrappedLinkCopyWith(_SnScrappedLink value, $Res Function(_SnScrappedLink) _then) = __$SnScrappedLinkCopyWithImpl; |   factory _$SnScrappedLinkCopyWith(_SnScrappedLink value, $Res Function(_SnScrappedLink) _then) = __$SnScrappedLinkCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String type, String url, String title, String? description, String? imageUrl, String faviconUrl, String siteName, String? contentType, String? author, DateTime? publishedDate |  String type, String url, String title, String? description, String? imageUrl, String? faviconUrl, String? siteName, String? contentType, String? author, DateTime? publishedDate | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -276,16 +276,16 @@ class __$SnScrappedLinkCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnScrappedLink | /// Create a copy of SnScrappedLink | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) { | @override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = freezed,Object? siteName = freezed,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) { | ||||||
|   return _then(_SnScrappedLink( |   return _then(_SnScrappedLink( | ||||||
| type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||||
| as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable | as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable | ||||||
| as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||||
| as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable | as String?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable | as String?,faviconUrl: freezed == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable | ||||||
| as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable | as String?,siteName: freezed == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable | ||||||
| as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable | as String?,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable | as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable | as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?, | as DateTime?, | ||||||
|   | |||||||
| @@ -13,8 +13,8 @@ _SnScrappedLink _$SnScrappedLinkFromJson(Map<String, dynamic> json) => | |||||||
|       title: json['title'] as String, |       title: json['title'] as String, | ||||||
|       description: json['description'] as String?, |       description: json['description'] as String?, | ||||||
|       imageUrl: json['image_url'] as String?, |       imageUrl: json['image_url'] as String?, | ||||||
|       faviconUrl: json['favicon_url'] as String, |       faviconUrl: json['favicon_url'] as String?, | ||||||
|       siteName: json['site_name'] as String, |       siteName: json['site_name'] as String?, | ||||||
|       contentType: json['content_type'] as String?, |       contentType: json['content_type'] as String?, | ||||||
|       author: json['author'] as String?, |       author: json['author'] as String?, | ||||||
|       publishedDate: |       publishedDate: | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ part 'poll.g.dart'; | |||||||
| sealed class SnPollWithStats with _$SnPollWithStats { | sealed class SnPollWithStats with _$SnPollWithStats { | ||||||
|   const factory SnPollWithStats({ |   const factory SnPollWithStats({ | ||||||
|     required Map<String, dynamic>? userAnswer, |     required Map<String, dynamic>? userAnswer, | ||||||
|     required Map<String, dynamic> stats, |     @Default({}) Map<String, dynamic> stats, | ||||||
|     required String id, |     required String id, | ||||||
|     required List<SnPollQuestion> questions, |     required List<SnPollQuestion> questions, | ||||||
|     String? title, |     String? title, | ||||||
|   | |||||||
| @@ -213,7 +213,7 @@ return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.titl | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnPollWithStats implements SnPollWithStats { | class _SnPollWithStats implements SnPollWithStats { | ||||||
|   const _SnPollWithStats({required final  Map<String, dynamic>? userAnswer, required final  Map<String, dynamic> stats, required this.id, required final  List<SnPollQuestion> questions, this.title, this.description, this.endedAt, required this.publisherId, required this.createdAt, required this.updatedAt, this.deletedAt}): _userAnswer = userAnswer,_stats = stats,_questions = questions; |   const _SnPollWithStats({required final  Map<String, dynamic>? userAnswer, final  Map<String, dynamic> stats = const {}, required this.id, required final  List<SnPollQuestion> questions, this.title, this.description, this.endedAt, required this.publisherId, required this.createdAt, required this.updatedAt, this.deletedAt}): _userAnswer = userAnswer,_stats = stats,_questions = questions; | ||||||
|   factory _SnPollWithStats.fromJson(Map<String, dynamic> json) => _$SnPollWithStatsFromJson(json); |   factory _SnPollWithStats.fromJson(Map<String, dynamic> json) => _$SnPollWithStatsFromJson(json); | ||||||
|  |  | ||||||
|  final  Map<String, dynamic>? _userAnswer; |  final  Map<String, dynamic>? _userAnswer; | ||||||
| @@ -226,7 +226,7 @@ class _SnPollWithStats implements SnPollWithStats { | |||||||
| } | } | ||||||
|  |  | ||||||
|  final  Map<String, dynamic> _stats; |  final  Map<String, dynamic> _stats; | ||||||
| @override Map<String, dynamic> get stats { | @override@JsonKey() Map<String, dynamic> get stats { | ||||||
|   if (_stats is EqualUnmodifiableMapView) return _stats; |   if (_stats is EqualUnmodifiableMapView) return _stats; | ||||||
|   // ignore: implicit_dynamic_type |   // ignore: implicit_dynamic_type | ||||||
|   return EqualUnmodifiableMapView(_stats); |   return EqualUnmodifiableMapView(_stats); | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ part of 'poll.dart'; | |||||||
| _SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) => | _SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) => | ||||||
|     _SnPollWithStats( |     _SnPollWithStats( | ||||||
|       userAnswer: json['user_answer'] as Map<String, dynamic>?, |       userAnswer: json['user_answer'] as Map<String, dynamic>?, | ||||||
|       stats: json['stats'] as Map<String, dynamic>, |       stats: json['stats'] as Map<String, dynamic>? ?? const {}, | ||||||
|       id: json['id'] as String, |       id: json['id'] as String, | ||||||
|       questions: |       questions: | ||||||
|           (json['questions'] as List<dynamic>) |           (json['questions'] as List<dynamic>) | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import 'package:island/models/file.dart'; | |||||||
| import 'package:island/models/post_category.dart'; | import 'package:island/models/post_category.dart'; | ||||||
| import 'package:island/models/post_tag.dart'; | import 'package:island/models/post_tag.dart'; | ||||||
| import 'package:island/models/publisher.dart'; | import 'package:island/models/publisher.dart'; | ||||||
|  | import 'package:island/models/realm.dart'; | ||||||
|  |  | ||||||
| part 'post.freezed.dart'; | part 'post.freezed.dart'; | ||||||
| part 'post.g.dart'; | part 'post.g.dart'; | ||||||
| @@ -18,6 +19,7 @@ sealed class SnPost with _$SnPost { | |||||||
|     @Default(null) DateTime? publishedAt, |     @Default(null) DateTime? publishedAt, | ||||||
|     @Default(0) int visibility, |     @Default(0) int visibility, | ||||||
|     String? content, |     String? content, | ||||||
|  |     String? slug, | ||||||
|     @Default(0) int type, |     @Default(0) int type, | ||||||
|     Map<String, dynamic>? meta, |     Map<String, dynamic>? meta, | ||||||
|     @Default(0) int viewsUnique, |     @Default(0) int viewsUnique, | ||||||
| @@ -31,6 +33,8 @@ sealed class SnPost with _$SnPost { | |||||||
|     SnPost? repliedPost, |     SnPost? repliedPost, | ||||||
|     String? forwardedPostId, |     String? forwardedPostId, | ||||||
|     SnPost? forwardedPost, |     SnPost? forwardedPost, | ||||||
|  |     String? realmId, | ||||||
|  |     SnRealm? realm, | ||||||
|     @Default([]) List<SnCloudFile> attachments, |     @Default([]) List<SnCloudFile> attachments, | ||||||
|     required SnPublisher publisher, |     required SnPublisher publisher, | ||||||
|     @Default({}) Map<String, int> reactionsCount, |     @Default({}) Map<String, int> reactionsCount, | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ T _$identity<T>(T value) => value; | |||||||
| /// @nodoc | /// @nodoc | ||||||
| mixin _$SnPost { | 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; 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; 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; 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 | /// Create a copy of SnPost | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @JsonKey(includeFromJson: false, includeToJson: false) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @@ -28,16 +28,16 @@ $SnPostCopyWith<SnPost> get copyWith => _$SnPostCopyWithImpl<SnPost>(this as SnP | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | 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.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)&&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.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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,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,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 | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, 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, 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, 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,11 +48,11 @@ abstract mixin class $SnPostCopyWith<$Res>  { | |||||||
|   factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl; |   factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl; | ||||||
| @useResult | @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, 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, 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, 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 | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| $SnPostCopyWith<$Res>? get threadedPost;$SnPostCopyWith<$Res>? get repliedPost;$SnPostCopyWith<$Res>? get forwardedPost;$SnPublisherCopyWith<$Res> get publisher; | $SnPostCopyWith<$Res>? get threadedPost;$SnPostCopyWith<$Res>? get repliedPost;$SnPostCopyWith<$Res>? get forwardedPost;$SnRealmCopyWith<$Res>? get realm;$SnPublisherCopyWith<$Res> get publisher; | ||||||
|  |  | ||||||
| } | } | ||||||
| /// @nodoc | /// @nodoc | ||||||
| @@ -65,7 +65,7 @@ class _$SnPostCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnPost | /// Create a copy of SnPost | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// 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? 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? 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? 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( |   return _then(_self.copyWith( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | 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 | as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -75,6 +75,7 @@ as String?,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: | |||||||
| as DateTime?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable | as DateTime?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?,visibility: null == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable | as DateTime?,visibility: null == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable | ||||||
| as int,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable | as int,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,slug: freezed == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||||
| as int,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable | as int,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, dynamic>?,viewsUnique: null == viewsUnique ? _self.viewsUnique : viewsUnique // ignore: cast_nullable_to_non_nullable | as Map<String, dynamic>?,viewsUnique: null == viewsUnique ? _self.viewsUnique : viewsUnique // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -88,7 +89,9 @@ as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repli | |||||||
| as String?,repliedPost: freezed == repliedPost ? _self.repliedPost : repliedPost // ignore: cast_nullable_to_non_nullable | as String?,repliedPost: freezed == repliedPost ? _self.repliedPost : repliedPost // ignore: cast_nullable_to_non_nullable | ||||||
| as SnPost?,forwardedPostId: freezed == forwardedPostId ? _self.forwardedPostId : forwardedPostId // ignore: cast_nullable_to_non_nullable | as SnPost?,forwardedPostId: freezed == forwardedPostId ? _self.forwardedPostId : forwardedPostId // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,forwardedPost: freezed == forwardedPost ? _self.forwardedPost : forwardedPost // ignore: cast_nullable_to_non_nullable | as String?,forwardedPost: freezed == forwardedPost ? _self.forwardedPost : forwardedPost // ignore: cast_nullable_to_non_nullable | ||||||
| as SnPost?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable | as SnPost?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_to_non_nullable | ||||||
|  | as SnRealm?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable | ||||||
| as List<SnCloudFile>,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable | as List<SnCloudFile>,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable | ||||||
| as SnPublisher,reactionsCount: null == reactionsCount ? _self.reactionsCount : reactionsCount // ignore: cast_nullable_to_non_nullable | as SnPublisher,reactionsCount: null == reactionsCount ? _self.reactionsCount : reactionsCount // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, int>,reactionsMade: null == reactionsMade ? _self.reactionsMade : reactionsMade // ignore: cast_nullable_to_non_nullable | as Map<String, int>,reactionsMade: null == reactionsMade ? _self.reactionsMade : reactionsMade // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -143,6 +146,18 @@ $SnPostCopyWith<$Res>? get forwardedPost { | |||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override | @override | ||||||
| @pragma('vm:prefer-inline') | @pragma('vm:prefer-inline') | ||||||
|  | $SnRealmCopyWith<$Res>? get realm { | ||||||
|  |     if (_self.realm == null) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return $SnRealmCopyWith<$Res>(_self.realm!, (value) { | ||||||
|  |     return _then(_self.copyWith(realm: value)); | ||||||
|  |   }); | ||||||
|  | }/// Create a copy of SnPost | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
| $SnPublisherCopyWith<$Res> get publisher { | $SnPublisherCopyWith<$Res> get publisher { | ||||||
|    |    | ||||||
|   return $SnPublisherCopyWith<$Res>(_self.publisher, (value) { |   return $SnPublisherCopyWith<$Res>(_self.publisher, (value) { | ||||||
| @@ -227,10 +242,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,  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,  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,  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) { | switch (_that) { | ||||||
| case _SnPost() when $default != null: | case _SnPost() when $default != null: | ||||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_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.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.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(); |   return orElse(); | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -248,10 +263,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,  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,  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,  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) { | switch (_that) { | ||||||
| case _SnPost(): | case _SnPost(): | ||||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_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.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.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` | /// A variant of `when` that fallback to returning `null` | ||||||
| /// | /// | ||||||
| @@ -265,10 +280,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,  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,  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,  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) { | switch (_that) { | ||||||
| case _SnPost() when $default != null: | case _SnPost() when $default != null: | ||||||
| return $default(_that.id,_that.title,_that.description,_that.language,_that.editedAt,_that.publishedAt,_that.visibility,_that.content,_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.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.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; |   return null; | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -280,7 +295,7 @@ return $default(_that.id,_that.title,_that.description,_that.language,_that.edit | |||||||
| @JsonSerializable() | @JsonSerializable() | ||||||
|  |  | ||||||
| class _SnPost implements SnPost { | 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.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, 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.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); |   factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json); | ||||||
|  |  | ||||||
| @override final  String id; | @override final  String id; | ||||||
| @@ -291,6 +306,7 @@ class _SnPost implements SnPost { | |||||||
| @override@JsonKey() final  DateTime? publishedAt; | @override@JsonKey() final  DateTime? publishedAt; | ||||||
| @override@JsonKey() final  int visibility; | @override@JsonKey() final  int visibility; | ||||||
| @override final  String? content; | @override final  String? content; | ||||||
|  | @override final  String? slug; | ||||||
| @override@JsonKey() final  int type; | @override@JsonKey() final  int type; | ||||||
|  final  Map<String, dynamic>? _meta; |  final  Map<String, dynamic>? _meta; | ||||||
| @override Map<String, dynamic>? get meta { | @override Map<String, dynamic>? get meta { | ||||||
| @@ -312,6 +328,8 @@ class _SnPost implements SnPost { | |||||||
| @override final  SnPost? repliedPost; | @override final  SnPost? repliedPost; | ||||||
| @override final  String? forwardedPostId; | @override final  String? forwardedPostId; | ||||||
| @override final  SnPost? forwardedPost; | @override final  SnPost? forwardedPost; | ||||||
|  | @override final  String? realmId; | ||||||
|  | @override final  SnRealm? realm; | ||||||
|  final  List<SnCloudFile> _attachments; |  final  List<SnCloudFile> _attachments; | ||||||
| @override@JsonKey() List<SnCloudFile> get attachments { | @override@JsonKey() List<SnCloudFile> get attachments { | ||||||
|   if (_attachments is EqualUnmodifiableListView) return _attachments; |   if (_attachments is EqualUnmodifiableListView) return _attachments; | ||||||
| @@ -380,16 +398,16 @@ Map<String, dynamic> toJson() { | |||||||
|  |  | ||||||
| @override | @override | ||||||
| bool operator ==(Object other) { | 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.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)&&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.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) | @JsonKey(includeFromJson: false, includeToJson: false) | ||||||
| @override | @override | ||||||
| int get hashCode => Object.hashAll([runtimeType,id,title,description,language,editedAt,publishedAt,visibility,content,type,const DeepCollectionEquality().hash(_meta),viewsUnique,viewsTotal,upvotes,downvotes,repliesCount,threadedPostId,threadedPost,repliedPostId,repliedPost,forwardedPostId,forwardedPost,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,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 | @override | ||||||
| String toString() { | String toString() { | ||||||
|   return 'SnPost(id: $id, title: $title, description: $description, language: $language, editedAt: $editedAt, publishedAt: $publishedAt, visibility: $visibility, content: $content, 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, 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, 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)'; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -400,11 +418,11 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> { | |||||||
|   factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl; |   factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl; | ||||||
| @override @useResult | @override @useResult | ||||||
| $Res call({ | $Res call({ | ||||||
|  String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, 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, 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, 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 | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  |  | ||||||
| @override $SnPostCopyWith<$Res>? get threadedPost;@override $SnPostCopyWith<$Res>? get repliedPost;@override $SnPostCopyWith<$Res>? get forwardedPost;@override $SnPublisherCopyWith<$Res> get publisher; | @override $SnPostCopyWith<$Res>? get threadedPost;@override $SnPostCopyWith<$Res>? get repliedPost;@override $SnPostCopyWith<$Res>? get forwardedPost;@override $SnRealmCopyWith<$Res>? get realm;@override $SnPublisherCopyWith<$Res> get publisher; | ||||||
|  |  | ||||||
| } | } | ||||||
| /// @nodoc | /// @nodoc | ||||||
| @@ -417,7 +435,7 @@ class __$SnPostCopyWithImpl<$Res> | |||||||
|  |  | ||||||
| /// Create a copy of SnPost | /// Create a copy of SnPost | ||||||
| /// with the given fields replaced by the non-null parameter values. | /// 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? 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? 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? 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( |   return _then(_SnPost( | ||||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | 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 | as String,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -427,6 +445,7 @@ as String?,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: | |||||||
| as DateTime?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable | as DateTime?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable | ||||||
| as DateTime?,visibility: null == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable | as DateTime?,visibility: null == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable | ||||||
| as int,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable | as int,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,slug: freezed == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable | ||||||
| as int,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable | as int,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, dynamic>?,viewsUnique: null == viewsUnique ? _self.viewsUnique : viewsUnique // ignore: cast_nullable_to_non_nullable | as Map<String, dynamic>?,viewsUnique: null == viewsUnique ? _self.viewsUnique : viewsUnique // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -440,7 +459,9 @@ as SnPost?,repliedPostId: freezed == repliedPostId ? _self.repliedPostId : repli | |||||||
| as String?,repliedPost: freezed == repliedPost ? _self.repliedPost : repliedPost // ignore: cast_nullable_to_non_nullable | as String?,repliedPost: freezed == repliedPost ? _self.repliedPost : repliedPost // ignore: cast_nullable_to_non_nullable | ||||||
| as SnPost?,forwardedPostId: freezed == forwardedPostId ? _self.forwardedPostId : forwardedPostId // ignore: cast_nullable_to_non_nullable | as SnPost?,forwardedPostId: freezed == forwardedPostId ? _self.forwardedPostId : forwardedPostId // ignore: cast_nullable_to_non_nullable | ||||||
| as String?,forwardedPost: freezed == forwardedPost ? _self.forwardedPost : forwardedPost // ignore: cast_nullable_to_non_nullable | as String?,forwardedPost: freezed == forwardedPost ? _self.forwardedPost : forwardedPost // ignore: cast_nullable_to_non_nullable | ||||||
| as SnPost?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable | as SnPost?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable | ||||||
|  | as String?,realm: freezed == realm ? _self.realm : realm // ignore: cast_nullable_to_non_nullable | ||||||
|  | as SnRealm?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable | ||||||
| as List<SnCloudFile>,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable | as List<SnCloudFile>,publisher: null == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable | ||||||
| as SnPublisher,reactionsCount: null == reactionsCount ? _self._reactionsCount : reactionsCount // ignore: cast_nullable_to_non_nullable | as SnPublisher,reactionsCount: null == reactionsCount ? _self._reactionsCount : reactionsCount // ignore: cast_nullable_to_non_nullable | ||||||
| as Map<String, int>,reactionsMade: null == reactionsMade ? _self._reactionsMade : reactionsMade // ignore: cast_nullable_to_non_nullable | as Map<String, int>,reactionsMade: null == reactionsMade ? _self._reactionsMade : reactionsMade // ignore: cast_nullable_to_non_nullable | ||||||
| @@ -496,6 +517,18 @@ $SnPostCopyWith<$Res>? get forwardedPost { | |||||||
| /// with the given fields replaced by the non-null parameter values. | /// with the given fields replaced by the non-null parameter values. | ||||||
| @override | @override | ||||||
| @pragma('vm:prefer-inline') | @pragma('vm:prefer-inline') | ||||||
|  | $SnRealmCopyWith<$Res>? get realm { | ||||||
|  |     if (_self.realm == null) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return $SnRealmCopyWith<$Res>(_self.realm!, (value) { | ||||||
|  |     return _then(_self.copyWith(realm: value)); | ||||||
|  |   }); | ||||||
|  | }/// Create a copy of SnPost | ||||||
|  | /// with the given fields replaced by the non-null parameter values. | ||||||
|  | @override | ||||||
|  | @pragma('vm:prefer-inline') | ||||||
| $SnPublisherCopyWith<$Res> get publisher { | $SnPublisherCopyWith<$Res> get publisher { | ||||||
|    |    | ||||||
|   return $SnPublisherCopyWith<$Res>(_self.publisher, (value) { |   return $SnPublisherCopyWith<$Res>(_self.publisher, (value) { | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost( | |||||||
|           : DateTime.parse(json['published_at'] as String), |           : DateTime.parse(json['published_at'] as String), | ||||||
|   visibility: (json['visibility'] as num?)?.toInt() ?? 0, |   visibility: (json['visibility'] as num?)?.toInt() ?? 0, | ||||||
|   content: json['content'] as String?, |   content: json['content'] as String?, | ||||||
|  |   slug: json['slug'] as String?, | ||||||
|   type: (json['type'] as num?)?.toInt() ?? 0, |   type: (json['type'] as num?)?.toInt() ?? 0, | ||||||
|   meta: json['meta'] as Map<String, dynamic>?, |   meta: json['meta'] as Map<String, dynamic>?, | ||||||
|   viewsUnique: (json['views_unique'] as num?)?.toInt() ?? 0, |   viewsUnique: (json['views_unique'] as num?)?.toInt() ?? 0, | ||||||
| @@ -43,6 +44,11 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost( | |||||||
|       json['forwarded_post'] == null |       json['forwarded_post'] == null | ||||||
|           ? null |           ? null | ||||||
|           : SnPost.fromJson(json['forwarded_post'] as Map<String, dynamic>), |           : SnPost.fromJson(json['forwarded_post'] as Map<String, dynamic>), | ||||||
|  |   realmId: json['realm_id'] as String?, | ||||||
|  |   realm: | ||||||
|  |       json['realm'] == null | ||||||
|  |           ? null | ||||||
|  |           : SnRealm.fromJson(json['realm'] as Map<String, dynamic>), | ||||||
|   attachments: |   attachments: | ||||||
|       (json['attachments'] as List<dynamic>?) |       (json['attachments'] as List<dynamic>?) | ||||||
|           ?.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>)) |           ?.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>)) | ||||||
| @@ -95,6 +101,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{ | |||||||
|   'published_at': instance.publishedAt?.toIso8601String(), |   'published_at': instance.publishedAt?.toIso8601String(), | ||||||
|   'visibility': instance.visibility, |   'visibility': instance.visibility, | ||||||
|   'content': instance.content, |   'content': instance.content, | ||||||
|  |   'slug': instance.slug, | ||||||
|   'type': instance.type, |   'type': instance.type, | ||||||
|   'meta': instance.meta, |   'meta': instance.meta, | ||||||
|   'views_unique': instance.viewsUnique, |   'views_unique': instance.viewsUnique, | ||||||
| @@ -108,6 +115,8 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{ | |||||||
|   'replied_post': instance.repliedPost?.toJson(), |   'replied_post': instance.repliedPost?.toJson(), | ||||||
|   'forwarded_post_id': instance.forwardedPostId, |   'forwarded_post_id': instance.forwardedPostId, | ||||||
|   'forwarded_post': instance.forwardedPost?.toJson(), |   'forwarded_post': instance.forwardedPost?.toJson(), | ||||||
|  |   'realm_id': instance.realmId, | ||||||
|  |   'realm': instance.realm?.toJson(), | ||||||
|   'attachments': instance.attachments.map((e) => e.toJson()).toList(), |   'attachments': instance.attachments.map((e) => e.toJson()).toList(), | ||||||
|   'publisher': instance.publisher.toJson(), |   'publisher': instance.publisher.toJson(), | ||||||
|   'reactions_count': instance.reactionsCount, |   'reactions_count': instance.reactionsCount, | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | import 'package:freezed_annotation/freezed_annotation.dart'; | ||||||
| import 'package:island/models/file.dart'; | import 'package:island/models/file.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
|  |  | ||||||
| part 'publisher.freezed.dart'; | part 'publisher.freezed.dart'; | ||||||
| part 'publisher.g.dart'; | part 'publisher.g.dart'; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | import 'package:freezed_annotation/freezed_annotation.dart'; | ||||||
| import 'package:island/models/file.dart'; | import 'package:island/models/file.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
|  |  | ||||||
| part 'realm.freezed.dart'; | part 'realm.freezed.dart'; | ||||||
| part 'realm.g.dart'; | part 'realm.g.dart'; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | import 'package:freezed_annotation/freezed_annotation.dart'; | ||||||
|  |  | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
|  |  | ||||||
| part 'relationship.freezed.dart'; | part 'relationship.freezed.dart'; | ||||||
| part 'relationship.g.dart'; | part 'relationship.g.dart'; | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | import 'package:freezed_annotation/freezed_annotation.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
|  |  | ||||||
| part 'wallet.freezed.dart'; | part 'wallet.freezed.dart'; | ||||||
| part 'wallet.g.dart'; | part 'wallet.g.dart'; | ||||||
|   | |||||||
| @@ -23,6 +23,8 @@ const kAppSoundEffects = 'app_sound_effects'; | |||||||
| const kAppAprilFoolFeatures = 'app_april_fool_features'; | const kAppAprilFoolFeatures = 'app_april_fool_features'; | ||||||
| const kAppWindowSize = 'app_window_size'; | const kAppWindowSize = 'app_window_size'; | ||||||
| const kAppEnterToSend = 'app_enter_to_send'; | const kAppEnterToSend = 'app_enter_to_send'; | ||||||
|  | const kFeaturedPostsCollapsedId = | ||||||
|  |     'featured_posts_collapsed_id'; // Key for storing the ID of the collapsed featured post | ||||||
|  |  | ||||||
| const Map<String, FilterQuality> kImageQualityLevel = { | const Map<String, FilterQuality> kImageQualityLevel = { | ||||||
|   'settingsImageQualityLowest': FilterQuality.none, |   'settingsImageQualityLowest': FilterQuality.none, | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import 'dart:developer'; | |||||||
| import 'package:firebase_analytics/firebase_analytics.dart'; | import 'package:firebase_analytics/firebase_analytics.dart'; | ||||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/config.dart'; | import 'package:island/pods/config.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
|  | import 'dart:io' show Platform; | ||||||
|  | import 'package:animations/animations.dart'; | ||||||
| import 'package:firebase_analytics/firebase_analytics.dart'; | import 'package:firebase_analytics/firebase_analytics.dart'; | ||||||
| import 'package:firebase_analytics/observer.dart'; |  | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter/foundation.dart' show kIsWeb; | ||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/screens/about.dart'; | import 'package:island/screens/about.dart'; | ||||||
| @@ -20,9 +22,9 @@ import 'package:island/screens/notification.dart'; | |||||||
| import 'package:island/screens/wallet.dart'; | import 'package:island/screens/wallet.dart'; | ||||||
| import 'package:island/screens/account/relationship.dart'; | import 'package:island/screens/account/relationship.dart'; | ||||||
| import 'package:island/screens/account/profile.dart'; | import 'package:island/screens/account/profile.dart'; | ||||||
| import 'package:island/screens/account/me/update.dart'; | import 'package:island/screens/account/me/profile_update.dart'; | ||||||
| import 'package:island/screens/account/leveling.dart'; | import 'package:island/screens/account/leveling.dart'; | ||||||
| import 'package:island/screens/account/me/settings.dart'; | import 'package:island/screens/account/me/account_settings.dart'; | ||||||
| import 'package:island/screens/chat/chat.dart'; | import 'package:island/screens/chat/chat.dart'; | ||||||
| import 'package:island/screens/chat/room.dart'; | import 'package:island/screens/chat/room.dart'; | ||||||
| import 'package:island/screens/chat/room_detail.dart'; | import 'package:island/screens/chat/room_detail.dart'; | ||||||
| @@ -56,13 +58,35 @@ final rootNavigatorKey = GlobalKey<NavigatorState>(); | |||||||
| final _shellNavigatorKey = GlobalKey<NavigatorState>(); | final _shellNavigatorKey = GlobalKey<NavigatorState>(); | ||||||
| final _tabsShellKey = GlobalKey<NavigatorState>(); | final _tabsShellKey = GlobalKey<NavigatorState>(); | ||||||
|  |  | ||||||
|  | Widget _tabPagesTransitionBuilder( | ||||||
|  |   BuildContext context, | ||||||
|  |   Animation<double> animation, | ||||||
|  |   Animation<double> secondaryAnimation, | ||||||
|  |   Widget child, | ||||||
|  | ) { | ||||||
|  |   return FadeThroughTransition( | ||||||
|  |     animation: animation, | ||||||
|  |     secondaryAnimation: secondaryAnimation, | ||||||
|  |     fillColor: Theme.of(context).colorScheme.surface, | ||||||
|  |     child: child, | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool get _supportsAnalytics => | ||||||
|  |     kIsWeb || | ||||||
|  |     Platform.isAndroid || | ||||||
|  |     Platform.isIOS || | ||||||
|  |     Platform.isMacOS || | ||||||
|  |     Platform.isWindows; | ||||||
|  |  | ||||||
| // Provider for the router | // Provider for the router | ||||||
| final routerProvider = Provider<GoRouter>((ref) { | final routerProvider = Provider<GoRouter>((ref) { | ||||||
|   return GoRouter( |   return GoRouter( | ||||||
|     navigatorKey: rootNavigatorKey, |     navigatorKey: rootNavigatorKey, | ||||||
|     initialLocation: '/', |     initialLocation: '/', | ||||||
|     observers: [ |     observers: [ | ||||||
|       FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance), |       if (_supportsAnalytics) | ||||||
|  |         FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance), | ||||||
|     ], |     ], | ||||||
|     routes: [ |     routes: [ | ||||||
|       ShellRoute( |       ShellRoute( | ||||||
| @@ -339,7 +363,12 @@ final routerProvider = Provider<GoRouter>((ref) { | |||||||
|               GoRoute( |               GoRoute( | ||||||
|                 name: 'explore', |                 name: 'explore', | ||||||
|                 path: '/', |                 path: '/', | ||||||
|                 builder: (context, state) => const ExploreScreen(), |                 pageBuilder: | ||||||
|  |                     (context, state) => CustomTransitionPage( | ||||||
|  |                       key: const ValueKey('explore'), | ||||||
|  |                       child: const ExploreScreen(), | ||||||
|  |                       transitionsBuilder: _tabPagesTransitionBuilder, | ||||||
|  |                     ), | ||||||
|               ), |               ), | ||||||
|               GoRoute( |               GoRoute( | ||||||
|                 name: 'postSearch', |                 name: 'postSearch', | ||||||
| @@ -389,8 +418,12 @@ final routerProvider = Provider<GoRouter>((ref) { | |||||||
|  |  | ||||||
|               // Chat tab |               // Chat tab | ||||||
|               ShellRoute( |               ShellRoute( | ||||||
|                 builder: |                 pageBuilder: | ||||||
|                     (context, state, child) => ChatShellScreen(child: child), |                     (context, state, child) => CustomTransitionPage( | ||||||
|  |                       key: const ValueKey('chat'), | ||||||
|  |                       child: ChatShellScreen(child: child), | ||||||
|  |                       transitionsBuilder: _tabPagesTransitionBuilder, | ||||||
|  |                     ), | ||||||
|                 routes: [ |                 routes: [ | ||||||
|                   GoRoute( |                   GoRoute( | ||||||
|                     name: 'chatList', |                     name: 'chatList', | ||||||
| @@ -433,7 +466,12 @@ final routerProvider = Provider<GoRouter>((ref) { | |||||||
|               GoRoute( |               GoRoute( | ||||||
|                 name: 'realmList', |                 name: 'realmList', | ||||||
|                 path: '/realms', |                 path: '/realms', | ||||||
|                 builder: (context, state) => const RealmListScreen(), |                 pageBuilder: | ||||||
|  |                     (context, state) => CustomTransitionPage( | ||||||
|  |                       key: const ValueKey('realms'), | ||||||
|  |                       child: const RealmListScreen(), | ||||||
|  |                       transitionsBuilder: _tabPagesTransitionBuilder, | ||||||
|  |                     ), | ||||||
|                 routes: [ |                 routes: [ | ||||||
|                   GoRoute( |                   GoRoute( | ||||||
|                     name: 'realmNew', |                     name: 'realmNew', | ||||||
| @@ -461,8 +499,12 @@ final routerProvider = Provider<GoRouter>((ref) { | |||||||
|  |  | ||||||
|               // Account tab |               // Account tab | ||||||
|               ShellRoute( |               ShellRoute( | ||||||
|                 builder: |                 pageBuilder: | ||||||
|                     (context, state, child) => AccountShellScreen(child: child), |                     (context, state, child) => CustomTransitionPage( | ||||||
|  |                       key: const ValueKey('account'), | ||||||
|  |                       child: AccountShellScreen(child: child), | ||||||
|  |                       transitionsBuilder: _tabPagesTransitionBuilder, | ||||||
|  |                     ), | ||||||
|                 routes: [ |                 routes: [ | ||||||
|                   GoRoute( |                   GoRoute( | ||||||
|                     name: 'account', |                     name: 'account', | ||||||
|   | |||||||
| @@ -178,7 +178,8 @@ class _AboutScreenState extends ConsumerState<AboutScreen> { | |||||||
|                                 context, |                                 context, | ||||||
|                                 icon: Symbols.label, |                                 icon: Symbols.label, | ||||||
|                                 label: 'aboutDeviceName'.tr(), |                                 label: 'aboutDeviceName'.tr(), | ||||||
|                                 value: _deviceInfo?.data['name'], |                                 value: | ||||||
|  |                                     _deviceInfo?.data['name'] ?? 'unknown'.tr(), | ||||||
|                               ), |                               ), | ||||||
|                               _buildInfoItem( |                               _buildInfoItem( | ||||||
|                                 context, |                                 context, | ||||||
|   | |||||||
| @@ -3,12 +3,14 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/pods/userinfo.dart'; | import 'package:island/pods/userinfo.dart'; | ||||||
| import 'package:island/screens/notification.dart'; | import 'package:island/screens/notification.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/widgets/account/account_name.dart'; | import 'package:island/widgets/account/account_name.dart'; | ||||||
| import 'package:island/widgets/account/status.dart'; | import 'package:island/widgets/account/status.dart'; | ||||||
| import 'package:island/widgets/account/leveling_progress.dart'; | import 'package:island/widgets/account/leveling_progress.dart'; | ||||||
|  | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| import 'package:island/widgets/content/cloud_files.dart'; | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
| import 'package:island/widgets/debug_sheet.dart'; | import 'package:island/widgets/debug_sheet.dart'; | ||||||
| @@ -303,7 +305,12 @@ class AccountScreen extends HookConsumerWidget { | |||||||
|               trailing: const Icon(Symbols.chevron_right), |               trailing: const Icon(Symbols.chevron_right), | ||||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), |               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||||
|               title: Text('logout').tr(), |               title: Text('logout').tr(), | ||||||
|               onTap: () { |               onTap: () async { | ||||||
|  |                 final apiClient = ref.watch(apiClientProvider); | ||||||
|  |                 showLoadingModal(context); | ||||||
|  |                 await apiClient.delete('/id/accounts/me/sessions/current'); | ||||||
|  |                 if (!context.mounted) return; | ||||||
|  |                 hideLoadingModal(context); | ||||||
|                 final userNotifier = ref.read(userInfoProvider.notifier); |                 final userNotifier = ref.read(userInfoProvider.notifier); | ||||||
|                 userNotifier.logOut(); |                 userNotifier.logOut(); | ||||||
|               }, |               }, | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:flutter/services.dart'; | import 'package:flutter/services.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/auth.dart'; | import 'package:island/models/auth.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/pods/userinfo.dart'; | import 'package:island/pods/userinfo.dart'; | ||||||
| import 'package:island/screens/account/me/settings_auth_factors.dart'; | import 'package:island/screens/account/me/settings_auth_factors.dart'; | ||||||
| @@ -15,7 +15,7 @@ import 'package:island/screens/account/me/settings_contacts.dart'; | |||||||
| import 'package:island/screens/auth/captcha.dart'; | import 'package:island/screens/auth/captcha.dart'; | ||||||
| import 'package:island/screens/auth/login.dart'; | import 'package:island/screens/auth/login.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/widgets/account/account_session_sheet.dart'; | import 'package:island/widgets/account/account_devices.dart'; | ||||||
| import 'package:island/widgets/alert.dart'; | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| import 'package:island/widgets/response.dart'; | import 'package:island/widgets/response.dart'; | ||||||
| @@ -23,7 +23,7 @@ import 'package:material_symbols_icons/symbols.dart'; | |||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| 
 | 
 | ||||||
| part 'settings.g.dart'; | part 'account_settings.g.dart'; | ||||||
| 
 | 
 | ||||||
| @riverpod | @riverpod | ||||||
| Future<List<SnAuthFactor>> authFactors(Ref ref) async { | Future<List<SnAuthFactor>> authFactors(Ref ref) async { | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
| 
 | 
 | ||||||
| part of 'settings.dart'; | part of 'account_settings.dart'; | ||||||
| 
 | 
 | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
| // RiverpodGenerator | // RiverpodGenerator | ||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import 'package:collection/collection.dart'; | ||||||
| import 'package:croppy/croppy.dart' hide cropImage; | import 'package:croppy/croppy.dart' hide cropImage; | ||||||
| import 'package:dropdown_button2/dropdown_button2.dart'; | import 'package:dropdown_button2/dropdown_button2.dart'; | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| @@ -7,7 +8,7 @@ import 'package:gap/gap.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:image_picker/image_picker.dart'; | import 'package:image_picker/image_picker.dart'; | ||||||
| import 'package:island/models/file.dart'; | import 'package:island/models/file.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/config.dart'; | import 'package:island/pods/config.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/pods/userinfo.dart'; | import 'package:island/pods/userinfo.dart'; | ||||||
| @@ -168,11 +169,18 @@ class UpdateProfileScreen extends HookConsumerWidget { | |||||||
|             'location': locationController.text, |             'location': locationController.text, | ||||||
|             'time_zone': timeZoneController.text, |             'time_zone': timeZoneController.text, | ||||||
|             'birthday': birthday.value?.toUtc().toIso8601String(), |             'birthday': birthday.value?.toUtc().toIso8601String(), | ||||||
|             'links': links.value, |             'links': | ||||||
|  |                 links.value | ||||||
|  |                     .where((e) => e.name.isNotEmpty && e.url.isNotEmpty) | ||||||
|  |                     .toList(), | ||||||
|           }, |           }, | ||||||
|         ); |         ); | ||||||
|         final userNotifier = ref.read(userInfoProvider.notifier); |         final userNotifier = ref.read(userInfoProvider.notifier); | ||||||
|         userNotifier.fetchUser(); |         userNotifier.fetchUser(); | ||||||
|  |         links.value = | ||||||
|  |             links.value | ||||||
|  |                 .where((e) => e.name.isNotEmpty && e.url.isNotEmpty) | ||||||
|  |                 .toList(); | ||||||
|       } catch (err) { |       } catch (err) { | ||||||
|         showErrorAlert(err); |         showErrorAlert(err); | ||||||
|       } finally { |       } finally { | ||||||
| @@ -568,6 +576,7 @@ class UpdateProfileScreen extends HookConsumerWidget { | |||||||
|                     children: [ |                     children: [ | ||||||
|                       for (var i = 0; i < links.value.length; i++) |                       for (var i = 0; i < links.value.length; i++) | ||||||
|                         Row( |                         Row( | ||||||
|  |                           key: ValueKey(links.value[i].hashCode), | ||||||
|                           crossAxisAlignment: CrossAxisAlignment.end, |                           crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|                           children: [ |                           children: [ | ||||||
|                             Expanded( |                             Expanded( | ||||||
| @@ -610,8 +619,10 @@ class UpdateProfileScreen extends HookConsumerWidget { | |||||||
|                             IconButton( |                             IconButton( | ||||||
|                               icon: const Icon(Symbols.delete), |                               icon: const Icon(Symbols.delete), | ||||||
|                               onPressed: () { |                               onPressed: () { | ||||||
|                                 links.value = List.from(links.value) |                                 links.value = | ||||||
|                                   ..removeAt(i); |                                     links.value | ||||||
|  |                                         .whereIndexed((idx, _) => idx != i) | ||||||
|  |                                         .toList(); | ||||||
|                               }, |                               }, | ||||||
|                             ), |                             ), | ||||||
|                           ], |                           ], | ||||||
| @@ -6,7 +6,7 @@ import 'package:gap/gap.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/auth.dart'; | import 'package:island/models/auth.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/screens/account/me/settings.dart'; | import 'package:island/screens/account/me/account_settings.dart'; | ||||||
| import 'package:island/screens/auth/oidc.native.dart'; | import 'package:island/screens/auth/oidc.native.dart'; | ||||||
| import 'package:island/services/text.dart'; | import 'package:island/services/text.dart'; | ||||||
| import 'package:island/services/time.dart'; | import 'package:island/services/time.dart'; | ||||||
| @@ -166,7 +166,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget { | |||||||
|               webAuthenticationOptions: WebAuthenticationOptions( |               webAuthenticationOptions: WebAuthenticationOptions( | ||||||
|                 clientId: 'dev.solsynth.solarpass', |                 clientId: 'dev.solsynth.solarpass', | ||||||
|                 redirectUri: Uri.parse( |                 redirectUri: Uri.parse( | ||||||
|                   'https://nt.solian.app/auth/callback/apple', |                   'https://id.solian.app/auth/callback/apple', | ||||||
|                 ), |                 ), | ||||||
|               ), |               ), | ||||||
|             ); |             ); | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/widgets/alert.dart'; | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/content/sheet.dart'; | import 'package:island/widgets/content/sheet.dart'; | ||||||
|   | |||||||
| @@ -2,12 +2,13 @@ import 'package:dio/dio.dart'; | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/chat.dart'; | import 'package:island/models/chat.dart'; | ||||||
| import 'package:island/models/relationship.dart'; | import 'package:island/models/relationship.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/config.dart'; | import 'package:island/pods/config.dart'; | ||||||
| import 'package:island/pods/event_calendar.dart'; | import 'package:island/pods/event_calendar.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| @@ -262,6 +263,10 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     final user = ref.watch(userInfoProvider); |     final user = ref.watch(userInfoProvider); | ||||||
|  |     final isCurrentUser = useMemoized( | ||||||
|  |       () => user.value?.id == account.value?.id, | ||||||
|  |       [user, account], | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     Widget accountBasicInfo(SnAccount data) => Padding( |     Widget accountBasicInfo(SnAccount data) => Padding( | ||||||
|       padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), |       padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), | ||||||
| @@ -589,7 +594,7 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                           child: CustomScrollView( |                           child: CustomScrollView( | ||||||
|                             slivers: [ |                             slivers: [ | ||||||
|                               SliverGap(24), |                               SliverGap(24), | ||||||
|                               if (user.value != null) |                               if (user.value != null && !isCurrentUser) | ||||||
|                                 SliverToBoxAdapter(child: accountAction(data)), |                                 SliverToBoxAdapter(child: accountAction(data)), | ||||||
|                               SliverToBoxAdapter( |                               SliverToBoxAdapter( | ||||||
|                                 child: Card( |                                 child: Card( | ||||||
| @@ -686,7 +691,7 @@ class AccountProfileScreen extends HookConsumerWidget { | |||||||
|                             data, |                             data, | ||||||
|                           ).padding(horizontal: 4), |                           ).padding(horizontal: 4), | ||||||
|                         ), |                         ), | ||||||
|                         if (user.value != null) |                         if (user.value != null && !isCurrentUser) | ||||||
|                           SliverToBoxAdapter( |                           SliverToBoxAdapter( | ||||||
|                             child: accountAction(data).padding(horizontal: 4), |                             child: accountAction(data).padding(horizontal: 4), | ||||||
|                           ), |                           ), | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; | |||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/screens/account/me/update.dart'; | import 'package:island/screens/account/me/profile_update.dart'; | ||||||
| import 'package:island/widgets/alert.dart'; | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|   | |||||||
| @@ -42,6 +42,22 @@ final Map<int, (String, String, IconData)> kFactorTypes = { | |||||||
|   4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm), |   4: ('authFactorPin', 'authFactorPinDescription', Symbols.nest_secure_alarm), | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | Future<String?> getDeviceName() async { | ||||||
|  |   if (kIsWeb) return null; | ||||||
|  |   String? name; | ||||||
|  |   if (Platform.isIOS) { | ||||||
|  |     final deviceInfo = await DeviceInfoPlugin().iosInfo; | ||||||
|  |     name = deviceInfo.name; | ||||||
|  |   } else if (Platform.isAndroid) { | ||||||
|  |     final deviceInfo = await DeviceInfoPlugin().androidInfo; | ||||||
|  |     name = deviceInfo.name; | ||||||
|  |   } else if (Platform.isWindows) { | ||||||
|  |     final deviceInfo = await DeviceInfoPlugin().windowsInfo; | ||||||
|  |     name = deviceInfo.computerName; | ||||||
|  |   } | ||||||
|  |   return name; | ||||||
|  | } | ||||||
|  |  | ||||||
| class LoginScreen extends HookConsumerWidget { | class LoginScreen extends HookConsumerWidget { | ||||||
|   const LoginScreen({super.key}); |   const LoginScreen({super.key}); | ||||||
|  |  | ||||||
| @@ -198,28 +214,6 @@ class _LoginCheckScreen extends HookConsumerWidget { | |||||||
|         wsNotifier.connect(); |         wsNotifier.connect(); | ||||||
|         if (context.mounted) Navigator.pop(context, true); |         if (context.mounted) Navigator.pop(context, true); | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       // Update the sessions' device name is available |  | ||||||
|       if (!kIsWeb) { |  | ||||||
|         String? name; |  | ||||||
|         if (Platform.isIOS) { |  | ||||||
|           final deviceInfo = await DeviceInfoPlugin().iosInfo; |  | ||||||
|           name = deviceInfo.name; |  | ||||||
|         } else if (Platform.isAndroid) { |  | ||||||
|           final deviceInfo = await DeviceInfoPlugin().androidInfo; |  | ||||||
|           name = deviceInfo.name; |  | ||||||
|         } else if (Platform.isWindows) { |  | ||||||
|           final deviceInfo = await DeviceInfoPlugin().windowsInfo; |  | ||||||
|           name = deviceInfo.computerName; |  | ||||||
|         } |  | ||||||
|         if (name != null) { |  | ||||||
|           final client = ref.watch(apiClientProvider); |  | ||||||
|           await client.patch( |  | ||||||
|             '/id/accounts/me/sessions/current/label', |  | ||||||
|             data: jsonEncode(name), |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect(() { | ||||||
| @@ -578,6 +572,7 @@ class _LoginLookupScreen extends HookConsumerWidget { | |||||||
|           data: { |           data: { | ||||||
|             'account': uname, |             'account': uname, | ||||||
|             'device_id': await getUdid(), |             'device_id': await getUdid(), | ||||||
|  |             'device_name': await getDeviceName(), | ||||||
|             'platform': |             'platform': | ||||||
|                 kIsWeb |                 kIsWeb | ||||||
|                     ? 1 |                     ? 1 | ||||||
| @@ -628,6 +623,7 @@ class _LoginLookupScreen extends HookConsumerWidget { | |||||||
|             'identity_token': credential.identityToken!, |             'identity_token': credential.identityToken!, | ||||||
|             'authorization_code': credential.authorizationCode, |             'authorization_code': credential.authorizationCode, | ||||||
|             'device_id': await getUdid(), |             'device_id': await getUdid(), | ||||||
|  |             'device_name': await getDeviceName(), | ||||||
|           }, |           }, | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ import 'package:island/widgets/alert.dart'; | |||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| import 'package:island/widgets/content/cloud_files.dart'; | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
| import 'package:island/widgets/content/sheet.dart'; | import 'package:island/widgets/content/sheet.dart'; | ||||||
| import 'package:island/widgets/realms/selection_dropdown.dart'; | import 'package:island/widgets/realm/realm_selection_dropdown.dart'; | ||||||
| import 'package:island/widgets/response.dart'; | import 'package:island/widgets/response.dart'; | ||||||
| import 'package:island/screens/tabs.dart'; | import 'package:island/screens/tabs.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -6,7 +6,7 @@ part of 'room.dart'; | |||||||
| // RiverpodGenerator | // RiverpodGenerator | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
|  |  | ||||||
| String _$messagesNotifierHash() => r'afc4d43f4948ec571118cef0321838a6cefc89c0'; | String _$messagesNotifierHash() => r'3740c02ab1e4dba9c8e619b63c40e2236a89b342'; | ||||||
|  |  | ||||||
| /// Copied from Dart SDK | /// Copied from Dart SDK | ||||||
| class _SystemHash { | class _SystemHash { | ||||||
|   | |||||||
| @@ -14,17 +14,19 @@ part 'poll_list.g.dart'; | |||||||
|  |  | ||||||
| @riverpod | @riverpod | ||||||
| class PollListNotifier extends _$PollListNotifier | class PollListNotifier extends _$PollListNotifier | ||||||
|     with CursorPagingNotifierMixin<SnPoll> { |     with CursorPagingNotifierMixin<SnPollWithStats> { | ||||||
|   static const int _pageSize = 20; |   static const int _pageSize = 20; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<CursorPagingData<SnPoll>> build(String? pubName) { |   Future<CursorPagingData<SnPollWithStats>> build(String? pubName) { | ||||||
|     // immediately load first page |     // immediately load first page | ||||||
|     return fetch(cursor: null); |     return fetch(cursor: null); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Future<CursorPagingData<SnPoll>> fetch({required String? cursor}) async { |   Future<CursorPagingData<SnPollWithStats>> fetch({ | ||||||
|  |     required String? cursor, | ||||||
|  |   }) async { | ||||||
|     final client = ref.read(apiClientProvider); |     final client = ref.read(apiClientProvider); | ||||||
|     final offset = cursor == null ? 0 : int.parse(cursor); |     final offset = cursor == null ? 0 : int.parse(cursor); | ||||||
|  |  | ||||||
| @@ -42,7 +44,7 @@ class PollListNotifier extends _$PollListNotifier | |||||||
|     ); |     ); | ||||||
|     final total = int.parse(response.headers.value('X-Total') ?? '0'); |     final total = int.parse(response.headers.value('X-Total') ?? '0'); | ||||||
|     final List<dynamic> data = response.data; |     final List<dynamic> data = response.data; | ||||||
|     final items = data.map((json) => SnPoll.fromJson(json)).toList(); |     final items = data.map((json) => SnPollWithStats.fromJson(json)).toList(); | ||||||
|  |  | ||||||
|     final hasMore = offset + items.length < total; |     final hasMore = offset + items.length < total; | ||||||
|     final nextCursor = hasMore ? (offset + items.length).toString() : null; |     final nextCursor = hasMore ? (offset + items.length).toString() : null; | ||||||
| @@ -55,6 +57,13 @@ class PollListNotifier extends _$PollListNotifier | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @riverpod | ||||||
|  | Future<SnPollWithStats> pollWithStats(Ref ref, String id) async { | ||||||
|  |   final apiClient = ref.watch(apiClientProvider); | ||||||
|  |   final resp = await apiClient.get('/sphere/polls/$id'); | ||||||
|  |   return SnPollWithStats.fromJson(resp.data); | ||||||
|  | } | ||||||
|  |  | ||||||
| class CreatorPollListScreen extends HookConsumerWidget { | class CreatorPollListScreen extends HookConsumerWidget { | ||||||
|   const CreatorPollListScreen({super.key, required this.pubName}); |   const CreatorPollListScreen({super.key, required this.pubName}); | ||||||
|  |  | ||||||
| @@ -64,7 +73,7 @@ class CreatorPollListScreen extends HookConsumerWidget { | |||||||
|     final result = await GoRouter.of( |     final result = await GoRouter.of( | ||||||
|       context, |       context, | ||||||
|     ).pushNamed('creatorPollNew', pathParameters: {'name': pubName}); |     ).pushNamed('creatorPollNew', pathParameters: {'name': pubName}); | ||||||
|     if (result is SnPoll && context.mounted) { |     if (result is SnPollWithStats && context.mounted) { | ||||||
|       Navigator.of(context).maybePop(result); |       Navigator.of(context).maybePop(result); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -92,8 +101,11 @@ class CreatorPollListScreen extends HookConsumerWidget { | |||||||
|                       if (index == widgetCount - 1) { |                       if (index == widgetCount - 1) { | ||||||
|                         return endItemView; |                         return endItemView; | ||||||
|                       } |                       } | ||||||
|                       final poll = data.items[index]; |                       final pollWithStats = data.items[index]; | ||||||
|                       return _CreatorPollItem(poll: poll, pubName: pubName); |                       return _CreatorPollItem( | ||||||
|  |                         pollWithStats: pollWithStats, | ||||||
|  |                         pubName: pubName, | ||||||
|  |                       ); | ||||||
|                     }, |                     }, | ||||||
|                   ), |                   ), | ||||||
|             ), |             ), | ||||||
| @@ -106,14 +118,14 @@ class CreatorPollListScreen extends HookConsumerWidget { | |||||||
|  |  | ||||||
| class _CreatorPollItem extends StatelessWidget { | class _CreatorPollItem extends StatelessWidget { | ||||||
|   final String pubName; |   final String pubName; | ||||||
|   const _CreatorPollItem({required this.poll, required this.pubName}); |   const _CreatorPollItem({required this.pollWithStats, required this.pubName}); | ||||||
|  |  | ||||||
|   final SnPoll poll; |   final SnPollWithStats pollWithStats; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     final theme = Theme.of(context); |     final theme = Theme.of(context); | ||||||
|     final ended = poll.endedAt; |     final ended = pollWithStats.endedAt; | ||||||
|     final endedText = |     final endedText = | ||||||
|         ended == null |         ended == null | ||||||
|             ? 'No end' |             ? 'No end' | ||||||
| @@ -123,15 +135,16 @@ class _CreatorPollItem extends StatelessWidget { | |||||||
|       margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), |       margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), | ||||||
|       clipBehavior: Clip.antiAlias, |       clipBehavior: Clip.antiAlias, | ||||||
|       child: ListTile( |       child: ListTile( | ||||||
|         title: Text(poll.title ?? 'Untitled poll'), |         title: Text(pollWithStats.title ?? 'Untitled poll'), | ||||||
|         subtitle: Column( |         subtitle: Column( | ||||||
|           crossAxisAlignment: CrossAxisAlignment.start, |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|           children: [ |           children: [ | ||||||
|             if (poll.description != null && poll.description!.isNotEmpty) |             if (pollWithStats.description != null && | ||||||
|  |                 pollWithStats.description!.isNotEmpty) | ||||||
|               Padding( |               Padding( | ||||||
|                 padding: const EdgeInsets.only(top: 4), |                 padding: const EdgeInsets.only(top: 4), | ||||||
|                 child: Text( |                 child: Text( | ||||||
|                   poll.description!, |                   pollWithStats.description!, | ||||||
|                   maxLines: 2, |                   maxLines: 2, | ||||||
|                   overflow: TextOverflow.ellipsis, |                   overflow: TextOverflow.ellipsis, | ||||||
|                 ), |                 ), | ||||||
| @@ -139,7 +152,7 @@ class _CreatorPollItem extends StatelessWidget { | |||||||
|             Padding( |             Padding( | ||||||
|               padding: const EdgeInsets.only(top: 4), |               padding: const EdgeInsets.only(top: 4), | ||||||
|               child: Text( |               child: Text( | ||||||
|                 'Questions: ${poll.questions.length} · Ends: $endedText', |                 'Questions: ${pollWithStats.questions.length} · Ends: $endedText', | ||||||
|                 style: theme.textTheme.bodySmall, |                 style: theme.textTheme.bodySmall, | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
| @@ -159,7 +172,7 @@ class _CreatorPollItem extends StatelessWidget { | |||||||
|                   onTap: () { |                   onTap: () { | ||||||
|                     GoRouter.of(context).pushNamed( |                     GoRouter.of(context).pushNamed( | ||||||
|                       'creatorPollEdit', |                       'creatorPollEdit', | ||||||
|                       pathParameters: {'name': pubName, 'id': poll.id}, |                       pathParameters: {'name': pubName, 'id': pollWithStats.id}, | ||||||
|                     ); |                     ); | ||||||
|                   }, |                   }, | ||||||
|                 ), |                 ), | ||||||
| @@ -170,8 +183,7 @@ class _CreatorPollItem extends StatelessWidget { | |||||||
|             context: context, |             context: context, | ||||||
|             useRootNavigator: true, |             useRootNavigator: true, | ||||||
|             isScrollControlled: true, |             isScrollControlled: true, | ||||||
|             builder: |             builder: (context) => PollFeedbackSheet(pollId: pollWithStats.id), | ||||||
|                 (context) => PollFeedbackSheet(pollId: poll.id, poll: poll), |  | ||||||
|           ); |           ); | ||||||
|         }, |         }, | ||||||
|       ), |       ), | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ part of 'poll_list.dart'; | |||||||
| // RiverpodGenerator | // RiverpodGenerator | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
|  |  | ||||||
| String _$pollListNotifierHash() => r'd3da24ff6bbb8f35b06d57fc41625dc0312508e4'; | String _$pollWithStatsHash() => r'6bb910046ce1e09368f9922dbec52fdc2cc86740'; | ||||||
|  |  | ||||||
| /// Copied from Dart SDK | /// Copied from Dart SDK | ||||||
| class _SystemHash { | class _SystemHash { | ||||||
| @@ -29,11 +29,133 @@ class _SystemHash { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /// See also [pollWithStats]. | ||||||
|  | @ProviderFor(pollWithStats) | ||||||
|  | const pollWithStatsProvider = PollWithStatsFamily(); | ||||||
|  |  | ||||||
|  | /// See also [pollWithStats]. | ||||||
|  | class PollWithStatsFamily extends Family<AsyncValue<SnPollWithStats>> { | ||||||
|  |   /// See also [pollWithStats]. | ||||||
|  |   const PollWithStatsFamily(); | ||||||
|  |  | ||||||
|  |   /// See also [pollWithStats]. | ||||||
|  |   PollWithStatsProvider call(String id) { | ||||||
|  |     return PollWithStatsProvider(id); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   PollWithStatsProvider getProviderOverride( | ||||||
|  |     covariant PollWithStatsProvider 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'pollWithStatsProvider'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// See also [pollWithStats]. | ||||||
|  | class PollWithStatsProvider extends AutoDisposeFutureProvider<SnPollWithStats> { | ||||||
|  |   /// See also [pollWithStats]. | ||||||
|  |   PollWithStatsProvider(String id) | ||||||
|  |     : this._internal( | ||||||
|  |         (ref) => pollWithStats(ref as PollWithStatsRef, id), | ||||||
|  |         from: pollWithStatsProvider, | ||||||
|  |         name: r'pollWithStatsProvider', | ||||||
|  |         debugGetCreateSourceHash: | ||||||
|  |             const bool.fromEnvironment('dart.vm.product') | ||||||
|  |                 ? null | ||||||
|  |                 : _$pollWithStatsHash, | ||||||
|  |         dependencies: PollWithStatsFamily._dependencies, | ||||||
|  |         allTransitiveDependencies: | ||||||
|  |             PollWithStatsFamily._allTransitiveDependencies, | ||||||
|  |         id: id, | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |   PollWithStatsProvider._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<SnPollWithStats> Function(PollWithStatsRef provider) create, | ||||||
|  |   ) { | ||||||
|  |     return ProviderOverride( | ||||||
|  |       origin: this, | ||||||
|  |       override: PollWithStatsProvider._internal( | ||||||
|  |         (ref) => create(ref as PollWithStatsRef), | ||||||
|  |         from: from, | ||||||
|  |         name: null, | ||||||
|  |         dependencies: null, | ||||||
|  |         allTransitiveDependencies: null, | ||||||
|  |         debugGetCreateSourceHash: null, | ||||||
|  |         id: id, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   AutoDisposeFutureProviderElement<SnPollWithStats> createElement() { | ||||||
|  |     return _PollWithStatsProviderElement(this); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   bool operator ==(Object other) { | ||||||
|  |     return other is PollWithStatsProvider && 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 PollWithStatsRef on AutoDisposeFutureProviderRef<SnPollWithStats> { | ||||||
|  |   /// The parameter `id` of this provider. | ||||||
|  |   String get id; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _PollWithStatsProviderElement | ||||||
|  |     extends AutoDisposeFutureProviderElement<SnPollWithStats> | ||||||
|  |     with PollWithStatsRef { | ||||||
|  |   _PollWithStatsProviderElement(super.provider); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   String get id => (origin as PollWithStatsProvider).id; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | String _$pollListNotifierHash() => r'd5b822e737788be8982f5cb3b501d460441930c1'; | ||||||
|  |  | ||||||
| abstract class _$PollListNotifier | abstract class _$PollListNotifier | ||||||
|     extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPoll>> { |     extends | ||||||
|  |         BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPollWithStats>> { | ||||||
|   late final String? pubName; |   late final String? pubName; | ||||||
|  |  | ||||||
|   FutureOr<CursorPagingData<SnPoll>> build(String? pubName); |   FutureOr<CursorPagingData<SnPollWithStats>> build(String? pubName); | ||||||
| } | } | ||||||
|  |  | ||||||
| /// See also [PollListNotifier]. | /// See also [PollListNotifier]. | ||||||
| @@ -42,7 +164,7 @@ const pollListNotifierProvider = PollListNotifierFamily(); | |||||||
|  |  | ||||||
| /// See also [PollListNotifier]. | /// See also [PollListNotifier]. | ||||||
| class PollListNotifierFamily | class PollListNotifierFamily | ||||||
|     extends Family<AsyncValue<CursorPagingData<SnPoll>>> { |     extends Family<AsyncValue<CursorPagingData<SnPollWithStats>>> { | ||||||
|   /// See also [PollListNotifier]. |   /// See also [PollListNotifier]. | ||||||
|   const PollListNotifierFamily(); |   const PollListNotifierFamily(); | ||||||
|  |  | ||||||
| @@ -78,7 +200,7 @@ class PollListNotifierProvider | |||||||
|     extends |     extends | ||||||
|         AutoDisposeAsyncNotifierProviderImpl< |         AutoDisposeAsyncNotifierProviderImpl< | ||||||
|           PollListNotifier, |           PollListNotifier, | ||||||
|           CursorPagingData<SnPoll> |           CursorPagingData<SnPollWithStats> | ||||||
|         > { |         > { | ||||||
|   /// See also [PollListNotifier]. |   /// See also [PollListNotifier]. | ||||||
|   PollListNotifierProvider(String? pubName) |   PollListNotifierProvider(String? pubName) | ||||||
| @@ -109,7 +231,7 @@ class PollListNotifierProvider | |||||||
|   final String? pubName; |   final String? pubName; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   FutureOr<CursorPagingData<SnPoll>> runNotifierBuild( |   FutureOr<CursorPagingData<SnPollWithStats>> runNotifierBuild( | ||||||
|     covariant PollListNotifier notifier, |     covariant PollListNotifier notifier, | ||||||
|   ) { |   ) { | ||||||
|     return notifier.build(pubName); |     return notifier.build(pubName); | ||||||
| @@ -134,7 +256,7 @@ class PollListNotifierProvider | |||||||
|   @override |   @override | ||||||
|   AutoDisposeAsyncNotifierProviderElement< |   AutoDisposeAsyncNotifierProviderElement< | ||||||
|     PollListNotifier, |     PollListNotifier, | ||||||
|     CursorPagingData<SnPoll> |     CursorPagingData<SnPollWithStats> | ||||||
|   > |   > | ||||||
|   createElement() { |   createElement() { | ||||||
|     return _PollListNotifierProviderElement(this); |     return _PollListNotifierProviderElement(this); | ||||||
| @@ -157,7 +279,7 @@ class PollListNotifierProvider | |||||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||||
| // ignore: unused_element | // ignore: unused_element | ||||||
| mixin PollListNotifierRef | mixin PollListNotifierRef | ||||||
|     on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPoll>> { |     on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnPollWithStats>> { | ||||||
|   /// The parameter `pubName` of this provider. |   /// The parameter `pubName` of this provider. | ||||||
|   String? get pubName; |   String? get pubName; | ||||||
| } | } | ||||||
| @@ -166,7 +288,7 @@ class _PollListNotifierProviderElement | |||||||
|     extends |     extends | ||||||
|         AutoDisposeAsyncNotifierProviderElement< |         AutoDisposeAsyncNotifierProviderElement< | ||||||
|           PollListNotifier, |           PollListNotifier, | ||||||
|           CursorPagingData<SnPoll> |           CursorPagingData<SnPollWithStats> | ||||||
|         > |         > | ||||||
|     with PollListNotifierRef { |     with PollListNotifierRef { | ||||||
|   _PollListNotifierProviderElement(super.provider); |   _PollListNotifierProviderElement(super.provider); | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ import 'package:island/services/responsive.dart'; | |||||||
| import 'package:island/widgets/alert.dart'; | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| import 'package:island/widgets/content/cloud_files.dart'; | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
| import 'package:island/widgets/realms/selection_dropdown.dart'; | import 'package:island/widgets/realm/realm_selection_dropdown.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import 'package:island/models/realm.dart'; | |||||||
| import 'package:island/models/webfeed.dart'; | import 'package:island/models/webfeed.dart'; | ||||||
| import 'package:island/pods/event_calendar.dart'; | import 'package:island/pods/event_calendar.dart'; | ||||||
| import 'package:island/pods/userinfo.dart'; | import 'package:island/pods/userinfo.dart'; | ||||||
|  | import 'package:island/screens/notification.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
| import 'package:island/widgets/account/fortune_graph.dart'; | import 'package:island/widgets/account/fortune_graph.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| @@ -30,6 +31,33 @@ import 'package:styled_widget/styled_widget.dart'; | |||||||
|  |  | ||||||
| part 'explore.g.dart'; | part 'explore.g.dart'; | ||||||
|  |  | ||||||
|  | Widget notificationIndicatorWidget( | ||||||
|  |   BuildContext context, { | ||||||
|  |   required int count, | ||||||
|  |   EdgeInsets? margin, | ||||||
|  | }) => Card( | ||||||
|  |   margin: margin, | ||||||
|  |   child: ListTile( | ||||||
|  |     shape: const RoundedRectangleBorder( | ||||||
|  |       borderRadius: BorderRadius.all(Radius.circular(8)), | ||||||
|  |     ), | ||||||
|  |     leading: const Icon(Symbols.notifications), | ||||||
|  |     title: Row( | ||||||
|  |       children: [ | ||||||
|  |         Text('notifications').tr().fontSize(14), | ||||||
|  |         const Gap(8), | ||||||
|  |         Badge(label: Text(count.toString())), | ||||||
|  |       ], | ||||||
|  |     ), | ||||||
|  |     trailing: const Icon(Symbols.chevron_right), | ||||||
|  |     minTileHeight: 40, | ||||||
|  |     contentPadding: EdgeInsets.only(left: 16, right: 15), | ||||||
|  |     onTap: () { | ||||||
|  |       GoRouter.of(context).pushNamed('notifications'); | ||||||
|  |     }, | ||||||
|  |   ), | ||||||
|  | ); | ||||||
|  |  | ||||||
| class ExploreScreen extends HookConsumerWidget { | class ExploreScreen extends HookConsumerWidget { | ||||||
|   const ExploreScreen({super.key}); |   const ExploreScreen({super.key}); | ||||||
|  |  | ||||||
| @@ -77,6 +105,10 @@ class ExploreScreen extends HookConsumerWidget { | |||||||
|  |  | ||||||
|     final user = ref.watch(userInfoProvider); |     final user = ref.watch(userInfoProvider); | ||||||
|  |  | ||||||
|  |     final notificationCount = ref.watch( | ||||||
|  |       notificationUnreadCountNotifierProvider, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return AppScaffold( |     return AppScaffold( | ||||||
|       isNoBackground: false, |       isNoBackground: false, | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
| @@ -185,7 +217,7 @@ class ExploreScreen extends HookConsumerWidget { | |||||||
|       floatingActionButtonLocation: TabbedFabLocation(context), |       floatingActionButtonLocation: TabbedFabLocation(context), | ||||||
|       body: Builder( |       body: Builder( | ||||||
|         builder: (context) { |         builder: (context) { | ||||||
|           final isWider = isWiderScreen(context); |           final isWide = isWideScreen(context); | ||||||
|  |  | ||||||
|           final bodyView = _buildActivityList( |           final bodyView = _buildActivityList( | ||||||
|             context, |             context, | ||||||
| @@ -193,40 +225,58 @@ class ExploreScreen extends HookConsumerWidget { | |||||||
|             currentFilter.value, |             currentFilter.value, | ||||||
|           ); |           ); | ||||||
|  |  | ||||||
|           if (isWider) { |           if (isWide) { | ||||||
|             return Row( |             return Row( | ||||||
|               children: [ |               children: [ | ||||||
|                 Flexible(flex: 3, child: bodyView.padding(left: 8)), |                 Flexible(flex: 3, child: bodyView.padding(left: 8)), | ||||||
|                 if (user.value != null) |                 if (user.value != null) | ||||||
|                   Flexible( |                   Flexible( | ||||||
|                     flex: 2, |                     flex: 2, | ||||||
|                     child: SingleChildScrollView( |                     child: Align( | ||||||
|                       child: Column( |                       alignment: Alignment.topCenter, | ||||||
|                         children: [ |                       child: SingleChildScrollView( | ||||||
|                           CheckInWidget( |                         child: Column( | ||||||
|                             margin: EdgeInsets.only( |                           children: [ | ||||||
|  |                             CheckInWidget( | ||||||
|  |                               margin: EdgeInsets.only( | ||||||
|  |                                 left: 8, | ||||||
|  |                                 right: 12, | ||||||
|  |                                 top: 16, | ||||||
|  |                               ), | ||||||
|  |                               onChecked: () { | ||||||
|  |                                 ref.invalidate( | ||||||
|  |                                   eventCalendarProvider(query.value), | ||||||
|  |                                 ); | ||||||
|  |                               }, | ||||||
|  |                             ), | ||||||
|  |                             if (notificationCount.value != null && | ||||||
|  |                                 notificationCount.value! > 0) | ||||||
|  |                               notificationIndicatorWidget( | ||||||
|  |                                 context, | ||||||
|  |                                 count: notificationCount.value ?? 0, | ||||||
|  |                                 margin: EdgeInsets.only( | ||||||
|  |                                   left: 8, | ||||||
|  |                                   right: 12, | ||||||
|  |                                   top: 8, | ||||||
|  |                                 ), | ||||||
|  |                               ), | ||||||
|  |                             PostFeaturedList().padding( | ||||||
|                               left: 8, |                               left: 8, | ||||||
|                               right: 12, |                               right: 12, | ||||||
|                               top: 16, |                               top: 8, | ||||||
|                             ), |                             ), | ||||||
|                             onChecked: () { |                             FortuneGraphWidget( | ||||||
|                               ref.invalidate( |                               margin: EdgeInsets.only( | ||||||
|                                 eventCalendarProvider(query.value), |                                 left: 8, | ||||||
|                               ); |                                 right: 12, | ||||||
|                             }, |                                 top: 8, | ||||||
|                           ), |                               ), | ||||||
|                           PostFeaturedList().padding( |                               events: events, | ||||||
|                             left: 8, |                               constrainWidth: true, | ||||||
|                             right: 12, |                               onPointSelected: onDaySelected, | ||||||
|                             top: 8, |                             ), | ||||||
|                           ), |                           ], | ||||||
|                           FortuneGraphWidget( |                         ), | ||||||
|                             margin: EdgeInsets.only(left: 8, right: 12, top: 8), |  | ||||||
|                             events: events, |  | ||||||
|                             constrainWidth: true, |  | ||||||
|                             onPointSelected: onDaySelected, |  | ||||||
|                           ), |  | ||||||
|                         ], |  | ||||||
|                       ), |                       ), | ||||||
|                     ), |                     ), | ||||||
|                   ) |                   ) | ||||||
| @@ -268,7 +318,7 @@ class ExploreScreen extends HookConsumerWidget { | |||||||
|       activityListNotifierProvider(filter).notifier, |       activityListNotifierProvider(filter).notifier, | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     final isWider = isWiderScreen(context); |     final isWide = isWideScreen(context); | ||||||
|  |  | ||||||
|     return RefreshIndicator( |     return RefreshIndicator( | ||||||
|       onRefresh: () => Future.sync(activitiesNotifier.forceRefresh), |       onRefresh: () => Future.sync(activitiesNotifier.forceRefresh), | ||||||
| @@ -283,7 +333,7 @@ class ExploreScreen extends HookConsumerWidget { | |||||||
|                 widgetCount: widgetCount, |                 widgetCount: widgetCount, | ||||||
|                 endItemView: endItemView, |                 endItemView: endItemView, | ||||||
|                 activitiesNotifier: activitiesNotifier, |                 activitiesNotifier: activitiesNotifier, | ||||||
|                 contentOnly: isWider || filter != null, |                 contentOnly: isWide || filter != null, | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|       ), |       ), | ||||||
| @@ -380,6 +430,10 @@ class _ActivityListView extends HookConsumerWidget { | |||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final user = ref.watch(userInfoProvider); |     final user = ref.watch(userInfoProvider); | ||||||
|  |  | ||||||
|  |     final notificationCount = ref.watch( | ||||||
|  |       notificationUnreadCountNotifierProvider, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return CustomScrollView( |     return CustomScrollView( | ||||||
|       slivers: [ |       slivers: [ | ||||||
|         SliverGap(12), |         SliverGap(12), | ||||||
| @@ -393,6 +447,14 @@ class _ActivityListView extends HookConsumerWidget { | |||||||
|           SliverToBoxAdapter( |           SliverToBoxAdapter( | ||||||
|             child: PostFeaturedList().padding(horizontal: 8, bottom: 4, top: 4), |             child: PostFeaturedList().padding(horizontal: 8, bottom: 4, top: 4), | ||||||
|           ), |           ), | ||||||
|  |         if (!contentOnly) | ||||||
|  |           SliverToBoxAdapter( | ||||||
|  |             child: notificationIndicatorWidget( | ||||||
|  |               context, | ||||||
|  |               count: notificationCount.value ?? 0, | ||||||
|  |               margin: EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 4), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|         SliverList.builder( |         SliverList.builder( | ||||||
|           itemCount: widgetCount, |           itemCount: widgetCount, | ||||||
|           itemBuilder: (context, index) { |           itemBuilder: (context, index) { | ||||||
|   | |||||||
| @@ -3,14 +3,17 @@ import 'dart:math' as math; | |||||||
|  |  | ||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:gap/gap.dart'; | ||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/pods/websocket.dart'; | import 'package:island/pods/websocket.dart'; | ||||||
| import 'package:island/route.dart'; | import 'package:island/route.dart'; | ||||||
|  | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| import 'package:island/widgets/content/markdown.dart'; | import 'package:island/widgets/content/markdown.dart'; | ||||||
|  | import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||||
| import 'package:relative_time/relative_time.dart'; | import 'package:relative_time/relative_time.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||||
| @@ -62,6 +65,10 @@ class NotificationUnreadCountNotifier | |||||||
|     final current = await future; |     final current = await future; | ||||||
|     state = AsyncData(math.max(current - count, 0)); |     state = AsyncData(math.max(current - count, 0)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void clear() async { | ||||||
|  |     state = AsyncData(0); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @riverpod | @riverpod | ||||||
| @@ -111,8 +118,27 @@ class NotificationScreen extends HookConsumerWidget { | |||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     Future<void> markAllRead() async { | ||||||
|  |       showLoadingModal(context); | ||||||
|  |       final apiClient = ref.watch(apiClientProvider); | ||||||
|  |       await apiClient.post('/pusher/notifications/all/read'); | ||||||
|  |       if (!context.mounted) return; | ||||||
|  |       hideLoadingModal(context); | ||||||
|  |       ref.invalidate(notificationListNotifierProvider); | ||||||
|  |       ref.watch(notificationUnreadCountNotifierProvider.notifier).clear(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return AppScaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar(title: const Text('notifications').tr()), |       appBar: AppBar( | ||||||
|  |         title: const Text('notifications').tr(), | ||||||
|  |         actions: [ | ||||||
|  |           IconButton( | ||||||
|  |             onPressed: markAllRead, | ||||||
|  |             icon: const Icon(Symbols.mark_as_unread), | ||||||
|  |           ), | ||||||
|  |           const Gap(8), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|       body: PagingHelperView( |       body: PagingHelperView( | ||||||
|         provider: notificationListNotifierProvider, |         provider: notificationListNotifierProvider, | ||||||
|         futureRefreshable: notificationListNotifierProvider.future, |         futureRefreshable: notificationListNotifierProvider.future, | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ part of 'notification.dart'; | |||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
|  |  | ||||||
| String _$notificationUnreadCountNotifierHash() => | String _$notificationUnreadCountNotifierHash() => | ||||||
|     r'd199abf0d16944587e747798399a267a790341f3'; |     r'0763b66bd64e5a9b7c317887e109ab367515dfa4'; | ||||||
|  |  | ||||||
| /// See also [NotificationUnreadCountNotifier]. | /// See also [NotificationUnreadCountNotifier]. | ||||||
| @ProviderFor(NotificationUnreadCountNotifier) | @ProviderFor(NotificationUnreadCountNotifier) | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import 'package:island/widgets/alert.dart'; | |||||||
| import 'package:island/models/poll.dart'; | import 'package:island/models/poll.dart'; | ||||||
| import 'package:island/widgets/app_scaffold.dart'; | import 'package:island/widgets/app_scaffold.dart'; | ||||||
| import 'package:uuid/uuid.dart'; | import 'package:uuid/uuid.dart'; | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  |  | ||||||
| class PollEditorState { | class PollEditorState { | ||||||
|   String? id; // for editing |   String? id; // for editing | ||||||
| @@ -110,7 +111,7 @@ class PollEditor extends Notifier<PollEditorState> { | |||||||
|               ? [ |               ? [ | ||||||
|                 SnPollOption( |                 SnPollOption( | ||||||
|                   id: const Uuid().v4(), |                   id: const Uuid().v4(), | ||||||
|                   label: 'Option 1', |                   label: 'pollOptionDefaultLabel'.tr(), | ||||||
|                   order: 0, |                   order: 0, | ||||||
|                 ), |                 ), | ||||||
|               ] |               ] | ||||||
| @@ -191,7 +192,7 @@ class PollEditor extends Notifier<PollEditorState> { | |||||||
|                 : [ |                 : [ | ||||||
|                   SnPollOption( |                   SnPollOption( | ||||||
|                     id: const Uuid().v4(), |                     id: const Uuid().v4(), | ||||||
|                     label: 'Option 1', |                     label: 'pollOptionDefaultLabel'.tr(), | ||||||
|                     order: 0, |                     order: 0, | ||||||
|                   ), |                   ), | ||||||
|                 ]) |                 ]) | ||||||
| @@ -389,7 +390,7 @@ class PollEditorScreen extends ConsumerWidget { | |||||||
|                 data: body, |                 data: body, | ||||||
|               )); |               )); | ||||||
|  |  | ||||||
|       showSnackBar(isUpdate ? 'Poll updated.' : 'Poll created.'); |       showSnackBar(isUpdate ? 'pollUpdated'.tr() : 'pollCreated'.tr()); | ||||||
|  |  | ||||||
|       if (!context.mounted) return; |       if (!context.mounted) return; | ||||||
|       Navigator.of(context).maybePop(res.data); |       Navigator.of(context).maybePop(res.data); | ||||||
| @@ -416,11 +417,11 @@ class PollEditorScreen extends ConsumerWidget { | |||||||
|  |  | ||||||
|     return AppScaffold( |     return AppScaffold( | ||||||
|       appBar: AppBar( |       appBar: AppBar( | ||||||
|         title: Text(model.id == null ? 'Create Poll' : 'Edit Poll'), |         title: Text(model.id == null ? 'pollCreate'.tr() : 'pollEdit'.tr()), | ||||||
|         actions: [ |         actions: [ | ||||||
|           if (kDebugMode) |           if (kDebugMode) | ||||||
|             IconButton( |             IconButton( | ||||||
|               tooltip: 'Preview JSON (debug)', |               tooltip: 'pollPreviewJsonDebug'.tr(), | ||||||
|               onPressed: () { |               onPressed: () { | ||||||
|                 _showDebugPreview(context, model); |                 _showDebugPreview(context, model); | ||||||
|               }, |               }, | ||||||
| @@ -439,8 +440,8 @@ class PollEditorScreen extends ConsumerWidget { | |||||||
|                 children: [ |                 children: [ | ||||||
|                   TextFormField( |                   TextFormField( | ||||||
|                     initialValue: model.title ?? '', |                     initialValue: model.title ?? '', | ||||||
|                     decoration: const InputDecoration( |                     decoration: InputDecoration( | ||||||
|                       labelText: 'Title', |                       labelText: 'title'.tr(), | ||||||
|                       border: OutlineInputBorder( |                       border: OutlineInputBorder( | ||||||
|                         borderRadius: BorderRadius.all(Radius.circular(16)), |                         borderRadius: BorderRadius.all(Radius.circular(16)), | ||||||
|                       ), |                       ), | ||||||
| @@ -452,7 +453,7 @@ class PollEditorScreen extends ConsumerWidget { | |||||||
|                         (_) => FocusManager.instance.primaryFocus?.unfocus(), |                         (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|                     validator: (v) { |                     validator: (v) { | ||||||
|                       if (v == null || v.trim().isEmpty) { |                       if (v == null || v.trim().isEmpty) { | ||||||
|                         return 'Title is required'; |                         return 'pollTitleRequired'.tr(); | ||||||
|                       } |                       } | ||||||
|                       return null; |                       return null; | ||||||
|                     }, |                     }, | ||||||
| @@ -460,8 +461,8 @@ class PollEditorScreen extends ConsumerWidget { | |||||||
|                   const Gap(12), |                   const Gap(12), | ||||||
|                   TextFormField( |                   TextFormField( | ||||||
|                     initialValue: model.description ?? '', |                     initialValue: model.description ?? '', | ||||||
|                     decoration: const InputDecoration( |                     decoration: InputDecoration( | ||||||
|                       labelText: 'Description', |                       labelText: 'description'.tr(), | ||||||
|                       alignLabelWithHint: true, |                       alignLabelWithHint: true, | ||||||
|                       border: OutlineInputBorder( |                       border: OutlineInputBorder( | ||||||
|                         borderRadius: BorderRadius.all(Radius.circular(16)), |                         borderRadius: BorderRadius.all(Radius.circular(16)), | ||||||
| @@ -482,7 +483,7 @@ class PollEditorScreen extends ConsumerWidget { | |||||||
|                   Row( |                   Row( | ||||||
|                     children: [ |                     children: [ | ||||||
|                       Text( |                       Text( | ||||||
|                         'Questions', |                         'questions'.tr(), | ||||||
|                         style: Theme.of(context).textTheme.titleLarge, |                         style: Theme.of(context).textTheme.titleLarge, | ||||||
|                       ), |                       ), | ||||||
|                       const Spacer(), |                       const Spacer(), | ||||||
| @@ -495,7 +496,7 @@ class PollEditorScreen extends ConsumerWidget { | |||||||
|                                   : controller.open(); |                                   : controller.open(); | ||||||
|                             }, |                             }, | ||||||
|                             icon: const Icon(Icons.add), |                             icon: const Icon(Icons.add), | ||||||
|                             label: const Text('Add question'), |                             label: Text('pollAddQuestion'.tr()), | ||||||
|                           ); |                           ); | ||||||
|                         }, |                         }, | ||||||
|                         menuChildren: |                         menuChildren: | ||||||
| @@ -514,9 +515,9 @@ class PollEditorScreen extends ConsumerWidget { | |||||||
|                   const Gap(8), |                   const Gap(8), | ||||||
|                   if (model.questions.isEmpty) |                   if (model.questions.isEmpty) | ||||||
|                     _EmptyState( |                     _EmptyState( | ||||||
|                       title: 'No questions yet', |                       title: 'pollNoQuestionsYet'.tr(), | ||||||
|                       subtitle: |                       subtitle: | ||||||
|                           'Use "Add question" to start building your poll.', |                           'pollNoQuestionsHint'.tr(), | ||||||
|                     ) |                     ) | ||||||
|                   else |                   else | ||||||
|                     ReorderableListView.builder( |                     ReorderableListView.builder( | ||||||
| @@ -585,7 +586,7 @@ class PollEditorScreen extends ConsumerWidget { | |||||||
|                   Navigator.of(context).maybePop(); |                   Navigator.of(context).maybePop(); | ||||||
|                 }, |                 }, | ||||||
|                 icon: const Icon(Icons.close), |                 icon: const Icon(Icons.close), | ||||||
|                 label: const Text('Cancel'), |                 label: Text('cancel'.tr()), | ||||||
|               ), |               ), | ||||||
|               const Spacer(), |               const Spacer(), | ||||||
|               FilledButton.icon( |               FilledButton.icon( | ||||||
| @@ -593,7 +594,7 @@ class PollEditorScreen extends ConsumerWidget { | |||||||
|                   _submitPoll(context, ref); |                   _submitPoll(context, ref); | ||||||
|                 }, |                 }, | ||||||
|                 icon: const Icon(Icons.cloud_upload_outlined), |                 icon: const Icon(Icons.cloud_upload_outlined), | ||||||
|                 label: Text(model.id == null ? 'Create' : 'Update'), |                 label: Text(model.id == null ? 'create'.tr() : 'update'.tr()), | ||||||
|               ), |               ), | ||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
| @@ -637,14 +638,14 @@ class PollEditorScreen extends ConsumerWidget { | |||||||
|       context: context, |       context: context, | ||||||
|       builder: |       builder: | ||||||
|           (_) => AlertDialog( |           (_) => AlertDialog( | ||||||
|             title: const Text('Debug Preview'), |             title: Text('pollDebugPreview'.tr()), | ||||||
|             content: SingleChildScrollView( |             content: SingleChildScrollView( | ||||||
|               child: SelectableText(buf.toString()), |               child: SelectableText(buf.toString()), | ||||||
|             ), |             ), | ||||||
|             actions: [ |             actions: [ | ||||||
|               TextButton( |               TextButton( | ||||||
|                 onPressed: () => Navigator.of(context).pop(), |                 onPressed: () => Navigator.of(context).pop(), | ||||||
|                 child: const Text('Close'), |                 child: Text('close'.tr()), | ||||||
|               ), |               ), | ||||||
|             ], |             ], | ||||||
|           ), |           ), | ||||||
| @@ -673,15 +674,15 @@ IconData _iconForType(SnPollQuestionType t) { | |||||||
| String _labelForType(SnPollQuestionType t) { | String _labelForType(SnPollQuestionType t) { | ||||||
|   switch (t) { |   switch (t) { | ||||||
|     case SnPollQuestionType.singleChoice: |     case SnPollQuestionType.singleChoice: | ||||||
|       return 'Single choice'; |       return 'pollQuestionTypeSingleChoice'.tr(); | ||||||
|     case SnPollQuestionType.multipleChoice: |     case SnPollQuestionType.multipleChoice: | ||||||
|       return 'Multiple choice'; |       return 'pollQuestionTypeMultipleChoice'.tr(); | ||||||
|     case SnPollQuestionType.freeText: |     case SnPollQuestionType.freeText: | ||||||
|       return 'Free text'; |       return 'pollQuestionTypeFreeText'.tr(); | ||||||
|     case SnPollQuestionType.yesNo: |     case SnPollQuestionType.yesNo: | ||||||
|       return 'Yes / No'; |       return 'pollQuestionTypeYesNo'.tr(); | ||||||
|     case SnPollQuestionType.rating: |     case SnPollQuestionType.rating: | ||||||
|       return 'Rating'; |       return 'pollQuestionTypeRating'.tr(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -698,8 +699,8 @@ class _EndDatePicker extends StatelessWidget { | |||||||
|       children: [ |       children: [ | ||||||
|         Expanded( |         Expanded( | ||||||
|           child: InputDecorator( |           child: InputDecorator( | ||||||
|             decoration: const InputDecoration( |             decoration: InputDecoration( | ||||||
|               labelText: 'End date & time (optional)', |               labelText: 'pollEndDateOptional'.tr(), | ||||||
|               border: OutlineInputBorder( |               border: OutlineInputBorder( | ||||||
|                 borderRadius: BorderRadius.all(Radius.circular(16)), |                 borderRadius: BorderRadius.all(Radius.circular(16)), | ||||||
|               ), |               ), | ||||||
| @@ -711,7 +712,7 @@ class _EndDatePicker extends StatelessWidget { | |||||||
|                 Icon(Icons.event, color: Theme.of(context).colorScheme.primary), |                 Icon(Icons.event, color: Theme.of(context).colorScheme.primary), | ||||||
|                 Text( |                 Text( | ||||||
|                   value == null |                   value == null | ||||||
|                       ? 'Not set' |                       ? 'notSet'.tr() | ||||||
|                       : MaterialLocalizations.of( |                       : MaterialLocalizations.of( | ||||||
|                         context, |                         context, | ||||||
|                       ).formatFullDate(value!), |                       ).formatFullDate(value!), | ||||||
| @@ -759,12 +760,12 @@ class _EndDatePicker extends StatelessWidget { | |||||||
|                     ); |                     ); | ||||||
|                     onChanged(dt); |                     onChanged(dt); | ||||||
|                   }, |                   }, | ||||||
|                   child: const Text('Pick'), |                   child: Text('pick'.tr()), | ||||||
|                 ), |                 ), | ||||||
|                 if (value != null) |                 if (value != null) | ||||||
|                   TextButton( |                   TextButton( | ||||||
|                     onPressed: () => onChanged(null), |                     onPressed: () => onChanged(null), | ||||||
|                     child: const Text('Clear'), |                     child: Text('clear'.tr()), | ||||||
|                   ), |                   ), | ||||||
|               ], |               ], | ||||||
|             ), |             ), | ||||||
| @@ -799,7 +800,7 @@ class _QuestionHeader extends StatelessWidget { | |||||||
|         child: const Icon(Icons.drag_handle), |         child: const Icon(Icons.drag_handle), | ||||||
|       ), |       ), | ||||||
|       title: Text( |       title: Text( | ||||||
|         question.title.isEmpty ? 'Untitled question' : question.title, |         question.title.isEmpty ? 'pollUntitledQuestion'.tr() : question.title, | ||||||
|         maxLines: 1, |         maxLines: 1, | ||||||
|         overflow: TextOverflow.ellipsis, |         overflow: TextOverflow.ellipsis, | ||||||
|       ), |       ), | ||||||
| @@ -808,17 +809,17 @@ class _QuestionHeader extends StatelessWidget { | |||||||
|         spacing: 4, |         spacing: 4, | ||||||
|         children: [ |         children: [ | ||||||
|           IconButton( |           IconButton( | ||||||
|             tooltip: 'Move up', |             tooltip: 'moveUp'.tr(), | ||||||
|             onPressed: onMoveUp, |             onPressed: onMoveUp, | ||||||
|             icon: const Icon(Icons.arrow_upward), |             icon: const Icon(Icons.arrow_upward), | ||||||
|           ), |           ), | ||||||
|           IconButton( |           IconButton( | ||||||
|             tooltip: 'Move down', |             tooltip: 'moveDown'.tr(), | ||||||
|             onPressed: onMoveDown, |             onPressed: onMoveDown, | ||||||
|             icon: const Icon(Icons.arrow_downward), |             icon: const Icon(Icons.arrow_downward), | ||||||
|           ), |           ), | ||||||
|           IconButton( |           IconButton( | ||||||
|             tooltip: 'Delete', |             tooltip: 'delete'.tr(), | ||||||
|             onPressed: onDelete, |             onPressed: onDelete, | ||||||
|             icon: const Icon(Icons.delete_outline), |             icon: const Icon(Icons.delete_outline), | ||||||
|             color: Theme.of(context).colorScheme.error, |             color: Theme.of(context).colorScheme.error, | ||||||
| @@ -853,7 +854,7 @@ class _QuestionEditor extends ConsumerWidget { | |||||||
|               onChanged: (t) => notifier.setQuestionType(index, t), |               onChanged: (t) => notifier.setQuestionType(index, t), | ||||||
|             ), |             ), | ||||||
|             FilterChip( |             FilterChip( | ||||||
|               label: const Text('Required'), |               label: Text('required'.tr()), | ||||||
|               selected: question.isRequired, |               selected: question.isRequired, | ||||||
|               onSelected: (v) => notifier.setQuestionRequired(index, v), |               onSelected: (v) => notifier.setQuestionRequired(index, v), | ||||||
|               avatar: Icon( |               avatar: Icon( | ||||||
| @@ -867,8 +868,8 @@ class _QuestionEditor extends ConsumerWidget { | |||||||
|         const Gap(12), |         const Gap(12), | ||||||
|         TextFormField( |         TextFormField( | ||||||
|           initialValue: question.title, |           initialValue: question.title, | ||||||
|           decoration: const InputDecoration( |           decoration: InputDecoration( | ||||||
|             labelText: 'Question title', |             labelText: 'pollQuestionTitle'.tr(), | ||||||
|             border: OutlineInputBorder( |             border: OutlineInputBorder( | ||||||
|               borderRadius: BorderRadius.all(Radius.circular(16)), |               borderRadius: BorderRadius.all(Radius.circular(16)), | ||||||
|             ), |             ), | ||||||
| @@ -879,7 +880,7 @@ class _QuestionEditor extends ConsumerWidget { | |||||||
|           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), |           onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|           validator: (v) { |           validator: (v) { | ||||||
|             if (v == null || v.trim().isEmpty) { |             if (v == null || v.trim().isEmpty) { | ||||||
|               return 'Question title is required'; |               return 'pollQuestionTitleRequired'.tr(); | ||||||
|             } |             } | ||||||
|             return null; |             return null; | ||||||
|           }, |           }, | ||||||
| @@ -887,8 +888,8 @@ class _QuestionEditor extends ConsumerWidget { | |||||||
|         const Gap(12), |         const Gap(12), | ||||||
|         TextFormField( |         TextFormField( | ||||||
|           initialValue: question.description ?? '', |           initialValue: question.description ?? '', | ||||||
|           decoration: const InputDecoration( |           decoration: InputDecoration( | ||||||
|             labelText: 'Question description (optional)', |             labelText: 'pollQuestionDescriptionOptional'.tr(), | ||||||
|             border: OutlineInputBorder( |             border: OutlineInputBorder( | ||||||
|               borderRadius: BorderRadius.all(Radius.circular(16)), |               borderRadius: BorderRadius.all(Radius.circular(16)), | ||||||
|             ), |             ), | ||||||
| @@ -902,7 +903,7 @@ class _QuestionEditor extends ConsumerWidget { | |||||||
|         ), |         ), | ||||||
|         if (question.options != null) ...[ |         if (question.options != null) ...[ | ||||||
|           const Gap(16), |           const Gap(16), | ||||||
|           Text('Options', style: Theme.of(context).textTheme.titleMedium), |           Text('options'.tr(), style: Theme.of(context).textTheme.titleMedium), | ||||||
|           const Gap(8), |           const Gap(8), | ||||||
|           _OptionsEditor(index: index, options: question.options!), |           _OptionsEditor(index: index, options: question.options!), | ||||||
|           const Gap(4), |           const Gap(4), | ||||||
| @@ -911,7 +912,7 @@ class _QuestionEditor extends ConsumerWidget { | |||||||
|             child: OutlinedButton.icon( |             child: OutlinedButton.icon( | ||||||
|               onPressed: () => notifier.addOption(index), |               onPressed: () => notifier.addOption(index), | ||||||
|               icon: const Icon(Icons.add), |               icon: const Icon(Icons.add), | ||||||
|               label: const Text('Add option'), |               label: Text('pollAddOption'.tr()), | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|         ], |         ], | ||||||
| @@ -937,8 +938,8 @@ class _QuestionTypePicker extends StatelessWidget { | |||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     return DropdownButtonFormField<SnPollQuestionType>( |     return DropdownButtonFormField<SnPollQuestionType>( | ||||||
|       value: value, |       value: value, | ||||||
|       decoration: const InputDecoration( |       decoration: InputDecoration( | ||||||
|         labelText: 'Type', |         labelText: 'Type'.tr(), | ||||||
|         border: OutlineInputBorder( |         border: OutlineInputBorder( | ||||||
|           borderRadius: BorderRadius.all(Radius.circular(16)), |           borderRadius: BorderRadius.all(Radius.circular(16)), | ||||||
|         ), |         ), | ||||||
| @@ -987,8 +988,8 @@ class _OptionsEditor extends ConsumerWidget { | |||||||
|                   child: TextFormField( |                   child: TextFormField( | ||||||
|                     key: ValueKey(options[i].id), |                     key: ValueKey(options[i].id), | ||||||
|                     initialValue: options[i].label, |                     initialValue: options[i].label, | ||||||
|                     decoration: const InputDecoration( |                     decoration: InputDecoration( | ||||||
|                       labelText: 'Option label', |                       labelText: 'pollOptionLabel'.tr(), | ||||||
|                       border: OutlineInputBorder( |                       border: OutlineInputBorder( | ||||||
|                         borderRadius: BorderRadius.all(Radius.circular(16)), |                         borderRadius: BorderRadius.all(Radius.circular(16)), | ||||||
|                       ), |                       ), | ||||||
| @@ -1003,7 +1004,7 @@ class _OptionsEditor extends ConsumerWidget { | |||||||
|                 SizedBox( |                 SizedBox( | ||||||
|                   width: 40, |                   width: 40, | ||||||
|                   child: IconButton( |                   child: IconButton( | ||||||
|                     tooltip: 'Move up', |                     tooltip: 'moveUp'.tr(), | ||||||
|                     onPressed: |                     onPressed: | ||||||
|                         i > 0 ? () => notifier.moveOptionUp(index, i) : null, |                         i > 0 ? () => notifier.moveOptionUp(index, i) : null, | ||||||
|                     icon: const Icon(Icons.arrow_upward), |                     icon: const Icon(Icons.arrow_upward), | ||||||
| @@ -1012,7 +1013,7 @@ class _OptionsEditor extends ConsumerWidget { | |||||||
|                 SizedBox( |                 SizedBox( | ||||||
|                   width: 40, |                   width: 40, | ||||||
|                   child: IconButton( |                   child: IconButton( | ||||||
|                     tooltip: 'Move down', |                     tooltip: 'moveDown'.tr(), | ||||||
|                     onPressed: |                     onPressed: | ||||||
|                         i < options.length - 1 |                         i < options.length - 1 | ||||||
|                             ? () => notifier.moveOptionDown(index, i) |                             ? () => notifier.moveOptionDown(index, i) | ||||||
| @@ -1023,7 +1024,7 @@ class _OptionsEditor extends ConsumerWidget { | |||||||
|                 SizedBox( |                 SizedBox( | ||||||
|                   width: 40, |                   width: 40, | ||||||
|                   child: IconButton( |                   child: IconButton( | ||||||
|                     tooltip: 'Delete', |                     tooltip: 'delete'.tr(), | ||||||
|                     onPressed: () => notifier.removeOption(index, i), |                     onPressed: () => notifier.removeOption(index, i), | ||||||
|                     icon: const Icon(Icons.close), |                     icon: const Icon(Icons.close), | ||||||
|                   ), |                   ), | ||||||
| @@ -1048,7 +1049,7 @@ class _TextAnswerPreview extends StatelessWidget { | |||||||
|       maxLines: long ? 4 : 1, |       maxLines: long ? 4 : 1, | ||||||
|       decoration: InputDecoration( |       decoration: InputDecoration( | ||||||
|         labelText: |         labelText: | ||||||
|             long ? 'Long text answer (preview)' : 'Short text answer (preview)', |             long ? 'pollLongTextAnswerPreview'.tr() : 'pollShortTextAnswerPreview'.tr(), | ||||||
|         border: const OutlineInputBorder( |         border: const OutlineInputBorder( | ||||||
|           borderRadius: BorderRadius.all(Radius.circular(16)), |           borderRadius: BorderRadius.all(Radius.circular(16)), | ||||||
|         ), |         ), | ||||||
| @@ -1082,9 +1083,9 @@ class _EmptyState extends StatelessWidget { | |||||||
|             child: Column( |             child: Column( | ||||||
|               crossAxisAlignment: CrossAxisAlignment.start, |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|               children: [ |               children: [ | ||||||
|                 Text(title, style: Theme.of(context).textTheme.titleMedium), |                 Text('pollNoQuestionsYet'.tr(), style: Theme.of(context).textTheme.titleMedium), | ||||||
|                 const Gap(4), |                 const Gap(4), | ||||||
|                 Text(subtitle, style: Theme.of(context).textTheme.bodyMedium), |                 Text('pollNoQuestionsHint'.tr(), style: Theme.of(context).textTheme.bodyMedium), | ||||||
|               ], |               ], | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|   | |||||||
| @@ -89,6 +89,9 @@ class ArticleComposeScreen extends HookConsumerWidget { | |||||||
|     }, [state]); |     }, [state]); | ||||||
|  |  | ||||||
|     final showPreview = useState(false); |     final showPreview = useState(false); | ||||||
|  |     final isAttachmentsExpanded = useState( | ||||||
|  |       true, | ||||||
|  |     ); // New state for attachments section | ||||||
|  |  | ||||||
|     // Initialize publisher once when data is available |     // Initialize publisher once when data is available | ||||||
|     useEffect(() { |     useEffect(() { | ||||||
| @@ -297,71 +300,88 @@ class ArticleComposeScreen extends HookConsumerWidget { | |||||||
|                 valueListenable: state.attachments, |                 valueListenable: state.attachments, | ||||||
|                 builder: (context, attachments, _) { |                 builder: (context, attachments, _) { | ||||||
|                   if (attachments.isEmpty) return const SizedBox.shrink(); |                   if (attachments.isEmpty) return const SizedBox.shrink(); | ||||||
|                   return Column( |                   return Theme( | ||||||
|                     crossAxisAlignment: CrossAxisAlignment.start, |                     data: Theme.of( | ||||||
|                     children: [ |                       context, | ||||||
|                       const Gap(16), |                     ).copyWith(dividerColor: Colors.transparent), | ||||||
|                       Text( |                     child: ExpansionTile( | ||||||
|                         'articleAttachmentHint'.tr(), |                       initiallyExpanded: isAttachmentsExpanded.value, | ||||||
|                         style: Theme.of(context).textTheme.bodySmall?.copyWith( |                       onExpansionChanged: (expanded) { | ||||||
|                           color: Theme.of(context).colorScheme.onSurfaceVariant, |                         isAttachmentsExpanded.value = expanded; | ||||||
|                         ), |                       }, | ||||||
|                       ).padding(bottom: 8), |                       collapsedBackgroundColor: | ||||||
|                       ValueListenableBuilder<Map<int, double>>( |                           Theme.of(context).colorScheme.surfaceContainer, | ||||||
|                         valueListenable: state.attachmentProgress, |                       title: Column( | ||||||
|                         builder: (context, progressMap, _) { |                         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                           return Wrap( |                         children: [ | ||||||
|                             spacing: 8, |                           Text('attachments').tr(), | ||||||
|                             runSpacing: 8, |                           Text( | ||||||
|                             children: [ |                             'articleAttachmentHint'.tr(), | ||||||
|                               for (var idx = 0; idx < attachments.length; idx++) |                             style: Theme.of( | ||||||
|                                 SizedBox( |                               context, | ||||||
|                                   width: 280, |                             ).textTheme.bodySmall?.copyWith( | ||||||
|                                   height: 280, |                               color: | ||||||
|                                   child: AttachmentPreview( |                                   Theme.of( | ||||||
|                                     item: attachments[idx], |                                     context, | ||||||
|                                     progress: progressMap[idx], |                                   ).colorScheme.onSurfaceVariant, | ||||||
|                                     onRequestUpload: |                             ), | ||||||
|                                         () => ComposeLogic.uploadAttachment( |                           ), | ||||||
|                                           ref, |                         ], | ||||||
|                                           state, |  | ||||||
|                                           idx, |  | ||||||
|                                         ), |  | ||||||
|                                     onUpdate: |  | ||||||
|                                         (value) => |  | ||||||
|                                             ComposeLogic.updateAttachment( |  | ||||||
|                                               state, |  | ||||||
|                                               value, |  | ||||||
|                                               idx, |  | ||||||
|                                             ), |  | ||||||
|                                     onDelete: |  | ||||||
|                                         () => ComposeLogic.deleteAttachment( |  | ||||||
|                                           ref, |  | ||||||
|                                           state, |  | ||||||
|                                           idx, |  | ||||||
|                                         ), |  | ||||||
|                                     onMove: (delta) { |  | ||||||
|                                       state |  | ||||||
|                                           .attachments |  | ||||||
|                                           .value = ComposeLogic.moveAttachment( |  | ||||||
|                                         state.attachments.value, |  | ||||||
|                                         idx, |  | ||||||
|                                         delta, |  | ||||||
|                                       ); |  | ||||||
|                                     }, |  | ||||||
|                                     onInsert: |  | ||||||
|                                         () => ComposeLogic.insertAttachment( |  | ||||||
|                                           ref, |  | ||||||
|                                           state, |  | ||||||
|                                           idx, |  | ||||||
|                                         ), |  | ||||||
|                                   ), |  | ||||||
|                                 ), |  | ||||||
|                             ], |  | ||||||
|                           ); |  | ||||||
|                         }, |  | ||||||
|                       ), |                       ), | ||||||
|                     ], |                       children: [ | ||||||
|  |                         ValueListenableBuilder<Map<int, double>>( | ||||||
|  |                           valueListenable: state.attachmentProgress, | ||||||
|  |                           builder: (context, progressMap, _) { | ||||||
|  |                             return Wrap( | ||||||
|  |                               runSpacing: 8, | ||||||
|  |                               spacing: 8, | ||||||
|  |                               children: [ | ||||||
|  |                                 for ( | ||||||
|  |                                   var idx = 0; | ||||||
|  |                                   idx < attachments.length; | ||||||
|  |                                   idx++ | ||||||
|  |                                 ) | ||||||
|  |                                   SizedBox( | ||||||
|  |                                     width: 180, | ||||||
|  |                                     height: 180, | ||||||
|  |                                     child: AttachmentPreview( | ||||||
|  |                                       isCompact: true, | ||||||
|  |                                       item: attachments[idx], | ||||||
|  |                                       progress: progressMap[idx], | ||||||
|  |                                       onRequestUpload: | ||||||
|  |                                           () => ComposeLogic.uploadAttachment( | ||||||
|  |                                             ref, | ||||||
|  |                                             state, | ||||||
|  |                                             idx, | ||||||
|  |                                           ), | ||||||
|  |                                       onUpdate: | ||||||
|  |                                           (value) => | ||||||
|  |                                               ComposeLogic.updateAttachment( | ||||||
|  |                                                 state, | ||||||
|  |                                                 value, | ||||||
|  |                                                 idx, | ||||||
|  |                                               ), | ||||||
|  |                                       onDelete: | ||||||
|  |                                           () => ComposeLogic.deleteAttachment( | ||||||
|  |                                             ref, | ||||||
|  |                                             state, | ||||||
|  |                                             idx, | ||||||
|  |                                           ), | ||||||
|  |                                       onInsert: | ||||||
|  |                                           () => ComposeLogic.insertAttachment( | ||||||
|  |                                             ref, | ||||||
|  |                                             state, | ||||||
|  |                                             idx, | ||||||
|  |                                           ), | ||||||
|  |                                     ), | ||||||
|  |                                   ), | ||||||
|  |                               ], | ||||||
|  |                             ); | ||||||
|  |                           }, | ||||||
|  |                         ), | ||||||
|  |                         Gap(16), | ||||||
|  |                       ], | ||||||
|  |                     ), | ||||||
|                   ); |                   ); | ||||||
|                 }, |                 }, | ||||||
|               ), |               ), | ||||||
|   | |||||||
| @@ -51,12 +51,12 @@ class PostSearchNotifier | |||||||
|       final offset = cursor == null ? 0 : int.parse(cursor); |       final offset = cursor == null ? 0 : int.parse(cursor); | ||||||
|  |  | ||||||
|       final response = await client.get( |       final response = await client.get( | ||||||
|         '/sphere/posts/search', |         '/sphere/posts', | ||||||
|         queryParameters: { |         queryParameters: { | ||||||
|           'query': _currentQuery, |           'query': _currentQuery, | ||||||
|           'offset': offset, |           'offset': offset, | ||||||
|           'take': _pageSize, |           'take': _pageSize, | ||||||
|           'useVector': false, |           'vector': false, | ||||||
|         }, |         }, | ||||||
|       ); |       ); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import 'package:gap/gap.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/post.dart'; | import 'package:island/models/post.dart'; | ||||||
| import 'package:island/models/publisher.dart'; | import 'package:island/models/publisher.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/config.dart'; | import 'package:island/pods/config.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/services/color.dart'; | import 'package:island/services/color.dart'; | ||||||
| @@ -147,7 +147,11 @@ class PublisherProfileScreen extends HookConsumerWidget { | |||||||
|             ), |             ), | ||||||
|             backgroundColor: Theme.of(context).colorScheme.primary, |             backgroundColor: Theme.of(context).colorScheme.primary, | ||||||
|             offset: Offset(0, 48), |             offset: Offset(0, 48), | ||||||
|             child: ProfilePictureWidget(file: data.picture, radius: 32), |             child: ProfilePictureWidget( | ||||||
|  |               file: data.picture, | ||||||
|  |               radius: 32, | ||||||
|  |               borderRadius: data.type == 0 ? null : 12, | ||||||
|  |             ), | ||||||
|           ), |           ), | ||||||
|           onTap: () { |           onTap: () { | ||||||
|             Navigator.pop(context, true); |             Navigator.pop(context, true); | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; | |||||||
| import 'package:go_router/go_router.dart'; | import 'package:go_router/go_router.dart'; | ||||||
| import 'package:island/main.dart'; | import 'package:island/main.dart'; | ||||||
| import 'package:island/route.dart'; | import 'package:island/route.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/websocket.dart'; | import 'package:island/pods/websocket.dart'; | ||||||
| import 'package:island/widgets/app_notification.dart'; | import 'package:island/widgets/app_notification.dart'; | ||||||
| import 'package:top_snackbar_flutter/top_snack_bar.dart'; | import 'package:top_snackbar_flutter/top_snack_bar.dart'; | ||||||
| @@ -26,7 +26,12 @@ StreamSubscription<WebSocketPacket> setupNotificationListener( | |||||||
|       final notification = SnNotification.fromJson(pkt.data!); |       final notification = SnNotification.fromJson(pkt.data!); | ||||||
|       showTopSnackBar( |       showTopSnackBar( | ||||||
|         globalOverlay.currentState!, |         globalOverlay.currentState!, | ||||||
|         NotificationCard(notification: notification), |         Center( | ||||||
|  |           child: ConstrainedBox( | ||||||
|  |             constraints: const BoxConstraints(maxWidth: 480), | ||||||
|  |             child: NotificationCard(notification: notification), | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|         onTap: () { |         onTap: () { | ||||||
|           if (notification.meta['action_uri'] != null) { |           if (notification.meta['action_uri'] != null) { | ||||||
|             var uri = notification.meta['action_uri'] as String; |             var uri = notification.meta['action_uri'] as String; | ||||||
| @@ -53,9 +58,9 @@ StreamSubscription<WebSocketPacket> setupNotificationListener( | |||||||
|                       (Platform.isMacOS || |                       (Platform.isMacOS || | ||||||
|                           Platform.isWindows || |                           Platform.isWindows || | ||||||
|                           Platform.isLinux)) |                           Platform.isLinux)) | ||||||
|                   ? 24 |                   ? 28 | ||||||
|                   // ignore: use_build_context_synchronously |                   // ignore: use_build_context_synchronously | ||||||
|                   : MediaQuery.of(context).padding.top + 8, |                   : MediaQuery.of(context).padding.top + 16, | ||||||
|           bottom: 16, |           bottom: 16, | ||||||
|         ), |         ), | ||||||
|       ); |       ); | ||||||
| @@ -67,7 +72,7 @@ Future<void> subscribePushNotification( | |||||||
|   Dio apiClient, { |   Dio apiClient, { | ||||||
|   bool detailedErrors = false, |   bool detailedErrors = false, | ||||||
| }) async { | }) async { | ||||||
|   if (Platform.isLinux){ |   if (Platform.isLinux) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   await FirebaseMessaging.instance.requestPermission( |   await FirebaseMessaging.instance.requestPermission( | ||||||
|   | |||||||
| @@ -3,33 +3,34 @@ import 'dart:convert'; | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/auth.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/services/responsive.dart'; | import 'package:island/services/responsive.dart'; | ||||||
|  | import 'package:island/services/udid.dart'; | ||||||
| import 'package:island/widgets/alert.dart'; | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/content/sheet.dart'; | import 'package:island/widgets/content/sheet.dart'; | ||||||
| import 'package:island/widgets/response.dart'; | import 'package:island/widgets/response.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| 
 | 
 | ||||||
| part 'account_session_sheet.g.dart'; | part 'account_devices.g.dart'; | ||||||
| 
 | 
 | ||||||
| @riverpod | @riverpod | ||||||
| Future<List<SnAuthDevice>> authDevices(Ref ref) async { | Future<List<SnAuthDeviceWithChallenge>> authDevices(Ref ref) async { | ||||||
|   final resp = await ref |   final resp = await ref | ||||||
|       .watch(apiClientProvider) |       .watch(apiClientProvider) | ||||||
|       .get('/id/accounts/me/devices'); |       .get('/id/accounts/me/devices'); | ||||||
|   final sessionId = resp.headers.value('x-auth-session'); |   final currentId = await getUdid(); | ||||||
|   final data = |   final data = | ||||||
|       resp.data.map<SnAuthDevice>((e) { |       resp.data.map<SnAuthDeviceWithChallenge>((e) { | ||||||
|         final ele = SnAuthDevice.fromJson(e); |         final ele = SnAuthDeviceWithChallenge.fromJson(e); | ||||||
|         return ele.copyWith(isCurrent: ele.sessions.first.id == sessionId); |         return ele.copyWith(isCurrent: ele.deviceId == currentId); | ||||||
|       }).toList(); |       }).toList(); | ||||||
|   return data; |   return data; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class _DeviceListTile extends StatelessWidget { | class _DeviceListTile extends StatelessWidget { | ||||||
|   final SnAuthDevice device; |   final SnAuthDeviceWithChallenge device; | ||||||
|   final Function(String) updateDeviceLabel; |   final Function(String) updateDeviceLabel; | ||||||
|   final Function(String) logoutDevice; |   final Function(String) logoutDevice; | ||||||
| 
 | 
 | ||||||
| @@ -57,17 +58,16 @@ class _DeviceListTile extends StatelessWidget { | |||||||
|       subtitle: Column( |       subtitle: Column( | ||||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, |         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|         children: [ |         children: [ | ||||||
|           Text('authSessionsCount'.plural(device.sessions.length)), |  | ||||||
|           Text( |           Text( | ||||||
|             'lastActiveAt'.tr( |             'lastActiveAt'.tr( | ||||||
|               args: [ |               args: [ | ||||||
|                 DateFormat().format( |                 DateFormat().format( | ||||||
|                   device.sessions.first.lastGrantedAt.toLocal(), |                   device.challenges.first.createdAt.toLocal(), | ||||||
|                 ), |                 ), | ||||||
|               ], |               ], | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|           Text(device.sessions.first.challenge.ipAddress), |           Text(device.challenges.first.ipAddress), | ||||||
|           if (device.isCurrent) |           if (device.isCurrent) | ||||||
|             Row( |             Row( | ||||||
|               children: [ |               children: [ | ||||||
| @@ -84,7 +84,7 @@ class _DeviceListTile extends StatelessWidget { | |||||||
|             ).padding(top: 4), |             ).padding(top: 4), | ||||||
|         ], |         ], | ||||||
|       ), |       ), | ||||||
|       title: Text(device.label ?? device.sessions.first.challenge.userAgent), |       title: Text(device.deviceLabel ?? device.deviceName), | ||||||
|       trailing: |       trailing: | ||||||
|           isWideScreen(context) |           isWideScreen(context) | ||||||
|               ? Row( |               ? Row( | ||||||
| @@ -93,14 +93,13 @@ class _DeviceListTile extends StatelessWidget { | |||||||
|                   IconButton( |                   IconButton( | ||||||
|                     icon: Icon(Icons.edit), |                     icon: Icon(Icons.edit), | ||||||
|                     tooltip: 'authDeviceEditLabel'.tr(), |                     tooltip: 'authDeviceEditLabel'.tr(), | ||||||
|                     onPressed: |                     onPressed: () => updateDeviceLabel(device.deviceId), | ||||||
|                         () => updateDeviceLabel(device.sessions.first.id), |  | ||||||
|                   ), |                   ), | ||||||
|                   if (!device.isCurrent) |                   if (!device.isCurrent) | ||||||
|                     IconButton( |                     IconButton( | ||||||
|                       icon: Icon(Icons.logout), |                       icon: Icon(Icons.logout), | ||||||
|                       tooltip: 'authDeviceLogout'.tr(), |                       tooltip: 'authDeviceLogout'.tr(), | ||||||
|                       onPressed: () => logoutDevice(device.sessions.first.id), |                       onPressed: () => logoutDevice(device.deviceId), | ||||||
|                     ), |                     ), | ||||||
|                 ], |                 ], | ||||||
|               ) |               ) | ||||||
| @@ -124,7 +123,7 @@ class AccountSessionSheet extends HookConsumerWidget { | |||||||
|       if (!confirm || !context.mounted) return; |       if (!confirm || !context.mounted) return; | ||||||
|       try { |       try { | ||||||
|         final apiClient = ref.watch(apiClientProvider); |         final apiClient = ref.watch(apiClientProvider); | ||||||
|         await apiClient.delete('/id/accounts/me/sessions/$sessionId'); |         await apiClient.delete('/id/accounts/me/devices/$sessionId'); | ||||||
|         ref.invalidate(authDevicesProvider); |         ref.invalidate(authDevicesProvider); | ||||||
|       } catch (err) { |       } catch (err) { | ||||||
|         showErrorAlert(err); |         showErrorAlert(err); | ||||||
| @@ -163,7 +162,7 @@ class AccountSessionSheet extends HookConsumerWidget { | |||||||
|       try { |       try { | ||||||
|         final apiClient = ref.watch(apiClientProvider); |         final apiClient = ref.watch(apiClientProvider); | ||||||
|         await apiClient.patch( |         await apiClient.patch( | ||||||
|           '/accounts/me/sessions/$sessionId/label', |           '/id/accounts/me/devices/$sessionId/label', | ||||||
|           data: jsonEncode(label), |           data: jsonEncode(label), | ||||||
|         ); |         ); | ||||||
|         ref.invalidate(authDevicesProvider); |         ref.invalidate(authDevicesProvider); | ||||||
| @@ -194,7 +193,7 @@ class AccountSessionSheet extends HookConsumerWidget { | |||||||
|                     ); |                     ); | ||||||
|                   } else { |                   } else { | ||||||
|                     return Dismissible( |                     return Dismissible( | ||||||
|                       key: Key('device-${device.sessions.first.id}'), |                       key: Key('device-${device.id}'), | ||||||
|                       direction: |                       direction: | ||||||
|                           device.isCurrent |                           device.isCurrent | ||||||
|                               ? DismissDirection.startToEnd |                               ? DismissDirection.startToEnd | ||||||
| @@ -213,7 +212,7 @@ class AccountSessionSheet extends HookConsumerWidget { | |||||||
|                       ), |                       ), | ||||||
|                       confirmDismiss: (direction) async { |                       confirmDismiss: (direction) async { | ||||||
|                         if (direction == DismissDirection.startToEnd) { |                         if (direction == DismissDirection.startToEnd) { | ||||||
|                           updateDeviceLabel(device.sessions.first.id); |                           updateDeviceLabel(device.deviceId); | ||||||
|                           return false; |                           return false; | ||||||
|                         } else { |                         } else { | ||||||
|                           final confirm = await showConfirmAlert( |                           final confirm = await showConfirmAlert( | ||||||
| @@ -221,7 +220,7 @@ class AccountSessionSheet extends HookConsumerWidget { | |||||||
|                             'authDeviceLogout'.tr(), |                             'authDeviceLogout'.tr(), | ||||||
|                           ); |                           ); | ||||||
|                           if (confirm && context.mounted) { |                           if (confirm && context.mounted) { | ||||||
|                             logoutDevice(device.sessions.first.id); |                             logoutDevice(device.deviceId); | ||||||
|                           } |                           } | ||||||
|                           return false; // Don't dismiss |                           return false; // Don't dismiss | ||||||
|                         } |                         } | ||||||
| @@ -1,17 +1,17 @@ | |||||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
| 
 | 
 | ||||||
| part of 'account_session_sheet.dart'; | part of 'account_devices.dart'; | ||||||
| 
 | 
 | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
| // RiverpodGenerator | // RiverpodGenerator | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
| 
 | 
 | ||||||
| String _$authDevicesHash() => r'8bc41a1ffc37df8e757c977b4ddae11db8faaeb5'; | String _$authDevicesHash() => r'feb19238f759921e51c888f8b443a3d7761e68da'; | ||||||
| 
 | 
 | ||||||
| /// See also [authDevices]. | /// See also [authDevices]. | ||||||
| @ProviderFor(authDevices) | @ProviderFor(authDevices) | ||||||
| final authDevicesProvider = | final authDevicesProvider = | ||||||
|     AutoDisposeFutureProvider<List<SnAuthDevice>>.internal( |     AutoDisposeFutureProvider<List<SnAuthDeviceWithChallenge>>.internal( | ||||||
|       authDevices, |       authDevices, | ||||||
|       name: r'authDevicesProvider', |       name: r'authDevicesProvider', | ||||||
|       debugGetCreateSourceHash: |       debugGetCreateSourceHash: | ||||||
| @@ -24,6 +24,7 @@ final authDevicesProvider = | |||||||
| 
 | 
 | ||||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||||
| // ignore: unused_element | // ignore: unused_element | ||||||
| typedef AuthDevicesRef = AutoDisposeFutureProviderRef<List<SnAuthDevice>>; | typedef AuthDevicesRef = | ||||||
|  |     AutoDisposeFutureProviderRef<List<SnAuthDeviceWithChallenge>>; | ||||||
| // ignore_for_file: type=lint | // 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 | // 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,7 +1,7 @@ | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/models/wallet.dart'; | import 'package:island/models/wallet.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/widgets/content/cloud_files.dart'; | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/models/badge.dart'; | import 'package:island/models/badge.dart'; | ||||||
|  |  | ||||||
| class BadgeList extends StatelessWidget { | class BadgeList extends StatelessWidget { | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import 'package:dio/dio.dart'; | |||||||
| import 'package:easy_localization/easy_localization.dart'; | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/screens/account/profile.dart'; | import 'package:island/screens/account/profile.dart'; | ||||||
| import 'package:island/services/time.dart'; | import 'package:island/services/time.dart'; | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; | |||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/pods/userinfo.dart'; | import 'package:island/pods/userinfo.dart'; | ||||||
| import 'package:island/widgets/account/status.dart'; | import 'package:island/widgets/account/status.dart'; | ||||||
|   | |||||||
| @@ -11,7 +11,12 @@ export 'content/alert.native.dart' | |||||||
| void showSnackBar(String message, {SnackBarAction? action}) { | void showSnackBar(String message, {SnackBarAction? action}) { | ||||||
|   showTopSnackBar( |   showTopSnackBar( | ||||||
|     globalOverlay.currentState!, |     globalOverlay.currentState!, | ||||||
|     Card(child: Text(message).padding(horizontal: 20, vertical: 16)), |     ConstrainedBox( | ||||||
|  |       constraints: const BoxConstraints(maxWidth: 480), | ||||||
|  |       child: Center( | ||||||
|  |         child: Card(child: Text(message).padding(horizontal: 20, vertical: 16)), | ||||||
|  |       ), | ||||||
|  |     ), | ||||||
|     snackBarPosition: SnackBarPosition.bottom, |     snackBarPosition: SnackBarPosition.bottom, | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| @@ -69,7 +74,7 @@ void showLoadingModal(BuildContext context) { | |||||||
|                 child: Column( |                 child: Column( | ||||||
|                   mainAxisSize: MainAxisSize.min, |                   mainAxisSize: MainAxisSize.min, | ||||||
|                   children: [ |                   children: [ | ||||||
|                     CircularProgressIndicator(year2023: true), |                     CircularProgressIndicator(year2023: false), | ||||||
|                     const Gap(24), |                     const Gap(24), | ||||||
|                     Text('loading'.tr()), |                     Text('loading'.tr()), | ||||||
|                   ], |                   ], | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/user.dart'; | import 'package:island/models/account.dart'; | ||||||
| import 'package:island/widgets/content/cloud_files.dart'; | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
| import 'package:material_symbols_icons/material_symbols_icons.dart'; | import 'package:material_symbols_icons/material_symbols_icons.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|   | |||||||
| @@ -88,6 +88,7 @@ class AttachmentPreview extends HookConsumerWidget { | |||||||
|   final Function? onInsert; |   final Function? onInsert; | ||||||
|   final Function(UniversalFile)? onUpdate; |   final Function(UniversalFile)? onUpdate; | ||||||
|   final Function? onRequestUpload; |   final Function? onRequestUpload; | ||||||
|  |   final bool isCompact; | ||||||
|  |  | ||||||
|   const AttachmentPreview({ |   const AttachmentPreview({ | ||||||
|     super.key, |     super.key, | ||||||
| @@ -98,6 +99,7 @@ class AttachmentPreview extends HookConsumerWidget { | |||||||
|     this.onDelete, |     this.onDelete, | ||||||
|     this.onUpdate, |     this.onUpdate, | ||||||
|     this.onInsert, |     this.onInsert, | ||||||
|  |     this.isCompact = false, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   // GlobalKey for selector |   // GlobalKey for selector | ||||||
| @@ -361,7 +363,7 @@ class AttachmentPreview extends HookConsumerWidget { | |||||||
|                     ), |                     ), | ||||||
|                 ], |                 ], | ||||||
|               ), |               ), | ||||||
|             ), |             ).center(), | ||||||
|             Row( |             Row( | ||||||
|               mainAxisAlignment: MainAxisAlignment.spaceBetween, |               mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
|               children: [ |               children: [ | ||||||
| @@ -458,11 +460,12 @@ class AttachmentPreview extends HookConsumerWidget { | |||||||
|                                       size: 16, |                                       size: 16, | ||||||
|                                       color: Colors.white, |                                       color: Colors.white, | ||||||
|                                     ), |                                     ), | ||||||
|                                     const Gap(8), |                                     if (!isCompact) const Gap(8), | ||||||
|                                     Text( |                                     if (!isCompact) | ||||||
|                                       'On-cloud', |                                       Text( | ||||||
|                                       style: TextStyle(color: Colors.white), |                                         'attachmentOnCloud'.tr(), | ||||||
|                                     ), |                                         style: TextStyle(color: Colors.white), | ||||||
|  |                                       ), | ||||||
|                                   ], |                                   ], | ||||||
|                                 ) |                                 ) | ||||||
|                                 : Row( |                                 : Row( | ||||||
| @@ -473,11 +476,12 @@ class AttachmentPreview extends HookConsumerWidget { | |||||||
|                                       size: 16, |                                       size: 16, | ||||||
|                                       color: Colors.white, |                                       color: Colors.white, | ||||||
|                                     ), |                                     ), | ||||||
|                                     const Gap(8), |                                     if (!isCompact) const Gap(8), | ||||||
|                                     Text( |                                     if (!isCompact) | ||||||
|                                       'On-device', |                                       Text( | ||||||
|                                       style: TextStyle(color: Colors.white), |                                         'attachmentOnDevice'.tr(), | ||||||
|                                     ), |                                         style: TextStyle(color: Colors.white), | ||||||
|  |                                       ), | ||||||
|                                   ], |                                   ], | ||||||
|                                 ), |                                 ), | ||||||
|                       ), |                       ), | ||||||
|   | |||||||
| @@ -31,6 +31,7 @@ class CloudFileList extends HookConsumerWidget { | |||||||
|   final bool disableZoomIn; |   final bool disableZoomIn; | ||||||
|   final bool disableConstraint; |   final bool disableConstraint; | ||||||
|   final EdgeInsets? padding; |   final EdgeInsets? padding; | ||||||
|  |   final bool isColumn; | ||||||
|   const CloudFileList({ |   const CloudFileList({ | ||||||
|     super.key, |     super.key, | ||||||
|     required this.files, |     required this.files, | ||||||
| @@ -40,6 +41,7 @@ class CloudFileList extends HookConsumerWidget { | |||||||
|     this.disableZoomIn = false, |     this.disableZoomIn = false, | ||||||
|     this.disableConstraint = false, |     this.disableConstraint = false, | ||||||
|     this.padding, |     this.padding, | ||||||
|  |     this.isColumn = false, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   double calculateAspectRatio() { |   double calculateAspectRatio() { | ||||||
| @@ -63,6 +65,74 @@ class CloudFileList extends HookConsumerWidget { | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     if (files.isEmpty) return const SizedBox.shrink(); |     if (files.isEmpty) return const SizedBox.shrink(); | ||||||
|  |  | ||||||
|  |     if (isColumn) { | ||||||
|  |       final children = <Widget>[]; | ||||||
|  |       const maxFiles = 2; | ||||||
|  |       final filesToShow = files.take(maxFiles).toList(); | ||||||
|  |  | ||||||
|  |       for (var i = 0; i < filesToShow.length; i++) { | ||||||
|  |         final file = filesToShow[i]; | ||||||
|  |         final isImage = file.mimeType?.startsWith('image') ?? false; | ||||||
|  |         final isAudio = file.mimeType?.startsWith('audio') ?? false; | ||||||
|  |         final widgetItem = ClipRRect( | ||||||
|  |           borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |           child: _CloudFileListEntry( | ||||||
|  |             file: file, | ||||||
|  |             heroTag: heroTags[i], | ||||||
|  |             isImage: isImage, | ||||||
|  |             disableZoomIn: disableZoomIn, | ||||||
|  |             onTap: () { | ||||||
|  |               if (!isImage) { | ||||||
|  |                 return; | ||||||
|  |               } | ||||||
|  |               if (!disableZoomIn) { | ||||||
|  |                 context.pushTransparentRoute( | ||||||
|  |                   CloudFileZoomIn(item: file, heroTag: heroTags[i]), | ||||||
|  |                   rootNavigator: true, | ||||||
|  |                 ); | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         Widget item; | ||||||
|  |         if (isAudio) { | ||||||
|  |           item = SizedBox(height: 120, child: widgetItem); | ||||||
|  |         } else { | ||||||
|  |           item = AspectRatio( | ||||||
|  |             aspectRatio: file.fileMeta?['ratio'] as double? ?? 1.0, | ||||||
|  |             child: widgetItem, | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |         children.add(item); | ||||||
|  |         if (i < filesToShow.length - 1) { | ||||||
|  |           children.add(const Gap(8)); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (files.length > maxFiles) { | ||||||
|  |         children.add(const Gap(8)); | ||||||
|  |         children.add( | ||||||
|  |           Text( | ||||||
|  |             'filesListAdditional'.plural(files.length - filesToShow.length), | ||||||
|  |             textAlign: TextAlign.center, | ||||||
|  |             style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||||
|  |               color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return Padding( | ||||||
|  |         padding: padding ?? EdgeInsets.zero, | ||||||
|  |         child: Column( | ||||||
|  |           mainAxisSize: MainAxisSize.min, | ||||||
|  |           crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |           children: children, | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|     if (files.length == 1) { |     if (files.length == 1) { | ||||||
|       final isImage = files.first.mimeType?.startsWith('image') ?? false; |       final isImage = files.first.mimeType?.startsWith('image') ?? false; | ||||||
|       final isAudio = files.first.mimeType?.startsWith('audio') ?? false; |       final isAudio = files.first.mimeType?.startsWith('audio') ?? false; | ||||||
|   | |||||||
| @@ -256,6 +256,7 @@ class ProfilePictureWidget extends ConsumerWidget { | |||||||
|   final String? fileId; |   final String? fileId; | ||||||
|   final SnCloudFile? file; |   final SnCloudFile? file; | ||||||
|   final double radius; |   final double radius; | ||||||
|  |   final double? borderRadius; | ||||||
|   final IconData? fallbackIcon; |   final IconData? fallbackIcon; | ||||||
|   final Color? fallbackColor; |   final Color? fallbackColor; | ||||||
|   const ProfilePictureWidget({ |   const ProfilePictureWidget({ | ||||||
| @@ -263,6 +264,7 @@ class ProfilePictureWidget extends ConsumerWidget { | |||||||
|     this.fileId, |     this.fileId, | ||||||
|     this.file, |     this.file, | ||||||
|     this.radius = 20, |     this.radius = 20, | ||||||
|  |     this.borderRadius, | ||||||
|     this.fallbackIcon, |     this.fallbackIcon, | ||||||
|     this.fallbackColor, |     this.fallbackColor, | ||||||
|   }); |   }); | ||||||
| @@ -273,7 +275,10 @@ class ProfilePictureWidget extends ConsumerWidget { | |||||||
|     final uri = '$serverUrl/drive/files/${file?.id ?? fileId}'; |     final uri = '$serverUrl/drive/files/${file?.id ?? fileId}'; | ||||||
|  |  | ||||||
|     return ClipRRect( |     return ClipRRect( | ||||||
|       borderRadius: BorderRadius.all(Radius.circular(radius)), |       borderRadius: | ||||||
|  |           borderRadius == null | ||||||
|  |               ? BorderRadius.all(Radius.circular(radius)) | ||||||
|  |               : BorderRadius.all(Radius.circular(borderRadius!)), | ||||||
|       child: Container( |       child: Container( | ||||||
|         width: radius * 2, |         width: radius * 2, | ||||||
|         height: radius * 2, |         height: radius * 2, | ||||||
|   | |||||||
| @@ -57,11 +57,11 @@ class EmbedLinkWidget extends StatelessWidget { | |||||||
|                     Row( |                     Row( | ||||||
|                       children: [ |                       children: [ | ||||||
|                         // Favicon |                         // Favicon | ||||||
|                         if (link.faviconUrl.isNotEmpty) ...[ |                         if (link.faviconUrl?.isNotEmpty ?? false) ...[ | ||||||
|                           ClipRRect( |                           ClipRRect( | ||||||
|                             borderRadius: BorderRadius.circular(4), |                             borderRadius: BorderRadius.circular(4), | ||||||
|                             child: UniversalImage( |                             child: UniversalImage( | ||||||
|                               uri: link.faviconUrl, |                               uri: link.faviconUrl!, | ||||||
|                               width: 16, |                               width: 16, | ||||||
|                               height: 16, |                               height: 16, | ||||||
|                               fit: BoxFit.cover, |                               fit: BoxFit.cover, | ||||||
| @@ -80,8 +80,8 @@ class EmbedLinkWidget extends StatelessWidget { | |||||||
|                         // Site name |                         // Site name | ||||||
|                         Expanded( |                         Expanded( | ||||||
|                           child: Text( |                           child: Text( | ||||||
|                             link.siteName.isNotEmpty |                             (link.siteName?.isNotEmpty ?? false) | ||||||
|                                 ? link.siteName |                                 ? link.siteName! | ||||||
|                                 : Uri.parse(link.url).host, |                                 : Uri.parse(link.url).host, | ||||||
|                             style: theme.textTheme.bodySmall?.copyWith( |                             style: theme.textTheme.bodySmall?.copyWith( | ||||||
|                               color: colorScheme.onSurfaceVariant, |                               color: colorScheme.onSurfaceVariant, | ||||||
|   | |||||||
| @@ -183,9 +183,15 @@ class MarkdownTextContent extends HookConsumerWidget { | |||||||
|                     ); |                     ); | ||||||
|                 } |                 } | ||||||
|               } |               } | ||||||
|               final content = ConstrainedBox( |               final content = ClipRRect( | ||||||
|                 constraints: BoxConstraints(maxHeight: 360), |                 borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|                 child: UniversalImage(uri: uri.toString(), fit: BoxFit.contain), |                 child: ConstrainedBox( | ||||||
|  |                   constraints: BoxConstraints(maxHeight: 360), | ||||||
|  |                   child: UniversalImage( | ||||||
|  |                     uri: uri.toString(), | ||||||
|  |                     fit: BoxFit.contain, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|               ); |               ); | ||||||
|               return content; |               return content; | ||||||
|             }, |             }, | ||||||
|   | |||||||
| @@ -1,9 +1,14 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:gap/gap.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/poll.dart'; | import 'package:island/models/poll.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
|  | import 'package:island/screens/creators/poll/poll_list.dart'; | ||||||
| import 'package:island/services/time.dart'; | import 'package:island/services/time.dart'; | ||||||
| import 'package:island/widgets/content/sheet.dart'; | import 'package:island/widgets/content/sheet.dart'; | ||||||
|  | import 'package:island/widgets/poll/poll_stats_widget.dart'; | ||||||
|  | import 'package:island/widgets/response.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
| @@ -52,59 +57,60 @@ class PollFeedbackNotifier extends _$PollFeedbackNotifier | |||||||
| class PollFeedbackSheet extends HookConsumerWidget { | class PollFeedbackSheet extends HookConsumerWidget { | ||||||
|   final String pollId; |   final String pollId; | ||||||
|   final String? title; |   final String? title; | ||||||
|   final SnPoll poll; |   const PollFeedbackSheet({super.key, required this.pollId, this.title}); | ||||||
|   final Map<String, dynamic>? stats; // stats object similar to PollSubmit |  | ||||||
|   const PollFeedbackSheet({ |  | ||||||
|     super.key, |  | ||||||
|     required this.pollId, |  | ||||||
|     required this.poll, |  | ||||||
|     this.title, |  | ||||||
|     this.stats, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final poll = ref.watch(pollWithStatsProvider(pollId)); | ||||||
|  |  | ||||||
|     return SheetScaffold( |     return SheetScaffold( | ||||||
|       titleText: title ?? 'Poll feedback', |       titleText: title ?? 'Poll feedback', | ||||||
|       child: Column( |       child: poll.when( | ||||||
|         crossAxisAlignment: CrossAxisAlignment.stretch, |         data: | ||||||
|         children: [ |             (data) => CustomScrollView( | ||||||
|           _PollHeader(poll: poll, stats: stats), |               slivers: [ | ||||||
|           const Divider(height: 1), |                 SliverToBoxAdapter(child: _PollHeader(poll: data)), | ||||||
|           Expanded( |                 SliverToBoxAdapter(child: const Divider(height: 1)), | ||||||
|             child: PagingHelperView( |                 SliverGap(4), | ||||||
|               provider: pollFeedbackNotifierProvider(pollId), |                 PagingHelperSliverView( | ||||||
|               futureRefreshable: pollFeedbackNotifierProvider(pollId).future, |                   provider: pollFeedbackNotifierProvider(pollId), | ||||||
|               notifierRefreshable: |                   futureRefreshable: | ||||||
|                   pollFeedbackNotifierProvider(pollId).notifier, |                       pollFeedbackNotifierProvider(pollId).future, | ||||||
|               contentBuilder: |                   notifierRefreshable: | ||||||
|                   (data, widgetCount, endItemView) => ListView.separated( |                       pollFeedbackNotifierProvider(pollId).notifier, | ||||||
|                     padding: const EdgeInsets.symmetric(vertical: 4), |                   contentBuilder: | ||||||
|                     itemCount: widgetCount, |                       (val, widgetCount, endItemView) => SliverList.separated( | ||||||
|                     itemBuilder: (context, index) { |                         itemCount: widgetCount, | ||||||
|                       if (index == widgetCount - 1) { |                         itemBuilder: (context, index) { | ||||||
|                         // Provided by PagingHelperView to indicate end/loading |                           if (index == widgetCount - 1) { | ||||||
|                         return endItemView; |                             // Provided by PagingHelperView to indicate end/loading | ||||||
|                       } |                             return endItemView; | ||||||
|                       final answer = data.items[index]; |                           } | ||||||
|                       return _PollAnswerTile(answer: answer, poll: poll); |                           final answer = val.items[index]; | ||||||
|                     }, |                           return _PollAnswerTile(answer: answer, poll: data); | ||||||
|                     separatorBuilder: |                         }, | ||||||
|                         (context, index) => |                         separatorBuilder: | ||||||
|                             const Divider(height: 1).padding(vertical: 4), |                             (context, index) => | ||||||
|                   ), |                                 const Divider(height: 1).padding(vertical: 4), | ||||||
|  |                       ), | ||||||
|  |                 ), | ||||||
|  |                 SliverGap(4 + MediaQuery.of(context).padding.bottom), | ||||||
|  |               ], | ||||||
|             ), |             ), | ||||||
|           ), |         error: | ||||||
|         ], |             (err, _) => ResponseErrorWidget( | ||||||
|  |               error: err, | ||||||
|  |               onRetry: () => ref.invalidate(pollWithStatsProvider(pollId)), | ||||||
|  |             ), | ||||||
|  |         loading: () => ResponseLoadingWidget(), | ||||||
|       ), |       ), | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class _PollHeader extends StatelessWidget { | class _PollHeader extends StatelessWidget { | ||||||
|   const _PollHeader({required this.poll, this.stats}); |   const _PollHeader({required this.poll}); | ||||||
|   final SnPoll poll; |   final SnPollWithStats poll; | ||||||
|   final Map<String, dynamic>? stats; |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
| @@ -112,18 +118,32 @@ class _PollHeader extends StatelessWidget { | |||||||
|  |  | ||||||
|     return Column( |     return Column( | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |       spacing: 12, | ||||||
|       children: [ |       children: [ | ||||||
|         if (poll.title != null) |         if (poll.title != null || (poll.description?.isNotEmpty ?? false)) | ||||||
|           Text(poll.title!, style: theme.textTheme.titleLarge), |           Column( | ||||||
|         if (poll.description != null) |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|           Padding( |             children: [ | ||||||
|             padding: const EdgeInsets.only(top: 2), |               if (poll.title != null) | ||||||
|             child: Text( |                 Text(poll.title!, style: theme.textTheme.titleLarge), | ||||||
|               poll.description!, |               if (poll.description?.isNotEmpty ?? false) | ||||||
|               style: theme.textTheme.bodyMedium?.copyWith( |                 Text( | ||||||
|                 color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7), |                   poll.description!, | ||||||
|               ), |                   style: theme.textTheme.bodyMedium?.copyWith( | ||||||
|             ), |                     color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         Text('pollQuestions').tr().fontSize(17).bold(), | ||||||
|  |         for (final q in poll.questions) | ||||||
|  |           Column( | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |             children: [ | ||||||
|  |               if (q.title.isNotEmpty) Text(q.title).bold(), | ||||||
|  |               if (q.description?.isNotEmpty ?? false) Text(q.description!), | ||||||
|  |               PollStatsWidget(question: q, stats: poll.stats), | ||||||
|  |             ], | ||||||
|           ), |           ), | ||||||
|       ], |       ], | ||||||
|     ).padding(horizontal: 20, vertical: 16); |     ).padding(horizontal: 20, vertical: 16); | ||||||
| @@ -132,7 +152,7 @@ class _PollHeader extends StatelessWidget { | |||||||
|  |  | ||||||
| class _PollAnswerTile extends StatelessWidget { | class _PollAnswerTile extends StatelessWidget { | ||||||
|   final SnPollAnswer answer; |   final SnPollAnswer answer; | ||||||
|   final SnPoll poll; |   final SnPollWithStats poll; | ||||||
|   const _PollAnswerTile({required this.answer, required this.poll}); |   const _PollAnswerTile({required this.answer, required this.poll}); | ||||||
|  |  | ||||||
|   String _formatPerQuestionAnswer( |   String _formatPerQuestionAnswer( | ||||||
|   | |||||||
							
								
								
									
										233
									
								
								lib/widgets/poll/poll_stats_widget.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								lib/widgets/poll/poll_stats_widget.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,233 @@ | |||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:island/models/poll.dart'; | ||||||
|  |  | ||||||
|  | class PollStatsWidget extends StatelessWidget { | ||||||
|  |   const PollStatsWidget({ | ||||||
|  |     super.key, | ||||||
|  |     required this.question, | ||||||
|  |     required this.stats, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   final SnPollQuestion question; | ||||||
|  |   final Map<String, dynamic>? stats; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     if (stats == null) return const SizedBox.shrink(); | ||||||
|  |     final raw = stats![question.id]; | ||||||
|  |     if (raw == null) return const SizedBox.shrink(); | ||||||
|  |  | ||||||
|  |     Widget? body; | ||||||
|  |  | ||||||
|  |     switch (question.type) { | ||||||
|  |       case SnPollQuestionType.rating: | ||||||
|  |         // rating: avg score (double or int) | ||||||
|  |         final avg = (raw['rating'] as num?)?.toDouble(); | ||||||
|  |         if (avg == null) break; | ||||||
|  |         final theme = Theme.of(context); | ||||||
|  |         body = Row( | ||||||
|  |           mainAxisAlignment: MainAxisAlignment.start, | ||||||
|  |           children: [ | ||||||
|  |             Icon(Icons.star, color: Colors.amber.shade600, size: 18), | ||||||
|  |             const SizedBox(width: 6), | ||||||
|  |             Text( | ||||||
|  |               avg.toStringAsFixed(1), | ||||||
|  |               style: theme.textTheme.labelMedium?.copyWith( | ||||||
|  |                 color: theme.colorScheme.onSurfaceVariant, | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ); | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case SnPollQuestionType.yesNo: | ||||||
|  |         // yes/no: map {true: count, false: count} | ||||||
|  |         if (raw is Map) { | ||||||
|  |           final int yes = | ||||||
|  |               (raw[true] is int) | ||||||
|  |                   ? raw[true] as int | ||||||
|  |                   : int.tryParse('${raw[true]}') ?? 0; | ||||||
|  |           final int no = | ||||||
|  |               (raw[false] is int) | ||||||
|  |                   ? raw[false] as int | ||||||
|  |                   : int.tryParse('${raw[false]}') ?? 0; | ||||||
|  |           final total = (yes + no).clamp(0, 1 << 31); | ||||||
|  |           final yesPct = total == 0 ? 0.0 : yes / total; | ||||||
|  |           final noPct = total == 0 ? 0.0 : no / total; | ||||||
|  |           final theme = Theme.of(context); | ||||||
|  |           body = Column( | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |             children: [ | ||||||
|  |               _BarStatRow( | ||||||
|  |                 label: 'Yes', | ||||||
|  |                 count: yes, | ||||||
|  |                 fraction: yesPct, | ||||||
|  |                 color: Colors.green.shade600, | ||||||
|  |               ), | ||||||
|  |               const SizedBox(height: 6), | ||||||
|  |               _BarStatRow( | ||||||
|  |                 label: 'No', | ||||||
|  |                 count: no, | ||||||
|  |                 fraction: noPct, | ||||||
|  |                 color: Colors.red.shade600, | ||||||
|  |               ), | ||||||
|  |               const SizedBox(height: 4), | ||||||
|  |               Text( | ||||||
|  |                 'Total: $total', | ||||||
|  |                 style: theme.textTheme.labelSmall?.copyWith( | ||||||
|  |                   color: theme.colorScheme.onSurfaceVariant, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case SnPollQuestionType.singleChoice: | ||||||
|  |       case SnPollQuestionType.multipleChoice: | ||||||
|  |         // map optionId -> count | ||||||
|  |         if (raw is Map) { | ||||||
|  |           final options = [...?question.options] | ||||||
|  |             ..sort((a, b) => a.order.compareTo(b.order)); | ||||||
|  |           final List<_OptionCount> items = []; | ||||||
|  |           int total = 0; | ||||||
|  |           for (final opt in options) { | ||||||
|  |             final dynamic v = raw[opt.id]; | ||||||
|  |             final int count = v is int ? v : int.tryParse('$v') ?? 0; | ||||||
|  |             total += count; | ||||||
|  |             items.add(_OptionCount(id: opt.id, label: opt.label, count: count)); | ||||||
|  |           } | ||||||
|  |           if (items.isNotEmpty) { | ||||||
|  |             items.sort( | ||||||
|  |               (a, b) => b.count.compareTo(a.count), | ||||||
|  |             ); // show highest first | ||||||
|  |           } | ||||||
|  |           body = Column( | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |             children: [ | ||||||
|  |               for (final it in items) | ||||||
|  |                 Padding( | ||||||
|  |                   padding: const EdgeInsets.only(bottom: 6), | ||||||
|  |                   child: _BarStatRow( | ||||||
|  |                     label: it.label, | ||||||
|  |                     count: it.count, | ||||||
|  |                     fraction: total == 0 ? 0 : it.count / total, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               if (items.isNotEmpty) | ||||||
|  |                 Text( | ||||||
|  |                   'Total: $total', | ||||||
|  |                   style: Theme.of(context).textTheme.labelSmall?.copyWith( | ||||||
|  |                     color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|  |  | ||||||
|  |       case SnPollQuestionType.freeText: | ||||||
|  |         // No stats | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (body == null) return Text('No stats available'); | ||||||
|  |  | ||||||
|  |     return Padding( | ||||||
|  |       padding: const EdgeInsets.only(top: 8), | ||||||
|  |       child: DecoratedBox( | ||||||
|  |         decoration: BoxDecoration( | ||||||
|  |           color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.35), | ||||||
|  |           borderRadius: BorderRadius.circular(8), | ||||||
|  |         ), | ||||||
|  |         child: Padding( | ||||||
|  |           padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), | ||||||
|  |           child: Column( | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |             children: [ | ||||||
|  |               Text( | ||||||
|  |                 'Stats', | ||||||
|  |                 style: Theme.of(context).textTheme.labelLarge?.copyWith( | ||||||
|  |                   color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |               const SizedBox(height: 8), | ||||||
|  |               body, | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _OptionCount { | ||||||
|  |   final String id; | ||||||
|  |   final String label; | ||||||
|  |   final int count; | ||||||
|  |   const _OptionCount({ | ||||||
|  |     required this.id, | ||||||
|  |     required this.label, | ||||||
|  |     required this.count, | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class _BarStatRow extends StatelessWidget { | ||||||
|  |   const _BarStatRow({ | ||||||
|  |     required this.label, | ||||||
|  |     required this.count, | ||||||
|  |     required this.fraction, | ||||||
|  |     this.color, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   final String label; | ||||||
|  |   final int count; | ||||||
|  |   final double fraction; | ||||||
|  |   final Color? color; | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final barColor = color ?? Theme.of(context).colorScheme.primary; | ||||||
|  |     final bgColor = Theme.of( | ||||||
|  |       context, | ||||||
|  |     ).colorScheme.surfaceVariant.withOpacity(0.6); | ||||||
|  |     final fg = | ||||||
|  |         (fraction.isNaN || fraction.isInfinite) | ||||||
|  |             ? 0.0 | ||||||
|  |             : fraction.clamp(0.0, 1.0); | ||||||
|  |  | ||||||
|  |     return Column( | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |       children: [ | ||||||
|  |         Text('$label · $count', style: Theme.of(context).textTheme.labelMedium), | ||||||
|  |         const SizedBox(height: 4), | ||||||
|  |         LayoutBuilder( | ||||||
|  |           builder: (context, constraints) { | ||||||
|  |             final width = constraints.maxWidth; | ||||||
|  |             final filled = width * fg; | ||||||
|  |             return Stack( | ||||||
|  |               children: [ | ||||||
|  |                 Container( | ||||||
|  |                   height: 8, | ||||||
|  |                   width: width, | ||||||
|  |                   decoration: BoxDecoration( | ||||||
|  |                     color: bgColor, | ||||||
|  |                     borderRadius: BorderRadius.circular(999), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |                 Container( | ||||||
|  |                   height: 8, | ||||||
|  |                   width: filled, | ||||||
|  |                   decoration: BoxDecoration( | ||||||
|  |                     color: barColor, | ||||||
|  |                     borderRadius: BorderRadius.circular(999), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |             ); | ||||||
|  |           }, | ||||||
|  |         ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,9 +1,11 @@ | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter/services.dart'; | import 'package:flutter/services.dart'; | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter_riverpod/flutter_riverpod.dart'; | import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||||||
| import 'package:island/models/poll.dart'; | import 'package:island/models/poll.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/widgets/alert.dart'; | import 'package:island/widgets/alert.dart'; | ||||||
|  | import 'package:island/widgets/poll/poll_stats_widget.dart'; | ||||||
|  |  | ||||||
| class PollSubmit extends ConsumerStatefulWidget { | class PollSubmit extends ConsumerStatefulWidget { | ||||||
|   const PollSubmit({ |   const PollSubmit({ | ||||||
| @@ -14,6 +16,7 @@ class PollSubmit extends ConsumerStatefulWidget { | |||||||
|     this.initialAnswers, |     this.initialAnswers, | ||||||
|     this.onCancel, |     this.onCancel, | ||||||
|     this.showProgress = true, |     this.showProgress = true, | ||||||
|  |     this.isReadonly = false, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   final SnPollWithStats poll; |   final SnPollWithStats poll; | ||||||
| @@ -31,6 +34,8 @@ class PollSubmit extends ConsumerStatefulWidget { | |||||||
|   /// Whether to show a progress indicator (e.g., "2 / N"). |   /// Whether to show a progress indicator (e.g., "2 / N"). | ||||||
|   final bool showProgress; |   final bool showProgress; | ||||||
|  |  | ||||||
|  |   final bool isReadonly; | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   ConsumerState<PollSubmit> createState() => _PollSubmitState(); |   ConsumerState<PollSubmit> createState() => _PollSubmitState(); | ||||||
| } | } | ||||||
| @@ -39,6 +44,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|   late final List<SnPollQuestion> _questions; |   late final List<SnPollQuestion> _questions; | ||||||
|   int _index = 0; |   int _index = 0; | ||||||
|   bool _submitting = false; |   bool _submitting = false; | ||||||
|  |   bool _isModifying = false; // New state to track if user is modifying answers | ||||||
|  |  | ||||||
|   /// Collected answers, keyed by questionId |   /// Collected answers, keyed by questionId | ||||||
|   late Map<String, dynamic> _answers; |   late Map<String, dynamic> _answers; | ||||||
| @@ -59,7 +65,14 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|     _questions = [...widget.poll.questions] |     _questions = [...widget.poll.questions] | ||||||
|       ..sort((a, b) => a.order.compareTo(b.order)); |       ..sort((a, b) => a.order.compareTo(b.order)); | ||||||
|     _answers = Map<String, dynamic>.from(widget.initialAnswers ?? {}); |     _answers = Map<String, dynamic>.from(widget.initialAnswers ?? {}); | ||||||
|     _loadCurrentIntoLocalState(); |     if (!widget.isReadonly) { | ||||||
|  |       _loadCurrentIntoLocalState(); | ||||||
|  |       // If initial answers are provided, set _isModifying to false initially | ||||||
|  |       // so the "Modify" button is shown. | ||||||
|  |       if (widget.initialAnswers != null && widget.initialAnswers!.isNotEmpty) { | ||||||
|  |         _isModifying = false; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
| @@ -74,7 +87,11 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|           [...widget.poll.questions] |           [...widget.poll.questions] | ||||||
|             ..sort((a, b) => a.order.compareTo(b.order)), |             ..sort((a, b) => a.order.compareTo(b.order)), | ||||||
|         ); |         ); | ||||||
|       _loadCurrentIntoLocalState(); |       if (!widget.isReadonly) { | ||||||
|  |         _loadCurrentIntoLocalState(); | ||||||
|  |         // If poll ID changes, reset modification state | ||||||
|  |         _isModifying = false; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -196,7 +213,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|       // Only call onSubmit after server accepts |       // Only call onSubmit after server accepts | ||||||
|       widget.onSubmit(Map<String, dynamic>.unmodifiable(_answers)); |       widget.onSubmit(Map<String, dynamic>.unmodifiable(_answers)); | ||||||
|  |  | ||||||
|       showSnackBar('Poll answer has been submitted.'); |       showSnackBar('pollAnswerSubmitted'.tr()); | ||||||
|       HapticFeedback.heavyImpact(); |       HapticFeedback.heavyImpact(); | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       showErrorAlert(e); |       showErrorAlert(e); | ||||||
| @@ -268,7 +285,8 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|               ], |               ], | ||||||
|             ), |             ), | ||||||
|           ), |           ), | ||||||
|         if (widget.showProgress) |         if (widget.showProgress && | ||||||
|  |             _isModifying) // Only show progress when modifying | ||||||
|           Text( |           Text( | ||||||
|             '${_index + 1} / ${_questions.length}', |             '${_index + 1} / ${_questions.length}', | ||||||
|             style: Theme.of(context).textTheme.labelMedium, |             style: Theme.of(context).textTheme.labelMedium, | ||||||
| @@ -310,154 +328,13 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildStats(BuildContext context, SnPollQuestion q) { |   Widget _buildStats(BuildContext context, SnPollQuestion q) { | ||||||
|     if (widget.stats == null) return const SizedBox.shrink(); |     return PollStatsWidget(question: q, stats: widget.stats); | ||||||
|     final raw = widget.stats![q.id]; |  | ||||||
|     if (raw == null) return const SizedBox.shrink(); |  | ||||||
|  |  | ||||||
|     Widget? body; |  | ||||||
|  |  | ||||||
|     switch (q.type) { |  | ||||||
|       case SnPollQuestionType.rating: |  | ||||||
|         // rating: avg score (double or int) |  | ||||||
|         final avg = (raw['rating'] as num?)?.toDouble(); |  | ||||||
|         if (avg == null) break; |  | ||||||
|         final theme = Theme.of(context); |  | ||||||
|         body = Row( |  | ||||||
|           mainAxisAlignment: MainAxisAlignment.start, |  | ||||||
|           children: [ |  | ||||||
|             Icon(Icons.star, color: Colors.amber.shade600, size: 18), |  | ||||||
|             const SizedBox(width: 6), |  | ||||||
|             Text( |  | ||||||
|               avg.toStringAsFixed(1), |  | ||||||
|               style: theme.textTheme.labelMedium?.copyWith( |  | ||||||
|                 color: theme.colorScheme.onSurfaceVariant, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ], |  | ||||||
|         ); |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|       case SnPollQuestionType.yesNo: |  | ||||||
|         // yes/no: map {true: count, false: count} |  | ||||||
|         if (raw is Map) { |  | ||||||
|           final int yes = |  | ||||||
|               (raw[true] is int) |  | ||||||
|                   ? raw[true] as int |  | ||||||
|                   : int.tryParse('${raw[true]}') ?? 0; |  | ||||||
|           final int no = |  | ||||||
|               (raw[false] is int) |  | ||||||
|                   ? raw[false] as int |  | ||||||
|                   : int.tryParse('${raw[false]}') ?? 0; |  | ||||||
|           final total = (yes + no).clamp(0, 1 << 31); |  | ||||||
|           final yesPct = total == 0 ? 0.0 : yes / total; |  | ||||||
|           final noPct = total == 0 ? 0.0 : no / total; |  | ||||||
|           final theme = Theme.of(context); |  | ||||||
|           body = Column( |  | ||||||
|             crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|             children: [ |  | ||||||
|               _BarStatRow( |  | ||||||
|                 label: 'Yes', |  | ||||||
|                 count: yes, |  | ||||||
|                 fraction: yesPct, |  | ||||||
|                 color: Colors.green.shade600, |  | ||||||
|               ), |  | ||||||
|               const SizedBox(height: 6), |  | ||||||
|               _BarStatRow( |  | ||||||
|                 label: 'No', |  | ||||||
|                 count: no, |  | ||||||
|                 fraction: noPct, |  | ||||||
|                 color: Colors.red.shade600, |  | ||||||
|               ), |  | ||||||
|               const SizedBox(height: 4), |  | ||||||
|               Text( |  | ||||||
|                 'Total: $total', |  | ||||||
|                 style: theme.textTheme.labelSmall?.copyWith( |  | ||||||
|                   color: theme.colorScheme.onSurfaceVariant, |  | ||||||
|                 ), |  | ||||||
|               ), |  | ||||||
|             ], |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|       case SnPollQuestionType.singleChoice: |  | ||||||
|       case SnPollQuestionType.multipleChoice: |  | ||||||
|         // map optionId -> count |  | ||||||
|         if (raw is Map) { |  | ||||||
|           final options = [...?q.options] |  | ||||||
|             ..sort((a, b) => a.order.compareTo(b.order)); |  | ||||||
|           final List<_OptionCount> items = []; |  | ||||||
|           int total = 0; |  | ||||||
|           for (final opt in options) { |  | ||||||
|             final dynamic v = raw[opt.id]; |  | ||||||
|             final int count = v is int ? v : int.tryParse('$v') ?? 0; |  | ||||||
|             total += count; |  | ||||||
|             items.add(_OptionCount(id: opt.id, label: opt.label, count: count)); |  | ||||||
|           } |  | ||||||
|           if (items.isNotEmpty) { |  | ||||||
|             items.sort( |  | ||||||
|               (a, b) => b.count.compareTo(a.count), |  | ||||||
|             ); // show highest first |  | ||||||
|           } |  | ||||||
|           body = Column( |  | ||||||
|             crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|             children: [ |  | ||||||
|               for (final it in items) |  | ||||||
|                 Padding( |  | ||||||
|                   padding: const EdgeInsets.only(bottom: 6), |  | ||||||
|                   child: _BarStatRow( |  | ||||||
|                     label: it.label, |  | ||||||
|                     count: it.count, |  | ||||||
|                     fraction: total == 0 ? 0 : it.count / total, |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|               if (items.isNotEmpty) |  | ||||||
|                 Text( |  | ||||||
|                   'Total: $total', |  | ||||||
|                   style: Theme.of(context).textTheme.labelSmall?.copyWith( |  | ||||||
|                     color: Theme.of(context).colorScheme.onSurfaceVariant, |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|             ], |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|         break; |  | ||||||
|  |  | ||||||
|       case SnPollQuestionType.freeText: |  | ||||||
|         // No stats |  | ||||||
|         break; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (body == null) return const SizedBox.shrink(); |  | ||||||
|  |  | ||||||
|     return Padding( |  | ||||||
|       padding: const EdgeInsets.only(top: 8), |  | ||||||
|       child: DecoratedBox( |  | ||||||
|         decoration: BoxDecoration( |  | ||||||
|           color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.35), |  | ||||||
|           borderRadius: BorderRadius.circular(8), |  | ||||||
|         ), |  | ||||||
|         child: Padding( |  | ||||||
|           padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), |  | ||||||
|           child: Column( |  | ||||||
|             crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|             children: [ |  | ||||||
|               Text( |  | ||||||
|                 'Stats', |  | ||||||
|                 style: Theme.of(context).textTheme.labelLarge?.copyWith( |  | ||||||
|                   color: Theme.of(context).colorScheme.onSurfaceVariant, |  | ||||||
|                 ), |  | ||||||
|               ), |  | ||||||
|               const SizedBox(height: 8), |  | ||||||
|               body, |  | ||||||
|             ], |  | ||||||
|           ), |  | ||||||
|         ), |  | ||||||
|       ), |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildBody(BuildContext context) { |   Widget _buildBody(BuildContext context) { | ||||||
|  |     if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) { | ||||||
|  |       return const SizedBox.shrink(); // Collapse input fields if already submitted and not modifying | ||||||
|  |     } | ||||||
|     final q = _current; |     final q = _current; | ||||||
|     switch (q.type) { |     switch (q.type) { | ||||||
|       case SnPollQuestionType.singleChoice: |       case SnPollQuestionType.singleChoice: | ||||||
| @@ -517,9 +394,9 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|       children: [ |       children: [ | ||||||
|         Expanded( |         Expanded( | ||||||
|           child: SegmentedButton<bool>( |           child: SegmentedButton<bool>( | ||||||
|             segments: const [ |             segments: [ | ||||||
|               ButtonSegment(value: true, label: Text('Yes')), |               ButtonSegment(value: true, label: Text('yes'.tr())), | ||||||
|               ButtonSegment(value: false, label: Text('No')), |               ButtonSegment(value: false, label: Text('no'.tr())), | ||||||
|             ], |             ], | ||||||
|             selected: _yesNoSelected == null ? {} : {_yesNoSelected!}, |             selected: _yesNoSelected == null ? {} : {_yesNoSelected!}, | ||||||
|             onSelectionChanged: (sel) { |             onSelectionChanged: (sel) { | ||||||
| @@ -568,12 +445,39 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|     final isLast = _index == _questions.length - 1; |     final isLast = _index == _questions.length - 1; | ||||||
|     final canProceed = _isCurrentAnswered() && !_submitting; |     final canProceed = _isCurrentAnswered() && !_submitting; | ||||||
|  |  | ||||||
|  |     if (widget.initialAnswers != null && !_isModifying && !widget.isReadonly) { | ||||||
|  |       // If poll is submitted and not in modification mode, show "Modify" button | ||||||
|  |       return FilledButton.icon( | ||||||
|  |         icon: const Icon(Icons.edit), | ||||||
|  |         label: Text('modifyAnswers'.tr()), | ||||||
|  |         onPressed: () { | ||||||
|  |           setState(() { | ||||||
|  |             _isModifying = true; | ||||||
|  |             _index = 0; // Reset to first question for modification | ||||||
|  |             _loadCurrentIntoLocalState(); | ||||||
|  |           }); | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return Row( |     return Row( | ||||||
|       children: [ |       children: [ | ||||||
|         OutlinedButton.icon( |         OutlinedButton.icon( | ||||||
|           icon: const Icon(Icons.arrow_back), |           icon: const Icon(Icons.arrow_back), | ||||||
|           label: Text(_index == 0 ? 'Cancel' : 'Back'), |           label: Text(_index == 0 ? 'cancel'.tr() : 'back'.tr()), | ||||||
|           onPressed: _submitting ? null : _back, |           onPressed: | ||||||
|  |               _submitting | ||||||
|  |                   ? null | ||||||
|  |                   : () { | ||||||
|  |                     if (_index == 0 && _isModifying) { | ||||||
|  |                       // If at first question and in modification mode, go back to submitted view | ||||||
|  |                       setState(() { | ||||||
|  |                         _isModifying = false; | ||||||
|  |                       }); | ||||||
|  |                     } else { | ||||||
|  |                       _back(); | ||||||
|  |                     } | ||||||
|  |                   }, | ||||||
|         ), |         ), | ||||||
|         const Spacer(), |         const Spacer(), | ||||||
|         FilledButton.icon( |         FilledButton.icon( | ||||||
| @@ -585,19 +489,188 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|                     child: CircularProgressIndicator(strokeWidth: 2), |                     child: CircularProgressIndicator(strokeWidth: 2), | ||||||
|                   ) |                   ) | ||||||
|                   : Icon(isLast ? Icons.check : Icons.arrow_forward), |                   : Icon(isLast ? Icons.check : Icons.arrow_forward), | ||||||
|           label: Text(isLast ? 'Submit' : 'Next'), |           label: Text(isLast ? 'submit'.tr() : 'next'.tr()), | ||||||
|           onPressed: canProceed ? _next : null, |           onPressed: canProceed ? _next : null, | ||||||
|         ), |         ), | ||||||
|       ], |       ], | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   Widget _buildSubmittedView(BuildContext context) { | ||||||
|  |     return Column( | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |       children: [ | ||||||
|  |         if (widget.poll.title != null || widget.poll.description != null) | ||||||
|  |           Padding( | ||||||
|  |             padding: const EdgeInsets.only(bottom: 12), | ||||||
|  |             child: Column( | ||||||
|  |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |               children: [ | ||||||
|  |                 if (widget.poll.title?.isNotEmpty ?? false) | ||||||
|  |                   Text( | ||||||
|  |                     widget.poll.title!, | ||||||
|  |                     style: Theme.of(context).textTheme.titleLarge, | ||||||
|  |                   ), | ||||||
|  |                 if (widget.poll.description?.isNotEmpty ?? false) | ||||||
|  |                   Padding( | ||||||
|  |                     padding: const EdgeInsets.only(top: 4), | ||||||
|  |                     child: Text( | ||||||
|  |                       widget.poll.description!, | ||||||
|  |                       style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||||
|  |                         color: Theme.of( | ||||||
|  |                           context, | ||||||
|  |                         ).textTheme.bodyMedium?.color?.withOpacity(0.7), | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         for (final q in _questions) | ||||||
|  |           Padding( | ||||||
|  |             padding: const EdgeInsets.only(bottom: 16.0), | ||||||
|  |             child: Column( | ||||||
|  |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |               children: [ | ||||||
|  |                 Row( | ||||||
|  |                   children: [ | ||||||
|  |                     Expanded( | ||||||
|  |                       child: Text( | ||||||
|  |                         q.title, | ||||||
|  |                         style: Theme.of(context).textTheme.titleMedium, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                     if (q.isRequired) | ||||||
|  |                       Padding( | ||||||
|  |                         padding: const EdgeInsets.only(left: 8), | ||||||
|  |                         child: Text( | ||||||
|  |                           '*', | ||||||
|  |                           style: Theme.of( | ||||||
|  |                             context, | ||||||
|  |                           ).textTheme.titleMedium?.copyWith( | ||||||
|  |                             color: Theme.of(context).colorScheme.error, | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |                 if (q.description != null) | ||||||
|  |                   Padding( | ||||||
|  |                     padding: const EdgeInsets.only(top: 4), | ||||||
|  |                     child: Text( | ||||||
|  |                       q.description!, | ||||||
|  |                       style: Theme.of(context).textTheme.bodySmall?.copyWith( | ||||||
|  |                         color: Theme.of( | ||||||
|  |                           context, | ||||||
|  |                         ).textTheme.bodySmall?.color?.withOpacity(0.7), | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 _buildStats(context, q), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Widget _buildReadonlyView(BuildContext context) { | ||||||
|  |     return Column( | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |       children: [ | ||||||
|  |         if (widget.poll.title != null || widget.poll.description != null) | ||||||
|  |           Padding( | ||||||
|  |             padding: const EdgeInsets.only(bottom: 12), | ||||||
|  |             child: Column( | ||||||
|  |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |               children: [ | ||||||
|  |                 if (widget.poll.title != null) | ||||||
|  |                   Text( | ||||||
|  |                     widget.poll.title!, | ||||||
|  |                     style: Theme.of(context).textTheme.titleLarge, | ||||||
|  |                   ), | ||||||
|  |                 if (widget.poll.description != null) | ||||||
|  |                   Padding( | ||||||
|  |                     padding: const EdgeInsets.only(top: 4), | ||||||
|  |                     child: Text( | ||||||
|  |                       widget.poll.description!, | ||||||
|  |                       style: Theme.of(context).textTheme.bodyMedium?.copyWith( | ||||||
|  |                         color: Theme.of( | ||||||
|  |                           context, | ||||||
|  |                         ).textTheme.bodyMedium?.color?.withOpacity(0.7), | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         for (final q in _questions) | ||||||
|  |           Padding( | ||||||
|  |             padding: const EdgeInsets.only(bottom: 16.0), | ||||||
|  |             child: Column( | ||||||
|  |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |               children: [ | ||||||
|  |                 Row( | ||||||
|  |                   children: [ | ||||||
|  |                     Expanded( | ||||||
|  |                       child: Text( | ||||||
|  |                         q.title, | ||||||
|  |                         style: Theme.of(context).textTheme.titleMedium, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                     if (q.isRequired) | ||||||
|  |                       Padding( | ||||||
|  |                         padding: const EdgeInsets.only(left: 8), | ||||||
|  |                         child: Text( | ||||||
|  |                           '*', | ||||||
|  |                           style: Theme.of( | ||||||
|  |                             context, | ||||||
|  |                           ).textTheme.titleMedium?.copyWith( | ||||||
|  |                             color: Theme.of(context).colorScheme.error, | ||||||
|  |                           ), | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |                 if (q.description != null) | ||||||
|  |                   Padding( | ||||||
|  |                     padding: const EdgeInsets.only(top: 4), | ||||||
|  |                     child: Text( | ||||||
|  |                       q.description!, | ||||||
|  |                       style: Theme.of(context).textTheme.bodySmall?.copyWith( | ||||||
|  |                         color: Theme.of( | ||||||
|  |                           context, | ||||||
|  |                         ).textTheme.bodySmall?.color?.withOpacity(0.7), | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 _buildStats(context, q), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context) { |   Widget build(BuildContext context) { | ||||||
|     if (_questions.isEmpty) { |     if (_questions.isEmpty) { | ||||||
|       return const SizedBox.shrink(); |       return const SizedBox.shrink(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // If poll is already submitted and not in readonly mode, and not in modification mode, show submitted view | ||||||
|  |     if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) { | ||||||
|  |       return Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |         children: [_buildSubmittedView(context), _buildNavBar(context)], | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // If poll is in readonly mode, show readonly view | ||||||
|  |     if (widget.isReadonly) { | ||||||
|  |       return _buildReadonlyView(context); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return Column( |     return Column( | ||||||
|       crossAxisAlignment: CrossAxisAlignment.stretch, |       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|       children: [ |       children: [ | ||||||
| @@ -617,77 +690,6 @@ class _PollSubmitState extends ConsumerState<PollSubmit> { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| class _OptionCount { |  | ||||||
|   final String id; |  | ||||||
|   final String label; |  | ||||||
|   final int count; |  | ||||||
|   const _OptionCount({ |  | ||||||
|     required this.id, |  | ||||||
|     required this.label, |  | ||||||
|     required this.count, |  | ||||||
|   }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| class _BarStatRow extends StatelessWidget { |  | ||||||
|   const _BarStatRow({ |  | ||||||
|     required this.label, |  | ||||||
|     required this.count, |  | ||||||
|     required this.fraction, |  | ||||||
|     this.color, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   final String label; |  | ||||||
|   final int count; |  | ||||||
|   final double fraction; |  | ||||||
|   final Color? color; |  | ||||||
|  |  | ||||||
|   @override |  | ||||||
|   Widget build(BuildContext context) { |  | ||||||
|     final barColor = color ?? Theme.of(context).colorScheme.primary; |  | ||||||
|     final bgColor = Theme.of( |  | ||||||
|       context, |  | ||||||
|     ).colorScheme.surfaceVariant.withOpacity(0.6); |  | ||||||
|     final fg = |  | ||||||
|         (fraction.isNaN || fraction.isInfinite) |  | ||||||
|             ? 0.0 |  | ||||||
|             : fraction.clamp(0.0, 1.0); |  | ||||||
|  |  | ||||||
|     return Column( |  | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|       children: [ |  | ||||||
|         Text('$label · $count', style: Theme.of(context).textTheme.labelMedium), |  | ||||||
|         const SizedBox(height: 4), |  | ||||||
|         LayoutBuilder( |  | ||||||
|           builder: (context, constraints) { |  | ||||||
|             final width = constraints.maxWidth; |  | ||||||
|             final filled = width * fg; |  | ||||||
|             return Stack( |  | ||||||
|               children: [ |  | ||||||
|                 Container( |  | ||||||
|                   height: 8, |  | ||||||
|                   width: width, |  | ||||||
|                   decoration: BoxDecoration( |  | ||||||
|                     color: bgColor, |  | ||||||
|                     borderRadius: BorderRadius.circular(999), |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|                 Container( |  | ||||||
|                   height: 8, |  | ||||||
|                   width: filled, |  | ||||||
|                   decoration: BoxDecoration( |  | ||||||
|                     color: barColor, |  | ||||||
|                     borderRadius: BorderRadius.circular(999), |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|               ], |  | ||||||
|             ); |  | ||||||
|           }, |  | ||||||
|         ), |  | ||||||
|       ], |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /// Simple fade/slide transition between questions. | /// Simple fade/slide transition between questions. | ||||||
| class _AnimatedStep extends StatelessWidget { | class _AnimatedStep extends StatelessWidget { | ||||||
|   const _AnimatedStep({super.key, required this.child}); |   const _AnimatedStep({super.key, required this.child}); | ||||||
|   | |||||||
| @@ -186,10 +186,9 @@ class ComposePollSheet extends HookConsumerWidget { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget? _buildPollSubtitle(SnPoll poll) { |   Widget? _buildPollSubtitle(SnPollWithStats poll) { | ||||||
|     try { |     try { | ||||||
|       final SnPoll dyn = poll; |       final List<SnPollQuestion> options = poll.questions; | ||||||
|       final List<SnPollQuestion> options = dyn.questions; |  | ||||||
|       if (options.isEmpty) return null; |       if (options.isEmpty) return null; | ||||||
|       final preview = options.take(3).map((e) => e.title).join(' · '); |       final preview = options.take(3).map((e) => e.title).join(' · '); | ||||||
|       if (preview.trim().isEmpty) return null; |       if (preview.trim().isEmpty) return null; | ||||||
|   | |||||||
| @@ -5,11 +5,15 @@ import 'package:flutter_hooks/flutter_hooks.dart'; | |||||||
| import 'package:gap/gap.dart'; | import 'package:gap/gap.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:island/models/post_category.dart'; | import 'package:island/models/post_category.dart'; | ||||||
|  | import 'package:island/models/realm.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
|  | import 'package:island/screens/realm/realms.dart'; | ||||||
|  | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
| import 'package:island/widgets/content/sheet.dart'; | import 'package:island/widgets/content/sheet.dart'; | ||||||
| import 'package:island/widgets/post/compose_shared.dart'; | import 'package:island/widgets/post/compose_shared.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
| import 'package:textfield_tags/textfield_tags.dart'; | import 'package:textfield_tags/textfield_tags.dart'; | ||||||
|  |  | ||||||
| part 'compose_settings_sheet.g.dart'; | part 'compose_settings_sheet.g.dart'; | ||||||
| @@ -129,7 +133,9 @@ class ComposeSettingsSheet extends HookConsumerWidget { | |||||||
|     // Listen to visibility changes to trigger rebuilds |     // Listen to visibility changes to trigger rebuilds | ||||||
|     final currentVisibility = useValueListenable(state.visibility); |     final currentVisibility = useValueListenable(state.visibility); | ||||||
|     final currentCategories = useValueListenable(state.categories); |     final currentCategories = useValueListenable(state.categories); | ||||||
|  |     final currentRealm = useValueListenable(state.realm); | ||||||
|     final postCategories = ref.watch(postCategoriesProvider); |     final postCategories = ref.watch(postCategoriesProvider); | ||||||
|  |     final userRealms = ref.watch(realmsJoinedProvider); | ||||||
|  |  | ||||||
|     IconData getVisibilityIcon(int visibilityValue) { |     IconData getVisibilityIcon(int visibilityValue) { | ||||||
|       switch (visibilityValue) { |       switch (visibilityValue) { | ||||||
| @@ -223,6 +229,24 @@ class ComposeSettingsSheet extends HookConsumerWidget { | |||||||
|           crossAxisAlignment: CrossAxisAlignment.start, |           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|           spacing: 16, |           spacing: 16, | ||||||
|           children: [ |           children: [ | ||||||
|  |             // Slug field | ||||||
|  |             TextField( | ||||||
|  |               controller: state.contentController, | ||||||
|  |               decoration: InputDecoration( | ||||||
|  |                 labelText: 'postSlug'.tr(), | ||||||
|  |                 hintText: 'postSlugHint'.tr(), | ||||||
|  |                 contentPadding: const EdgeInsets.symmetric( | ||||||
|  |                   vertical: 9, | ||||||
|  |                   horizontal: 16, | ||||||
|  |                 ), | ||||||
|  |                 border: OutlineInputBorder( | ||||||
|  |                   borderRadius: BorderRadius.circular(12), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |               onTapOutside: | ||||||
|  |                   (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||||
|  |             ), | ||||||
|  |  | ||||||
|             // Tags field |             // Tags field | ||||||
|             TextFieldTags( |             TextFieldTags( | ||||||
|               textfieldTagsController: state.tagsController, |               textfieldTagsController: state.tagsController, | ||||||
| @@ -244,7 +268,6 @@ class ComposeSettingsSheet extends HookConsumerWidget { | |||||||
|             ), |             ), | ||||||
|  |  | ||||||
|             // Categories field |             // Categories field | ||||||
|             // FIXME: Sometimes the entire dropdown crashes: 'package:flutter/src/rendering/stack.dart': Failed assertion: line 799 pos 12: 'firstChild == null || child != null': is not true. |  | ||||||
|             DropdownButtonFormField2<SnPostCategory>( |             DropdownButtonFormField2<SnPostCategory>( | ||||||
|               isExpanded: true, |               isExpanded: true, | ||||||
|               decoration: InputDecoration( |               decoration: InputDecoration( | ||||||
| @@ -306,7 +329,7 @@ class ComposeSettingsSheet extends HookConsumerWidget { | |||||||
|               value: currentCategories.isEmpty ? null : currentCategories.last, |               value: currentCategories.isEmpty ? null : currentCategories.last, | ||||||
|               onChanged: (_) {}, |               onChanged: (_) {}, | ||||||
|               selectedItemBuilder: (context) { |               selectedItemBuilder: (context) { | ||||||
|                 return currentCategories.map((item) { |                 return (postCategories.value ?? []).map((item) { | ||||||
|                   return SingleChildScrollView( |                   return SingleChildScrollView( | ||||||
|                     scrollDirection: Axis.horizontal, |                     scrollDirection: Axis.horizontal, | ||||||
|                     child: Row( |                     child: Row( | ||||||
| @@ -335,12 +358,89 @@ class ComposeSettingsSheet extends HookConsumerWidget { | |||||||
|                   ); |                   ); | ||||||
|                 }).toList(); |                 }).toList(); | ||||||
|               }, |               }, | ||||||
|  |               buttonStyleData: const ButtonStyleData( | ||||||
|  |                 padding: EdgeInsets.only(left: 16, right: 8), | ||||||
|  |                 height: 38, | ||||||
|  |               ), | ||||||
|  |               menuItemStyleData: const MenuItemStyleData( | ||||||
|  |                 height: 38, | ||||||
|  |                 padding: EdgeInsets.zero, | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |  | ||||||
|  |             // Realm selection | ||||||
|  |             DropdownButtonFormField2<SnRealm?>( | ||||||
|  |               isExpanded: true, | ||||||
|  |               decoration: InputDecoration( | ||||||
|  |                 contentPadding: const EdgeInsets.symmetric(vertical: 9), | ||||||
|  |                 border: OutlineInputBorder( | ||||||
|  |                   borderRadius: BorderRadius.circular(12), | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |               hint: Text('realm'.tr(), style: const TextStyle(fontSize: 15)), | ||||||
|  |               items: [ | ||||||
|  |                 DropdownMenuItem<SnRealm?>( | ||||||
|  |                   value: null, | ||||||
|  |                   child: Row( | ||||||
|  |                     children: [ | ||||||
|  |                       const CircleAvatar( | ||||||
|  |                         radius: 16, | ||||||
|  |                         child: Icon(Symbols.link_off, fill: 1), | ||||||
|  |                       ), | ||||||
|  |                       const SizedBox(width: 12), | ||||||
|  |                       Text('postUnlinkRealm').tr(), | ||||||
|  |                     ], | ||||||
|  |                   ).padding(left: 16, right: 8), | ||||||
|  |                 ), | ||||||
|  |                 if (userRealms.hasValue) | ||||||
|  |                   ...(userRealms.value ?? []).map( | ||||||
|  |                     (realm) => DropdownMenuItem<SnRealm?>( | ||||||
|  |                       value: realm, | ||||||
|  |                       child: Row( | ||||||
|  |                         children: [ | ||||||
|  |                           ProfilePictureWidget( | ||||||
|  |                             fileId: realm.picture?.id, | ||||||
|  |                             fallbackIcon: Symbols.workspaces, | ||||||
|  |                             radius: 16, | ||||||
|  |                           ), | ||||||
|  |                           const SizedBox(width: 12), | ||||||
|  |                           Text(realm.name), | ||||||
|  |                         ], | ||||||
|  |                       ).padding(left: 16, right: 8), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |               ], | ||||||
|  |               value: currentRealm, | ||||||
|  |               onChanged: (value) { | ||||||
|  |                 state.realm.value = value; | ||||||
|  |               }, | ||||||
|  |               selectedItemBuilder: (context) { | ||||||
|  |                 return (userRealms.value ?? []).map((_) { | ||||||
|  |                   return Row( | ||||||
|  |                     children: [ | ||||||
|  |                       if (currentRealm == null) | ||||||
|  |                         const CircleAvatar( | ||||||
|  |                           radius: 16, | ||||||
|  |                           child: Icon(Symbols.link_off, fill: 1), | ||||||
|  |                         ) | ||||||
|  |                       else | ||||||
|  |                         ProfilePictureWidget( | ||||||
|  |                           fileId: currentRealm.picture?.id, | ||||||
|  |                           fallbackIcon: Symbols.workspaces, | ||||||
|  |                           radius: 16, | ||||||
|  |                         ), | ||||||
|  |                       const SizedBox(width: 12), | ||||||
|  |                       Text(currentRealm?.name ?? 'postUnlinkRealm'.tr()), | ||||||
|  |                     ], | ||||||
|  |                   ); | ||||||
|  |                 }).toList(); | ||||||
|  |               }, | ||||||
|               buttonStyleData: const ButtonStyleData( |               buttonStyleData: const ButtonStyleData( | ||||||
|                 padding: EdgeInsets.only(left: 16, right: 8), |                 padding: EdgeInsets.only(left: 16, right: 8), | ||||||
|                 height: 40, |                 height: 40, | ||||||
|               ), |               ), | ||||||
|               menuItemStyleData: const MenuItemStyleData( |               menuItemStyleData: const MenuItemStyleData( | ||||||
|                 height: 40, |                 height: 56, | ||||||
|                 padding: EdgeInsets.zero, |                 padding: EdgeInsets.zero, | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import 'package:island/models/file.dart'; | |||||||
| import 'package:island/models/post.dart'; | import 'package:island/models/post.dart'; | ||||||
| import 'package:island/models/post_category.dart'; | import 'package:island/models/post_category.dart'; | ||||||
| import 'package:island/models/publisher.dart'; | import 'package:island/models/publisher.dart'; | ||||||
|  | import 'package:island/models/realm.dart'; | ||||||
| import 'package:island/pods/config.dart'; | import 'package:island/pods/config.dart'; | ||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/services/file.dart'; | import 'package:island/services/file.dart'; | ||||||
| @@ -26,6 +27,7 @@ class ComposeState { | |||||||
|   final TextEditingController titleController; |   final TextEditingController titleController; | ||||||
|   final TextEditingController descriptionController; |   final TextEditingController descriptionController; | ||||||
|   final TextEditingController contentController; |   final TextEditingController contentController; | ||||||
|  |   final TextEditingController slugController; | ||||||
|   final ValueNotifier<int> visibility; |   final ValueNotifier<int> visibility; | ||||||
|   final ValueNotifier<List<UniversalFile>> attachments; |   final ValueNotifier<List<UniversalFile>> attachments; | ||||||
|   final ValueNotifier<Map<int, double>> attachmentProgress; |   final ValueNotifier<Map<int, double>> attachmentProgress; | ||||||
| @@ -33,6 +35,7 @@ class ComposeState { | |||||||
|   final ValueNotifier<bool> submitting; |   final ValueNotifier<bool> submitting; | ||||||
|   final ValueNotifier<List<SnPostCategory>> categories; |   final ValueNotifier<List<SnPostCategory>> categories; | ||||||
|   StringTagController tagsController; |   StringTagController tagsController; | ||||||
|  |   final ValueNotifier<SnRealm?> realm; | ||||||
|   final String draftId; |   final String draftId; | ||||||
|   int postType; |   int postType; | ||||||
|   // Linked poll id for this compose session (nullable) |   // Linked poll id for this compose session (nullable) | ||||||
| @@ -43,6 +46,7 @@ class ComposeState { | |||||||
|     required this.titleController, |     required this.titleController, | ||||||
|     required this.descriptionController, |     required this.descriptionController, | ||||||
|     required this.contentController, |     required this.contentController, | ||||||
|  |     required this.slugController, | ||||||
|     required this.visibility, |     required this.visibility, | ||||||
|     required this.attachments, |     required this.attachments, | ||||||
|     required this.attachmentProgress, |     required this.attachmentProgress, | ||||||
| @@ -50,6 +54,7 @@ class ComposeState { | |||||||
|     required this.submitting, |     required this.submitting, | ||||||
|     required this.tagsController, |     required this.tagsController, | ||||||
|     required this.categories, |     required this.categories, | ||||||
|  |     required this.realm, | ||||||
|     required this.draftId, |     required this.draftId, | ||||||
|     this.postType = 0, |     this.postType = 0, | ||||||
|     String? pollId, |     String? pollId, | ||||||
| @@ -104,6 +109,7 @@ class ComposeLogic { | |||||||
|         text: originalPost?.description, |         text: originalPost?.description, | ||||||
|       ), |       ), | ||||||
|       contentController: TextEditingController(text: originalPost?.content), |       contentController: TextEditingController(text: originalPost?.content), | ||||||
|  |       slugController: TextEditingController(text: originalPost?.slug), | ||||||
|       visibility: ValueNotifier<int>(originalPost?.visibility ?? 0), |       visibility: ValueNotifier<int>(originalPost?.visibility ?? 0), | ||||||
|       submitting: ValueNotifier<bool>(false), |       submitting: ValueNotifier<bool>(false), | ||||||
|       attachmentProgress: ValueNotifier<Map<int, double>>({}), |       attachmentProgress: ValueNotifier<Map<int, double>>({}), | ||||||
| @@ -112,6 +118,7 @@ class ComposeLogic { | |||||||
|       categories: ValueNotifier<List<SnPostCategory>>( |       categories: ValueNotifier<List<SnPostCategory>>( | ||||||
|         originalPost?.categories ?? [], |         originalPost?.categories ?? [], | ||||||
|       ), |       ), | ||||||
|  |       realm: ValueNotifier(originalPost?.realm), | ||||||
|       draftId: id, |       draftId: id, | ||||||
|       postType: postType, |       postType: postType, | ||||||
|       // initialize without poll by default |       // initialize without poll by default | ||||||
| @@ -135,12 +142,14 @@ class ComposeLogic { | |||||||
|       titleController: TextEditingController(text: draft.title), |       titleController: TextEditingController(text: draft.title), | ||||||
|       descriptionController: TextEditingController(text: draft.description), |       descriptionController: TextEditingController(text: draft.description), | ||||||
|       contentController: TextEditingController(text: draft.content), |       contentController: TextEditingController(text: draft.content), | ||||||
|  |       slugController: TextEditingController(text: draft.slug), | ||||||
|       visibility: ValueNotifier<int>(draft.visibility), |       visibility: ValueNotifier<int>(draft.visibility), | ||||||
|       submitting: ValueNotifier<bool>(false), |       submitting: ValueNotifier<bool>(false), | ||||||
|       attachmentProgress: ValueNotifier<Map<int, double>>({}), |       attachmentProgress: ValueNotifier<Map<int, double>>({}), | ||||||
|       currentPublisher: ValueNotifier<SnPublisher?>(null), |       currentPublisher: ValueNotifier<SnPublisher?>(null), | ||||||
|       tagsController: tagsController, |       tagsController: tagsController, | ||||||
|       categories: ValueNotifier<List<SnPostCategory>>([]), |       categories: ValueNotifier<List<SnPostCategory>>([]), | ||||||
|  |       realm: ValueNotifier(null), | ||||||
|       draftId: draft.id, |       draftId: draft.id, | ||||||
|       postType: postType, |       postType: postType, | ||||||
|       pollId: null, |       pollId: null, | ||||||
| @@ -629,6 +638,8 @@ class ComposeLogic { | |||||||
|         'title': state.titleController.text, |         'title': state.titleController.text, | ||||||
|         'description': state.descriptionController.text, |         'description': state.descriptionController.text, | ||||||
|         'content': state.contentController.text, |         'content': state.contentController.text, | ||||||
|  |         if (state.slugController.text.isNotEmpty) | ||||||
|  |           'slug': state.slugController.text, | ||||||
|         'visibility': state.visibility.value, |         'visibility': state.visibility.value, | ||||||
|         'attachments': |         'attachments': | ||||||
|             state.attachments.value |             state.attachments.value | ||||||
| @@ -640,6 +651,7 @@ class ComposeLogic { | |||||||
|         if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id, |         if (forwardedPost != null) 'forwarded_post_id': forwardedPost.id, | ||||||
|         'tags': state.tagsController.getTags, |         'tags': state.tagsController.getTags, | ||||||
|         'categories': state.categories.value.map((e) => e.slug).toList(), |         'categories': state.categories.value.map((e) => e.slug).toList(), | ||||||
|  |         if (state.realm.value != null) 'realm_id': state.realm.value?.id, | ||||||
|         if (state.pollId.value != null) 'poll_id': state.pollId.value, |         if (state.pollId.value != null) 'poll_id': state.pollId.value, | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
| @@ -733,6 +745,7 @@ class ComposeLogic { | |||||||
|     state.currentPublisher.dispose(); |     state.currentPublisher.dispose(); | ||||||
|     state.tagsController.dispose(); |     state.tagsController.dispose(); | ||||||
|     state.categories.dispose(); |     state.categories.dispose(); | ||||||
|  |     state.realm.dispose(); | ||||||
|     state.pollId.dispose(); |     state.pollId.dispose(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import 'package:material_symbols_icons/symbols.dart'; | |||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| import 'package:island/widgets/post/post_item.dart'; | import 'package:island/widgets/post/post_item.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  | import 'package:island/pods/config.dart'; // Import config.dart for shared preferences keys and provider | ||||||
|  |  | ||||||
| part 'post_featured.g.dart'; | part 'post_featured.g.dart'; | ||||||
|  |  | ||||||
| @@ -25,7 +26,13 @@ class PostFeaturedList extends HookConsumerWidget { | |||||||
|     final featuredPostsAsync = ref.watch(featuredPostsProvider); |     final featuredPostsAsync = ref.watch(featuredPostsProvider); | ||||||
|  |  | ||||||
|     final pageViewController = usePageController(); |     final pageViewController = usePageController(); | ||||||
|  |     final prefs = ref.watch(sharedPreferencesProvider); | ||||||
|     final pageViewCurrent = useState(0); |     final pageViewCurrent = useState(0); | ||||||
|  |     final previousFirstPostId = useState<String?>(null); | ||||||
|  |     final storedCollapsedId = useState<String?>( | ||||||
|  |       prefs.getString(kFeaturedPostsCollapsedId), | ||||||
|  |     ); | ||||||
|  |     final isCollapsed = useState(false); | ||||||
|  |  | ||||||
|     useEffect(() { |     useEffect(() { | ||||||
|       pageViewController.addListener(() { |       pageViewController.addListener(() { | ||||||
| @@ -34,6 +41,59 @@ class PostFeaturedList extends HookConsumerWidget { | |||||||
|       return null; |       return null; | ||||||
|     }, [pageViewController]); |     }, [pageViewController]); | ||||||
|  |  | ||||||
|  |     // Log isCollapsed state changes | ||||||
|  |     useEffect(() { | ||||||
|  |       debugPrint( | ||||||
|  |         'PostFeaturedList: isCollapsed changed to ${isCollapsed.value}', | ||||||
|  |       ); | ||||||
|  |       return null; | ||||||
|  |     }, [isCollapsed.value]); | ||||||
|  |  | ||||||
|  |     useEffect(() { | ||||||
|  |       if (featuredPostsAsync.hasValue && featuredPostsAsync.value!.isNotEmpty) { | ||||||
|  |         final currentFirstPostId = featuredPostsAsync.value!.first.id; | ||||||
|  |         debugPrint( | ||||||
|  |           'PostFeaturedList: Current first post ID: $currentFirstPostId', | ||||||
|  |         ); | ||||||
|  |         debugPrint( | ||||||
|  |           'PostFeaturedList: Previous first post ID: ${previousFirstPostId.value}', | ||||||
|  |         ); | ||||||
|  |         debugPrint( | ||||||
|  |           'PostFeaturedList: Stored collapsed ID: ${storedCollapsedId.value}', | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         if (previousFirstPostId.value == null) { | ||||||
|  |           // Initial load | ||||||
|  |           previousFirstPostId.value = currentFirstPostId; | ||||||
|  |           isCollapsed.value = (storedCollapsedId.value == currentFirstPostId); | ||||||
|  |           debugPrint( | ||||||
|  |             'PostFeaturedList: Initial load. isCollapsed set to ${isCollapsed.value}', | ||||||
|  |           ); | ||||||
|  |         } else if (previousFirstPostId.value != currentFirstPostId) { | ||||||
|  |           // First post changed, expand by default | ||||||
|  |           previousFirstPostId.value = currentFirstPostId; | ||||||
|  |           isCollapsed.value = false; | ||||||
|  |           prefs.remove( | ||||||
|  |             kFeaturedPostsCollapsedId, | ||||||
|  |           ); // Clear stored ID if post changes | ||||||
|  |           debugPrint( | ||||||
|  |             'PostFeaturedList: First post changed. isCollapsed set to false.', | ||||||
|  |           ); | ||||||
|  |         } else { | ||||||
|  |           // Same first post, maintain current collapse state | ||||||
|  |           // No change needed for isCollapsed.value unless manually toggled | ||||||
|  |           debugPrint( | ||||||
|  |             'PostFeaturedList: Same first post. Maintaining current collapse state.', | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         debugPrint( | ||||||
|  |           'PostFeaturedList: featuredPostsAsync has no value or is empty.', | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       return null; | ||||||
|  |     }, [featuredPostsAsync.value]); | ||||||
|  |  | ||||||
|     return ClipRRect( |     return ClipRRect( | ||||||
|       borderRadius: const BorderRadius.all(Radius.circular(8)), |       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|       child: Card( |       child: Card( | ||||||
| @@ -73,29 +133,69 @@ class PostFeaturedList extends HookConsumerWidget { | |||||||
|                   }, |                   }, | ||||||
|                   icon: const Icon(Symbols.arrow_right), |                   icon: const Icon(Symbols.arrow_right), | ||||||
|                 ), |                 ), | ||||||
|  |                 IconButton( | ||||||
|  |                   padding: EdgeInsets.zero, | ||||||
|  |                   visualDensity: VisualDensity.compact, | ||||||
|  |                   constraints: const BoxConstraints(), | ||||||
|  |                   onPressed: () { | ||||||
|  |                     isCollapsed.value = !isCollapsed.value; | ||||||
|  |                     debugPrint( | ||||||
|  |                       'PostFeaturedList: Manual toggle. isCollapsed set to ${isCollapsed.value}', | ||||||
|  |                     ); | ||||||
|  |                     if (isCollapsed.value && | ||||||
|  |                         featuredPostsAsync.hasValue && | ||||||
|  |                         featuredPostsAsync.value!.isNotEmpty) { | ||||||
|  |                       prefs.setString( | ||||||
|  |                         kFeaturedPostsCollapsedId, | ||||||
|  |                         featuredPostsAsync.value!.first.id, | ||||||
|  |                       ); | ||||||
|  |                       debugPrint( | ||||||
|  |                         'PostFeaturedList: Stored collapsed ID: ${featuredPostsAsync.value!.first.id}', | ||||||
|  |                       ); | ||||||
|  |                     } else { | ||||||
|  |                       prefs.remove(kFeaturedPostsCollapsedId); | ||||||
|  |                       debugPrint( | ||||||
|  |                         'PostFeaturedList: Removed stored collapsed ID.', | ||||||
|  |                       ); | ||||||
|  |                     } | ||||||
|  |                   }, | ||||||
|  |                   icon: Icon( | ||||||
|  |                     isCollapsed.value | ||||||
|  |                         ? Symbols.expand_more | ||||||
|  |                         : Symbols.expand_less, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|               ], |               ], | ||||||
|             ).padding(horizontal: 16, vertical: 8), |             ).padding(horizontal: 16, vertical: 8), | ||||||
|             featuredPostsAsync.when( |             AnimatedSize( | ||||||
|               loading: () => const Center(child: CircularProgressIndicator()), |               duration: const Duration(milliseconds: 300), | ||||||
|               error: (error, stack) => Center(child: Text('Error: $error')), |               curve: Curves.easeInOut, | ||||||
|               data: (posts) { |               child: Visibility( | ||||||
|                 return SizedBox( |                 visible: !isCollapsed.value, | ||||||
|                   height: 320, |                 child: featuredPostsAsync.when( | ||||||
|                   child: PageView.builder( |                   loading: | ||||||
|                     controller: pageViewController, |                       () => const Center(child: CircularProgressIndicator()), | ||||||
|                     scrollDirection: Axis.horizontal, |                   error: (error, stack) => Center(child: Text('Error: $error')), | ||||||
|                     itemCount: posts.length, |                   data: (posts) { | ||||||
|                     itemBuilder: (context, index) { |                     return SizedBox( | ||||||
|                       return SingleChildScrollView( |                       height: 320, | ||||||
|                         child: PostActionableItem( |                       child: PageView.builder( | ||||||
|                           item: posts[index], |                         controller: pageViewController, | ||||||
|                           borderRadius: 8, |                         scrollDirection: Axis.horizontal, | ||||||
|                         ), |                         itemCount: posts.length, | ||||||
|                       ); |                         itemBuilder: (context, index) { | ||||||
|                     }, |                           return SingleChildScrollView( | ||||||
|                   ), |                             child: PostActionableItem( | ||||||
|                 ); |                               item: posts[index], | ||||||
|               }, |                               borderRadius: 8, | ||||||
|  |                             ), | ||||||
|  |                           ); | ||||||
|  |                         }, | ||||||
|  |                       ), | ||||||
|  |                     ); | ||||||
|  |                   }, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|             ), |             ), | ||||||
|           ], |           ], | ||||||
|         ), |         ), | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -7,11 +7,9 @@ import 'package:island/models/post.dart'; | |||||||
| import 'package:island/pods/network.dart'; | import 'package:island/pods/network.dart'; | ||||||
| import 'package:island/services/time.dart'; | import 'package:island/services/time.dart'; | ||||||
| import 'package:island/widgets/alert.dart'; | import 'package:island/widgets/alert.dart'; | ||||||
| import 'package:island/widgets/content/cloud_file_collection.dart'; |  | ||||||
| import 'package:island/widgets/content/markdown.dart'; |  | ||||||
| import 'package:island/widgets/post/post_item.dart'; | import 'package:island/widgets/post/post_item.dart'; | ||||||
|  | import 'package:island/widgets/post/post_shared.dart'; | ||||||
| import 'package:material_symbols_icons/symbols.dart'; | import 'package:material_symbols_icons/symbols.dart'; | ||||||
| import 'package:styled_widget/styled_widget.dart'; |  | ||||||
| import 'package:super_context_menu/super_context_menu.dart'; | import 'package:super_context_menu/super_context_menu.dart'; | ||||||
|  |  | ||||||
| class PostItemCreator extends HookConsumerWidget { | class PostItemCreator extends HookConsumerWidget { | ||||||
| @@ -81,7 +79,6 @@ class PostItemCreator extends HookConsumerWidget { | |||||||
|               title: 'copyLink'.tr(), |               title: 'copyLink'.tr(), | ||||||
|               image: MenuImage.icon(Symbols.link), |               image: MenuImage.icon(Symbols.link), | ||||||
|               callback: () { |               callback: () { | ||||||
|                 // Copy post link to clipboard |  | ||||||
|                 context.pushNamed( |                 context.pushNamed( | ||||||
|                   'postDetail', |                   'postDetail', | ||||||
|                   pathParameters: {'id': item.id}, |                   pathParameters: {'id': item.id}, | ||||||
| @@ -105,8 +102,9 @@ class PostItemCreator extends HookConsumerWidget { | |||||||
|             child: Column( |             child: Column( | ||||||
|               crossAxisAlignment: CrossAxisAlignment.start, |               crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|               children: [ |               children: [ | ||||||
|                 _buildPostHeader(context), |                 PostHeader(item: item), | ||||||
|                 _buildPostContent(context), |                 PostBody(item: item), | ||||||
|  |                 ReferencedPostWidget(item: item), | ||||||
|                 const Gap(16), |                 const Gap(16), | ||||||
|                 _buildAnalyticsSection(context), |                 _buildAnalyticsSection(context), | ||||||
|               ], |               ], | ||||||
| @@ -117,128 +115,12 @@ class PostItemCreator extends HookConsumerWidget { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   Widget _buildPostHeader(BuildContext context) { |  | ||||||
|     return Column( |  | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|       children: [ |  | ||||||
|         // Post ID and timestamp row |  | ||||||
|         Row( |  | ||||||
|           children: [ |  | ||||||
|             Container( |  | ||||||
|               padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), |  | ||||||
|               decoration: BoxDecoration( |  | ||||||
|                 color: Theme.of(context).colorScheme.primaryContainer, |  | ||||||
|                 borderRadius: BorderRadius.circular(4), |  | ||||||
|               ), |  | ||||||
|               child: Text( |  | ||||||
|                 'ID: ${item.id.substring(0, 6)}', |  | ||||||
|                 style: TextStyle( |  | ||||||
|                   fontSize: 12, |  | ||||||
|                   fontWeight: FontWeight.bold, |  | ||||||
|                   color: Theme.of(context).colorScheme.onPrimaryContainer, |  | ||||||
|                 ), |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|             const Spacer(), |  | ||||||
|             Icon( |  | ||||||
|               _getVisibilityIcon(item.visibility), |  | ||||||
|               size: 16, |  | ||||||
|               color: Theme.of(context).colorScheme.secondary, |  | ||||||
|             ), |  | ||||||
|             const SizedBox(width: 4), |  | ||||||
|             Text( |  | ||||||
|               _getVisibilityText(item.visibility).tr(), |  | ||||||
|               style: TextStyle( |  | ||||||
|                 fontSize: 12, |  | ||||||
|                 color: Theme.of(context).colorScheme.secondary, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|             const Gap(8), |  | ||||||
|             Text( |  | ||||||
|               item.publishedAt?.formatSystem() ?? '', |  | ||||||
|               style: TextStyle( |  | ||||||
|                 fontSize: 12, |  | ||||||
|                 color: Theme.of(context).colorScheme.secondary, |  | ||||||
|               ), |  | ||||||
|             ), |  | ||||||
|           ], |  | ||||||
|         ), |  | ||||||
|         const Gap(8), |  | ||||||
|  |  | ||||||
|         // Title and description |  | ||||||
|         if (item.title?.isNotEmpty ?? false) |  | ||||||
|           Text( |  | ||||||
|             item.title!, |  | ||||||
|             style: Theme.of( |  | ||||||
|               context, |  | ||||||
|             ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), |  | ||||||
|           ), |  | ||||||
|         if (item.description?.isNotEmpty ?? false) |  | ||||||
|           Text( |  | ||||||
|             item.description!, |  | ||||||
|             style: Theme.of(context).textTheme.bodyMedium?.copyWith( |  | ||||||
|               color: Theme.of(context).colorScheme.onSurfaceVariant, |  | ||||||
|             ), |  | ||||||
|           ).padding(top: 4), |  | ||||||
|       ], |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildPostContent(BuildContext context) { |  | ||||||
|     return Column( |  | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |  | ||||||
|       children: [ |  | ||||||
|         // Content preview |  | ||||||
|         if (item.content?.isNotEmpty ?? false) |  | ||||||
|           Container( |  | ||||||
|             margin: const EdgeInsets.only(top: 12), |  | ||||||
|             child: MarkdownTextContent(content: item.content!), |  | ||||||
|           ), |  | ||||||
|  |  | ||||||
|         // Attachments |  | ||||||
|         if (item.attachments.isNotEmpty) |  | ||||||
|           CloudFileList( |  | ||||||
|             files: item.attachments, |  | ||||||
|             maxWidth: MediaQuery.of(context).size.width * 0.85, |  | ||||||
|             padding: EdgeInsets.only(top: 8), |  | ||||||
|           ), |  | ||||||
|  |  | ||||||
|         // Reference post indicator |  | ||||||
|         if (item.repliedPost != null || item.forwardedPost != null) |  | ||||||
|           Container( |  | ||||||
|             margin: const EdgeInsets.only(top: 8), |  | ||||||
|             child: Row( |  | ||||||
|               children: [ |  | ||||||
|                 Icon( |  | ||||||
|                   item.repliedPost != null ? Symbols.reply : Symbols.forward, |  | ||||||
|                   size: 16, |  | ||||||
|                   color: Theme.of(context).colorScheme.secondary, |  | ||||||
|                 ), |  | ||||||
|                 const Gap(4), |  | ||||||
|                 Text( |  | ||||||
|                   item.repliedPost != null |  | ||||||
|                       ? 'repliedTo'.tr() |  | ||||||
|                       : 'forwarded'.tr(), |  | ||||||
|                   style: TextStyle( |  | ||||||
|                     fontSize: 12, |  | ||||||
|                     color: Theme.of(context).colorScheme.secondary, |  | ||||||
|                   ), |  | ||||||
|                 ), |  | ||||||
|               ], |  | ||||||
|             ), |  | ||||||
|           ), |  | ||||||
|       ], |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   Widget _buildAnalyticsSection(BuildContext context) { |   Widget _buildAnalyticsSection(BuildContext context) { | ||||||
|     return Column( |     return Column( | ||||||
|       crossAxisAlignment: CrossAxisAlignment.start, |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|       children: [ |       children: [ | ||||||
|         Text('Analytics', style: Theme.of(context).textTheme.titleSmall), |         Text('Analytics', style: Theme.of(context).textTheme.titleSmall), | ||||||
|         const Gap(8), |         const Gap(8), | ||||||
|  |  | ||||||
|         // Engagement metrics in a card |  | ||||||
|         Card( |         Card( | ||||||
|           elevation: 1, |           elevation: 1, | ||||||
|           margin: EdgeInsets.zero, |           margin: EdgeInsets.zero, | ||||||
| @@ -279,15 +161,9 @@ class PostItemCreator extends HookConsumerWidget { | |||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|         const Gap(16), |         const Gap(16), | ||||||
|  |  | ||||||
|         // Reactions summary |  | ||||||
|         if (item.reactionsCount.isNotEmpty) _buildReactionsSection(context), |         if (item.reactionsCount.isNotEmpty) _buildReactionsSection(context), | ||||||
|  |  | ||||||
|         // Metadata section |  | ||||||
|         if (item.meta != null && item.meta!.isNotEmpty) |         if (item.meta != null && item.meta!.isNotEmpty) | ||||||
|           _buildMetadataSection(context), |           _buildMetadataSection(context), | ||||||
|  |  | ||||||
|         // Creation and modification timestamps |  | ||||||
|         const Gap(16), |         const Gap(16), | ||||||
|         Row( |         Row( | ||||||
|           mainAxisAlignment: MainAxisAlignment.spaceBetween, |           mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||||
| @@ -425,31 +301,3 @@ class PostItemCreator extends HookConsumerWidget { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| // Helper method to get the appropriate icon for each visibility status |  | ||||||
| IconData _getVisibilityIcon(int visibility) { |  | ||||||
|   switch (visibility) { |  | ||||||
|     case 1: // Friends |  | ||||||
|       return Symbols.group; |  | ||||||
|     case 2: // Unlisted |  | ||||||
|       return Symbols.link_off; |  | ||||||
|     case 3: // Private |  | ||||||
|       return Symbols.lock; |  | ||||||
|     default: // Public (0) or unknown |  | ||||||
|       return Symbols.public; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Helper method to get the translation key for each visibility status |  | ||||||
| String _getVisibilityText(int visibility) { |  | ||||||
|   switch (visibility) { |  | ||||||
|     case 1: // Friends |  | ||||||
|       return 'postVisibilityFriends'; |  | ||||||
|     case 2: // Unlisted |  | ||||||
|       return 'postVisibilityUnlisted'; |  | ||||||
|     case 3: // Private |  | ||||||
|       return 'postVisibilityPrivate'; |  | ||||||
|     default: // Public (0) or unknown |  | ||||||
|       return 'postVisibilityPublic'; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|   | |||||||
							
								
								
									
										135
									
								
								lib/widgets/post/post_item_screenshot.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								lib/widgets/post/post_item_screenshot.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | |||||||
|  | import 'package:collection/collection.dart'; | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:gap/gap.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/models/post.dart'; | ||||||
|  | import 'package:island/widgets/post/post_shared.dart'; | ||||||
|  | import 'package:qr_flutter/qr_flutter.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
|  | class PostItemScreenshot extends ConsumerWidget { | ||||||
|  |   final SnPost item; | ||||||
|  |   final EdgeInsets? padding; | ||||||
|  |   final bool isFullPost; | ||||||
|  |   final bool isShowReference; | ||||||
|  |   const PostItemScreenshot({ | ||||||
|  |     super.key, | ||||||
|  |     required this.item, | ||||||
|  |     this.padding, | ||||||
|  |     this.isFullPost = false, | ||||||
|  |     this.isShowReference = true, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final renderingPadding = | ||||||
|  |         padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 8); | ||||||
|  |  | ||||||
|  |     final mostReaction = | ||||||
|  |         item.reactionsCount.isEmpty | ||||||
|  |             ? null | ||||||
|  |             : item.reactionsCount.entries | ||||||
|  |                 .sortedBy((e) => e.value) | ||||||
|  |                 .map((e) => e.key) | ||||||
|  |                 .last; | ||||||
|  |  | ||||||
|  |     final isDark = MediaQuery.of(context).platformBrightness == Brightness.dark; | ||||||
|  |  | ||||||
|  |     return Material( | ||||||
|  |       elevation: 0, | ||||||
|  |       color: Theme.of(context).colorScheme.surface, | ||||||
|  |       child: Column( | ||||||
|  |         mainAxisSize: MainAxisSize.min, | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: [ | ||||||
|  |           Gap(renderingPadding.vertical), | ||||||
|  |           PostHeader( | ||||||
|  |             item: item, | ||||||
|  |             isFullPost: isFullPost, | ||||||
|  |             isInteractive: false, | ||||||
|  |             renderingPadding: renderingPadding, | ||||||
|  |             isRelativeTime: false, | ||||||
|  |             trailing: | ||||||
|  |                 mostReaction != null | ||||||
|  |                     ? Row( | ||||||
|  |                       children: [ | ||||||
|  |                         Text( | ||||||
|  |                           kReactionTemplates[mostReaction]?.icon ?? '', | ||||||
|  |                           style: const TextStyle(fontSize: 20), | ||||||
|  |                         ), | ||||||
|  |                         const Gap(4), | ||||||
|  |                         Text( | ||||||
|  |                           'x${item.reactionsCount[mostReaction]}', | ||||||
|  |                           style: const TextStyle(fontSize: 11), | ||||||
|  |                         ), | ||||||
|  |                       ], | ||||||
|  |                     ) | ||||||
|  |                     : null, | ||||||
|  |           ), | ||||||
|  |           PostBody( | ||||||
|  |             item: item, | ||||||
|  |             renderingPadding: renderingPadding, | ||||||
|  |             isFullPost: isFullPost, | ||||||
|  |             isTextSelectable: false, | ||||||
|  |             isInteractive: false, | ||||||
|  |           ), | ||||||
|  |           if (isShowReference) | ||||||
|  |             ReferencedPostWidget( | ||||||
|  |               item: item, | ||||||
|  |               isInteractive: false, | ||||||
|  |               renderingPadding: renderingPadding, | ||||||
|  |             ), | ||||||
|  |           Container( | ||||||
|  |             color: Theme.of(context).colorScheme.surfaceContainerLow, | ||||||
|  |             margin: const EdgeInsets.only(top: 8), | ||||||
|  |             padding: EdgeInsets.symmetric( | ||||||
|  |               horizontal: renderingPadding.horizontal, | ||||||
|  |               vertical: 4, | ||||||
|  |             ), | ||||||
|  |             child: Row( | ||||||
|  |               children: [ | ||||||
|  |                 SizedBox( | ||||||
|  |                   width: 44, | ||||||
|  |                   height: 44, | ||||||
|  |                   child: Image.asset( | ||||||
|  |                     'assets/icons/icon${isDark ? '-dark' : ''}.png', | ||||||
|  |                     width: 40, | ||||||
|  |                     height: 40, | ||||||
|  |                   ), | ||||||
|  |                 ).padding(vertical: 8, right: 12), | ||||||
|  |                 Expanded( | ||||||
|  |                   child: Column( | ||||||
|  |                     crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                     children: [ | ||||||
|  |                       const Text( | ||||||
|  |                         'Solar Network', | ||||||
|  |                         style: TextStyle( | ||||||
|  |                           fontSize: 14, | ||||||
|  |                           fontWeight: FontWeight.bold, | ||||||
|  |                         ), | ||||||
|  |                       ), | ||||||
|  |                       const Text( | ||||||
|  |                         'sharePostSlogan', | ||||||
|  |                         style: TextStyle(fontSize: 12), | ||||||
|  |                       ).tr().opacity(0.9), | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |                 QrImageView( | ||||||
|  |                   data: 'https://solian.app/posts/${item.id}', | ||||||
|  |                   version: QrVersions.auto, | ||||||
|  |                   size: 60, | ||||||
|  |                   errorCorrectionLevel: QrErrorCorrectLevel.M, | ||||||
|  |                   backgroundColor: Colors.transparent, | ||||||
|  |                   foregroundColor: Theme.of(context).colorScheme.onSurface, | ||||||
|  |                   padding: const EdgeInsets.all(8), | ||||||
|  |                 ), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										871
									
								
								lib/widgets/post/post_shared.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										871
									
								
								lib/widgets/post/post_shared.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,871 @@ | |||||||
|  | import 'dart:math' as math; | ||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
|  | import 'package:gap/gap.dart'; | ||||||
|  | import 'package:go_router/go_router.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:island/models/embed.dart'; | ||||||
|  | import 'package:island/models/poll.dart'; | ||||||
|  | import 'package:island/models/post.dart'; | ||||||
|  | import 'package:island/pods/network.dart'; | ||||||
|  | import 'package:island/services/responsive.dart'; | ||||||
|  | import 'package:island/services/time.dart'; | ||||||
|  | import 'package:island/utils/mapping.dart'; | ||||||
|  | import 'package:island/widgets/account/account_name.dart'; | ||||||
|  | import 'package:island/widgets/alert.dart'; | ||||||
|  | import 'package:island/widgets/content/cloud_file_collection.dart'; | ||||||
|  | import 'package:island/widgets/content/cloud_files.dart'; | ||||||
|  | import 'package:island/widgets/content/embed/link.dart'; | ||||||
|  | import 'package:island/widgets/content/markdown.dart'; | ||||||
|  | import 'package:island/widgets/poll/poll_submit.dart'; | ||||||
|  | import 'package:island/widgets/post/post_replies_sheet.dart'; | ||||||
|  | import 'package:material_symbols_icons/symbols.dart'; | ||||||
|  | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
|  | import 'package:styled_widget/styled_widget.dart'; | ||||||
|  |  | ||||||
|  | part 'post_shared.g.dart'; | ||||||
|  |  | ||||||
|  | @riverpod | ||||||
|  | Future<SnPost?> postFeaturedReply(Ref ref, String id) async { | ||||||
|  |   final client = ref.watch(apiClientProvider); | ||||||
|  |   try { | ||||||
|  |     final resp = await client.get('/sphere/posts/$id/replies/featured'); | ||||||
|  |     return SnPost.fromJson(resp.data); | ||||||
|  |   } catch (_) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class PostVisibilityHelpers { | ||||||
|  |   static IconData getVisibilityIcon(int visibility) { | ||||||
|  |     switch (visibility) { | ||||||
|  |       case 1: | ||||||
|  |         return Symbols.group; | ||||||
|  |       case 2: | ||||||
|  |         return Symbols.link_off; | ||||||
|  |       case 3: | ||||||
|  |         return Symbols.lock; | ||||||
|  |       default: | ||||||
|  |         return Symbols.public; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static String getVisibilityText(int visibility) { | ||||||
|  |     switch (visibility) { | ||||||
|  |       case 1: | ||||||
|  |         return 'postVisibilityFriends'; | ||||||
|  |       case 2: | ||||||
|  |         return 'postVisibilityUnlisted'; | ||||||
|  |       case 3: | ||||||
|  |         return 'postVisibilityPrivate'; | ||||||
|  |       default: | ||||||
|  |         return 'postVisibilityPublic'; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class PostReplyPreview extends HookConsumerWidget { | ||||||
|  |   final SnPost parent; | ||||||
|  |   final bool isOpenable; | ||||||
|  |   final bool isCompact; | ||||||
|  |   final bool isAutoload; | ||||||
|  |   final VoidCallback? onOpen; | ||||||
|  |   const PostReplyPreview({ | ||||||
|  |     super.key, | ||||||
|  |     required this.parent, | ||||||
|  |     this.isOpenable = false, | ||||||
|  |     this.isCompact = false, | ||||||
|  |     this.isAutoload = true, | ||||||
|  |     this.onOpen, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final posts = useState<List<SnPost>>([]); | ||||||
|  |     final loading = useState(false); | ||||||
|  |  | ||||||
|  |     Future<void> fetchMoreReplies({int pageSize = 3}) async { | ||||||
|  |       final client = ref.read(apiClientProvider); | ||||||
|  |       loading.value = true; | ||||||
|  |  | ||||||
|  |       try { | ||||||
|  |         final response = await client.get( | ||||||
|  |           '/sphere/posts/${parent.id}/replies', | ||||||
|  |           queryParameters: {'offset': posts.value.length, 'take': pageSize}, | ||||||
|  |         ); | ||||||
|  |         try { | ||||||
|  |           posts.value = [ | ||||||
|  |             ...posts.value, | ||||||
|  |             ...response.data.map((e) => SnPost.fromJson(e)), | ||||||
|  |           ]; | ||||||
|  |         } catch (_) { | ||||||
|  |           // ignore disposed | ||||||
|  |         } | ||||||
|  |       } catch (err) { | ||||||
|  |         showErrorAlert(err); | ||||||
|  |       } finally { | ||||||
|  |         try { | ||||||
|  |           loading.value = false; | ||||||
|  |         } catch (_) { | ||||||
|  |           // ignore disposed | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     useEffect(() { | ||||||
|  |       if (isAutoload) fetchMoreReplies(); | ||||||
|  |       return null; | ||||||
|  |     }, [parent]); | ||||||
|  |  | ||||||
|  |     final featuredReply = | ||||||
|  |         isOpenable ? null : ref.watch(PostFeaturedReplyProvider(parent.id)); | ||||||
|  |  | ||||||
|  |     final itemWidget = | ||||||
|  |         isOpenable | ||||||
|  |             ? Column( | ||||||
|  |               children: [ | ||||||
|  |                 for (final post in posts.value) | ||||||
|  |                   Column( | ||||||
|  |                     children: [ | ||||||
|  |                       InkWell( | ||||||
|  |                         child: Row( | ||||||
|  |                           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                           spacing: 8, | ||||||
|  |                           children: [ | ||||||
|  |                             ProfilePictureWidget( | ||||||
|  |                               file: post.publisher.picture, | ||||||
|  |                               radius: 12, | ||||||
|  |                             ).padding(top: 4), | ||||||
|  |                             if (post.content?.isNotEmpty ?? false) | ||||||
|  |                               Expanded( | ||||||
|  |                                 child: MarkdownTextContent( | ||||||
|  |                                   content: post.content!, | ||||||
|  |                                 ).padding(top: 2), | ||||||
|  |                               ) | ||||||
|  |                             else | ||||||
|  |                               Expanded( | ||||||
|  |                                 child: Text( | ||||||
|  |                                   'postHasAttachments', | ||||||
|  |                                 ).plural(post.attachments.length), | ||||||
|  |                               ), | ||||||
|  |                           ], | ||||||
|  |                         ), | ||||||
|  |                         onTap: () { | ||||||
|  |                           onOpen?.call(); | ||||||
|  |                           context.pushNamed( | ||||||
|  |                             'postDetail', | ||||||
|  |                             pathParameters: {'id': post.id}, | ||||||
|  |                           ); | ||||||
|  |                         }, | ||||||
|  |                       ), | ||||||
|  |                       if (post.repliesCount > 0) | ||||||
|  |                         PostReplyPreview( | ||||||
|  |                           parent: post, | ||||||
|  |                           isOpenable: true, | ||||||
|  |                           isCompact: true, | ||||||
|  |                           isAutoload: false, | ||||||
|  |                           onOpen: onOpen, | ||||||
|  |                         ).padding(left: 24), | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|  |                 if (loading.value) | ||||||
|  |                   Row( | ||||||
|  |                     spacing: 8, | ||||||
|  |                     children: [ | ||||||
|  |                       SizedBox( | ||||||
|  |                         width: 16, | ||||||
|  |                         height: 16, | ||||||
|  |                         child: CircularProgressIndicator(), | ||||||
|  |                       ), | ||||||
|  |                       Text('loading').tr(), | ||||||
|  |                     ], | ||||||
|  |                   ) | ||||||
|  |                 else if (posts.value.length < parent.repliesCount) | ||||||
|  |                   InkWell( | ||||||
|  |                     child: Row( | ||||||
|  |                       spacing: 8, | ||||||
|  |                       children: [ | ||||||
|  |                         const Icon(Symbols.keyboard_arrow_down, size: 20), | ||||||
|  |                         Text('repliesLoadMore').tr(), | ||||||
|  |                       ], | ||||||
|  |                     ), | ||||||
|  |                     onTap: () { | ||||||
|  |                       fetchMoreReplies(); | ||||||
|  |                     }, | ||||||
|  |                   ), | ||||||
|  |               ], | ||||||
|  |             ) | ||||||
|  |             : (featuredReply!).map( | ||||||
|  |               data: | ||||||
|  |                   (data) => Row( | ||||||
|  |                     crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |                     spacing: 8, | ||||||
|  |                     children: [ | ||||||
|  |                       ProfilePictureWidget( | ||||||
|  |                         file: data.value?.publisher.picture, | ||||||
|  |                         radius: 12, | ||||||
|  |                       ).padding(top: 4), | ||||||
|  |                       if (data.value?.content?.isNotEmpty ?? false) | ||||||
|  |                         Expanded( | ||||||
|  |                           child: MarkdownTextContent( | ||||||
|  |                             content: data.value!.content!, | ||||||
|  |                           ), | ||||||
|  |                         ) | ||||||
|  |                       else | ||||||
|  |                         Expanded( | ||||||
|  |                           child: Text( | ||||||
|  |                             'postHasAttachments', | ||||||
|  |                           ).plural(data.value?.attachments.length ?? 0), | ||||||
|  |                         ), | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|  |               error: | ||||||
|  |                   (e) => Row( | ||||||
|  |                     spacing: 8, | ||||||
|  |                     children: [ | ||||||
|  |                       const Icon(Symbols.close, size: 18), | ||||||
|  |                       Text(e.error.toString()), | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|  |               loading: | ||||||
|  |                   (_) => Row( | ||||||
|  |                     spacing: 8, | ||||||
|  |                     children: [ | ||||||
|  |                       SizedBox( | ||||||
|  |                         width: 16, | ||||||
|  |                         height: 16, | ||||||
|  |                         child: CircularProgressIndicator(), | ||||||
|  |                       ), | ||||||
|  |                       Text('loading').tr(), | ||||||
|  |                     ], | ||||||
|  |                   ), | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |     final contentWidget = | ||||||
|  |         isCompact | ||||||
|  |             ? itemWidget | ||||||
|  |             : Container( | ||||||
|  |               padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), | ||||||
|  |               decoration: BoxDecoration( | ||||||
|  |                 color: Theme.of(context).colorScheme.surfaceContainerLow, | ||||||
|  |                 border: Border.all( | ||||||
|  |                   color: Theme.of(context).dividerColor.withOpacity(0.5), | ||||||
|  |                 ), | ||||||
|  |                 borderRadius: BorderRadius.all(Radius.circular(8)), | ||||||
|  |               ), | ||||||
|  |               child: Column( | ||||||
|  |                 crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |                 spacing: 4, | ||||||
|  |                 children: [ | ||||||
|  |                   Text('repliesCount') | ||||||
|  |                       .plural(parent.repliesCount) | ||||||
|  |                       .fontSize(15) | ||||||
|  |                       .bold() | ||||||
|  |                       .padding(horizontal: 5), | ||||||
|  |                   itemWidget, | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |     return InkWell( | ||||||
|  |       borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |       onTap: () { | ||||||
|  |         showModalBottomSheet( | ||||||
|  |           context: context, | ||||||
|  |           isScrollControlled: true, | ||||||
|  |           useRootNavigator: true, | ||||||
|  |           builder: (context) => PostRepliesSheet(post: parent), | ||||||
|  |         ); | ||||||
|  |       }, | ||||||
|  |       child: contentWidget, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class PostTruncateHint extends StatelessWidget { | ||||||
|  |   final bool isCompact; | ||||||
|  |   final EdgeInsets? margin; | ||||||
|  |   final bool withArrow; | ||||||
|  |  | ||||||
|  |   const PostTruncateHint({ | ||||||
|  |     super.key, | ||||||
|  |     this.isCompact = false, | ||||||
|  |     this.margin, | ||||||
|  |     this.withArrow = false, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Container( | ||||||
|  |       margin: margin ?? EdgeInsets.only(top: isCompact ? 4 : 8), | ||||||
|  |       padding: EdgeInsets.symmetric( | ||||||
|  |         horizontal: isCompact ? 8 : 12, | ||||||
|  |         vertical: isCompact ? 4 : 8, | ||||||
|  |       ), | ||||||
|  |       decoration: BoxDecoration( | ||||||
|  |         color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3), | ||||||
|  |         borderRadius: BorderRadius.circular(8), | ||||||
|  |         border: Border.all( | ||||||
|  |           color: Theme.of(context).colorScheme.outline.withOpacity(0.2), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       child: Row( | ||||||
|  |         mainAxisSize: MainAxisSize.min, | ||||||
|  |         children: [ | ||||||
|  |           Icon( | ||||||
|  |             Symbols.more_horiz, | ||||||
|  |             size: isCompact ? 14 : 16, | ||||||
|  |             color: Theme.of(context).colorScheme.secondary, | ||||||
|  |           ), | ||||||
|  |           SizedBox(width: isCompact ? 4 : 6), | ||||||
|  |           Flexible( | ||||||
|  |             child: Text( | ||||||
|  |               'postTruncated'.tr(), | ||||||
|  |               style: TextStyle( | ||||||
|  |                 fontSize: isCompact ? 10 : 12, | ||||||
|  |                 color: Theme.of(context).colorScheme.secondary, | ||||||
|  |                 fontStyle: FontStyle.italic, | ||||||
|  |               ), | ||||||
|  |               maxLines: 1, | ||||||
|  |               overflow: TextOverflow.ellipsis, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |           if (withArrow) ...[ | ||||||
|  |             SizedBox(width: isCompact ? 3 : 4), | ||||||
|  |             Icon( | ||||||
|  |               Symbols.arrow_forward, | ||||||
|  |               size: isCompact ? 12 : 14, | ||||||
|  |               color: Theme.of(context).colorScheme.secondary, | ||||||
|  |             ), | ||||||
|  |           ], | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class ReferencedPostWidget extends StatelessWidget { | ||||||
|  |   final SnPost item; | ||||||
|  |   final bool isInteractive; | ||||||
|  |   final EdgeInsets renderingPadding; | ||||||
|  |  | ||||||
|  |   const ReferencedPostWidget({ | ||||||
|  |     super.key, | ||||||
|  |     required this.item, | ||||||
|  |     this.isInteractive = true, | ||||||
|  |     this.renderingPadding = EdgeInsets.zero, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     final referencePost = item.repliedPost ?? item.forwardedPost; | ||||||
|  |     if (referencePost == null) return const SizedBox.shrink(); | ||||||
|  |  | ||||||
|  |     final isReply = item.repliedPost != null; | ||||||
|  |  | ||||||
|  |     final content = Container( | ||||||
|  |       padding: EdgeInsets.symmetric( | ||||||
|  |         horizontal: renderingPadding.horizontal, | ||||||
|  |         vertical: 8, | ||||||
|  |       ), | ||||||
|  |       margin: EdgeInsets.only( | ||||||
|  |         top: 8, | ||||||
|  |         left: renderingPadding.vertical, | ||||||
|  |         right: renderingPadding.vertical, | ||||||
|  |       ), | ||||||
|  |       decoration: BoxDecoration( | ||||||
|  |         color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5), | ||||||
|  |         borderRadius: BorderRadius.circular(12), | ||||||
|  |         border: Border.all( | ||||||
|  |           color: Theme.of(context).dividerColor.withOpacity(0.5), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |       child: Column( | ||||||
|  |         crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |         children: [ | ||||||
|  |           Row( | ||||||
|  |             children: [ | ||||||
|  |               Icon( | ||||||
|  |                 isReply ? Symbols.reply : Symbols.forward, | ||||||
|  |                 size: 16, | ||||||
|  |                 color: Theme.of(context).colorScheme.secondary, | ||||||
|  |               ), | ||||||
|  |               const SizedBox(width: 6), | ||||||
|  |               Text( | ||||||
|  |                 isReply ? 'repliedTo'.tr() : 'forwarded'.tr(), | ||||||
|  |                 style: TextStyle( | ||||||
|  |                   color: Theme.of(context).colorScheme.secondary, | ||||||
|  |                   fontWeight: FontWeight.w500, | ||||||
|  |                   fontSize: 12, | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |           const SizedBox(height: 8), | ||||||
|  |           Row( | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |             children: [ | ||||||
|  |               ProfilePictureWidget( | ||||||
|  |                 fileId: referencePost.publisher.picture?.id, | ||||||
|  |                 radius: 16, | ||||||
|  |               ), | ||||||
|  |               const SizedBox(width: 8), | ||||||
|  |               Expanded( | ||||||
|  |                 child: Column( | ||||||
|  |                   crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                   children: [ | ||||||
|  |                     Text( | ||||||
|  |                       referencePost.publisher.nick, | ||||||
|  |                       style: const TextStyle( | ||||||
|  |                         fontWeight: FontWeight.bold, | ||||||
|  |                         fontSize: 14, | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                     if (referencePost.visibility != 0) | ||||||
|  |                       Row( | ||||||
|  |                         mainAxisSize: MainAxisSize.min, | ||||||
|  |                         children: [ | ||||||
|  |                           Icon( | ||||||
|  |                             PostVisibilityHelpers.getVisibilityIcon( | ||||||
|  |                               referencePost.visibility, | ||||||
|  |                             ), | ||||||
|  |                             size: 12, | ||||||
|  |                             color: Theme.of(context).colorScheme.secondary, | ||||||
|  |                           ), | ||||||
|  |                           const SizedBox(width: 4), | ||||||
|  |                           Text( | ||||||
|  |                             PostVisibilityHelpers.getVisibilityText( | ||||||
|  |                               referencePost.visibility, | ||||||
|  |                             ).tr(), | ||||||
|  |                             style: TextStyle( | ||||||
|  |                               fontSize: 10, | ||||||
|  |                               color: Theme.of(context).colorScheme.secondary, | ||||||
|  |                             ), | ||||||
|  |                           ), | ||||||
|  |                         ], | ||||||
|  |                       ).padding(top: 2, bottom: 2), | ||||||
|  |                     if (referencePost.title?.isNotEmpty ?? false) | ||||||
|  |                       Text( | ||||||
|  |                         referencePost.title!, | ||||||
|  |                         style: TextStyle( | ||||||
|  |                           fontWeight: FontWeight.bold, | ||||||
|  |                           fontSize: 13, | ||||||
|  |                           color: Theme.of(context).colorScheme.onSurface, | ||||||
|  |                         ), | ||||||
|  |                       ).padding(top: 2, bottom: 2), | ||||||
|  |                     if (referencePost.description?.isNotEmpty ?? false) | ||||||
|  |                       Text( | ||||||
|  |                         referencePost.description!, | ||||||
|  |                         style: TextStyle( | ||||||
|  |                           fontSize: 12, | ||||||
|  |                           color: Theme.of(context).colorScheme.onSurfaceVariant, | ||||||
|  |                         ), | ||||||
|  |                         maxLines: 2, | ||||||
|  |                         overflow: TextOverflow.ellipsis, | ||||||
|  |                       ).padding(bottom: 2), | ||||||
|  |                     if (referencePost.content?.isNotEmpty ?? false) | ||||||
|  |                       MarkdownTextContent( | ||||||
|  |                         content: referencePost.content!, | ||||||
|  |                         textStyle: const TextStyle(fontSize: 14), | ||||||
|  |                         isSelectable: false, | ||||||
|  |                         linesMargin: | ||||||
|  |                             referencePost.type == 0 | ||||||
|  |                                 ? const EdgeInsets.only(bottom: 4) | ||||||
|  |                                 : null, | ||||||
|  |                         attachments: item.attachments, | ||||||
|  |                       ).padding(bottom: 4), | ||||||
|  |                     if (referencePost.isTruncated) | ||||||
|  |                       const PostTruncateHint( | ||||||
|  |                         isCompact: true, | ||||||
|  |                         margin: EdgeInsets.only(top: 4, bottom: 8), | ||||||
|  |                       ), | ||||||
|  |                     if (referencePost.attachments.isNotEmpty && | ||||||
|  |                         referencePost.type != 1) | ||||||
|  |                       Row( | ||||||
|  |                         mainAxisSize: MainAxisSize.min, | ||||||
|  |                         children: [ | ||||||
|  |                           Icon( | ||||||
|  |                             Symbols.attach_file, | ||||||
|  |                             size: 12, | ||||||
|  |                             color: Theme.of(context).colorScheme.secondary, | ||||||
|  |                           ), | ||||||
|  |                           const SizedBox(width: 4), | ||||||
|  |                           Text( | ||||||
|  |                             'postHasAttachments'.plural( | ||||||
|  |                               referencePost.attachments.length, | ||||||
|  |                             ), | ||||||
|  |                             style: TextStyle( | ||||||
|  |                               color: Theme.of(context).colorScheme.secondary, | ||||||
|  |                               fontSize: 12, | ||||||
|  |                             ), | ||||||
|  |                           ), | ||||||
|  |                         ], | ||||||
|  |                       ).padding(vertical: 2), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ], | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (!isInteractive) { | ||||||
|  |       return content; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return content.gestures( | ||||||
|  |       onTap: | ||||||
|  |           () => context.pushNamed( | ||||||
|  |             'postDetail', | ||||||
|  |             pathParameters: {'id': referencePost.id}, | ||||||
|  |           ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class PostHeader extends StatelessWidget { | ||||||
|  |   final SnPost item; | ||||||
|  |   final bool isFullPost; | ||||||
|  |   final Widget? trailing; | ||||||
|  |   final bool isInteractive; | ||||||
|  |   final EdgeInsets renderingPadding; | ||||||
|  |   final bool isRelativeTime; | ||||||
|  |  | ||||||
|  |   const PostHeader({ | ||||||
|  |     super.key, | ||||||
|  |     required this.item, | ||||||
|  |     this.isFullPost = false, | ||||||
|  |     this.trailing, | ||||||
|  |     this.isInteractive = true, | ||||||
|  |     this.renderingPadding = EdgeInsets.zero, | ||||||
|  |     this.isRelativeTime = true, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context) { | ||||||
|  |     return Row( | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |       spacing: 12, | ||||||
|  |       children: [ | ||||||
|  |         GestureDetector( | ||||||
|  |           onTap: | ||||||
|  |               isInteractive | ||||||
|  |                   ? () { | ||||||
|  |                     context.pushNamed( | ||||||
|  |                       'publisherProfile', | ||||||
|  |                       pathParameters: {'name': item.publisher.name}, | ||||||
|  |                     ); | ||||||
|  |                   } | ||||||
|  |                   : null, | ||||||
|  |           child: ProfilePictureWidget( | ||||||
|  |             file: item.publisher.picture, | ||||||
|  |             radius: 16, | ||||||
|  |             borderRadius: item.publisher.type == 0 ? null : 6, | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         Expanded( | ||||||
|  |           child: Column( | ||||||
|  |             mainAxisSize: MainAxisSize.min, | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |             children: [ | ||||||
|  |               Row( | ||||||
|  |                 crossAxisAlignment: CrossAxisAlignment.center, | ||||||
|  |                 spacing: 4, | ||||||
|  |                 children: [ | ||||||
|  |                   Text(item.publisher.nick).bold(), | ||||||
|  |                   if (item.publisher.verification != null) | ||||||
|  |                     VerificationMark(mark: item.publisher.verification!), | ||||||
|  |                   if (item.realm == null) | ||||||
|  |                     Text('@${item.publisher.name}').fontSize(11) | ||||||
|  |                   else | ||||||
|  |                     ...([ | ||||||
|  |                       const Icon(Symbols.arrow_right, size: 14), | ||||||
|  |                       InkWell( | ||||||
|  |                         child: Row( | ||||||
|  |                           mainAxisSize: MainAxisSize.min, | ||||||
|  |                           spacing: 5, | ||||||
|  |                           children: [ | ||||||
|  |                             Text(item.realm!.name), | ||||||
|  |                             ProfilePictureWidget( | ||||||
|  |                               file: item.realm!.picture, | ||||||
|  |                               fallbackIcon: Symbols.group, | ||||||
|  |                               radius: 9, | ||||||
|  |                             ), | ||||||
|  |                           ], | ||||||
|  |                         ), | ||||||
|  |                         onTap: () { | ||||||
|  |                           GoRouter.of(context).pushNamed( | ||||||
|  |                             'realmDetail', | ||||||
|  |                             pathParameters: {'slug': item.realm!.slug}, | ||||||
|  |                           ); | ||||||
|  |                         }, | ||||||
|  |                       ), | ||||||
|  |                     ]), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |               Row( | ||||||
|  |                 spacing: 6, | ||||||
|  |                 crossAxisAlignment: CrossAxisAlignment.end, | ||||||
|  |                 children: [ | ||||||
|  |                   Text( | ||||||
|  |                     !isFullPost && isRelativeTime | ||||||
|  |                         ? (item.publishedAt ?? item.createdAt)!.formatRelative( | ||||||
|  |                           context, | ||||||
|  |                         ) | ||||||
|  |                         : (item.publishedAt ?? item.createdAt)!.formatSystem(), | ||||||
|  |                   ).fontSize(10), | ||||||
|  |                   if (item.editedAt != null) | ||||||
|  |                     Text( | ||||||
|  |                       'editedAt'.tr( | ||||||
|  |                         args: [ | ||||||
|  |                           !isFullPost && isRelativeTime | ||||||
|  |                               ? item.editedAt!.formatRelative(context) | ||||||
|  |                               : item.editedAt!.formatSystem(), | ||||||
|  |                         ], | ||||||
|  |                       ), | ||||||
|  |                     ).fontSize(10), | ||||||
|  |                   if (item.visibility != 0) | ||||||
|  |                     Text( | ||||||
|  |                       PostVisibilityHelpers.getVisibilityText( | ||||||
|  |                         item.visibility, | ||||||
|  |                       ).tr(), | ||||||
|  |                     ).fontSize(10), | ||||||
|  |                 ], | ||||||
|  |               ), | ||||||
|  |             ], | ||||||
|  |           ), | ||||||
|  |         ), | ||||||
|  |         if (trailing != null) trailing!, | ||||||
|  |       ], | ||||||
|  |     ).padding(horizontal: renderingPadding.horizontal, bottom: 4); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class PostBody extends ConsumerWidget { | ||||||
|  |   final SnPost item; | ||||||
|  |   final bool isFullPost; | ||||||
|  |   final bool isTextSelectable; | ||||||
|  |   final Widget? translationSection; | ||||||
|  |   final bool isInteractive; | ||||||
|  |   final EdgeInsets renderingPadding; | ||||||
|  |  | ||||||
|  |   const PostBody({ | ||||||
|  |     super.key, | ||||||
|  |     required this.item, | ||||||
|  |     this.isFullPost = false, | ||||||
|  |     this.isTextSelectable = true, | ||||||
|  |     this.translationSection, | ||||||
|  |     this.isInteractive = true, | ||||||
|  |     this.renderingPadding = EdgeInsets.zero, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     return Column( | ||||||
|  |       crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |       children: [ | ||||||
|  |         if (!isFullPost && item.type == 1) | ||||||
|  |           Container( | ||||||
|  |             decoration: BoxDecoration( | ||||||
|  |               color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||||
|  |               border: Border.all( | ||||||
|  |                 color: Theme.of(context).dividerColor.withOpacity(0.5), | ||||||
|  |               ), | ||||||
|  |               borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||||
|  |             ), | ||||||
|  |             padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), | ||||||
|  |             margin: EdgeInsets.only( | ||||||
|  |               top: 4, | ||||||
|  |               left: renderingPadding.horizontal, | ||||||
|  |               right: renderingPadding.vertical, | ||||||
|  |             ), | ||||||
|  |             child: Column( | ||||||
|  |               mainAxisSize: MainAxisSize.min, | ||||||
|  |               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |               children: [ | ||||||
|  |                 Align( | ||||||
|  |                   alignment: Alignment.centerLeft, | ||||||
|  |                   child: Badge( | ||||||
|  |                     label: const Text('postArticle').tr(), | ||||||
|  |                     backgroundColor: Theme.of(context).colorScheme.primary, | ||||||
|  |                     textColor: Theme.of(context).colorScheme.onPrimary, | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |                 const Gap(4), | ||||||
|  |                 if (item.title != null) | ||||||
|  |                   Text( | ||||||
|  |                     item.title!, | ||||||
|  |                     style: Theme.of(context).textTheme.titleMedium!.copyWith( | ||||||
|  |                       fontWeight: FontWeight.bold, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 if (item.description != null) | ||||||
|  |                   Text( | ||||||
|  |                     item.description!, | ||||||
|  |                     style: Theme.of(context).textTheme.bodyMedium, | ||||||
|  |                   ) | ||||||
|  |                 else | ||||||
|  |                   MarkdownTextContent(content: '${item.content!}...'), | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ) | ||||||
|  |         else if ((item.content?.isNotEmpty ?? false) || | ||||||
|  |             (item.title?.isNotEmpty ?? false) || | ||||||
|  |             (item.description?.isNotEmpty ?? false)) | ||||||
|  |           Padding( | ||||||
|  |             padding: EdgeInsets.only( | ||||||
|  |               left: renderingPadding.horizontal, | ||||||
|  |               right: renderingPadding.horizontal, | ||||||
|  |             ), | ||||||
|  |             child: Column( | ||||||
|  |               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|  |               children: [ | ||||||
|  |                 if ((item.title?.isNotEmpty ?? false) || | ||||||
|  |                     (item.description?.isNotEmpty ?? false)) | ||||||
|  |                   Column( | ||||||
|  |                     crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |                     children: [ | ||||||
|  |                       if (item.title?.isNotEmpty ?? false) | ||||||
|  |                         Text( | ||||||
|  |                           item.title!, | ||||||
|  |                           style: Theme.of(context).textTheme.titleMedium! | ||||||
|  |                               .copyWith(fontWeight: FontWeight.bold), | ||||||
|  |                         ), | ||||||
|  |                       if (item.description?.isNotEmpty ?? false) | ||||||
|  |                         Text( | ||||||
|  |                           item.description!, | ||||||
|  |                           style: Theme.of(context).textTheme.bodyMedium, | ||||||
|  |                         ), | ||||||
|  |                     ], | ||||||
|  |                   ).padding(bottom: 4), | ||||||
|  |                 MarkdownTextContent( | ||||||
|  |                   content: | ||||||
|  |                       item.isTruncated ? '${item.content!}...' : item.content!, | ||||||
|  |                   isSelectable: isTextSelectable, | ||||||
|  |                 ), | ||||||
|  |                 if (translationSection != null) translationSection!, | ||||||
|  |               ], | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         if (item.isTruncated && item.type != 1) | ||||||
|  |           PostTruncateHint( | ||||||
|  |             isCompact: true, | ||||||
|  |             withArrow: isInteractive, | ||||||
|  |             margin: EdgeInsets.only( | ||||||
|  |               top: 4, | ||||||
|  |               bottom: 4, | ||||||
|  |               left: renderingPadding.horizontal, | ||||||
|  |               right: renderingPadding.horizontal, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         if (item.attachments.isNotEmpty && item.type != 1) | ||||||
|  |           CloudFileList( | ||||||
|  |             files: item.attachments, | ||||||
|  |             isColumn: !isInteractive, | ||||||
|  |             padding: EdgeInsets.symmetric( | ||||||
|  |               horizontal: renderingPadding.horizontal, | ||||||
|  |               vertical: 4, | ||||||
|  |             ), | ||||||
|  |           ), | ||||||
|  |         if (item.tags.isNotEmpty || item.categories.isNotEmpty) | ||||||
|  |           Column( | ||||||
|  |             mainAxisSize: MainAxisSize.min, | ||||||
|  |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|  |             spacing: 2, | ||||||
|  |             children: [ | ||||||
|  |               if (item.tags.isNotEmpty) | ||||||
|  |                 Wrap( | ||||||
|  |                   runAlignment: WrapAlignment.center, | ||||||
|  |                   spacing: 8, | ||||||
|  |                   children: [ | ||||||
|  |                     const Icon(Symbols.label, size: 16).padding(top: 2), | ||||||
|  |                     for (final tag | ||||||
|  |                         in isFullPost ? item.tags : item.tags.take(3)) | ||||||
|  |                       InkWell( | ||||||
|  |                         onTap: | ||||||
|  |                             isInteractive | ||||||
|  |                                 ? () { | ||||||
|  |                                   GoRouter.of(context).pushNamed( | ||||||
|  |                                     'postTagDetail', | ||||||
|  |                                     pathParameters: {'slug': tag.slug}, | ||||||
|  |                                   ); | ||||||
|  |                                 } | ||||||
|  |                                 : null, | ||||||
|  |                         child: Text('#${tag.name ?? tag.slug}'), | ||||||
|  |                       ), | ||||||
|  |                     if (!isFullPost && item.tags.length > 3) | ||||||
|  |                       Text('+${item.tags.length - 3}').opacity(0.6), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |               if (item.categories.isNotEmpty) | ||||||
|  |                 Wrap( | ||||||
|  |                   runAlignment: WrapAlignment.center, | ||||||
|  |                   spacing: 8, | ||||||
|  |                   children: [ | ||||||
|  |                     const Icon(Symbols.category, size: 16).padding(top: 2), | ||||||
|  |                     for (final category | ||||||
|  |                         in isFullPost | ||||||
|  |                             ? item.categories | ||||||
|  |                             : item.categories.take(2)) | ||||||
|  |                       InkWell( | ||||||
|  |                         onTap: | ||||||
|  |                             isInteractive | ||||||
|  |                                 ? () { | ||||||
|  |                                   GoRouter.of(context).pushNamed( | ||||||
|  |                                     'postCategoryDetail', | ||||||
|  |                                     pathParameters: {'slug': category.slug}, | ||||||
|  |                                   ); | ||||||
|  |                                 } | ||||||
|  |                                 : null, | ||||||
|  |                         child: Text(category.categoryDisplayTitle), | ||||||
|  |                       ), | ||||||
|  |                     if (!isFullPost && item.categories.length > 2) | ||||||
|  |                       Text('+${item.categories.length - 2}').opacity(0.6), | ||||||
|  |                   ], | ||||||
|  |                 ), | ||||||
|  |             ], | ||||||
|  |           ).padding(horizontal: renderingPadding.horizontal + 4, top: 4), | ||||||
|  |         if (item.meta?['embeds'] != null) | ||||||
|  |           ...((item.meta!['embeds'] as List<dynamic>) | ||||||
|  |               .map((embedData) => convertMapKeysToSnakeCase(embedData)) | ||||||
|  |               .map( | ||||||
|  |                 (embedData) => switch (embedData['type']) { | ||||||
|  |                   'link' => EmbedLinkWidget( | ||||||
|  |                     link: SnScrappedLink.fromJson(embedData), | ||||||
|  |                     maxWidth: math.min( | ||||||
|  |                       MediaQuery.of(context).size.width, | ||||||
|  |                       kWideScreenWidth, | ||||||
|  |                     ), | ||||||
|  |                     margin: EdgeInsets.only( | ||||||
|  |                       top: 4, | ||||||
|  |                       bottom: 4, | ||||||
|  |                       left: renderingPadding.horizontal, | ||||||
|  |                       right: renderingPadding.horizontal, | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                   'poll' => Card( | ||||||
|  |                     margin: EdgeInsets.symmetric( | ||||||
|  |                       horizontal: renderingPadding.horizontal, | ||||||
|  |                       vertical: 8, | ||||||
|  |                     ), | ||||||
|  |                     child: | ||||||
|  |                         embedData['poll'] == null | ||||||
|  |                             ? const Text('Poll was not loaded...') | ||||||
|  |                             : PollSubmit( | ||||||
|  |                               initialAnswers: | ||||||
|  |                                   embedData['poll']?['user_answer']?['answer'], | ||||||
|  |                               stats: embedData['poll']?['stats'], | ||||||
|  |                               poll: SnPollWithStats.fromJson(embedData['poll']), | ||||||
|  |                               onSubmit: (_) {}, | ||||||
|  |                               isReadonly: !isInteractive, | ||||||
|  |                             ).padding(horizontal: 16, vertical: 12), | ||||||
|  |                   ), | ||||||
|  |                   _ => Text('Unable show embed: ${embedData['type']}'), | ||||||
|  |                 }, | ||||||
|  |               )), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
| 
 | 
 | ||||||
| part of 'post_item.dart'; | part of 'post_shared.dart'; | ||||||
| 
 | 
 | ||||||
| // ************************************************************************** | // ************************************************************************** | ||||||
| // RiverpodGenerator | // RiverpodGenerator | ||||||
| @@ -340,22 +340,33 @@ class _ShareSheetState extends ConsumerState<ShareSheet> { | |||||||
|   Future<void> _shareToSystem() async { |   Future<void> _shareToSystem() async { | ||||||
|     if (!widget.toSystem) return; |     if (!widget.toSystem) return; | ||||||
|  |  | ||||||
|  |     final box = context.findRenderObject() as RenderBox?; | ||||||
|  |  | ||||||
|     setState(() => _isLoading = true); |     setState(() => _isLoading = true); | ||||||
|     try { |     try { | ||||||
|       switch (widget.content.type) { |       switch (widget.content.type) { | ||||||
|         case ShareContentType.text: |         case ShareContentType.text: | ||||||
|           if (widget.content.text?.isNotEmpty == true) { |           if (widget.content.text?.isNotEmpty == true) { | ||||||
|             await Share.share(widget.content.text!); |             await Share.share( | ||||||
|  |               widget.content.text!, | ||||||
|  |               sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, | ||||||
|  |             ); | ||||||
|           } |           } | ||||||
|           break; |           break; | ||||||
|         case ShareContentType.link: |         case ShareContentType.link: | ||||||
|           if (widget.content.link?.isNotEmpty == true) { |           if (widget.content.link?.isNotEmpty == true) { | ||||||
|             await Share.share(widget.content.link!); |             await Share.share( | ||||||
|  |               widget.content.link!, | ||||||
|  |               sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, | ||||||
|  |             ); | ||||||
|           } |           } | ||||||
|           break; |           break; | ||||||
|         case ShareContentType.file: |         case ShareContentType.file: | ||||||
|           if (widget.content.files?.isNotEmpty == true) { |           if (widget.content.files?.isNotEmpty == true) { | ||||||
|             await Share.shareXFiles(widget.content.files!); |             await Share.shareXFiles( | ||||||
|  |               widget.content.files!, | ||||||
|  |               sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, | ||||||
|  |             ); | ||||||
|           } |           } | ||||||
|           break; |           break; | ||||||
|       } |       } | ||||||
| @@ -879,7 +890,8 @@ class _LinkPreview extends ConsumerWidget { | |||||||
|             crossAxisAlignment: CrossAxisAlignment.start, |             crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|             children: [ |             children: [ | ||||||
|               // Favicon and image |               // Favicon and image | ||||||
|               if (embed.imageUrl != null || embed.faviconUrl.isNotEmpty) |               if (embed.imageUrl != null || | ||||||
|  |                   (embed.faviconUrl?.isNotEmpty ?? false)) | ||||||
|                 Container( |                 Container( | ||||||
|                   width: 60, |                   width: 60, | ||||||
|                   height: 60, |                   height: 60, | ||||||
| @@ -899,11 +911,14 @@ class _LinkPreview extends ConsumerWidget { | |||||||
|                               errorBuilder: (context, error, stackTrace) { |                               errorBuilder: (context, error, stackTrace) { | ||||||
|                                 return _buildFaviconFallback( |                                 return _buildFaviconFallback( | ||||||
|                                   context, |                                   context, | ||||||
|                                   embed.faviconUrl, |                                   embed.faviconUrl ?? '', | ||||||
|                                 ); |                                 ); | ||||||
|                               }, |                               }, | ||||||
|                             ) |                             ) | ||||||
|                             : _buildFaviconFallback(context, embed.faviconUrl), |                             : _buildFaviconFallback( | ||||||
|  |                               context, | ||||||
|  |                               embed.faviconUrl ?? '', | ||||||
|  |                             ), | ||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|               // Content |               // Content | ||||||
| @@ -912,9 +927,9 @@ class _LinkPreview extends ConsumerWidget { | |||||||
|                   crossAxisAlignment: CrossAxisAlignment.start, |                   crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                   children: [ |                   children: [ | ||||||
|                     // Site name |                     // Site name | ||||||
|                     if (embed.siteName.isNotEmpty) |                     if (embed.siteName?.isNotEmpty ?? false) | ||||||
|                       Text( |                       Text( | ||||||
|                         embed.siteName, |                         embed.siteName!, | ||||||
|                         style: Theme.of(context).textTheme.labelSmall?.copyWith( |                         style: Theme.of(context).textTheme.labelSmall?.copyWith( | ||||||
|                           color: Theme.of(context).colorScheme.primary, |                           color: Theme.of(context).colorScheme.primary, | ||||||
|                         ), |                         ), | ||||||
|   | |||||||
| @@ -41,7 +41,7 @@ endif() | |||||||
| # of modifying this function. | # of modifying this function. | ||||||
| function(APPLY_STANDARD_SETTINGS TARGET) | function(APPLY_STANDARD_SETTINGS TARGET) | ||||||
|   target_compile_features(${TARGET} PUBLIC cxx_std_14) |   target_compile_features(${TARGET} PUBLIC cxx_std_14) | ||||||
|   target_compile_options(${TARGET} PRIVATE -Wall -Werror) |   target_compile_options(${TARGET} PRIVATE -Wall -Wextra) | ||||||
|   target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>") |   target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>") | ||||||
|   target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>") |   target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>") | ||||||
| endfunction() | endfunction() | ||||||
|   | |||||||
| @@ -195,7 +195,7 @@ PODS: | |||||||
|   - PromisesObjC (2.4.0) |   - PromisesObjC (2.4.0) | ||||||
|   - PromisesSwift (2.4.0): |   - PromisesSwift (2.4.0): | ||||||
|     - PromisesObjC (= 2.4.0) |     - PromisesObjC (= 2.4.0) | ||||||
|   - record_macos (1.0.0): |   - record_macos (1.1.0): | ||||||
|     - FlutterMacOS |     - FlutterMacOS | ||||||
|   - SAMKeychain (1.5.3) |   - SAMKeychain (1.5.3) | ||||||
|   - share_plus (0.0.1): |   - share_plus (0.0.1): | ||||||
| @@ -422,7 +422,7 @@ SPEC CHECKSUMS: | |||||||
|   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 |   path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 | ||||||
|   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 |   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 | ||||||
|   PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 |   PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 | ||||||
|   record_macos: 295d70bd5fb47145df78df7b80e6697cd18403c0 |   record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b | ||||||
|   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c |   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c | ||||||
|   share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc |   share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc | ||||||
|   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 |   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 | ||||||
|   | |||||||
							
								
								
									
										138
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										138
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -301,10 +301,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: connectivity_plus |       name: connectivity_plus | ||||||
|       sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" |       sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "6.1.4" |     version: "6.1.5" | ||||||
|   connectivity_plus_platform_interface: |   connectivity_plus_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -313,6 +313,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.0.1" |     version: "2.0.1" | ||||||
|  |   console: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: console | ||||||
|  |       sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "4.1.0" | ||||||
|   convert: |   convert: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -405,10 +413,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: dart_webrtc |       name: dart_webrtc | ||||||
|       sha256: a2ae542cdadc21359022adedc26138fa3487cc3b3547c24ff4f556681869e28c |       sha256: "3bfa069a8b14a53ba506f6dd529e9b88c878ba0cc238f311051a39bf1e53d075" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.5.3+hotfix.4" |     version: "1.5.3+hotfix.5" | ||||||
|   dbus: |   dbus: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -557,10 +565,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: file_picker |       name: file_picker | ||||||
|       sha256: "970d33d79e1da667b6da222575fd7f2e30e323ca76251504477e6d51405b2d9a" |       sha256: ef7d2a085c1b1d69d17b6842d0734aad90156de08df6bd3c12496d0bd6ddf8e2 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "10.2.4" |     version: "10.3.1" | ||||||
|   file_selector_linux: |   file_selector_linux: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -746,10 +754,10 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: flutter_hooks |       name: flutter_hooks | ||||||
|       sha256: b772e710d16d7a20c0740c4f855095026b31c7eb5ba3ab67d2bd52021cd9461d |       sha256: c3df76c62bb3a9f9bee75c57cdab40abab6123b734c1cd7e9b26a5dbd436eceb | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.21.2" |     version: "0.21.3" | ||||||
|   flutter_inappwebview: |   flutter_inappwebview: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -1109,6 +1117,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.1" |     version: "3.0.1" | ||||||
|  |   get_it: | ||||||
|  |     dependency: transitive | ||||||
|  |     description: | ||||||
|  |       name: get_it | ||||||
|  |       sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "8.2.0" | ||||||
|   glob: |   glob: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1217,66 +1233,66 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: image_picker |       name: image_picker | ||||||
|       sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" |       sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.1.2" |     version: "1.2.0" | ||||||
|   image_picker_android: |   image_picker_android: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: image_picker_android |       name: image_picker_android | ||||||
|       sha256: b08e9a04d0f8d91f4a6e767a745b9871bfbc585410205c311d0492de20a7ccd6 |       sha256: e83b2b05141469c5e19d77e1dfa11096b6b1567d09065b2265d7c6904560050c | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.8.12+25" |     version: "0.8.13" | ||||||
|   image_picker_for_web: |   image_picker_for_web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: image_picker_for_web |       name: image_picker_for_web | ||||||
|       sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" |       sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.6" |     version: "3.1.0" | ||||||
|   image_picker_ios: |   image_picker_ios: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: image_picker_ios |       name: image_picker_ios | ||||||
|       sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" |       sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.8.12+2" |     version: "0.8.13" | ||||||
|   image_picker_linux: |   image_picker_linux: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: image_picker_linux |       name: image_picker_linux | ||||||
|       sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" |       sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.2.1+2" |     version: "0.2.2" | ||||||
|   image_picker_macos: |   image_picker_macos: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: image_picker_macos |       name: image_picker_macos | ||||||
|       sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" |       sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.2.1+2" |     version: "0.2.2" | ||||||
|   image_picker_platform_interface: |   image_picker_platform_interface: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: image_picker_platform_interface |       name: image_picker_platform_interface | ||||||
|       sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" |       sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.10.1" |     version: "2.11.0" | ||||||
|   image_picker_windows: |   image_picker_windows: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: image_picker_windows |       name: image_picker_windows | ||||||
|       sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" |       sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "0.2.1+1" |     version: "0.2.2" | ||||||
|   intl: |   intl: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1581,6 +1597,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.0.0" |     version: "3.0.0" | ||||||
|  |   msix: | ||||||
|  |     dependency: "direct dev" | ||||||
|  |     description: | ||||||
|  |       name: msix | ||||||
|  |       sha256: f88033fcb9e0dd8de5b18897cbebbd28ea30596810f4a7c86b12b0c03ace87e5 | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.16.12" | ||||||
|   native_exif: |   native_exif: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -1625,18 +1649,18 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: package_info_plus |       name: package_info_plus | ||||||
|       sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" |       sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "8.3.0" |     version: "8.3.1" | ||||||
|   package_info_plus_platform_interface: |   package_info_plus_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: package_info_plus_platform_interface |       name: package_info_plus_platform_interface | ||||||
|       sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" |       sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "3.2.0" |     version: "3.2.1" | ||||||
|   palette_generator: |   palette_generator: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -1809,10 +1833,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: protobuf |       name: protobuf | ||||||
|       sha256: "6153efcc92a06910918f3db8231fd2cf828ac81e50ebd87adc8f8a8cb3caff0e" |       sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "4.1.1" |     version: "4.2.0" | ||||||
|   provider: |   provider: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -1873,58 +1897,58 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: record |       name: record | ||||||
|       sha256: daeb3f9b3fea9797094433fe6e49a879d8e4ca4207740bc6dc7e4a58764f0817 |       sha256: "3d08502b77edf2a864aa6e4cd7874b983d42a80f3689431da053cc5e85c1ad21" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "6.0.0" |     version: "6.1.0" | ||||||
|   record_android: |   record_android: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: record_android |       name: record_android | ||||||
|       sha256: "97d7122455f30de89a01c6c244c839085be6b12abca251fc0e78f67fed73628b" |       sha256: "8b170e33d9866f9b51e01a767d7e1ecb97b9ecd629950bd87a47c79359ec57f8" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.3.3" |     version: "1.4.0" | ||||||
|   record_ios: |   record_ios: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: record_ios |       name: record_ios | ||||||
|       sha256: "73706ebbece6150654c9d6f57897cf9b622c581148304132ba85dba15df0fdfb" |       sha256: ad97d0a75933c44bcf5aff648e86e32fc05eb61f8fbef190f14968c8eaf86692 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.0.0" |     version: "1.1.0" | ||||||
|   record_linux: |   record_linux: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: record_linux |       name: record_linux | ||||||
|       sha256: "0626678a092c75ce6af1e32fe7fd1dea709b92d308bc8e3b6d6348e2430beb95" |       sha256: "785e8e8d6db109aa606d0669d95aaae416458aaa39782bb0abe0bee74eee17d7" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.1.1" |     version: "1.2.0" | ||||||
|   record_macos: |   record_macos: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: record_macos |       name: record_macos | ||||||
|       sha256: "02240833fde16c33fcf2c589f3e08d4394b704761b4a3bb609d872ff3043fbbd" |       sha256: f1399bca76a1634da109e5b0cba764ed8332a2b4da49c704c66d2c553405ed81 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.0.0" |     version: "1.1.0" | ||||||
|   record_platform_interface: |   record_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: record_platform_interface |       name: record_platform_interface | ||||||
|       sha256: c1ad38f51e4af88a085b3e792a22c685cb3e7c23fc37aa7ce44c4cf18f25fe89 |       sha256: b0065fdf1ec28f5a634d676724d388a77e43ce7646fb049949f58c69f3fcb4ed | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.3.0" |     version: "1.4.0" | ||||||
|   record_web: |   record_web: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: record_web |       name: record_web | ||||||
|       sha256: a12856d0b3dd03d336b4b10d7520a8b3e21649a06a8f95815318feaa8f07adbb |       sha256: "4f0adf20c9ccafcc02d71111fd91fba1ca7b17a7453902593e5a9b25b74a5c56" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.1.9" |     version: "1.2.0" | ||||||
|   record_windows: |   record_windows: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -2021,6 +2045,14 @@ packages: | |||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "2.1.0" |     version: "2.1.0" | ||||||
|  |   screenshot: | ||||||
|  |     dependency: "direct main" | ||||||
|  |     description: | ||||||
|  |       name: screenshot | ||||||
|  |       sha256: "63817697a7835e6ce82add4228e15d233b74d42975c143ad8cfe07009fab866b" | ||||||
|  |       url: "https://pub.dev" | ||||||
|  |     source: hosted | ||||||
|  |     version: "3.0.0" | ||||||
|   scroll_to_index: |   scroll_to_index: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -2041,18 +2073,18 @@ packages: | |||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
|       name: share_plus |       name: share_plus | ||||||
|       sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0 |       sha256: d7dc0630a923883c6328ca31b89aa682bacbf2f8304162d29f7c6aaff03a27a1 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "11.0.0" |     version: "11.1.0" | ||||||
|   share_plus_platform_interface: |   share_plus_platform_interface: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: share_plus_platform_interface |       name: share_plus_platform_interface | ||||||
|       sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef" |       sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "6.0.0" |     version: "6.1.0" | ||||||
|   shared_preferences: |   shared_preferences: | ||||||
|     dependency: "direct main" |     dependency: "direct main" | ||||||
|     description: |     description: | ||||||
| @@ -2174,10 +2206,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: source_helper |       name: source_helper | ||||||
|       sha256: "4f81479fe5194a622cdd1713fe1ecb683a6e6c85cd8cec8e2e35ee5ab3fdf2a1" |       sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.3.6" |     version: "1.3.7" | ||||||
|   source_span: |   source_span: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -2536,10 +2568,10 @@ packages: | |||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
|       name: vector_graphics_compiler |       name: vector_graphics_compiler | ||||||
|       sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331" |       sha256: ca81fdfaf62a5ab45d7296614aea108d2c7d0efca8393e96174bf4d51e6725b0 | ||||||
|       url: "https://pub.dev" |       url: "https://pub.dev" | ||||||
|     source: hosted |     source: hosted | ||||||
|     version: "1.1.17" |     version: "1.1.18" | ||||||
|   vector_math: |   vector_math: | ||||||
|     dependency: transitive |     dependency: transitive | ||||||
|     description: |     description: | ||||||
| @@ -2686,4 +2718,4 @@ packages: | |||||||
|     version: "3.1.3" |     version: "3.1.3" | ||||||
| sdks: | sdks: | ||||||
|   dart: ">=3.8.0 <4.0.0" |   dart: ">=3.8.0 <4.0.0" | ||||||
|   flutter: ">=3.29.0" |   flutter: ">=3.32.0" | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								pubspec.yaml
									
									
									
									
									
								
							| @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev | |||||||
| # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html | ||||||
| # In Windows, build-name is used as the major, minor, and patch parts | # In Windows, build-name is used as the major, minor, and patch parts | ||||||
| # of the product and file versions while build-number is used as the build suffix. | # of the product and file versions while build-number is used as the build suffix. | ||||||
| version: 3.2.0+124 | version: 3.2.0+125 | ||||||
|  |  | ||||||
| environment: | environment: | ||||||
|   sdk: ^3.7.2 |   sdk: ^3.7.2 | ||||||
| @@ -36,7 +36,7 @@ dependencies: | |||||||
|   # The following adds the Cupertino Icons font to your application. |   # The following adds the Cupertino Icons font to your application. | ||||||
|   # Use with the CupertinoIcons class for iOS style icons. |   # Use with the CupertinoIcons class for iOS style icons. | ||||||
|   cupertino_icons: ^1.0.8 |   cupertino_icons: ^1.0.8 | ||||||
|   flutter_hooks: ^0.21.2 |   flutter_hooks: ^0.21.3 | ||||||
|   hooks_riverpod: ^2.6.1 |   hooks_riverpod: ^2.6.1 | ||||||
|   bitsdojo_window: ^0.1.6 |   bitsdojo_window: ^0.1.6 | ||||||
|   go_router: ^16.1.0 |   go_router: ^16.1.0 | ||||||
| @@ -67,16 +67,16 @@ dependencies: | |||||||
|   easy_localization: ^3.0.8 |   easy_localization: ^3.0.8 | ||||||
|   flutter_inappwebview: ^6.1.5 |   flutter_inappwebview: ^6.1.5 | ||||||
|   animations: ^2.0.11 |   animations: ^2.0.11 | ||||||
|   package_info_plus: ^8.3.0 |   package_info_plus: ^8.3.1 | ||||||
|   device_info_plus: ^11.5.0 |   device_info_plus: ^11.5.0 | ||||||
|   tus_client_dart: |   tus_client_dart: | ||||||
|     git: https://github.com/LittleSheep2Code/tus_client.git |     git: https://github.com/LittleSheep2Code/tus_client.git | ||||||
|   cross_file: ^0.3.4+2 |   cross_file: ^0.3.4+2 | ||||||
|   image_picker: ^1.1.2 |   image_picker: ^1.2.0 | ||||||
|   file_picker: ^10.2.4 |   file_picker: ^10.3.1 | ||||||
|   riverpod_annotation: ^2.6.1 |   riverpod_annotation: ^2.6.1 | ||||||
|   image_picker_platform_interface: ^2.10.1 |   image_picker_platform_interface: ^2.11.0 | ||||||
|   image_picker_android: ^0.8.12+25 |   image_picker_android: ^0.8.13 | ||||||
|   super_context_menu: ^0.9.1 |   super_context_menu: ^0.9.1 | ||||||
|   modal_bottom_sheet: ^3.0.0 |   modal_bottom_sheet: ^3.0.0 | ||||||
|   firebase_messaging: ^16.0.0 |   firebase_messaging: ^16.0.0 | ||||||
| @@ -107,7 +107,7 @@ dependencies: | |||||||
|   livekit_client: ^2.5.0+hotfix.1 |   livekit_client: ^2.5.0+hotfix.1 | ||||||
|   pasteboard: ^0.4.0 |   pasteboard: ^0.4.0 | ||||||
|   flutter_colorpicker: ^1.1.0 |   flutter_colorpicker: ^1.1.0 | ||||||
|   record: ^6.0.0 |   record: ^6.1.0 | ||||||
|   qr_flutter: ^4.1.0 |   qr_flutter: ^4.1.0 | ||||||
|   flutter_otp_text_field: ^1.5.1+1 |   flutter_otp_text_field: ^1.5.1+1 | ||||||
|   palette_generator: ^0.3.3+7 |   palette_generator: ^0.3.3+7 | ||||||
| @@ -121,7 +121,7 @@ dependencies: | |||||||
|   local_auth: ^2.3.0 |   local_auth: ^2.3.0 | ||||||
|   flutter_secure_storage: ^9.2.4 |   flutter_secure_storage: ^9.2.4 | ||||||
|   flutter_math_fork: ^0.7.4 |   flutter_math_fork: ^0.7.4 | ||||||
|   share_plus: ^11.0.0 |   share_plus: ^11.1.0 | ||||||
|   receive_sharing_intent: ^1.8.1 |   receive_sharing_intent: ^1.8.1 | ||||||
|   top_snackbar_flutter: ^3.3.0 |   top_snackbar_flutter: ^3.3.0 | ||||||
|   textfield_tags: |   textfield_tags: | ||||||
| @@ -137,6 +137,7 @@ dependencies: | |||||||
|   firebase_crashlytics: ^5.0.0 |   firebase_crashlytics: ^5.0.0 | ||||||
|   firebase_analytics: ^12.0.0 |   firebase_analytics: ^12.0.0 | ||||||
|   material_color_utilities: ^0.11.1 |   material_color_utilities: ^0.11.1 | ||||||
|  |   screenshot: ^3.0.0 | ||||||
|  |  | ||||||
| dev_dependencies: | dev_dependencies: | ||||||
|   flutter_test: |   flutter_test: | ||||||
| @@ -156,6 +157,7 @@ dev_dependencies: | |||||||
|   riverpod_lint: ^2.6.5 |   riverpod_lint: ^2.6.5 | ||||||
|   drift_dev: ^2.28.0 |   drift_dev: ^2.28.0 | ||||||
|   flutter_launcher_icons: ^0.14.4 |   flutter_launcher_icons: ^0.14.4 | ||||||
|  |   msix: ^3.16.12 | ||||||
|  |  | ||||||
| # For information on the generic Dart part of this file, see the | # For information on the generic Dart part of this file, see the | ||||||
| # following page: https://dart.dev/tools/pub/pubspec | # following page: https://dart.dev/tools/pub/pubspec | ||||||
| @@ -225,3 +227,11 @@ flutter_native_splash: | |||||||
|   image_dark: "assets/icons/icon-dark.png" |   image_dark: "assets/icons/icon-dark.png" | ||||||
|   color: "#ffffff" |   color: "#ffffff" | ||||||
|   color_dark: "#121212" |   color_dark: "#121212" | ||||||
|  |  | ||||||
|  | msix_config: | ||||||
|  |   display_name: Solian | ||||||
|  |   publisher_display_name: Solsynth LLC | ||||||
|  |   identity_name: dev.solian.app | ||||||
|  |   msix_version: 3.2.0.0 | ||||||
|  |   logo_path: .\assets\icons\icon.png | ||||||
|  |   capabilities: internetClientServer, location, microphone, webcam | ||||||
							
								
								
									
										52
									
								
								setup.iss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								setup.iss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | ; ================================================== | ||||||
|  | #define AppVersion "3.2.0" | ||||||
|  | #define BuildNumber "124" | ||||||
|  | ; ================================================== | ||||||
|  |  | ||||||
|  | #define FullVersion AppVersion + "." + BuildNumber | ||||||
|  |  | ||||||
|  | [Setup] | ||||||
|  | AppName=Solian | ||||||
|  | AppVersion={#AppVersion} | ||||||
|  | AppPublisher=Solsynth | ||||||
|  | AppPublisherURL=https://solsynth.dev | ||||||
|  | AppSupportURL=https://kb.solsynth.dev/zh/solar-network | ||||||
|  | AppUpdatesURL=https://github.com/Solsynth/Solian/releases | ||||||
|  | AppCopyright=Copyright © 2025 Solsynth | ||||||
|  | VersionInfoVersion={#FullVersion} | ||||||
|  | UninstallDisplayName=Solian | ||||||
|  | UninstallDisplayIcon={app}\Solian.exe | ||||||
|  |  | ||||||
|  | DefaultDirName={commonpf}\Solian | ||||||
|  | UsePreviousAppDir=no | ||||||
|  |  | ||||||
|  | OutputDir=.\Installer | ||||||
|  | OutputBaseFilename=windows-x86_64-setup | ||||||
|  | SetupIconFile=.\assets\icons\icon.ico   | ||||||
|  |  | ||||||
|  | Compression=lzma2/ultra64 | ||||||
|  | SolidCompression=yes | ||||||
|  | LZMAUseSeparateProcess=yes | ||||||
|  | LZMANumBlockThreads=4 | ||||||
|  |  | ||||||
|  | ArchitecturesAllowed=x64compatible | ||||||
|  | PrivilegesRequired=admin | ||||||
|  |  | ||||||
|  | [Files] | ||||||
|  | Source: ".\build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs | ||||||
|  |  | ||||||
|  | [Icons] | ||||||
|  | Name: "{group}\Solian"; Filename: "{app}\Solian.exe";IconFilename: "{app}\Solian.exe" | ||||||
|  | Name: "{group}\{cm:UninstallProgram,Solian}"; Filename: "{uninstallexe}" | ||||||
|  | Name: "{autodesktop}\Solian"; Filename: "{app}\Solian.exe"; Tasks: desktopicon | ||||||
|  |  | ||||||
|  | [Tasks] | ||||||
|  | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked | ||||||
|  |  | ||||||
|  | [Run] | ||||||
|  | Filename: "{app}\Solian.exe"; Description: "Launch Solian"; Flags: nowait postinstall skipifsilent | ||||||
|  |  | ||||||
|  | [UninstallDelete] | ||||||
|  | Type: filesandordirs; Name: "{userappdata}\dev.solsynth\Solian" | ||||||
|  | Type: files; Name: "{group}\Solian.lnk" ; | ||||||
|  | Type: files; Name: "{autodesktop}\Solian.lnk" ; | ||||||
		Reference in New Issue
	
	Block a user