Compare commits
	
		
			35 Commits
		
	
	
		
			9bdf8ba346
			...
			3.1.0+123
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4e4bd99598 | |||
| d1fbe5f15e | |||
| c061ef2132 | |||
| c378309bdd | |||
| b2c5d64fc5 | |||
|  | 5371637b16 | ||
| c5cbf0af37 | |||
| 1a31e22450 | |||
|  | 49db54529d | ||
| 8e0c0c6054 | |||
| f3d1183076 | |||
| a9f7f0cce0 | |||
| f2943f8411 | |||
| 808e7dcffa | |||
| 9bed4fa6fb | |||
| e6255a340b | |||
| 78bf319fb7 | |||
| 36a966d582 | |||
| f72b268d36 | |||
| 44ef31034e | |||
| 229dc2186f | |||
| a2f9a1efb4 | |||
|  | 823e3c5de6 | ||
|  | faac7bac35 | ||
| 1fac1bfe02 | |||
| 9394b1d9c8 | |||
| 43dd13bac4 | |||
| 65bc372103 | |||
| 6558854a7a | |||
| 892035ab27 | |||
| 87ae8d2ff4 | |||
| 15c2dbaa0d | |||
| 6b3338b885 | |||
| bb00b1bc6a | |||
| 5e1a15ada2 | 
| @@ -51,6 +51,12 @@ android { | ||||
|     buildTypes { | ||||
|         release { | ||||
|             signingConfig = signingConfigs.getByName("release") | ||||
|  | ||||
|             isMinifyEnabled = true | ||||
|             proguardFiles( | ||||
|                 getDefaultProguardFile("proguard-android-optimize.txt"), | ||||
|                 "proguard-rules.pro" | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -58,7 +64,7 @@ android { | ||||
| dependencies { | ||||
|     implementation("com.google.android.material:material:1.12.0") | ||||
|     implementation("com.github.bumptech.glide:glide:4.16.0") | ||||
|     implementation("com.squareup.okhttp3:okhttp:4.12.0") | ||||
|     implementation("com.squareup.okhttp3:okhttp:5.1.0") | ||||
| } | ||||
|  | ||||
| flutter { | ||||
|   | ||||
							
								
								
									
										5
									
								
								android/app/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								android/app/proguard-rules.pro
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| # JNI Zero initialization (required for WebRTC native method registration) | ||||
| -keep class livekit.org.jni_zero.JniInit { | ||||
|     # Keep the init method un-obfuscated for native code callback | ||||
|     private static java.lang.Object[] init(); | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								android/app/src/main/res/drawable/ic_notification.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								android/app/src/main/res/drawable/ic_notification.png
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 70 KiB | 
| @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME | ||||
| distributionPath=wrapper/dists | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
| zipStorePath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip | ||||
|   | ||||
| @@ -18,11 +18,11 @@ pluginManagement { | ||||
|  | ||||
| plugins { | ||||
|     id("dev.flutter.flutter-plugin-loader") version "1.0.0" | ||||
|     id("com.android.application") version "8.10.1" apply false | ||||
|     id("com.android.application") version "8.12.0" apply false | ||||
|     // START: FlutterFire Configuration | ||||
|     id("com.google.gms.google-services") version("4.3.15") apply false | ||||
|     // END: FlutterFire Configuration | ||||
|     id("org.jetbrains.kotlin.android") version "1.8.22" apply false | ||||
|     id("org.jetbrains.kotlin.android") version("2.2.0") apply false | ||||
| } | ||||
|  | ||||
| include(":app") | ||||
|   | ||||
| @@ -706,6 +706,7 @@ | ||||
|   "copyToClipboardTooltip": "Copy to clipboard", | ||||
|   "postForwardingTo": "Forwarding to", | ||||
|   "postReplyingTo": "Replying to", | ||||
|   "postReplyPlaceholder": "Post your reply", | ||||
|   "postEditing": "You are editing an existing post", | ||||
|   "postArticle": "Article", | ||||
|   "aboutDeviceName": "Device Name", | ||||
| @@ -786,5 +787,10 @@ | ||||
|   "links": "Links", | ||||
|   "addLink": "Add link", | ||||
|   "linkKey": "Link Name", | ||||
|   "linkValue": "URL" | ||||
|   "linkValue": "URL", | ||||
|   "debugOptions": "Debug Options", | ||||
|   "joinedAt": "Joined at {}", | ||||
|   "searchAccounts": "Search accounts...", | ||||
|   "webFeeds": "Web Feeds", | ||||
|   "polls": "Polls" | ||||
| } | ||||
|   | ||||
| @@ -46,7 +46,7 @@ | ||||
|   "delete": "删除", | ||||
|   "deletePublisher": "删除发布者", | ||||
|   "deletePublisherHint": "确定要删除此发布者吗?这也会删除此发布者下的所有帖子和收藏。", | ||||
|   "somethingWentWrong": "发生了一些错误", | ||||
|   "somethingWentWrong": "发生了一些错误……", | ||||
|   "deletePost": "删除帖子", | ||||
|   "deletePostHint": "确定要删除这篇帖子吗?", | ||||
|   "copyLink": "复制链接", | ||||
| @@ -120,14 +120,9 @@ | ||||
|     "other": "{}个附件" | ||||
|   }, | ||||
|   "edited": "已编辑", | ||||
|   "editedAt": "编辑于 {}", | ||||
|   "addVideo": "添加视频", | ||||
|   "addPhoto": "添加照片", | ||||
|   "addFile": "添加文件", | ||||
|   "addAttachmentById": "通过 ID 添加附件", | ||||
|   "enterFileId": "输入文件 ID", | ||||
|   "fileIdCannotBeEmpty": "文件 ID 不能为空", | ||||
|   "failedToFetchFile": "获取文件失败: {}", | ||||
|   "createDirectMessage": "创建新私人消息", | ||||
|   "gotoDirectMessage": "前往私信", | ||||
|   "react": "反应", | ||||
| @@ -350,7 +345,7 @@ | ||||
|   "accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。", | ||||
|   "unauthorized": "未授权", | ||||
|   "unauthorizedHint": "您未登录或会话已过期,请重新登录。", | ||||
|   "publisherBelongsTo": "属于 {}", | ||||
|   "publisherBelongsTo": "属于", | ||||
|   "postContent": "内容", | ||||
|   "postSettings": "设置", | ||||
|   "postPublisherUnselected": "未指定发布者", | ||||
| @@ -478,7 +473,7 @@ | ||||
|   "description": "描述", | ||||
|   "pinCode": "PIN 码", | ||||
|   "biometric": "生物识别", | ||||
|   "enterPinToConfirm": "请输入您的 6 位数字 PIN 以确认付款", | ||||
|   "enterPinToConfirm": "请输入您的6位数字 PIN 以确认付款", | ||||
|   "clearPin": "清除 PIN 码", | ||||
|   "useBiometricToConfirm": "使用生物特征认证来确认付款", | ||||
|   "touchSensorToAuthenticate": "触摸传感器进行身份验证", | ||||
| @@ -495,20 +490,29 @@ | ||||
|   "paymentError": "付款失败: {error}", | ||||
|   "usePinInstead": "使用 PIN 码", | ||||
|   "levelProgress": "等级进度", | ||||
|   "unlockedFeatures": "已解锁的功能", | ||||
|   "unlockedFeaturesDescription": "在您当前级别上解锁的功能将显示在这里。", | ||||
|   "stellarMembership": "恒星计划", | ||||
|   "upgradeYourPlan": "升级您的计划", | ||||
|   "chooseYourPlan": "选择你的方案", | ||||
|   "currentMembership": "当前:{}", | ||||
|   "currentMembershipMember": "恒星计划「{}」级会员", | ||||
|   "membershipExpires": "过期于:{}", | ||||
|   "membershipTierStellar": "恒星", | ||||
|   "membershipTierNova": "新星", | ||||
|   "membershipTierSupernova": "超新星", | ||||
|   "membershipTierUnknown": "未知", | ||||
|   "membershipPriceStellar": "每月 1200 源点,至少需要 3 级", | ||||
|   "membershipPriceNova": "每月 2400 源点,至少需要 6 级", | ||||
|   "membershipPriceSupernova": "每月 3600 源点,至少需要 9 级", | ||||
|   "membershipPriceStellar": "每月 10 金点", | ||||
|   "membershipPriceNova": "每月 20 金点", | ||||
|   "membershipPriceSupernova": "每月 30 金点", | ||||
|   "membershipFeatureBasic": "基础功能", | ||||
|   "membershipFeaturePrioritySupport": "优先支持", | ||||
|   "membershipFeatureAdFree": "无广告", | ||||
|   "membershipFeatureAllPrimary": "所有主要功能", | ||||
|   "membershipFeatureAdvancedCustomization": "高级自定义", | ||||
|   "membershipFeatureEarlyAccess": "抢先体验", | ||||
|   "membershipFeatureAllNova": "所有「新星」功能", | ||||
|   "membershipFeatureExclusiveContent": "限定内容", | ||||
|   "membershipFeatureVipSupport": "VIP 支持", | ||||
|   "membershipCurrentBadge": "当前", | ||||
|   "restorePurchase": "恢复购买", | ||||
|   "restorePurchaseDescription": "输入您付款的提供商和订单 ID 以恢复您的购买。", | ||||
| @@ -518,11 +522,167 @@ | ||||
|   "enterOrderId": "输入您的订单 ID", | ||||
|   "restore": "恢复", | ||||
|   "keyboardShortcuts": "键盘快捷键", | ||||
|   "safetyReport": "举报", | ||||
|   "safetyReportTitle": "举报", | ||||
|   "safetyReportDescription": "通过举报不合适的内容和行为来维护我们社区的稳定。", | ||||
|   "safetyReportType": "举报类型", | ||||
|   "safetyReportReason": "更多证据", | ||||
|   "safetyReportReasonHint": "请提供更多证据……", | ||||
|   "safetyReportSubmit": "提交举报", | ||||
|   "safetyReportSubmitting": "提交中……", | ||||
|   "safetyReportSuccess": "举报成功,感谢您参与维护社区健康发展。", | ||||
|   "safetyReportError": "举报失败,请稍后重试。", | ||||
|   "safetyReportReasonRequired": "请提供举报证据", | ||||
|   "safetyReportTypeSpam": "垃圾或导向错误", | ||||
|   "safetyReportTypeHarassment": "骚扰或暴力行为", | ||||
|   "safetyReportTypeHateSpeech": "歧视言论", | ||||
|   "safetyReportTypeViolence": "威胁或暴力内容", | ||||
|   "safetyReportTypeAdultContent": "成人内容", | ||||
|   "safetyReportTypeIntellectualProperty": "抄袭", | ||||
|   "safetyReportTypeOther": "其它", | ||||
|   "safetyReportTypeInappropriate": "不良内容", | ||||
|   "safetyReportTypeCopyright": "版权侵害", | ||||
|   "safetyReportSuccessTitle": "举报成功", | ||||
|   "safetyReportErrorTitle": "错误", | ||||
|   "discover": "发现", | ||||
|   "joinRealm": "加入领域", | ||||
|   "removePublisherMember": "移除发布者", | ||||
|   "removePublisherMemberHint": "你确定要将这个成员从发布者中移除?", | ||||
|   "drafts": "草稿箱", | ||||
|   "noDrafts": "无草稿", | ||||
|   "articleDrafts": "文章草稿", | ||||
|   "postDrafts": "帖子草稿", | ||||
|   "saveDraft": "保存草稿", | ||||
|   "draftSaved": "草稿已保存", | ||||
|   "draftSaveFailed": "保存草稿失败", | ||||
|   "clearAllDrafts": "清除全部草稿", | ||||
|   "clearAllDraftsConfirm": "你确定要清除全部草稿?这一操作无法撤销。", | ||||
|   "clearAll": "清除所有", | ||||
|   "untitled": "未命名", | ||||
|   "noContent": "内容为空", | ||||
|   "justNow": "刚刚", | ||||
|   "minutesAgo": "{} 分钟以前", | ||||
|   "hoursAgo": "{} 小时以前", | ||||
|   "daysAgo": "{} 天以前", | ||||
|   "public": "公开的", | ||||
|   "unlisted": "不列出", | ||||
|   "friends": "朋友", | ||||
|   "selected": "选择的", | ||||
|   "private": "私密的", | ||||
|   "postContentEmpty": "发布的内容不能为空", | ||||
|   "share": "分享", | ||||
|   "sharePost": "分享帖子", | ||||
|   "quickActions": "快捷操作", | ||||
|   "post": "帖子", | ||||
|   "copy": "复制", | ||||
|   "sendToChat": "发送到聊天", | ||||
|   "failedToShareToPost": "分享到帖子失败:{}", | ||||
|   "shareToChatComingSoon": "分享到聊天的功能即将到来", | ||||
|   "failedToShareToChat": "分享到聊天失败:{}", | ||||
|   "shareToSpecificChatComingSoon": "分享到 {} 的功能即将到来", | ||||
|   "directChat": "私信", | ||||
|   "systemShareComingSoon": "系统分享功能即将到来", | ||||
|   "failedToShareToSystem": "分享到系统失败:{}", | ||||
|   "failedToCopy": "复制失败:{}", | ||||
|   "noChatRoomsAvailable": "没有聊天室可用", | ||||
|   "failedToLoadChats": "加载聊天室失败", | ||||
|   "contentToShare": "要分享的内容:", | ||||
|   "unknownChat": "未知聊天室", | ||||
|   "addAdditionalMessage": "添加额外消息……", | ||||
|   "uploadingFiles": "上传文件中……", | ||||
|   "sharedSuccessfully": "分享成功!", | ||||
|   "shareSuccess": "分享成功!", | ||||
|   "shareToSpecificChatSuccess": "分享到 {} 成功!", | ||||
|   "wouldYouLikeToGoToChat": "你想要前往聊天页面吗?", | ||||
|   "no": "是", | ||||
|   "yes": "否", | ||||
|   "navigateToChat": "前往聊天室", | ||||
|   "wouldYouLikeToNavigateToChat": "你想要前往聊天页面吗?", | ||||
|   "abuseReport": "举报", | ||||
|   "abuseReportTitle": "举报内容", | ||||
|   "abuseReportDescription": "通过举报不合适的内容和行为来帮助我们维护社区的健康稳定发展。", | ||||
|   "abuseReportType": "举报类型", | ||||
|   "abuseReportReason": "额外细节", | ||||
|   "abuseReportReasonHint": "请提供更多关于此的细节……", | ||||
|   "abuseReportSubmit": "提交举报", | ||||
|   "abuseReportSuccess": "举报提交成功,感谢你为社区维护作出贡献。", | ||||
|   "abuseReportError": "无法提交举报,请稍后再试。", | ||||
|   "abuseReportReasonRequired": "请提供关于此事件的细节", | ||||
|   "abuseReportSuccessTitle": "举报已提交", | ||||
|   "abuseReportErrorTitle": "错误", | ||||
|   "abuseReportTypeSpam": "垃圾或错误信息", | ||||
|   "abuseReportTypeHarassment": "骚扰或滥用", | ||||
|   "abuseReportTypeInappropriate": "不合适的内容", | ||||
|   "abuseReportTypeViolence": "暴力或人身威胁", | ||||
|   "abuseReportTypeCopyright": "版权侵犯", | ||||
|   "abuseReportTypeImpersonation": "冒充", | ||||
|   "abuseReportTypeOffensiveContent": "冒犯性内容", | ||||
|   "abuseReportTypePrivacyViolation": "隐私侵犯", | ||||
|   "abuseReportTypeIllegalContent": "违法内容", | ||||
|   "abuseReportTypeOther": "其他", | ||||
|   "tags": "标签", | ||||
|   "tagsHint": "输入标签,用英文逗号分隔", | ||||
|   "categories": "分类", | ||||
|   "categoriesHint": "输入分类,由逗号隔开", | ||||
|   "chatNotJoined": "你还没有加入这个聊天。", | ||||
|   "chatUnableJoin": "由于该聊天的访问设置使你无法加入。", | ||||
|   "chatJoin": "加入聊天", | ||||
|   "realmJoin": "加入领域", | ||||
|   "realmJoinSuccess": "成功加入领域。", | ||||
|   "discoverRealms": "发现领域", | ||||
|   "discoverPublishers": "发现发布者", | ||||
|   "search": "搜索", | ||||
|   "publisherMembers": "合作者", | ||||
|   "developerHub": "开发者中心", | ||||
|   "developerHubUnselectedHint": "选择一名开发者查看总结数据或成为一名。", | ||||
|   "enrollDeveloper": "成为一名开发者", | ||||
|   "enrollDeveloperHint": "让你的一个发布者成为开发者。", | ||||
|   "noPublishersToEnroll": "你没有可以成为开发者的发布者。", | ||||
|   "totalCustomApps": "所有应用套件", | ||||
|   "customApps": "应用套件", | ||||
|   "noCustomApps": "还没有应用套件。", | ||||
|   "createCustomApp": "创建应用套件", | ||||
|   "editCustomApp": "编辑应用套件", | ||||
|   "deleteCustomApp": "删除应用套件", | ||||
|   "deleteCustomAppHint": "你确定要删除这个应用套件吗?这一步无法撤销。", | ||||
|   "publicRealm": "公开领域", | ||||
|   "publicRealmDescription": "所有人都可以预览这个领域的内容。", | ||||
|   "communityRealm": "领域", | ||||
|   "communityRealmDescription": "所有人都可以加入该领域并参与讨论,并将在发现和反馈页面显示。", | ||||
|   "publicChat": "公开聊天", | ||||
|   "publicChatDescription": "任何人都可以预览此聊天的内容。包括未加入的机器人。", | ||||
|   "communityChat": "社区聊天", | ||||
|   "communityChatDescription": "所有人都可以加入该聊天并参与参与讨论。", | ||||
|   "appLinks": "应用链接", | ||||
|   "homePageUrl": "主页链接", | ||||
|   "privacyPolicyUrl": "隐私政策链接", | ||||
|   "termsOfServiceUrl": "用户协议链接", | ||||
|   "oauthConfig": "OAuth 配置", | ||||
|   "clientUri": "客户端 URI", | ||||
|   "redirectUris": "重定向 URIs", | ||||
|   "addRedirectUri": "添加重定向 URI", | ||||
|   "allowedScopes": "允许的范围", | ||||
|   "requirePkce": "需要 PKCE", | ||||
|   "allowOfflineAccess": "允许离线访问", | ||||
|   "redirectUri": "重定向 URI", | ||||
|   "redirectUriHint": "重定向 URI 用于 OAuth 认证,但您的项目状态转为线上时我们会验证请求中的重定向 URI 是否符合此配置。", | ||||
|   "uriRequired": "这个 URI 是必须填写的。", | ||||
|   "uriInvalid": "无效 URI。", | ||||
|   "add": "添加", | ||||
|   "addScope": "添加范围", | ||||
|   "scope": "范围", | ||||
|   "publisherFeatures": "功能", | ||||
|   "publisherFeatureDevelop": "开发者计划", | ||||
|   "publisherFeatureDevelopDescription": "为你的开发者解锁包括应用套件,API 及更多开发功能。", | ||||
|   "publisherFeatureDevelopHint": "目前该功能还在开发中,你需要邀请才可解锁。", | ||||
|   "learnMore": "了解更多", | ||||
|   "discoverWebArticles": "来自站外的文章", | ||||
|   "webArticlesStand": "文章亭", | ||||
|   "about": "关于", | ||||
|   "membershipCancel": "取消会员订阅", | ||||
|   "membershipCancelConfirm": "您确定要取消您的会员订阅?", | ||||
|   "membershipCancelHint": "您确定要取消您的会员订阅吗?您将不会再被收费。您的会员资格将在当前计费周期结束前保持有效。并且您在当前订阅结束之前无法重新订阅。", | ||||
|   "membershipCancelSuccess": "您的会员订阅已成功取消。", | ||||
|   "membershipCancel": "取消会员资格", | ||||
|   "membershipCancelConfirm": "你确定要取消会员资格吗?", | ||||
|   "membershipCancelHint": "你确定要取消会员资格吗?你将不会再次被扣费。你的会员资格将在当前计费周期结束前保持有效。并且你将无法重新订阅,直到当前订阅结束。", | ||||
|   "membershipCancelSuccess": "你的会员资格已成功取消。", | ||||
|   "aboutScreenTitle": "关于", | ||||
|   "aboutScreenVersionInfo": "版本 {} ({})", | ||||
|   "aboutScreenAppInfoSectionTitle": "应用信息", | ||||
| @@ -532,18 +692,103 @@ | ||||
|   "aboutScreenLinksSectionTitle": "链接", | ||||
|   "aboutScreenPrivacyPolicyTitle": "隐私政策", | ||||
|   "aboutScreenTermsOfServiceTitle": "服务条款", | ||||
|   "aboutScreenOpenSourceLicensesTitle": "开源许可证", | ||||
|   "aboutScreenOpenSourceLicensesTitle": "开源许可", | ||||
|   "aboutScreenDeveloperSectionTitle": "开发者", | ||||
|   "aboutScreenContactUsTitle": "联系我们", | ||||
|   "aboutScreenLicenseTitle": "许可证", | ||||
|   "aboutScreenLicenseTitle": "许可", | ||||
|   "aboutScreenLicenseContent": "GNU Affero General Public License v3.0", | ||||
|   "aboutScreenCopyright": "版权所有 © 索尔辛茨 {}", | ||||
|   "aboutScreenMadeWith": "由 Solar Network Team 用 ❤︎️ 制作", | ||||
|   "aboutScreenFailedToLoadPackageInfo": "加载包信息失败:{error}", | ||||
|   "aboutScreenCopyright": "版权所有 © Solsynth {}", | ||||
|   "aboutScreenMadeWith": "由 Solar Network 团队用 ❤︎️ 制作", | ||||
|   "aboutScreenFailedToLoadPackageInfo": "无法加载包信息:{error}", | ||||
|   "copiedToClipboard": "已复制到剪贴板", | ||||
|   "copyToClipboardTooltip": "复制到剪贴板", | ||||
|   "postForwardingTo": "转发给", | ||||
|   "postReplyingTo": "回复给", | ||||
|   "postEditing": "您正在编辑现有帖子", | ||||
|   "postArticle": "文章" | ||||
|   "postForwardingTo": "正在转发到", | ||||
|   "postReplyingTo": "正在回复", | ||||
|   "postReplyPlaceholder": "发表你的回复", | ||||
|   "postEditing": "你正在编辑一个现有的帖子", | ||||
|   "postArticle": "文章", | ||||
|   "aboutDeviceName": "设备名称", | ||||
|   "aboutDeviceIdentifier": "设备标识符", | ||||
|   "donate": "捐赠", | ||||
|   "donateDescription": "支持我们继续开发 Solar Network,并保持服务器运行。", | ||||
|   "fileId": "文件ID", | ||||
|   "fileIdHint": "文件ID是你通过 Solar Network Drive 上传文件后获得的ID。", | ||||
|   "translate": "翻译", | ||||
|   "translating": "正在翻译", | ||||
|   "translated": "已翻译", | ||||
|   "reactionThumbUp": "点赞", | ||||
|   "reactionThumbDown": "踩", | ||||
|   "reactionJustOkay": "还行", | ||||
|   "reactionCry": "哭", | ||||
|   "reactionConfuse": "困惑", | ||||
|   "reactionClap": "鼓掌", | ||||
|   "reactionLaugh": "笑", | ||||
|   "reactionAngry": "生气", | ||||
|   "reactionParty": "派对", | ||||
|   "reactionPray": "祈祷", | ||||
|   "reactionHeart": "心", | ||||
|   "selectMicrophone": "选择麦克风", | ||||
|   "selectCamera": "选择摄像头", | ||||
|   "switchedTo": "已切换到 {}", | ||||
|   "connecting": "正在连接", | ||||
|   "reconnecting": "正在重新连接", | ||||
|   "disconnected": "已断开连接", | ||||
|   "connected": "已连接", | ||||
|   "repliesLoadMore": "加载更多回复", | ||||
|   "attachmentsRecentUploads": "最近上传", | ||||
|   "attachmentsManualInput": "手动输入", | ||||
|   "crop": "裁剪", | ||||
|   "rename": "重命名", | ||||
|   "markAsSensitive": "标记为敏感", | ||||
|   "fileName": "文件名", | ||||
|   "sensitiveCategories.language": "语言", | ||||
|   "sensitiveCategories.sexualContent": "色情内容", | ||||
|   "sensitiveCategories.violence": "暴力", | ||||
|   "sensitiveCategories.profanity": "亵渎", | ||||
|   "sensitiveCategories.hateSpeech": "仇恨言论", | ||||
|   "sensitiveCategories.racism": "种族主义", | ||||
|   "sensitiveCategories.adultContent": "成人内容", | ||||
|   "sensitiveCategories.drugAbuse": "药物滥用", | ||||
|   "sensitiveCategories.alcoholAbuse": "酗酒", | ||||
|   "sensitiveCategories.gambling": "赌博", | ||||
|   "sensitiveCategories.selfHarm": "自残", | ||||
|   "sensitiveCategories.childAbuse": "虐待儿童", | ||||
|   "sensitiveCategories.other": "其他", | ||||
|   "poll": "投票", | ||||
|   "pollsRecent": "最近投票", | ||||
|   "pollCreateNew": "创建新投票", | ||||
|   "pollCreateNewHint": "为你的帖子创建一个新投票。选择一个发布者然后继续。", | ||||
|   "publisher": "发布者", | ||||
|   "publisherHint": "输入发布者名称", | ||||
|   "publisherCannotBeEmpty": "发布者不能为空", | ||||
|   "operationFailed": "操作失败:{}", | ||||
|   "stickerMarketplace": "贴纸市场", | ||||
|   "stickerPackAdded": "贴纸包已添加到你的收藏", | ||||
|   "stickerPackRemoved": "贴纸包已从你的收藏中移除", | ||||
|   "addPack": "添加贴纸包", | ||||
|   "removePack": "移除贴纸包", | ||||
|   "browseAndAddStickers": "浏览并添加贴纸包", | ||||
|   "stickerPack": "贴纸包", | ||||
|   "postCategoryTechnology": "科技", | ||||
|   "postCategoryTravel": "旅行", | ||||
|   "postCategoryFood": "美食", | ||||
|   "postCategoryHealth": "健康", | ||||
|   "postCategoryScience": "科学", | ||||
|   "postCategorySports": "体育", | ||||
|   "postCategoryFinance": "金融", | ||||
|   "postCategoryLife": "生活", | ||||
|   "postCategoryArt": "艺术", | ||||
|   "postCategoryStudy": "学习", | ||||
|   "postCategoryGaming": "游戏", | ||||
|   "postCategoryProgramming": "编程", | ||||
|   "postCategoryMusic": "音乐", | ||||
|   "links": "链接", | ||||
|   "addLink": "添加链接", | ||||
|   "linkKey": "链接名称", | ||||
|   "linkValue": "URL", | ||||
|   "debugOptions": "调试选项", | ||||
|   "joinedAt": "加入于 {}", | ||||
|   "searchAccounts": "搜索帐号……", | ||||
|   "webFeeds": "订阅源", | ||||
|   "polls": "投票" | ||||
| } | ||||
|   | ||||
| @@ -73,6 +73,8 @@ PODS: | ||||
|     - GoogleUtilities/UserDefaults (~> 8.1) | ||||
|     - nanopb (~> 3.30910.0) | ||||
|   - Flutter (1.0.0) | ||||
|   - flutter_app_update (0.0.1): | ||||
|     - Flutter | ||||
|   - flutter_inappwebview_ios (0.0.1): | ||||
|     - Flutter | ||||
|     - flutter_inappwebview_ios/Core (= 0.0.1) | ||||
| @@ -178,25 +180,25 @@ PODS: | ||||
|   - sqflite_darwin (0.0.4): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - sqlite3 (3.50.3): | ||||
|     - sqlite3/common (= 3.50.3) | ||||
|   - sqlite3/common (3.50.3) | ||||
|   - sqlite3/dbstatvtab (3.50.3): | ||||
|   - sqlite3 (3.50.4): | ||||
|     - sqlite3/common (= 3.50.4) | ||||
|   - sqlite3/common (3.50.4) | ||||
|   - sqlite3/dbstatvtab (3.50.4): | ||||
|     - sqlite3/common | ||||
|   - sqlite3/fts5 (3.50.3): | ||||
|   - sqlite3/fts5 (3.50.4): | ||||
|     - sqlite3/common | ||||
|   - sqlite3/math (3.50.3): | ||||
|   - sqlite3/math (3.50.4): | ||||
|     - sqlite3/common | ||||
|   - sqlite3/perf-threadsafe (3.50.3): | ||||
|   - sqlite3/perf-threadsafe (3.50.4): | ||||
|     - sqlite3/common | ||||
|   - sqlite3/rtree (3.50.3): | ||||
|   - sqlite3/rtree (3.50.4): | ||||
|     - sqlite3/common | ||||
|   - sqlite3/session (3.50.3): | ||||
|   - sqlite3/session (3.50.4): | ||||
|     - sqlite3/common | ||||
|   - sqlite3_flutter_libs (0.0.1): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|     - sqlite3 (~> 3.50.3) | ||||
|     - sqlite3 (~> 3.50.4) | ||||
|     - sqlite3/dbstatvtab | ||||
|     - sqlite3/fts5 | ||||
|     - sqlite3/math | ||||
| @@ -223,6 +225,7 @@ DEPENDENCIES: | ||||
|   - firebase_core (from `.symlinks/plugins/firebase_core/ios`) | ||||
|   - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) | ||||
|   - Flutter (from `Flutter`) | ||||
|   - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`) | ||||
|   - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) | ||||
|   - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) | ||||
|   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) | ||||
| @@ -293,6 +296,8 @@ EXTERNAL SOURCES: | ||||
|     :path: ".symlinks/plugins/firebase_messaging/ios" | ||||
|   Flutter: | ||||
|     :path: Flutter | ||||
|   flutter_app_update: | ||||
|     :path: ".symlinks/plugins/flutter_app_update/ios" | ||||
|   flutter_inappwebview_ios: | ||||
|     :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" | ||||
|   flutter_keyboard_visibility: | ||||
| @@ -372,6 +377,7 @@ SPEC CHECKSUMS: | ||||
|   FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988 | ||||
|   FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde | ||||
|   Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 | ||||
|   flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9 | ||||
|   flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 | ||||
|   flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 | ||||
|   flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf | ||||
| @@ -406,8 +412,8 @@ SPEC CHECKSUMS: | ||||
|   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 | ||||
|   sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 | ||||
|   sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 | ||||
|   sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81 | ||||
|   sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e | ||||
|   sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b | ||||
|   sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 | ||||
|   super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 | ||||
|   SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 | ||||
|   url_launcher_ios: 694010445543906933d732453a59da0a173ae33d | ||||
|   | ||||
| @@ -34,7 +34,7 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate { | ||||
|         } | ||||
|          | ||||
|         let serverUrl = UserDefaults.standard.getServerUrl() | ||||
|         let url = "\(serverUrl)/chat/\(metadata["room_id"] ?? "")/messages" | ||||
|         let url = "\(serverUrl)/sphere/chat/\(metadata["room_id"] ?? "")/messages" | ||||
|          | ||||
|         let parameters: [String: Any?] = [ | ||||
|             "content": textResponse.userText, | ||||
|   | ||||
| @@ -8,7 +8,7 @@ | ||||
| import Foundation | ||||
|  | ||||
| func getAttachmentUrl(for identifier: String) -> String { | ||||
|     let serverBaseUrl = "https://nt.solian.app" | ||||
|     let serverBaseUrl = "https://api.solian.app" | ||||
|      | ||||
|     return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/files/\(identifier)" | ||||
|     return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/drive/files/\(identifier)" | ||||
| } | ||||
|   | ||||
| @@ -28,9 +28,9 @@ import 'package:relative_time/relative_time.dart'; | ||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||
| import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; | ||||
| import 'package:flutter_native_splash/flutter_native_splash.dart'; | ||||
| import 'package:island/widgets/keyboard_navigation.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
| import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect; | ||||
| import 'package:island/services/update_service.dart'; | ||||
|  | ||||
| @pragma('vm:entry-point') | ||||
| Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { | ||||
| @@ -144,15 +144,6 @@ void main() async { | ||||
|       ), | ||||
|     ), | ||||
|   ); | ||||
|  | ||||
|   // Schedule update check shortly after startup, when a context is available. | ||||
|   // Uses the global overlay key to obtain a BuildContext safely. | ||||
|   WidgetsBinding.instance.addPostFrameCallback((_) { | ||||
|     final ctx = globalOverlay.currentContext; | ||||
|     if (ctx != null) { | ||||
|       UpdateService().checkForUpdates(ctx); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Router will be provided through Riverpod | ||||
| @@ -181,6 +172,9 @@ class IslandApp extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     useEffect(() { | ||||
|       if (!kIsWeb && Platform.isLinux) { | ||||
|         return null; | ||||
|       } | ||||
|       const channel = MethodChannel('dev.solsynth.solian/notifications'); | ||||
|  | ||||
|       Future<void> handleInitialLink() async { | ||||
| @@ -251,30 +245,32 @@ class IslandApp extends HookConsumerWidget { | ||||
|  | ||||
|     final router = ref.watch(routerProvider); | ||||
|  | ||||
|     return MaterialApp.router( | ||||
|       theme: theme?.light, | ||||
|       darkTheme: theme?.dark, | ||||
|       themeMode: ThemeMode.system, | ||||
|       routerConfig: router, | ||||
|       supportedLocales: context.supportedLocales, | ||||
|       localizationsDelegates: [ | ||||
|         ...context.localizationDelegates, | ||||
|         CroppyLocalizations.delegate, | ||||
|         RelativeTimeLocalizations.delegate, | ||||
|       ], | ||||
|       locale: context.locale, | ||||
|       builder: (context, child) { | ||||
|         return Overlay( | ||||
|           key: globalOverlay, | ||||
|           initialEntries: [ | ||||
|             OverlayEntry( | ||||
|               builder: | ||||
|                   (_) => | ||||
|                       WindowScaffold(child: child ?? const SizedBox.shrink()), | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       }, | ||||
|     return KeyboardNavigation( | ||||
|       child: MaterialApp.router( | ||||
|         theme: theme?.light, | ||||
|         darkTheme: theme?.dark, | ||||
|         themeMode: ThemeMode.system, | ||||
|         routerConfig: router, | ||||
|         supportedLocales: context.supportedLocales, | ||||
|         localizationsDelegates: [ | ||||
|           ...context.localizationDelegates, | ||||
|           CroppyLocalizations.delegate, | ||||
|           RelativeTimeLocalizations.delegate, | ||||
|         ], | ||||
|         locale: context.locale, | ||||
|         builder: (context, child) { | ||||
|           return Overlay( | ||||
|             key: globalOverlay, | ||||
|             initialEntries: [ | ||||
|               OverlayEntry( | ||||
|                 builder: | ||||
|                     (_) => | ||||
|                         WindowScaffold(child: child ?? const SizedBox.shrink()), | ||||
|               ), | ||||
|             ], | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,14 +1,26 @@ | ||||
| import 'package:freezed_annotation/freezed_annotation.dart'; | ||||
| import 'package:island/models/publisher.dart'; | ||||
|  | ||||
| part 'developer.freezed.dart'; | ||||
| part 'developer.g.dart'; | ||||
|  | ||||
| @freezed | ||||
| sealed class SnDeveloper with _$SnDeveloper { | ||||
|   const factory SnDeveloper({ | ||||
|     required String id, | ||||
|     required String publisherId, | ||||
|     SnPublisher? publisher, | ||||
|   }) = _SnDeveloper; | ||||
|  | ||||
|   factory SnDeveloper.fromJson(Map<String, dynamic> json) => | ||||
|       _$SnDeveloperFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class DeveloperStats with _$DeveloperStats { | ||||
|   const factory DeveloperStats({ | ||||
|     @Default(0) int totalCustomApps, | ||||
|   }) = _DeveloperStats; | ||||
|   const factory DeveloperStats({@Default(0) int totalCustomApps}) = | ||||
|       _DeveloperStats; | ||||
|  | ||||
|   factory DeveloperStats.fromJson(Map<String, dynamic> json) => | ||||
|       _$DeveloperStatsFromJson(json); | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,293 @@ part of 'developer.dart'; | ||||
| // dart format off | ||||
| T _$identity<T>(T value) => value; | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnDeveloper { | ||||
|  | ||||
|  String get id; String get publisherId; SnPublisher? get publisher; | ||||
| /// Create a copy of SnDeveloper | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnDeveloperCopyWith<SnDeveloper> get copyWith => _$SnDeveloperCopyWithImpl<SnDeveloper>(this as SnDeveloper, _$identity); | ||||
|  | ||||
|   /// Serializes this SnDeveloper to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is SnDeveloper&&(identical(other.id, id) || other.id == id)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,publisherId,publisher); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnDeveloper(id: $id, publisherId: $publisherId, publisher: $publisher)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $SnDeveloperCopyWith<$Res>  { | ||||
|   factory $SnDeveloperCopyWith(SnDeveloper value, $Res Function(SnDeveloper) _then) = _$SnDeveloperCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String publisherId, SnPublisher? publisher | ||||
| }); | ||||
|  | ||||
|  | ||||
| $SnPublisherCopyWith<$Res>? get publisher; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$SnDeveloperCopyWithImpl<$Res> | ||||
|     implements $SnDeveloperCopyWith<$Res> { | ||||
|   _$SnDeveloperCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final SnDeveloper _self; | ||||
|   final $Res Function(SnDeveloper) _then; | ||||
|  | ||||
| /// Create a copy of SnDeveloper | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? publisherId = null,Object? publisher = freezed,}) { | ||||
|   return _then(_self.copyWith( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable | ||||
| as String,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable | ||||
| as SnPublisher?, | ||||
|   )); | ||||
| } | ||||
| /// Create a copy of SnDeveloper | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnPublisherCopyWith<$Res>? get publisher { | ||||
|     if (_self.publisher == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) { | ||||
|     return _then(_self.copyWith(publisher: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Adds pattern-matching-related methods to [SnDeveloper]. | ||||
| extension SnDeveloperPatterns on SnDeveloper { | ||||
| /// 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( _SnDeveloper value)?  $default,{required TResult orElse(),}){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnDeveloper() 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( _SnDeveloper value)  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnDeveloper(): | ||||
| 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( _SnDeveloper value)?  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _SnDeveloper() 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 publisherId,  SnPublisher? publisher)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnDeveloper() when $default != null: | ||||
| return $default(_that.id,_that.publisherId,_that.publisher);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 publisherId,  SnPublisher? publisher)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnDeveloper(): | ||||
| return $default(_that.id,_that.publisherId,_that.publisher);} | ||||
| } | ||||
| /// 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 publisherId,  SnPublisher? publisher)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnDeveloper() when $default != null: | ||||
| return $default(_that.id,_that.publisherId,_that.publisher);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnDeveloper implements SnDeveloper { | ||||
|   const _SnDeveloper({required this.id, required this.publisherId, this.publisher}); | ||||
|   factory _SnDeveloper.fromJson(Map<String, dynamic> json) => _$SnDeveloperFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @override final  String publisherId; | ||||
| @override final  SnPublisher? publisher; | ||||
|  | ||||
| /// Create a copy of SnDeveloper | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$SnDeveloperCopyWith<_SnDeveloper> get copyWith => __$SnDeveloperCopyWithImpl<_SnDeveloper>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$SnDeveloperToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnDeveloper&&(identical(other.id, id) || other.id == id)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,id,publisherId,publisher); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'SnDeveloper(id: $id, publisherId: $publisherId, publisher: $publisher)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$SnDeveloperCopyWith<$Res> implements $SnDeveloperCopyWith<$Res> { | ||||
|   factory _$SnDeveloperCopyWith(_SnDeveloper value, $Res Function(_SnDeveloper) _then) = __$SnDeveloperCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String publisherId, SnPublisher? publisher | ||||
| }); | ||||
|  | ||||
|  | ||||
| @override $SnPublisherCopyWith<$Res>? get publisher; | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$SnDeveloperCopyWithImpl<$Res> | ||||
|     implements _$SnDeveloperCopyWith<$Res> { | ||||
|   __$SnDeveloperCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _SnDeveloper _self; | ||||
|   final $Res Function(_SnDeveloper) _then; | ||||
|  | ||||
| /// Create a copy of SnDeveloper | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? publisherId = null,Object? publisher = freezed,}) { | ||||
|   return _then(_SnDeveloper( | ||||
| id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable | ||||
| as String,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable | ||||
| as String,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable | ||||
| as SnPublisher?, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| /// Create a copy of SnDeveloper | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override | ||||
| @pragma('vm:prefer-inline') | ||||
| $SnPublisherCopyWith<$Res>? get publisher { | ||||
|     if (_self.publisher == null) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) { | ||||
|     return _then(_self.copyWith(publisher: value)); | ||||
|   }); | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$DeveloperStats { | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,22 @@ part of 'developer.dart'; | ||||
| // JsonSerializableGenerator | ||||
| // ************************************************************************** | ||||
|  | ||||
| _SnDeveloper _$SnDeveloperFromJson(Map<String, dynamic> json) => _SnDeveloper( | ||||
|   id: json['id'] as String, | ||||
|   publisherId: json['publisher_id'] as String, | ||||
|   publisher: | ||||
|       json['publisher'] == null | ||||
|           ? null | ||||
|           : SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>), | ||||
| ); | ||||
|  | ||||
| Map<String, dynamic> _$SnDeveloperToJson(_SnDeveloper instance) => | ||||
|     <String, dynamic>{ | ||||
|       'id': instance.id, | ||||
|       'publisher_id': instance.publisherId, | ||||
|       'publisher': instance.publisher?.toJson(), | ||||
|     }; | ||||
|  | ||||
| _DeveloperStats _$DeveloperStatsFromJson(Map<String, dynamic> json) => | ||||
|     _DeveloperStats( | ||||
|       totalCustomApps: (json['total_custom_apps'] as num?)?.toInt() ?? 0, | ||||
|   | ||||
| @@ -25,6 +25,32 @@ sealed class SnAccount with _$SnAccount { | ||||
|       _$SnAccountFromJson(json); | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class ProfileLink with _$ProfileLink { | ||||
|   const factory ProfileLink({required String name, required String url}) = | ||||
|       _ProfileLink; | ||||
|  | ||||
|   factory ProfileLink.fromJson(Map<String, dynamic> json) => | ||||
|       _$ProfileLinkFromJson(json); | ||||
| } | ||||
|  | ||||
| class ProfileLinkConverter | ||||
|     implements JsonConverter<List<ProfileLink>, dynamic> { | ||||
|   const ProfileLinkConverter(); | ||||
|  | ||||
|   @override | ||||
|   List<ProfileLink> fromJson(dynamic json) { | ||||
|     return json is List<dynamic> | ||||
|         ? json.map((e) => ProfileLink.fromJson(e)).cast<ProfileLink>().toList() | ||||
|         : <ProfileLink>[]; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   List<dynamic> toJson(List<ProfileLink> object) { | ||||
|     return object.map((e) => e.toJson()).toList(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @freezed | ||||
| sealed class SnAccountProfile with _$SnAccountProfile { | ||||
|   const factory SnAccountProfile({ | ||||
| @@ -38,7 +64,7 @@ sealed class SnAccountProfile with _$SnAccountProfile { | ||||
|     @Default('') String location, | ||||
|     @Default('') String timeZone, | ||||
|     DateTime? birthday, | ||||
|     @Default({}) Map<String, String> links, | ||||
|     @ProfileLinkConverter() @Default([]) List<ProfileLink> links, | ||||
|     DateTime? lastSeenAt, | ||||
|     SnAccountBadge? activeBadge, | ||||
|     required int experience, | ||||
|   | ||||
| @@ -347,10 +347,270 @@ $SnWalletSubscriptionRefCopyWith<$Res>? get perkSubscription { | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$ProfileLink { | ||||
|  | ||||
|  String get name; String get url; | ||||
| /// Create a copy of ProfileLink | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| $ProfileLinkCopyWith<ProfileLink> get copyWith => _$ProfileLinkCopyWithImpl<ProfileLink>(this as ProfileLink, _$identity); | ||||
|  | ||||
|   /// Serializes this ProfileLink to a JSON map. | ||||
|   Map<String, dynamic> toJson(); | ||||
|  | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is ProfileLink&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,name,url); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'ProfileLink(name: $name, url: $url)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class $ProfileLinkCopyWith<$Res>  { | ||||
|   factory $ProfileLinkCopyWith(ProfileLink value, $Res Function(ProfileLink) _then) = _$ProfileLinkCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String name, String url | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class _$ProfileLinkCopyWithImpl<$Res> | ||||
|     implements $ProfileLinkCopyWith<$Res> { | ||||
|   _$ProfileLinkCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final ProfileLink _self; | ||||
|   final $Res Function(ProfileLink) _then; | ||||
|  | ||||
| /// Create a copy of ProfileLink | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? url = null,}) { | ||||
|   return _then(_self.copyWith( | ||||
| name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable | ||||
| as String, | ||||
|   )); | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Adds pattern-matching-related methods to [ProfileLink]. | ||||
| extension ProfileLinkPatterns on ProfileLink { | ||||
| /// 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( _ProfileLink value)?  $default,{required TResult orElse(),}){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _ProfileLink() 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( _ProfileLink value)  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _ProfileLink(): | ||||
| 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( _ProfileLink value)?  $default,){ | ||||
| final _that = this; | ||||
| switch (_that) { | ||||
| case _ProfileLink() 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 name,  String url)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _ProfileLink() when $default != null: | ||||
| return $default(_that.name,_that.url);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 name,  String url)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _ProfileLink(): | ||||
| return $default(_that.name,_that.url);} | ||||
| } | ||||
| /// 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 name,  String url)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _ProfileLink() when $default != null: | ||||
| return $default(_that.name,_that.url);case _: | ||||
|   return null; | ||||
|  | ||||
| } | ||||
| } | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _ProfileLink implements ProfileLink { | ||||
|   const _ProfileLink({required this.name, required this.url}); | ||||
|   factory _ProfileLink.fromJson(Map<String, dynamic> json) => _$ProfileLinkFromJson(json); | ||||
|  | ||||
| @override final  String name; | ||||
| @override final  String url; | ||||
|  | ||||
| /// Create a copy of ProfileLink | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @pragma('vm:prefer-inline') | ||||
| _$ProfileLinkCopyWith<_ProfileLink> get copyWith => __$ProfileLinkCopyWithImpl<_ProfileLink>(this, _$identity); | ||||
|  | ||||
| @override | ||||
| Map<String, dynamic> toJson() { | ||||
|   return _$ProfileLinkToJson(this, ); | ||||
| } | ||||
|  | ||||
| @override | ||||
| bool operator ==(Object other) { | ||||
|   return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProfileLink&&(identical(other.name, name) || other.name == name)&&(identical(other.url, url) || other.url == url)); | ||||
| } | ||||
|  | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @override | ||||
| int get hashCode => Object.hash(runtimeType,name,url); | ||||
|  | ||||
| @override | ||||
| String toString() { | ||||
|   return 'ProfileLink(name: $name, url: $url)'; | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
| /// @nodoc | ||||
| abstract mixin class _$ProfileLinkCopyWith<$Res> implements $ProfileLinkCopyWith<$Res> { | ||||
|   factory _$ProfileLinkCopyWith(_ProfileLink value, $Res Function(_ProfileLink) _then) = __$ProfileLinkCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String name, String url | ||||
| }); | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| } | ||||
| /// @nodoc | ||||
| class __$ProfileLinkCopyWithImpl<$Res> | ||||
|     implements _$ProfileLinkCopyWith<$Res> { | ||||
|   __$ProfileLinkCopyWithImpl(this._self, this._then); | ||||
|  | ||||
|   final _ProfileLink _self; | ||||
|   final $Res Function(_ProfileLink) _then; | ||||
|  | ||||
| /// Create a copy of ProfileLink | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? url = null,}) { | ||||
|   return _then(_ProfileLink( | ||||
| name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable | ||||
| as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable | ||||
| as String, | ||||
|   )); | ||||
| } | ||||
|  | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| /// @nodoc | ||||
| mixin _$SnAccountProfile { | ||||
|  | ||||
|  String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday; Map<String, String> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
|  String get id; String get firstName; String get middleName; String get lastName; String get bio; String get gender; String get pronouns; String get location; String get timeZone; DateTime? get birthday;@ProfileLinkConverter() List<ProfileLink> get links; DateTime? get lastSeenAt; SnAccountBadge? get activeBadge; int get experience; int get level; double get levelingProgress; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; | ||||
| /// Create a copy of SnAccountProfile | ||||
| /// with the given fields replaced by the non-null parameter values. | ||||
| @JsonKey(includeFromJson: false, includeToJson: false) | ||||
| @@ -383,7 +643,7 @@ abstract mixin class $SnAccountProfileCopyWith<$Res>  { | ||||
|   factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl; | ||||
| @useResult | ||||
| $Res call({ | ||||
|  String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
|  String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -413,7 +673,7 @@ as String,location: null == location ? _self.location : location // ignore: cast | ||||
| as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable | ||||
| as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?,links: null == links ? _self.links : links // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, String>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable | ||||
| as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable | ||||
| as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable | ||||
| as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable | ||||
| @@ -554,7 +814,7 @@ return $default(_that);case _: | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday,  Map<String, String> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday, @ProfileLinkConverter()  List<ProfileLink> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,{required TResult orElse(),}) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccountProfile() when $default != null: | ||||
| return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
| @@ -575,7 +835,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday,  Map<String, String> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday, @ProfileLinkConverter()  List<ProfileLink> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccountProfile(): | ||||
| return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);} | ||||
| @@ -592,7 +852,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b | ||||
| /// } | ||||
| /// ``` | ||||
|  | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday,  Map<String, String> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||
| @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id,  String firstName,  String middleName,  String lastName,  String bio,  String gender,  String pronouns,  String location,  String timeZone,  DateTime? birthday, @ProfileLinkConverter()  List<ProfileLink> links,  DateTime? lastSeenAt,  SnAccountBadge? activeBadge,  int experience,  int level,  double levelingProgress,  SnCloudFile? picture,  SnCloudFile? background,  SnVerificationMark? verification,  DateTime createdAt,  DateTime updatedAt,  DateTime? deletedAt)?  $default,) {final _that = this; | ||||
| switch (_that) { | ||||
| case _SnAccountProfile() when $default != null: | ||||
| return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: | ||||
| @@ -607,7 +867,7 @@ return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.b | ||||
| @JsonSerializable() | ||||
|  | ||||
| class _SnAccountProfile implements SnAccountProfile { | ||||
|   const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, final  Map<String, String> links = const {}, this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links; | ||||
|   const _SnAccountProfile({required this.id, this.firstName = '', this.middleName = '', this.lastName = '', this.bio = '', this.gender = '', this.pronouns = '', this.location = '', this.timeZone = '', this.birthday, @ProfileLinkConverter() final  List<ProfileLink> links = const [], this.lastSeenAt, this.activeBadge, required this.experience, required this.level, required this.levelingProgress, required this.picture, required this.background, required this.verification, required this.createdAt, required this.updatedAt, required this.deletedAt}): _links = links; | ||||
|   factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json); | ||||
|  | ||||
| @override final  String id; | ||||
| @@ -620,11 +880,11 @@ class _SnAccountProfile implements SnAccountProfile { | ||||
| @override@JsonKey() final  String location; | ||||
| @override@JsonKey() final  String timeZone; | ||||
| @override final  DateTime? birthday; | ||||
|  final  Map<String, String> _links; | ||||
| @override@JsonKey() Map<String, String> get links { | ||||
|   if (_links is EqualUnmodifiableMapView) return _links; | ||||
|  final  List<ProfileLink> _links; | ||||
| @override@JsonKey()@ProfileLinkConverter() List<ProfileLink> get links { | ||||
|   if (_links is EqualUnmodifiableListView) return _links; | ||||
|   // ignore: implicit_dynamic_type | ||||
|   return EqualUnmodifiableMapView(_links); | ||||
|   return EqualUnmodifiableListView(_links); | ||||
| } | ||||
|  | ||||
| @override final  DateTime? lastSeenAt; | ||||
| @@ -672,7 +932,7 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi | ||||
|   factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl; | ||||
| @override @useResult | ||||
| $Res call({ | ||||
|  String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday, Map<String, String> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
|  String id, String firstName, String middleName, String lastName, String bio, String gender, String pronouns, String location, String timeZone, DateTime? birthday,@ProfileLinkConverter() List<ProfileLink> links, DateTime? lastSeenAt, SnAccountBadge? activeBadge, int experience, int level, double levelingProgress, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt | ||||
| }); | ||||
|  | ||||
|  | ||||
| @@ -702,7 +962,7 @@ as String,location: null == location ? _self.location : location // ignore: cast | ||||
| as String,timeZone: null == timeZone ? _self.timeZone : timeZone // ignore: cast_nullable_to_non_nullable | ||||
| as String,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?,links: null == links ? _self._links : links // ignore: cast_nullable_to_non_nullable | ||||
| as Map<String, String>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable | ||||
| as List<ProfileLink>,lastSeenAt: freezed == lastSeenAt ? _self.lastSeenAt : lastSeenAt // ignore: cast_nullable_to_non_nullable | ||||
| as DateTime?,activeBadge: freezed == activeBadge ? _self.activeBadge : activeBadge // ignore: cast_nullable_to_non_nullable | ||||
| as SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable | ||||
| as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable | ||||
|   | ||||
| @@ -47,6 +47,12 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) => | ||||
|       'deleted_at': instance.deletedAt?.toIso8601String(), | ||||
|     }; | ||||
|  | ||||
| _ProfileLink _$ProfileLinkFromJson(Map<String, dynamic> json) => | ||||
|     _ProfileLink(name: json['name'] as String, url: json['url'] as String); | ||||
|  | ||||
| Map<String, dynamic> _$ProfileLinkToJson(_ProfileLink instance) => | ||||
|     <String, dynamic>{'name': instance.name, 'url': instance.url}; | ||||
|  | ||||
| _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) => | ||||
|     _SnAccountProfile( | ||||
|       id: json['id'] as String, | ||||
| @@ -63,10 +69,9 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) => | ||||
|               ? null | ||||
|               : DateTime.parse(json['birthday'] as String), | ||||
|       links: | ||||
|           (json['links'] as Map<String, dynamic>?)?.map( | ||||
|             (k, e) => MapEntry(k, e as String), | ||||
|           ) ?? | ||||
|           const {}, | ||||
|           json['links'] == null | ||||
|               ? const [] | ||||
|               : const ProfileLinkConverter().fromJson(json['links']), | ||||
|       lastSeenAt: | ||||
|           json['last_seen_at'] == null | ||||
|               ? null | ||||
| @@ -116,7 +121,7 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) => | ||||
|       'location': instance.location, | ||||
|       'time_zone': instance.timeZone, | ||||
|       'birthday': instance.birthday?.toIso8601String(), | ||||
|       'links': instance.links, | ||||
|       'links': const ProfileLinkConverter().toJson(instance.links), | ||||
|       'last_seen_at': instance.lastSeenAt?.toIso8601String(), | ||||
|       'active_badge': instance.activeBadge?.toJson(), | ||||
|       'experience': instance.experience, | ||||
|   | ||||
| @@ -7,12 +7,12 @@ import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/services/udid.native.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:package_info_plus/package_info_plus.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:island/services/update_service.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
| @@ -102,235 +102,226 @@ class _AboutScreenState extends ConsumerState<AboutScreen> { | ||||
|               ? const Center(child: CircularProgressIndicator()) | ||||
|               : _errorMessage != null | ||||
|               ? Center(child: Text(_errorMessage!)) | ||||
|               : SingleChildScrollView( | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                   children: [ | ||||
|                     const SizedBox(height: 24), | ||||
|                     // App Icon and Name | ||||
|                     CircleAvatar( | ||||
|                       radius: 50, | ||||
|                       backgroundColor: theme.colorScheme.primary.withOpacity( | ||||
|                         0.1, | ||||
|                       ), | ||||
|                       child: Image.asset( | ||||
|                         'assets/icons/icon.png', | ||||
|                         width: 56, | ||||
|                         height: 56, | ||||
|                       ), | ||||
|                     ), | ||||
|                     const SizedBox(height: 16), | ||||
|                     Text( | ||||
|                       _packageInfo.appName, | ||||
|                       style: theme.textTheme.headlineSmall?.copyWith( | ||||
|                         fontWeight: FontWeight.bold, | ||||
|                       ), | ||||
|                     ), | ||||
|                     Text( | ||||
|                       'aboutScreenVersionInfo'.tr( | ||||
|                         args: [_packageInfo.version, _packageInfo.buildNumber], | ||||
|                       ), | ||||
|                       style: theme.textTheme.bodyMedium?.copyWith( | ||||
|                         color: theme.textTheme.bodySmall?.color, | ||||
|                       ), | ||||
|                     ), | ||||
|                     const SizedBox(height: 32), | ||||
|  | ||||
|                     // App Info Card | ||||
|                     _buildSection( | ||||
|                       context, | ||||
|                       title: 'aboutScreenAppInfoSectionTitle'.tr(), | ||||
|               : Center( | ||||
|                 child: ConstrainedBox( | ||||
|                   constraints: const BoxConstraints(maxWidth: 540), | ||||
|                   child: SingleChildScrollView( | ||||
|                     child: Column( | ||||
|                       crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                       children: [ | ||||
|                         _buildInfoItem( | ||||
|                           context, | ||||
|                           icon: Symbols.info, | ||||
|                           label: 'aboutScreenPackageNameLabel'.tr(), | ||||
|                           value: _packageInfo.packageName, | ||||
|                         ), | ||||
|                         _buildInfoItem( | ||||
|                           context, | ||||
|                           icon: Symbols.update, | ||||
|                           label: 'aboutScreenVersionLabel'.tr(), | ||||
|                           value: _packageInfo.version, | ||||
|                         ), | ||||
|                         _buildInfoItem( | ||||
|                           context, | ||||
|                           icon: Symbols.build, | ||||
|                           label: 'aboutScreenBuildNumberLabel'.tr(), | ||||
|                           value: _packageInfo.buildNumber, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|  | ||||
|                     if (_deviceInfo != null) const SizedBox(height: 16), | ||||
|  | ||||
|                     if (_deviceInfo != null) | ||||
|                       _buildSection( | ||||
|                         context, | ||||
|                         title: 'Device Information', | ||||
|                         children: [ | ||||
|                           _buildInfoItem( | ||||
|                             context, | ||||
|                             icon: Symbols.label, | ||||
|                             label: 'aboutDeviceName'.tr(), | ||||
|                             value: _deviceInfo?.data['name'], | ||||
|                         const SizedBox(height: 24), | ||||
|                         // App Icon and Name | ||||
|                         CircleAvatar( | ||||
|                           radius: 50, | ||||
|                           backgroundColor: theme.colorScheme.primary | ||||
|                               .withOpacity(0.1), | ||||
|                           child: Image.asset( | ||||
|                             'assets/icons/icon.png', | ||||
|                             width: 56, | ||||
|                             height: 56, | ||||
|                           ), | ||||
|                           _buildInfoItem( | ||||
|                             context, | ||||
|                             icon: Symbols.fingerprint, | ||||
|                             label: 'aboutDeviceIdentifier'.tr(), | ||||
|                             value: _deviceUdid ?? 'N/A', | ||||
|                             copyable: true, | ||||
|                         ), | ||||
|                         const SizedBox(height: 16), | ||||
|                         Text( | ||||
|                           _packageInfo.appName, | ||||
|                           style: theme.textTheme.headlineSmall?.copyWith( | ||||
|                             fontWeight: FontWeight.bold, | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|  | ||||
|                     const SizedBox(height: 16), | ||||
|  | ||||
|                     // Links Card | ||||
|                     _buildSection( | ||||
|                       context, | ||||
|                       title: 'aboutScreenLinksSectionTitle'.tr(), | ||||
|                       children: [ | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Symbols.system_update, | ||||
|                           title: 'Check for updates', | ||||
|                           onTap: () async { | ||||
|                             // Fetch latest release and show the unified sheet | ||||
|                             final svc = UpdateService(); | ||||
|                             // Reuse service fetch + compare to decide content | ||||
|                             final release = await svc.fetchLatestRelease(); | ||||
|                             if (release != null) { | ||||
|                               await svc.showUpdateSheet(context, release); | ||||
|                             } else { | ||||
|                               // Fallback: show a simple sheet indicating no info | ||||
|                               // Use your SheetScaffold for consistent styling | ||||
|                               // Show a minimal message | ||||
|                               // ignore: use_build_context_synchronously | ||||
|                               showModalBottomSheet( | ||||
|                                 context: context, | ||||
|                                 isScrollControlled: true, | ||||
|                                 useSafeArea: true, | ||||
|                                 showDragHandle: true, | ||||
|                                 backgroundColor: | ||||
|                                     Theme.of(context).colorScheme.surface, | ||||
|                                 builder: | ||||
|                                     (_) => const SheetScaffold( | ||||
|                                       titleText: 'Update', | ||||
|                                       child: Center( | ||||
|                                         child: Padding( | ||||
|                                           padding: EdgeInsets.all(24), | ||||
|                                           child: Text( | ||||
|                                             'Unable to fetch release info at this time.', | ||||
|                                           ), | ||||
|                                         ), | ||||
|                                       ), | ||||
|                                     ), | ||||
|                               ); | ||||
|                             } | ||||
|                           }, | ||||
|                         ), | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Symbols.privacy_tip, | ||||
|                           title: 'aboutScreenPrivacyPolicyTitle'.tr(), | ||||
|                           onTap: | ||||
|                               () => _launchURL( | ||||
|                                 'https://solsynth.dev/terms/privacy-policy', | ||||
|                               ), | ||||
|                         ), | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Symbols.description, | ||||
|                           title: 'aboutScreenTermsOfServiceTitle'.tr(), | ||||
|                           onTap: | ||||
|                               () => _launchURL( | ||||
|                                 'https://solsynth.dev/terms/user-agreement', | ||||
|                               ), | ||||
|                         ), | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Symbols.code, | ||||
|                           title: 'aboutScreenOpenSourceLicensesTitle'.tr(), | ||||
|                           onTap: () { | ||||
|                             showLicensePage( | ||||
|                               context: context, | ||||
|                               applicationName: _packageInfo.appName, | ||||
|                               applicationVersion: | ||||
|                                   'Version ${_packageInfo.version}', | ||||
|                             ); | ||||
|                           }, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|  | ||||
|                     const SizedBox(height: 16), | ||||
|  | ||||
|                     // Developer Info | ||||
|                     _buildSection( | ||||
|                       context, | ||||
|                       title: 'aboutScreenDeveloperSectionTitle'.tr(), | ||||
|                       children: [ | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Symbols.email, | ||||
|                           title: 'aboutScreenContactUsTitle'.tr(), | ||||
|                           subtitle: 'lily@solsynth.dev', | ||||
|                           onTap: () => _launchURL('mailto:lily@solsynth.dev'), | ||||
|                         ), | ||||
|                         _buildListTile( | ||||
|                           context, | ||||
|                           icon: Symbols.copyright, | ||||
|                           title: 'aboutScreenLicenseTitle'.tr(), | ||||
|                           subtitle: 'aboutScreenLicenseContent'.tr( | ||||
|                             args: [DateTime.now().year.toString()], | ||||
|                         Text( | ||||
|                           'aboutScreenVersionInfo'.tr( | ||||
|                             args: [ | ||||
|                               _packageInfo.version, | ||||
|                               _packageInfo.buildNumber, | ||||
|                             ], | ||||
|                           ), | ||||
|                           onTap: | ||||
|                               () => _launchURL( | ||||
|                                 'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt', | ||||
|                               ), | ||||
|                         ), | ||||
|                         if (kIsWeb || !(Platform.isMacOS || Platform.isIOS)) | ||||
|                           _buildListTile( | ||||
|                             context, | ||||
|                             icon: Symbols.favorite, | ||||
|                             title: 'donate'.tr(), | ||||
|                             subtitle: 'donateDescription'.tr(), | ||||
|                             onTap: () { | ||||
|                               launchUrlString( | ||||
|                                 'https://afdian.com/@littlesheep', | ||||
|                               ); | ||||
|                             }, | ||||
|                           style: theme.textTheme.bodyMedium?.copyWith( | ||||
|                             color: theme.textTheme.bodySmall?.color, | ||||
|                           ), | ||||
|                       ], | ||||
|                     ), | ||||
|                         ), | ||||
|                         const SizedBox(height: 32), | ||||
|  | ||||
|                     const SizedBox(height: 32), | ||||
|  | ||||
|                     // Copyright | ||||
|                     Padding( | ||||
|                       padding: const EdgeInsets.all(16.0), | ||||
|                       child: Column( | ||||
|                         children: [ | ||||
|                           Text( | ||||
|                             'aboutScreenCopyright'.tr( | ||||
|                               args: [DateTime.now().year.toString()], | ||||
|                         // App Info Card | ||||
|                         _buildSection( | ||||
|                           context, | ||||
|                           title: 'aboutScreenAppInfoSectionTitle'.tr(), | ||||
|                           children: [ | ||||
|                             _buildInfoItem( | ||||
|                               context, | ||||
|                               icon: Symbols.info, | ||||
|                               label: 'aboutScreenPackageNameLabel'.tr(), | ||||
|                               value: _packageInfo.packageName, | ||||
|                             ), | ||||
|                             style: theme.textTheme.bodySmall, | ||||
|                             textAlign: TextAlign.center, | ||||
|                           ), | ||||
|                           const Gap(1), | ||||
|                           Text( | ||||
|                             'aboutScreenMadeWith'.tr(), | ||||
|                             textAlign: TextAlign.center, | ||||
|                           ).fontSize(10).opacity(0.8), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ), | ||||
|                             _buildInfoItem( | ||||
|                               context, | ||||
|                               icon: Symbols.update, | ||||
|                               label: 'aboutScreenVersionLabel'.tr(), | ||||
|                               value: _packageInfo.version, | ||||
|                             ), | ||||
|                             _buildInfoItem( | ||||
|                               context, | ||||
|                               icon: Symbols.build, | ||||
|                               label: 'aboutScreenBuildNumberLabel'.tr(), | ||||
|                               value: _packageInfo.buildNumber, | ||||
|                             ), | ||||
|                           ], | ||||
|                         ), | ||||
|  | ||||
|                     Gap(MediaQuery.of(context).padding.bottom + 16), | ||||
|                   ], | ||||
|                         if (_deviceInfo != null) const SizedBox(height: 16), | ||||
|  | ||||
|                         if (_deviceInfo != null) | ||||
|                           _buildSection( | ||||
|                             context, | ||||
|                             title: 'Device Information', | ||||
|                             children: [ | ||||
|                               _buildInfoItem( | ||||
|                                 context, | ||||
|                                 icon: Symbols.label, | ||||
|                                 label: 'aboutDeviceName'.tr(), | ||||
|                                 value: _deviceInfo?.data['name'], | ||||
|                               ), | ||||
|                               _buildInfoItem( | ||||
|                                 context, | ||||
|                                 icon: Symbols.fingerprint, | ||||
|                                 label: 'aboutDeviceIdentifier'.tr(), | ||||
|                                 value: _deviceUdid ?? 'N/A', | ||||
|                                 copyable: true, | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|  | ||||
|                         const SizedBox(height: 16), | ||||
|  | ||||
|                         // Links Card | ||||
|                         _buildSection( | ||||
|                           context, | ||||
|                           title: 'aboutScreenLinksSectionTitle'.tr(), | ||||
|                           children: [ | ||||
|                             _buildListTile( | ||||
|                               context, | ||||
|                               icon: Symbols.system_update, | ||||
|                               title: 'Check for updates', | ||||
|                               onTap: () async { | ||||
|                                 // Fetch latest release and show the unified sheet | ||||
|                                 final svc = UpdateService(); | ||||
|                                 // Reuse service fetch + compare to decide content | ||||
|                                 showLoadingModal(context); | ||||
|                                 final release = await svc.fetchLatestRelease(); | ||||
|                                 if (!context.mounted) return; | ||||
|                                 hideLoadingModal(context); | ||||
|                                 if (release != null) { | ||||
|                                   await svc.showUpdateSheet(context, release); | ||||
|                                 } else { | ||||
|                                   showInfoAlert( | ||||
|                                     'Currently cannot get update from the GitHub.', | ||||
|                                     'Unable to check for updates', | ||||
|                                   ); | ||||
|                                 } | ||||
|                               }, | ||||
|                             ), | ||||
|                             _buildListTile( | ||||
|                               context, | ||||
|                               icon: Symbols.privacy_tip, | ||||
|                               title: 'aboutScreenPrivacyPolicyTitle'.tr(), | ||||
|                               onTap: | ||||
|                                   () => _launchURL( | ||||
|                                     'https://solsynth.dev/terms/privacy-policy', | ||||
|                                   ), | ||||
|                             ), | ||||
|                             _buildListTile( | ||||
|                               context, | ||||
|                               icon: Symbols.description, | ||||
|                               title: 'aboutScreenTermsOfServiceTitle'.tr(), | ||||
|                               onTap: | ||||
|                                   () => _launchURL( | ||||
|                                     'https://solsynth.dev/terms/user-agreement', | ||||
|                                   ), | ||||
|                             ), | ||||
|                             _buildListTile( | ||||
|                               context, | ||||
|                               icon: Symbols.code, | ||||
|                               title: 'aboutScreenOpenSourceLicensesTitle'.tr(), | ||||
|                               onTap: () { | ||||
|                                 showLicensePage( | ||||
|                                   context: context, | ||||
|                                   applicationName: _packageInfo.appName, | ||||
|                                   applicationVersion: | ||||
|                                       'Version ${_packageInfo.version}', | ||||
|                                 ); | ||||
|                               }, | ||||
|                             ), | ||||
|                           ], | ||||
|                         ), | ||||
|  | ||||
|                         const SizedBox(height: 16), | ||||
|  | ||||
|                         // Developer Info | ||||
|                         _buildSection( | ||||
|                           context, | ||||
|                           title: 'aboutScreenDeveloperSectionTitle'.tr(), | ||||
|                           children: [ | ||||
|                             _buildListTile( | ||||
|                               context, | ||||
|                               icon: Symbols.email, | ||||
|                               title: 'aboutScreenContactUsTitle'.tr(), | ||||
|                               subtitle: 'lily@solsynth.dev', | ||||
|                               onTap: | ||||
|                                   () => _launchURL('mailto:lily@solsynth.dev'), | ||||
|                             ), | ||||
|                             _buildListTile( | ||||
|                               context, | ||||
|                               icon: Symbols.copyright, | ||||
|                               title: 'aboutScreenLicenseTitle'.tr(), | ||||
|                               subtitle: 'aboutScreenLicenseContent'.tr( | ||||
|                                 args: [DateTime.now().year.toString()], | ||||
|                               ), | ||||
|                               onTap: | ||||
|                                   () => _launchURL( | ||||
|                                     'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt', | ||||
|                                   ), | ||||
|                             ), | ||||
|                             if (kIsWeb || !(Platform.isMacOS || Platform.isIOS)) | ||||
|                               _buildListTile( | ||||
|                                 context, | ||||
|                                 icon: Symbols.favorite, | ||||
|                                 title: 'donate'.tr(), | ||||
|                                 subtitle: 'donateDescription'.tr(), | ||||
|                                 onTap: () { | ||||
|                                   launchUrlString( | ||||
|                                     'https://afdian.com/@littlesheep', | ||||
|                                   ); | ||||
|                                 }, | ||||
|                               ), | ||||
|                           ], | ||||
|                         ), | ||||
|  | ||||
|                         const SizedBox(height: 32), | ||||
|  | ||||
|                         // Copyright | ||||
|                         Padding( | ||||
|                           padding: const EdgeInsets.all(16.0), | ||||
|                           child: Column( | ||||
|                             children: [ | ||||
|                               Text( | ||||
|                                 'aboutScreenCopyright'.tr( | ||||
|                                   args: [DateTime.now().year.toString()], | ||||
|                                 ), | ||||
|                                 style: theme.textTheme.bodySmall, | ||||
|                                 textAlign: TextAlign.center, | ||||
|                               ), | ||||
|                               const Gap(1), | ||||
|                               Text( | ||||
|                                 'aboutScreenMadeWith'.tr(), | ||||
|                                 textAlign: TextAlign.center, | ||||
|                               ).fontSize(10).opacity(0.8), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ), | ||||
|  | ||||
|                         Gap(MediaQuery.of(context).padding.bottom + 16), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|     ); | ||||
|   | ||||
| @@ -1,12 +1,8 @@ | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/message.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/screens/notification.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| @@ -15,6 +11,7 @@ import 'package:island/widgets/account/status.dart'; | ||||
| import 'package:island/widgets/account/leveling_progress.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/debug_sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| @@ -239,7 +236,7 @@ class AccountScreen extends HookConsumerWidget { | ||||
|             ), | ||||
|             ListTile( | ||||
|               minTileHeight: 48, | ||||
|               title: Text('abuseReports').tr(), | ||||
|               title: Text('abuseReport').tr(), | ||||
|               contentPadding: const EdgeInsets.symmetric(horizontal: 24), | ||||
|               leading: const Icon(Symbols.gavel), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
| @@ -276,30 +273,6 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                 context.pushNamed('accountSettings'); | ||||
|               }, | ||||
|             ), | ||||
|             if (kDebugMode) const Divider(height: 1).padding(vertical: 8), | ||||
|             if (kDebugMode) | ||||
|               ListTile( | ||||
|                 minTileHeight: 48, | ||||
|                 leading: const Icon(Symbols.copy_all), | ||||
|                 trailing: const Icon(Symbols.chevron_right), | ||||
|                 contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                 title: Text('Copy access token'), | ||||
|                 onTap: () async { | ||||
|                   final tk = ref.watch(tokenProvider); | ||||
|                   Clipboard.setData(ClipboardData(text: tk!.token)); | ||||
|                 }, | ||||
|               ), | ||||
|             if (kDebugMode) | ||||
|               ListTile( | ||||
|                 minTileHeight: 48, | ||||
|                 leading: const Icon(Symbols.delete), | ||||
|                 trailing: const Icon(Symbols.chevron_right), | ||||
|                 contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|                 title: Text('Reset database'), | ||||
|                 onTap: () async { | ||||
|                   resetDatabase(ref); | ||||
|                 }, | ||||
|               ), | ||||
|             const Divider(height: 1).padding(vertical: 8), | ||||
|             ListTile( | ||||
|               minTileHeight: 48, | ||||
| @@ -311,6 +284,19 @@ class AccountScreen extends HookConsumerWidget { | ||||
|                 context.pushNamed('about'); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
|               minTileHeight: 48, | ||||
|               leading: const Icon(Symbols.bug_report), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               title: Text('debugOptions').tr(), | ||||
|               onTap: () { | ||||
|                 showModalBottomSheet( | ||||
|                   context: context, | ||||
|                   builder: (context) => DebugSheet(), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|             ListTile( | ||||
|               minTileHeight: 48, | ||||
|               leading: const Icon(Symbols.logout), | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import 'package:gap/gap.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:image_picker/image_picker.dart'; | ||||
| import 'package:island/models/file.dart'; | ||||
| import 'package:island/models/user.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| @@ -95,11 +96,7 @@ class UpdateProfileScreen extends HookConsumerWidget { | ||||
|     final usernameController = useTextEditingController(text: user.value!.name); | ||||
|     final nicknameController = useTextEditingController(text: user.value!.nick); | ||||
|     final language = useState(user.value!.language); | ||||
|     final links = useState<List<Map<String, String>>>( | ||||
|       user.value!.profile.links.entries | ||||
|           .map((e) => {'key': e.key, 'value': e.value}) | ||||
|           .toList(), | ||||
|     ); | ||||
|     final links = useState<List<ProfileLink>>(user.value!.profile.links); | ||||
|  | ||||
|     void updateBasicInfo() async { | ||||
|       if (!formKeyBasicInfo.currentState!.validate()) return; | ||||
| @@ -171,7 +168,7 @@ class UpdateProfileScreen extends HookConsumerWidget { | ||||
|             'location': locationController.text, | ||||
|             'time_zone': timeZoneController.text, | ||||
|             'birthday': birthday.value?.toUtc().toIso8601String(), | ||||
|             'links': {for (var e in links.value) e['key']!: e['value']!}, | ||||
|             'links': links.value, | ||||
|           }, | ||||
|         ); | ||||
|         final userNotifier = ref.read(userInfoProvider.notifier); | ||||
| @@ -575,13 +572,15 @@ class UpdateProfileScreen extends HookConsumerWidget { | ||||
|                           children: [ | ||||
|                             Expanded( | ||||
|                               child: TextFormField( | ||||
|                                 initialValue: links.value[i]['key'], | ||||
|                                 initialValue: links.value[i].name, | ||||
|                                 decoration: InputDecoration( | ||||
|                                   labelText: 'linkKey'.tr(), | ||||
|                                   isDense: true, | ||||
|                                 ), | ||||
|                                 onChanged: (value) { | ||||
|                                   links.value[i]['key'] = value; | ||||
|                                   links.value[i] = links.value[i].copyWith( | ||||
|                                     name: value, | ||||
|                                   ); | ||||
|                                 }, | ||||
|                                 onTapOutside: | ||||
|                                     (_) => | ||||
| @@ -592,13 +591,15 @@ class UpdateProfileScreen extends HookConsumerWidget { | ||||
|                             const Gap(8), | ||||
|                             Expanded( | ||||
|                               child: TextFormField( | ||||
|                                 initialValue: links.value[i]['value'], | ||||
|                                 initialValue: links.value[i].url, | ||||
|                                 decoration: InputDecoration( | ||||
|                                   labelText: 'linkValue'.tr(), | ||||
|                                   isDense: true, | ||||
|                                 ), | ||||
|                                 onChanged: (value) { | ||||
|                                   links.value[i]['value'] = value; | ||||
|                                   links.value[i] = links.value[i].copyWith( | ||||
|                                     url: value, | ||||
|                                   ); | ||||
|                                 }, | ||||
|                                 onTapOutside: | ||||
|                                     (_) => | ||||
| @@ -620,7 +621,7 @@ class UpdateProfileScreen extends HookConsumerWidget { | ||||
|                         child: FilledButton.icon( | ||||
|                           onPressed: () { | ||||
|                             links.value = List.from(links.value) | ||||
|                               ..add({'key': '', 'value': ''}); | ||||
|                               ..add(ProfileLink(name: '', url: '')); | ||||
|                           }, | ||||
|                           label: Text('addLink').tr(), | ||||
|                           icon: const Icon(Symbols.add), | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| @@ -13,6 +14,7 @@ import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/userinfo.dart'; | ||||
| import 'package:island/services/color.dart'; | ||||
| import 'package:island/services/responsive.dart'; | ||||
| import 'package:island/services/text.dart'; | ||||
| import 'package:island/services/time.dart'; | ||||
| import 'package:island/services/timezone/native.dart'; | ||||
| import 'package:island/widgets/account/account_name.dart'; | ||||
| @@ -30,6 +32,7 @@ import 'package:palette_generator/palette_generator.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| import 'package:share_plus/share_plus.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
|  | ||||
| part 'profile.g.dart'; | ||||
|  | ||||
| @@ -194,6 +197,15 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|  | ||||
|     List<Widget> buildSubcolumn(SnAccount data) { | ||||
|       return [ | ||||
|         Row( | ||||
|           spacing: 6, | ||||
|           children: [ | ||||
|             const Icon(Symbols.join, size: 17, fill: 1), | ||||
|             Text( | ||||
|               'joinedAt'.tr(args: [data.createdAt.formatCustom('yyyy-MM-dd')]), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|         if (data.profile.birthday != null) | ||||
|           Row( | ||||
|             spacing: 6, | ||||
| @@ -320,7 +332,7 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|               spacing: 2, | ||||
|               children: buildSubcolumn(data), | ||||
|             ), | ||||
|           if (data.profile.timeZone.isNotEmpty) | ||||
|           if (data.profile.timeZone.isNotEmpty && !kIsWeb) | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
| @@ -350,6 +362,32 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|       ).padding(horizontal: 24, vertical: 16), | ||||
|     ); | ||||
|  | ||||
|     Widget accountProfileLinks(SnAccount data) => Card( | ||||
|       child: Column( | ||||
|         crossAxisAlignment: CrossAxisAlignment.start, | ||||
|         children: [ | ||||
|           Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4), | ||||
|           for (final link in data.profile.links) | ||||
|             ListTile( | ||||
|               title: Text(link.name.capitalizeEachWord()), | ||||
|               subtitle: Text(link.url), | ||||
|               contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|               trailing: const Icon(Symbols.chevron_right), | ||||
|               shape: RoundedRectangleBorder( | ||||
|                 borderRadius: const BorderRadius.all(Radius.circular(8)), | ||||
|               ), | ||||
|               onTap: () { | ||||
|                 if (!link.url.startsWith('http') && !link.url.contains('://')) { | ||||
|                   launchUrlString('https://${link.url}'); | ||||
|                 } else { | ||||
|                   launchUrlString(link.url); | ||||
|                 } | ||||
|               }, | ||||
|             ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     Widget accountAction(SnAccount data) => Card( | ||||
|       child: Column( | ||||
|         children: [ | ||||
| @@ -452,7 +490,7 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|       ).padding(horizontal: 16, vertical: 8), | ||||
|       ).padding(horizontal: 16, vertical: 12), | ||||
|     ); | ||||
|  | ||||
|     return account.when( | ||||
| @@ -537,6 +575,10 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|                               SliverToBoxAdapter( | ||||
|                                 child: accountProfileBio(data).padding(top: 4), | ||||
|                               ), | ||||
|                               if (data.profile.links.isNotEmpty) | ||||
|                                 SliverToBoxAdapter( | ||||
|                                   child: accountProfileLinks(data), | ||||
|                                 ), | ||||
|                               SliverToBoxAdapter( | ||||
|                                 child: accountProfileDetail(data), | ||||
|                               ), | ||||
| @@ -633,6 +675,12 @@ class AccountProfileScreen extends HookConsumerWidget { | ||||
|                         SliverToBoxAdapter( | ||||
|                           child: accountProfileBio(data).padding(horizontal: 4), | ||||
|                         ), | ||||
|                         if (data.profile.links.isNotEmpty) | ||||
|                           SliverToBoxAdapter( | ||||
|                             child: accountProfileLinks( | ||||
|                               data, | ||||
|                             ).padding(horizontal: 4), | ||||
|                           ), | ||||
|                         SliverToBoxAdapter( | ||||
|                           child: accountProfileDetail( | ||||
|                             data, | ||||
|   | ||||
| @@ -216,6 +216,7 @@ class RelationshipScreen extends HookConsumerWidget { | ||||
|       final result = await showModalBottomSheet( | ||||
|         context: context, | ||||
|         useRootNavigator: true, | ||||
|         isScrollControlled: true, | ||||
|         builder: (context) => AccountPickerSheet(), | ||||
|       ); | ||||
|       if (result == null) return; | ||||
|   | ||||
| @@ -227,6 +227,7 @@ class ChatListScreen extends HookConsumerWidget { | ||||
|       final result = await showModalBottomSheet( | ||||
|         context: context, | ||||
|         useRootNavigator: true, | ||||
|         isScrollControlled: true, | ||||
|         builder: (context) => const AccountPickerSheet(), | ||||
|       ); | ||||
|       if (result == null) return; | ||||
|   | ||||
| @@ -339,7 +339,7 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|                             } | ||||
|  | ||||
|                             await apiClient.post( | ||||
|                               '/chat/${chatRoom.value!.id}/members/me', | ||||
|                               '/sphere/chat/${chatRoom.value!.id}/members/me', | ||||
|                             ); | ||||
|                             ref.invalidate(chatroomIdentityProvider(id)); | ||||
|                           } catch (err) { | ||||
| @@ -929,7 +929,7 @@ class ChatRoomScreen extends HookConsumerWidget { | ||||
|                             if (attachment.isOnCloud) { | ||||
|                               final client = ref.watch(apiClientProvider); | ||||
|                               await client.delete( | ||||
|                                 '/files/${attachment.data.id}', | ||||
|                                 '/drive/files/${attachment.data.id}', | ||||
|                               ); | ||||
|                             } | ||||
|                             final clone = List.of(attachments.value); | ||||
|   | ||||
| @@ -589,6 +589,7 @@ class _ChatMemberListSheet extends HookConsumerWidget { | ||||
|       final result = await showModalBottomSheet( | ||||
|         context: context, | ||||
|         useRootNavigator: true, | ||||
|         isScrollControlled: true, | ||||
|         builder: (context) => const AccountPickerSheet(), | ||||
|       ); | ||||
|       if (result == null) return; | ||||
| @@ -727,7 +728,7 @@ class _ChatMemberListSheet extends HookConsumerWidget { | ||||
|                                       apiClientProvider, | ||||
|                                     ); | ||||
|                                     await apiClient.delete( | ||||
|                                       '/chat/$roomId/members/${member.accountId}', | ||||
|                                       '/sphere/chat/$roomId/members/${member.accountId}', | ||||
|                                     ); | ||||
|                                     // Refresh both providers | ||||
|                                     memberNotifier.reset(); | ||||
|   | ||||
| @@ -382,7 +382,7 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|                           ), | ||||
|                           ListTile( | ||||
|                             minTileHeight: 48, | ||||
|                             title: const Text('Polls'), | ||||
|                             title: Text('polls').tr(), | ||||
|                             trailing: const Icon(Symbols.chevron_right), | ||||
|                             leading: const Icon(Symbols.poll), | ||||
|                             contentPadding: const EdgeInsets.symmetric( | ||||
| @@ -419,7 +419,7 @@ class CreatorHubScreen extends HookConsumerWidget { | ||||
|                           ), | ||||
|                           ListTile( | ||||
|                             minTileHeight: 48, | ||||
|                             title: const Text('Web Feeds').tr(), | ||||
|                             title: const Text('webFeeds').tr(), | ||||
|                             trailing: const Icon(Symbols.chevron_right), | ||||
|                             leading: const Icon(Symbols.rss_feed), | ||||
|                             contentPadding: const EdgeInsets.symmetric( | ||||
| @@ -659,7 +659,7 @@ class PublisherMemberNotifier extends StateNotifier<PublisherMemberState> { | ||||
|  | ||||
|     try { | ||||
|       final response = await _apiClient.get( | ||||
|         '/publishers/$publisherUname/members', | ||||
|         '/sphere/publishers/$publisherUname/members', | ||||
|         queryParameters: {'offset': offset, 'take': take}, | ||||
|       ); | ||||
|  | ||||
| @@ -708,6 +708,7 @@ class _PublisherMemberListSheet extends HookConsumerWidget { | ||||
|  | ||||
|     Future<void> invitePerson() async { | ||||
|       final result = await showModalBottomSheet( | ||||
|         useRootNavigator: true, | ||||
|         isScrollControlled: true, | ||||
|         context: context, | ||||
|         builder: (context) => const AccountPickerSheet(), | ||||
| @@ -719,6 +720,9 @@ class _PublisherMemberListSheet extends HookConsumerWidget { | ||||
|           '/publishers/$publisherUname/invites', | ||||
|           data: {'related_user_id': result.id, 'role': 0}, | ||||
|         ); | ||||
|         // Refresh both providers | ||||
|         memberNotifier.reset(); | ||||
|         await memberNotifier.loadMore(); | ||||
|         ref.invalidate(memberListProvider); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
| @@ -822,6 +826,9 @@ class _PublisherMemberListSheet extends HookConsumerWidget { | ||||
|                                       ), | ||||
|                                 ).then((value) { | ||||
|                                   if (value != null) { | ||||
|                                     // Refresh both providers | ||||
|                                     memberNotifier.reset(); | ||||
|                                     memberNotifier.loadMore(); | ||||
|                                     ref.invalidate(memberListProvider); | ||||
|                                   } | ||||
|                                 }); | ||||
| @@ -843,6 +850,9 @@ class _PublisherMemberListSheet extends HookConsumerWidget { | ||||
|                                     await apiClient.delete( | ||||
|                                       '/publishers/$publisherUname/members/${member.accountId}', | ||||
|                                     ); | ||||
|                                     // Refresh both providers | ||||
|                                     memberNotifier.reset(); | ||||
|                                     memberNotifier.loadMore(); | ||||
|                                     ref.invalidate(memberListProvider); | ||||
|                                   } catch (err) { | ||||
|                                     showErrorAlert(err); | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/poll.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:island/widgets/poll/poll_feedback.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||
| @@ -70,7 +71,7 @@ class CreatorPollListScreen extends HookConsumerWidget { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar(title: const Text('Polls')), | ||||
|       floatingActionButton: FloatingActionButton( | ||||
|         onPressed: () => _createPoll(context), | ||||
|   | ||||
| @@ -58,7 +58,7 @@ class StickerPackDetailScreen extends HookConsumerWidget { | ||||
|       try { | ||||
|         showLoadingModal(context); | ||||
|         final apiClient = ref.watch(apiClientProvider); | ||||
|         await apiClient.delete('/stickers/$id/content/${sticker.id}'); | ||||
|         await apiClient.delete('/sphere/stickers/$id/content/${sticker.id}'); | ||||
|         ref.invalidate(stickerPackContentProvider(id)); | ||||
|       } catch (err) { | ||||
|         showErrorAlert(err); | ||||
| @@ -180,6 +180,7 @@ class StickerPackDetailScreen extends HookConsumerWidget { | ||||
|                                               .pushNamed( | ||||
|                                                 'creatorStickerEdit', | ||||
|                                                 pathParameters: { | ||||
|                                                   'name': pubName, | ||||
|                                                   'packId': id, | ||||
|                                                   'id': sticker.id, | ||||
|                                                 }, | ||||
| @@ -297,7 +298,7 @@ class _StickerPackActionMenu extends HookConsumerWidget { | ||||
|                 ).then((confirm) { | ||||
|                   if (confirm) { | ||||
|                     final client = ref.watch(apiClientProvider); | ||||
|                     client.delete('/stickers/$packId'); | ||||
|                     client.delete('/sphere/stickers/$packId'); | ||||
|                     ref.invalidate(stickerPacksNotifierProvider); | ||||
|                     if (context.mounted) context.pop(true); | ||||
|                   } | ||||
| @@ -325,7 +326,7 @@ Future<SnSticker?> stickerPackSticker( | ||||
|   if (query == null) return null; | ||||
|   final apiClient = ref.watch(apiClientProvider); | ||||
|   final resp = await apiClient.get( | ||||
|     '/stickers/${query.packId}/content/${query.id}', | ||||
|     '/sphere/stickers/${query.packId}/content/${query.id}', | ||||
|   ); | ||||
|   if (resp.data == null) return null; | ||||
|   return SnSticker.fromJson(resp.data); | ||||
| @@ -379,8 +380,8 @@ class EditStickersScreen extends HookConsumerWidget { | ||||
|       try { | ||||
|         final resp = await apiClient.request( | ||||
|           id == null | ||||
|               ? '/stickers/$packId/content' | ||||
|               : '/stickers/$packId/content/$id', | ||||
|               ? '/sphere/stickers/$packId/content' | ||||
|               : '/sphere/stickers/$packId/content/$id', | ||||
|           data: {'slug': slugController.text, 'image_id': imageController.text}, | ||||
|           options: Options(method: id == null ? 'POST' : 'PATCH'), | ||||
|         ); | ||||
|   | ||||
| @@ -151,7 +151,7 @@ class _StickerPackContentProviderElement | ||||
| } | ||||
|  | ||||
| String _$stickerPackStickerHash() => | ||||
|     r'36f524c047e632236d5597aaaa8678ed86599602'; | ||||
|     r'5c553666b3a63530bdebae4b7cd52f303c5ab3a0'; | ||||
|  | ||||
| /// See also [stickerPackSticker]. | ||||
| @ProviderFor(stickerPackSticker) | ||||
|   | ||||
| @@ -31,7 +31,7 @@ class StickersScreen extends HookConsumerWidget { | ||||
|               context | ||||
|                   .pushNamed( | ||||
|                     'creatorStickerPackNew', | ||||
|                     queryParameters: {'name': pubName}, | ||||
|                     pathParameters: {'name': pubName}, | ||||
|                   ) | ||||
|                   .then((value) { | ||||
|                     if (value != null) { | ||||
| @@ -187,10 +187,8 @@ class EditStickerPacksScreen extends HookConsumerWidget { | ||||
|             'description': descriptionController.text, | ||||
|             'prefix': prefixController.text, | ||||
|           }, | ||||
|           options: Options( | ||||
|             method: packId == null ? 'POST' : 'PATCH', | ||||
|             headers: {'X-Pub': pubName}, | ||||
|           ), | ||||
|           queryParameters: {'pub': pubName}, | ||||
|           options: Options(method: packId == null ? 'POST' : 'PATCH'), | ||||
|         ); | ||||
|         if (!context.mounted) return; | ||||
|         context.pop(SnStickerPack.fromJson(resp.data)); | ||||
|   | ||||
| @@ -114,10 +114,11 @@ class WebFeedEditScreen extends HookConsumerWidget { | ||||
|  | ||||
|     return feedAsync.when( | ||||
|       loading: | ||||
|           () => | ||||
|               const Scaffold(body: Center(child: CircularProgressIndicator())), | ||||
|           () => const AppScaffold( | ||||
|             body: Center(child: CircularProgressIndicator()), | ||||
|           ), | ||||
|       error: | ||||
|           (error, stack) => Scaffold( | ||||
|           (error, stack) => AppScaffold( | ||||
|             appBar: AppBar(title: const Text('Error')), | ||||
|             body: Center(child: Text('Error: $error')), | ||||
|           ), | ||||
|   | ||||
| @@ -30,12 +30,12 @@ Future<DeveloperStats?> developerStats(Ref ref, String? uname) async { | ||||
| } | ||||
|  | ||||
| @riverpod | ||||
| Future<List<SnPublisher>> developers(Ref ref) async { | ||||
| Future<List<SnDeveloper>> developers(Ref ref) async { | ||||
|   final client = ref.watch(apiClientProvider); | ||||
|   final resp = await client.get('/develop/developers'); | ||||
|   return resp.data | ||||
|       .map((e) => SnPublisher.fromJson(e)) | ||||
|       .cast<SnPublisher>() | ||||
|       .map((e) => SnDeveloper.fromJson(e)) | ||||
|       .cast<SnDeveloper>() | ||||
|       .toList(); | ||||
| } | ||||
|  | ||||
| @@ -74,25 +74,25 @@ class DeveloperHubScreen extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     final developers = ref.watch(developersProvider); | ||||
|     final currentDeveloper = useState<SnPublisher?>( | ||||
|     final currentDeveloper = useState<SnDeveloper?>( | ||||
|       developers.value?.firstOrNull, | ||||
|     ); | ||||
|  | ||||
|     final List<DropdownMenuItem<SnPublisher>> developersMenu = developers.when( | ||||
|     final List<DropdownMenuItem<SnDeveloper>> developersMenu = developers.when( | ||||
|       data: | ||||
|           (data) => | ||||
|               data | ||||
|                   .map( | ||||
|                     (item) => DropdownMenuItem<SnPublisher>( | ||||
|                     (item) => DropdownMenuItem<SnDeveloper>( | ||||
|                       value: item, | ||||
|                       child: ListTile( | ||||
|                         minTileHeight: 48, | ||||
|                         leading: ProfilePictureWidget( | ||||
|                           radius: 16, | ||||
|                           fileId: item.picture?.id, | ||||
|                           fileId: item.publisher?.picture?.id, | ||||
|                         ), | ||||
|                         title: Text(item.nick), | ||||
|                         subtitle: Text('@${item.name}'), | ||||
|                         title: Text(item.publisher!.nick), | ||||
|                         subtitle: Text('@${item.publisher!.name}'), | ||||
|                         trailing: | ||||
|                             currentDeveloper.value?.id == item.id | ||||
|                                 ? const Icon(Icons.check) | ||||
| @@ -107,7 +107,7 @@ class DeveloperHubScreen extends HookConsumerWidget { | ||||
|     ); | ||||
|  | ||||
|     final developerStats = ref.watch( | ||||
|       developerStatsProvider(currentDeveloper.value?.name), | ||||
|       developerStatsProvider(currentDeveloper.value?.publisher?.name), | ||||
|     ); | ||||
|  | ||||
|     return AppScaffold( | ||||
| @@ -117,7 +117,7 @@ class DeveloperHubScreen extends HookConsumerWidget { | ||||
|         title: Text('developerHub').tr(), | ||||
|         actions: [ | ||||
|           DropdownButtonHideUnderline( | ||||
|             child: DropdownButton2<SnPublisher>( | ||||
|             child: DropdownButton2<SnDeveloper>( | ||||
|               alignment: Alignment.centerRight, | ||||
|               value: currentDeveloper.value, | ||||
|               hint: CircleAvatar( | ||||
| @@ -139,7 +139,7 @@ class DeveloperHubScreen extends HookConsumerWidget { | ||||
|                   ...developersMenu.map( | ||||
|                     (e) => ProfilePictureWidget( | ||||
|                       radius: 16, | ||||
|                       fileId: e.value?.picture?.id, | ||||
|                       fileId: e.value?.publisher?.picture?.id, | ||||
|                     ).center().padding(right: 8), | ||||
|                   ), | ||||
|                 ]; | ||||
| @@ -193,10 +193,12 @@ class DeveloperHubScreen extends HookConsumerWidget { | ||||
|                           ...(developers.value?.map( | ||||
|                                 (developer) => ListTile( | ||||
|                                   leading: ProfilePictureWidget( | ||||
|                                     file: developer.picture, | ||||
|                                     file: developer.publisher?.picture, | ||||
|                                   ), | ||||
|                                   title: Text(developer.publisher!.nick), | ||||
|                                   subtitle: Text( | ||||
|                                     '@${developer.publisher!.name}', | ||||
|                                   ), | ||||
|                                   title: Text(developer.nick), | ||||
|                                   subtitle: Text('@${developer.name}'), | ||||
|                                   onTap: () { | ||||
|                                     currentDeveloper.value = developer; | ||||
|                                   }, | ||||
| @@ -243,7 +245,8 @@ class DeveloperHubScreen extends HookConsumerWidget { | ||||
|                               context.pushNamed( | ||||
|                                 'developerApps', | ||||
|                                 pathParameters: { | ||||
|                                   'name': currentDeveloper.value!.name, | ||||
|                                   'name': | ||||
|                                       currentDeveloper.value!.publisher!.name, | ||||
|                                 }, | ||||
|                               ); | ||||
|                             }, | ||||
| @@ -257,7 +260,9 @@ class DeveloperHubScreen extends HookConsumerWidget { | ||||
|               error: err, | ||||
|               onRetry: () { | ||||
|                 ref.invalidate( | ||||
|                   developerStatsProvider(currentDeveloper.value?.name), | ||||
|                   developerStatsProvider( | ||||
|                     currentDeveloper.value?.publisher!.name, | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
| @@ -354,7 +359,7 @@ class _DeveloperEnrollmentSheet extends HookConsumerWidget { | ||||
|                     ? Center( | ||||
|                       child: | ||||
|                           Text( | ||||
|                             'noPublishersToEnroll', | ||||
|                             'noDevelopersToEnroll', | ||||
|                             textAlign: TextAlign.center, | ||||
|                           ).tr(), | ||||
|                     ) | ||||
|   | ||||
| @@ -149,12 +149,12 @@ class _DeveloperStatsProviderElement | ||||
|   String? get uname => (origin as DeveloperStatsProvider).uname; | ||||
| } | ||||
|  | ||||
| String _$developersHash() => r'04f25db31f511f651a5add128d56631236ed0b39'; | ||||
| String _$developersHash() => r'252341098617ac398ce133994453f318dd3edbd2'; | ||||
|  | ||||
| /// See also [developers]. | ||||
| @ProviderFor(developers) | ||||
| final developersProvider = | ||||
|     AutoDisposeFutureProvider<List<SnPublisher>>.internal( | ||||
|     AutoDisposeFutureProvider<List<SnDeveloper>>.internal( | ||||
|       developers, | ||||
|       name: r'developersProvider', | ||||
|       debugGetCreateSourceHash: | ||||
| @@ -167,6 +167,6 @@ final developersProvider = | ||||
|  | ||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||
| // ignore: unused_element | ||||
| typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnPublisher>>; | ||||
| typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnDeveloper>>; | ||||
| // ignore_for_file: type=lint | ||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import 'package:gap/gap.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/models/poll.dart'; | ||||
| import 'package:island/widgets/app_scaffold.dart'; | ||||
| import 'package:uuid/uuid.dart'; | ||||
|  | ||||
| class PollEditorState { | ||||
| @@ -413,7 +414,7 @@ class PollEditorScreen extends ConsumerWidget { | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     return Scaffold( | ||||
|     return AppScaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text(model.id == null ? 'Create Poll' : 'Edit Poll'), | ||||
|         actions: [ | ||||
| @@ -428,175 +429,175 @@ class PollEditorScreen extends ConsumerWidget { | ||||
|           const Gap(8), | ||||
|         ], | ||||
|       ), | ||||
|       body: SafeArea( | ||||
|         child: Form( | ||||
|           key: ValueKey(model.id), | ||||
|           child: ListView( | ||||
|             padding: const EdgeInsets.all(16), | ||||
|             children: [ | ||||
|               TextFormField( | ||||
|                 initialValue: model.title ?? '', | ||||
|                 decoration: const InputDecoration( | ||||
|                   labelText: 'Title', | ||||
|                   border: OutlineInputBorder( | ||||
|                     borderRadius: BorderRadius.all(Radius.circular(16)), | ||||
|                   ), | ||||
|                 ), | ||||
|                 textInputAction: TextInputAction.next, | ||||
|                 maxLength: 256, | ||||
|                 onChanged: notifier.setTitle, | ||||
|                 onTapOutside: | ||||
|                     (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 validator: (v) { | ||||
|                   if (v == null || v.trim().isEmpty) { | ||||
|                     return 'Title is required'; | ||||
|                   } | ||||
|                   return null; | ||||
|                 }, | ||||
|               ), | ||||
|               const Gap(12), | ||||
|               TextFormField( | ||||
|                 initialValue: model.description ?? '', | ||||
|                 decoration: const InputDecoration( | ||||
|                   labelText: 'Description', | ||||
|                   alignLabelWithHint: true, | ||||
|                   border: OutlineInputBorder( | ||||
|                     borderRadius: BorderRadius.all(Radius.circular(16)), | ||||
|                   ), | ||||
|                 ), | ||||
|                 maxLines: 3, | ||||
|                 maxLength: 4096, | ||||
|                 onChanged: notifier.setDescription, | ||||
|                 onTapOutside: | ||||
|                     (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|               ), | ||||
|               const Gap(12), | ||||
|               _EndDatePicker( | ||||
|                 value: model.endedAt, | ||||
|                 onChanged: notifier.setEndedAt, | ||||
|               ), | ||||
|               const Gap(24), | ||||
|               Row( | ||||
|       body: Column( | ||||
|         children: [ | ||||
|           Expanded( | ||||
|             child: Form( | ||||
|               key: ValueKey(model.id), | ||||
|               child: ListView( | ||||
|                 padding: const EdgeInsets.all(16), | ||||
|                 children: [ | ||||
|                   Text( | ||||
|                     'Questions', | ||||
|                     style: Theme.of(context).textTheme.titleLarge, | ||||
|                   ), | ||||
|                   const Spacer(), | ||||
|                   MenuAnchor( | ||||
|                     builder: (context, controller, child) { | ||||
|                       return FilledButton.icon( | ||||
|                         onPressed: () { | ||||
|                           controller.isOpen | ||||
|                               ? controller.close() | ||||
|                               : controller.open(); | ||||
|                         }, | ||||
|                         icon: const Icon(Icons.add), | ||||
|                         label: const Text('Add question'), | ||||
|                       ); | ||||
|                   TextFormField( | ||||
|                     initialValue: model.title ?? '', | ||||
|                     decoration: const InputDecoration( | ||||
|                       labelText: 'Title', | ||||
|                       border: OutlineInputBorder( | ||||
|                         borderRadius: BorderRadius.all(Radius.circular(16)), | ||||
|                       ), | ||||
|                     ), | ||||
|                     textInputAction: TextInputAction.next, | ||||
|                     maxLength: 256, | ||||
|                     onChanged: notifier.setTitle, | ||||
|                     onTapOutside: | ||||
|                         (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                     validator: (v) { | ||||
|                       if (v == null || v.trim().isEmpty) { | ||||
|                         return 'Title is required'; | ||||
|                       } | ||||
|                       return null; | ||||
|                     }, | ||||
|                     menuChildren: | ||||
|                         SnPollQuestionType.values | ||||
|                             .map( | ||||
|                               (t) => MenuItemButton( | ||||
|                                 leadingIcon: Icon(_iconForType(t)), | ||||
|                                 onPressed: () => notifier.addQuestion(t), | ||||
|                                 child: Text(_labelForType(t)), | ||||
|                               ), | ||||
|                             ) | ||||
|                             .toList(), | ||||
|                   ), | ||||
|                   const Gap(12), | ||||
|                   TextFormField( | ||||
|                     initialValue: model.description ?? '', | ||||
|                     decoration: const InputDecoration( | ||||
|                       labelText: 'Description', | ||||
|                       alignLabelWithHint: true, | ||||
|                       border: OutlineInputBorder( | ||||
|                         borderRadius: BorderRadius.all(Radius.circular(16)), | ||||
|                       ), | ||||
|                     ), | ||||
|                     maxLines: 3, | ||||
|                     maxLength: 4096, | ||||
|                     onChanged: notifier.setDescription, | ||||
|                     onTapOutside: | ||||
|                         (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                   ), | ||||
|                   const Gap(12), | ||||
|                   _EndDatePicker( | ||||
|                     value: model.endedAt, | ||||
|                     onChanged: notifier.setEndedAt, | ||||
|                   ), | ||||
|                   const Gap(24), | ||||
|                   Row( | ||||
|                     children: [ | ||||
|                       Text( | ||||
|                         'Questions', | ||||
|                         style: Theme.of(context).textTheme.titleLarge, | ||||
|                       ), | ||||
|                       const Spacer(), | ||||
|                       MenuAnchor( | ||||
|                         builder: (context, controller, child) { | ||||
|                           return FilledButton.icon( | ||||
|                             onPressed: () { | ||||
|                               controller.isOpen | ||||
|                                   ? controller.close() | ||||
|                                   : controller.open(); | ||||
|                             }, | ||||
|                             icon: const Icon(Icons.add), | ||||
|                             label: const Text('Add question'), | ||||
|                           ); | ||||
|                         }, | ||||
|                         menuChildren: | ||||
|                             SnPollQuestionType.values | ||||
|                                 .map( | ||||
|                                   (t) => MenuItemButton( | ||||
|                                     leadingIcon: Icon(_iconForType(t)), | ||||
|                                     onPressed: () => notifier.addQuestion(t), | ||||
|                                     child: Text(_labelForType(t)), | ||||
|                                   ), | ||||
|                                 ) | ||||
|                                 .toList(), | ||||
|                       ), | ||||
|                     ], | ||||
|                   ), | ||||
|                   const Gap(8), | ||||
|                   if (model.questions.isEmpty) | ||||
|                     _EmptyState( | ||||
|                       title: 'No questions yet', | ||||
|                       subtitle: | ||||
|                           'Use "Add question" to start building your poll.', | ||||
|                     ) | ||||
|                   else | ||||
|                     ReorderableListView.builder( | ||||
|                       shrinkWrap: true, | ||||
|                       physics: const NeverScrollableScrollPhysics(), | ||||
|                       itemCount: model.questions.length, | ||||
|                       onReorder: (oldIndex, newIndex) { | ||||
|                         // Convert to stepwise moves using provided functions | ||||
|                         if (newIndex > oldIndex) newIndex -= 1; | ||||
|                         final steps = newIndex - oldIndex; | ||||
|                         if (steps == 0) return; | ||||
|                         if (steps > 0) { | ||||
|                           for (int i = 0; i < steps; i++) { | ||||
|                             notifier.moveQuestionDown(oldIndex + i); | ||||
|                           } | ||||
|                         } else { | ||||
|                           for (int i = 0; i > steps; i--) { | ||||
|                             notifier.moveQuestionUp(oldIndex + i); | ||||
|                           } | ||||
|                         } | ||||
|                       }, | ||||
|                       buildDefaultDragHandles: false, | ||||
|                       itemBuilder: (context, index) { | ||||
|                         final q = model.questions[index]; | ||||
|                         return Card( | ||||
|                           key: ValueKey('q_$index'), | ||||
|                           margin: const EdgeInsets.symmetric(vertical: 8), | ||||
|                           clipBehavior: Clip.antiAlias, | ||||
|                           child: Column( | ||||
|                             children: [ | ||||
|                               _QuestionHeader( | ||||
|                                 index: index, | ||||
|                                 question: q, | ||||
|                                 onMoveUp: | ||||
|                                     index > 0 | ||||
|                                         ? () => notifier.moveQuestionUp(index) | ||||
|                                         : null, | ||||
|                                 onMoveDown: | ||||
|                                     index < model.questions.length - 1 | ||||
|                                         ? () => notifier.moveQuestionDown(index) | ||||
|                                         : null, | ||||
|                                 onDelete: () => notifier.removeQuestion(index), | ||||
|                               ), | ||||
|                               const Divider(height: 1), | ||||
|                               Padding( | ||||
|                                 padding: const EdgeInsets.all(16), | ||||
|                                 child: _QuestionEditor( | ||||
|                                   index: index, | ||||
|                                   question: q, | ||||
|                                 ), | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ); | ||||
|                       }, | ||||
|                     ), | ||||
|                   const Gap(96), | ||||
|                 ], | ||||
|               ), | ||||
|               const Gap(8), | ||||
|               if (model.questions.isEmpty) | ||||
|                 _EmptyState( | ||||
|                   title: 'No questions yet', | ||||
|                   subtitle: 'Use "Add question" to start building your poll.', | ||||
|                 ) | ||||
|               else | ||||
|                 ReorderableListView.builder( | ||||
|                   shrinkWrap: true, | ||||
|                   physics: const NeverScrollableScrollPhysics(), | ||||
|                   itemCount: model.questions.length, | ||||
|                   onReorder: (oldIndex, newIndex) { | ||||
|                     // Convert to stepwise moves using provided functions | ||||
|                     if (newIndex > oldIndex) newIndex -= 1; | ||||
|                     final steps = newIndex - oldIndex; | ||||
|                     if (steps == 0) return; | ||||
|                     if (steps > 0) { | ||||
|                       for (int i = 0; i < steps; i++) { | ||||
|                         notifier.moveQuestionDown(oldIndex + i); | ||||
|                       } | ||||
|                     } else { | ||||
|                       for (int i = 0; i > steps; i--) { | ||||
|                         notifier.moveQuestionUp(oldIndex + i); | ||||
|                       } | ||||
|                     } | ||||
|                   }, | ||||
|                   buildDefaultDragHandles: false, | ||||
|                   itemBuilder: (context, index) { | ||||
|                     final q = model.questions[index]; | ||||
|                     return Card( | ||||
|                       key: ValueKey('q_$index'), | ||||
|                       margin: const EdgeInsets.symmetric(vertical: 8), | ||||
|                       clipBehavior: Clip.antiAlias, | ||||
|                       child: Column( | ||||
|                         children: [ | ||||
|                           _QuestionHeader( | ||||
|                             index: index, | ||||
|                             question: q, | ||||
|                             onMoveUp: | ||||
|                                 index > 0 | ||||
|                                     ? () => notifier.moveQuestionUp(index) | ||||
|                                     : null, | ||||
|                             onMoveDown: | ||||
|                                 index < model.questions.length - 1 | ||||
|                                     ? () => notifier.moveQuestionDown(index) | ||||
|                                     : null, | ||||
|                             onDelete: () => notifier.removeQuestion(index), | ||||
|                           ), | ||||
|                           const Divider(height: 1), | ||||
|                           Padding( | ||||
|                             padding: const EdgeInsets.all(16), | ||||
|                             child: _QuestionEditor(index: index, question: q), | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ); | ||||
|                   }, | ||||
|                 ), | ||||
|               const Gap(96), | ||||
|             ), | ||||
|           ), | ||||
|           Row( | ||||
|             children: [ | ||||
|               OutlinedButton.icon( | ||||
|                 onPressed: () { | ||||
|                   Navigator.of(context).maybePop(); | ||||
|                 }, | ||||
|                 icon: const Icon(Icons.close), | ||||
|                 label: const Text('Cancel'), | ||||
|               ), | ||||
|               const Spacer(), | ||||
|               FilledButton.icon( | ||||
|                 onPressed: () { | ||||
|                   _submitPoll(context, ref); | ||||
|                 }, | ||||
|                 icon: const Icon(Icons.cloud_upload_outlined), | ||||
|                 label: Text(model.id == null ? 'Create' : 'Update'), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|       bottomNavigationBar: Padding( | ||||
|         padding: EdgeInsets.fromLTRB( | ||||
|           16, | ||||
|           8, | ||||
|           16, | ||||
|           16 + MediaQuery.of(context).padding.bottom, | ||||
|         ), | ||||
|         child: Row( | ||||
|           children: [ | ||||
|             OutlinedButton.icon( | ||||
|               onPressed: () { | ||||
|                 Navigator.of(context).maybePop(); | ||||
|               }, | ||||
|               icon: const Icon(Icons.close), | ||||
|               label: const Text('Cancel'), | ||||
|             ), | ||||
|             const Spacer(), | ||||
|             FilledButton.icon( | ||||
|               onPressed: () { | ||||
|                 _submitPoll(context, ref); | ||||
|               }, | ||||
|               icon: const Icon(Icons.cloud_upload_outlined), | ||||
|               label: Text(model.id == null ? 'Create' : 'Update'), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -92,6 +92,7 @@ class PostDetailScreen extends HookConsumerWidget { | ||||
|                   right: 0, | ||||
|                   child: Material( | ||||
|                     elevation: 2, | ||||
|                     color: Theme.of(context).colorScheme.surfaceContainer, | ||||
|                     child: postState | ||||
|                         .when( | ||||
|                           data: | ||||
| @@ -107,8 +108,8 @@ class PostDetailScreen extends HookConsumerWidget { | ||||
|                           error: (_, _) => const SizedBox.shrink(), | ||||
|                         ) | ||||
|                         .padding( | ||||
|                           bottom: MediaQuery.of(context).padding.bottom + 16, | ||||
|                           top: 16, | ||||
|                           bottom: MediaQuery.of(context).padding.bottom + 8, | ||||
|                           top: 8, | ||||
|                           horizontal: 16, | ||||
|                         ), | ||||
|                   ), | ||||
|   | ||||
| @@ -488,6 +488,7 @@ class _RealmMemberListSheet extends HookConsumerWidget { | ||||
|     Future<void> invitePerson() async { | ||||
|       final result = await showModalBottomSheet( | ||||
|         isScrollControlled: true, | ||||
|         useRootNavigator: true, | ||||
|         context: context, | ||||
|         builder: (context) => const AccountPickerSheet(), | ||||
|       ); | ||||
|   | ||||
| @@ -67,6 +67,9 @@ Future<void> subscribePushNotification( | ||||
|   Dio apiClient, { | ||||
|   bool detailedErrors = false, | ||||
| }) async { | ||||
|   if (Platform.isLinux){ | ||||
|     return; | ||||
|   } | ||||
|   await FirebaseMessaging.instance.requestPermission( | ||||
|     alert: true, | ||||
|     badge: true, | ||||
|   | ||||
| @@ -1,19 +1,28 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:developer'; | ||||
| import 'dart:io'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_app_update/azhon_app_update.dart'; | ||||
| import 'package:flutter_app_update/update_model.dart'; | ||||
| import 'package:island/widgets/content/markdown.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:package_info_plus/package_info_plus.dart'; | ||||
| import 'package:collection/collection.dart'; // Added for firstWhereOrNull | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:url_launcher/url_launcher.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
|  | ||||
| /// Data model for a GitHub release we care about | ||||
| class GithubReleaseInfo { | ||||
|   final String tagName; // e.g. 3.1.0+118 | ||||
|   final String name; // release title | ||||
|   final String body; // changelog markdown | ||||
|   final String htmlUrl; // release page | ||||
|   final String tagName; | ||||
|   final String name; | ||||
|   final String body; | ||||
|   final String htmlUrl; | ||||
|   final DateTime createdAt; | ||||
|   final List<GithubReleaseAsset> assets; | ||||
|  | ||||
|   const GithubReleaseInfo({ | ||||
|     required this.tagName, | ||||
| @@ -21,9 +30,28 @@ class GithubReleaseInfo { | ||||
|     required this.body, | ||||
|     required this.htmlUrl, | ||||
|     required this.createdAt, | ||||
|     this.assets = const [], | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /// Data model for a GitHub release asset | ||||
| class GithubReleaseAsset { | ||||
|   final String name; | ||||
|   final String browserDownloadUrl; | ||||
|  | ||||
|   const GithubReleaseAsset({ | ||||
|     required this.name, | ||||
|     required this.browserDownloadUrl, | ||||
|   }); | ||||
|  | ||||
|   factory GithubReleaseAsset.fromJson(Map<String, dynamic> json) { | ||||
|     return GithubReleaseAsset( | ||||
|       name: json['name'] as String, | ||||
|       browserDownloadUrl: json['browser_download_url'] as String, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Parses version and build number from "x.y.z+build" | ||||
| class _ParsedVersion implements Comparable<_ParsedVersion> { | ||||
|   final int major; | ||||
| @@ -62,7 +90,7 @@ class _ParsedVersion implements Comparable<_ParsedVersion> { | ||||
| } | ||||
|  | ||||
| class UpdateService { | ||||
|   UpdateService({Dio? dio}) | ||||
|   UpdateService({Dio? dio, this.useProxy = false}) | ||||
|     : _dio = | ||||
|           dio ?? | ||||
|           Dio( | ||||
| @@ -78,6 +106,9 @@ class UpdateService { | ||||
|           ); | ||||
|  | ||||
|   final Dio _dio; | ||||
|   final bool useProxy; | ||||
|  | ||||
|   static const _proxyBaseUrl = 'https://ghfast.top/'; | ||||
|  | ||||
|   static const _releasesLatestApi = | ||||
|       'https://api.github.com/repos/solsynth/solian/releases/latest'; | ||||
| @@ -85,31 +116,52 @@ class UpdateService { | ||||
|   /// Checks GitHub for the latest release and compares against the current app version. | ||||
|   /// If update is available, shows a bottom sheet with changelog and an action to open release page. | ||||
|   Future<void> checkForUpdates(BuildContext context) async { | ||||
|     log('[Update] Checking for updates...'); | ||||
|     try { | ||||
|       final release = await fetchLatestRelease(); | ||||
|       if (release == null) return; | ||||
|       if (release == null) { | ||||
|         log('[Update] No latest release found or could not fetch.'); | ||||
|         return; | ||||
|       } | ||||
|       log('[Update] Fetched latest release: ${release.tagName}'); | ||||
|  | ||||
|       final info = await PackageInfo.fromPlatform(); | ||||
|       final localVersionStr = '${info.version}+${info.buildNumber}'; | ||||
|       log('[Update] Local app version: $localVersionStr'); | ||||
|  | ||||
|       final latest = _ParsedVersion.tryParse(release.tagName); | ||||
|       final local = _ParsedVersion.tryParse(localVersionStr); | ||||
|  | ||||
|       if (latest == null || local == null) { | ||||
|         log( | ||||
|           '[Update] Failed to parse versions. Latest: ${release.tagName}, Local: $localVersionStr', | ||||
|         ); | ||||
|         // If parsing fails, do nothing silently | ||||
|         return; | ||||
|       } | ||||
|       log('[Update] Parsed versions. Latest: $latest, Local: $local'); | ||||
|  | ||||
|       final needsUpdate = latest.compareTo(local) > 0; | ||||
|       if (!needsUpdate) return; | ||||
|       if (!needsUpdate) { | ||||
|         log('[Update] App is up to date. No update needed.'); | ||||
|         return; | ||||
|       } | ||||
|       log('[Update] Update available! Latest: $latest, Local: $local'); | ||||
|  | ||||
|       if (!context.mounted) return; | ||||
|       if (!context.mounted) { | ||||
|         log('[Update] Context not mounted, cannot show update sheet.'); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Delay to ensure UI is ready (if called at startup) | ||||
|       await Future.delayed(const Duration(milliseconds: 100)); | ||||
|  | ||||
|       await showUpdateSheet(context, release); | ||||
|     } catch (_) { | ||||
|       if (context.mounted) { | ||||
|         await showUpdateSheet(context, release); | ||||
|         log('[Update] Update sheet shown.'); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       log('[Update] Error checking for updates: $e'); | ||||
|       // Ignore errors (network, api, etc.) | ||||
|       return; | ||||
|     } | ||||
| @@ -126,25 +178,68 @@ class UpdateService { | ||||
|       context: context, | ||||
|       isScrollControlled: true, | ||||
|       useRootNavigator: true, | ||||
|       builder: | ||||
|           (ctx) => _UpdateSheet( | ||||
|             release: release, | ||||
|             onOpen: () async { | ||||
|               final uri = Uri.parse(release.htmlUrl); | ||||
|               if (await canLaunchUrl(uri)) { | ||||
|                 await launchUrl(uri, mode: LaunchMode.externalApplication); | ||||
|               } | ||||
|             }, | ||||
|           ), | ||||
|       builder: (ctx) { | ||||
|         String? androidUpdateUrl; | ||||
|         if (Platform.isAndroid) { | ||||
|           androidUpdateUrl = _getAndroidUpdateUrl(release.assets); | ||||
|         } | ||||
|         return _UpdateSheet( | ||||
|           release: release, | ||||
|           onOpen: () async { | ||||
|             final uri = Uri.parse(release.htmlUrl); | ||||
|             if (await canLaunchUrl(uri)) { | ||||
|               await launchUrl(uri, mode: LaunchMode.externalApplication); | ||||
|             } | ||||
|           }, | ||||
|           androidUpdateUrl: androidUpdateUrl, | ||||
|           useProxy: useProxy, // Pass the useProxy flag | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   String? _getAndroidUpdateUrl(List<GithubReleaseAsset> assets) { | ||||
|     final arm64 = assets.firstWhereOrNull( | ||||
|       (asset) => asset.name == 'app-arm64-v8a-release.apk', | ||||
|     ); | ||||
|     final armeabi = assets.firstWhereOrNull( | ||||
|       (asset) => asset.name == 'app-armeabi-v7a-release.apk', | ||||
|     ); | ||||
|     final x86_64 = assets.firstWhereOrNull( | ||||
|       (asset) => asset.name == 'app-x86_64-release.apk', | ||||
|     ); | ||||
|  | ||||
|     // Prioritize arm64, then armeabi, then x86_64 | ||||
|     if (arm64 != null) { | ||||
|       return arm64.browserDownloadUrl; | ||||
|     } else if (armeabi != null) { | ||||
|       return armeabi.browserDownloadUrl; | ||||
|     } else if (x86_64 != null) { | ||||
|       return x86_64.browserDownloadUrl; | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   /// Fetch the latest release info from GitHub. | ||||
|   /// Public so other screens (e.g., About) can manually trigger update checks. | ||||
|   Future<GithubReleaseInfo?> fetchLatestRelease() async { | ||||
|     final resp = await _dio.get(_releasesLatestApi); | ||||
|     if (resp.statusCode != 200) return null; | ||||
|     final apiEndpoint = | ||||
|         useProxy | ||||
|             ? '$_proxyBaseUrl${Uri.encodeComponent(_releasesLatestApi)}' | ||||
|             : _releasesLatestApi; | ||||
|  | ||||
|     log( | ||||
|       '[Update] Fetching latest release from GitHub API: $apiEndpoint (Proxy: $useProxy)', | ||||
|     ); | ||||
|     final resp = await _dio.get(apiEndpoint); | ||||
|     if (resp.statusCode != 200) { | ||||
|       log( | ||||
|         '[Update] Failed to fetch latest release. Status code: ${resp.statusCode}', | ||||
|       ); | ||||
|       return null; | ||||
|     } | ||||
|     final data = resp.data as Map<String, dynamic>; | ||||
|     log('[Update] Successfully fetched release data.'); | ||||
|  | ||||
|     final tagName = (data['tag_name'] ?? '').toString(); | ||||
|     final name = (data['name'] ?? tagName).toString(); | ||||
| @@ -152,25 +247,70 @@ class UpdateService { | ||||
|     final htmlUrl = (data['html_url'] ?? '').toString(); | ||||
|     final createdAtStr = (data['created_at'] ?? '').toString(); | ||||
|     final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now(); | ||||
|     final assetsData = | ||||
|         (data['assets'] as List<dynamic>?) | ||||
|             ?.map((e) => GithubReleaseAsset.fromJson(e as Map<String, dynamic>)) | ||||
|             .toList() ?? | ||||
|         []; | ||||
|  | ||||
|     if (tagName.isEmpty || htmlUrl.isEmpty) return null; | ||||
|     if (tagName.isEmpty || htmlUrl.isEmpty) { | ||||
|       log( | ||||
|         '[Update] Missing tag_name or html_url in release data. TagName: "$tagName", HtmlUrl: "$htmlUrl"', | ||||
|       ); | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     log('[Update] Returning GithubReleaseInfo for tag: $tagName'); | ||||
|     return GithubReleaseInfo( | ||||
|       tagName: tagName, | ||||
|       name: name, | ||||
|       body: body, | ||||
|       htmlUrl: htmlUrl, | ||||
|       createdAt: createdAt, | ||||
|       assets: assetsData, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _UpdateSheet extends StatelessWidget { | ||||
|   const _UpdateSheet({required this.release, required this.onOpen}); | ||||
| class _UpdateSheet extends StatefulWidget { | ||||
|   const _UpdateSheet({ | ||||
|     required this.release, | ||||
|     required this.onOpen, | ||||
|     this.androidUpdateUrl, | ||||
|     this.useProxy = false, | ||||
|   }); | ||||
|  | ||||
|   final String? androidUpdateUrl; | ||||
|   final bool useProxy; | ||||
|   final GithubReleaseInfo release; | ||||
|   final VoidCallback onOpen; | ||||
|  | ||||
|   @override | ||||
|   State<_UpdateSheet> createState() => _UpdateSheetState(); | ||||
| } | ||||
|  | ||||
| class _UpdateSheetState extends State<_UpdateSheet> { | ||||
|   late bool _useProxy; | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     _useProxy = widget.useProxy; | ||||
|   } | ||||
|  | ||||
|   Future<void> _installUpdate(String url) async { | ||||
|     final downloadUrl = | ||||
|         _useProxy ? 'https://ghfast.top/${Uri.encodeComponent(url)}' : url; | ||||
|  | ||||
|     UpdateModel model = UpdateModel( | ||||
|       downloadUrl, | ||||
|       "solian-update-${widget.release.tagName}.apk", | ||||
|       "launcher_icon", | ||||
|       'https://apps.apple.com/us/app/solian/id6499032345', | ||||
|     ); | ||||
|     AzhonAppUpdate.update(model); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final theme = Theme.of(context); | ||||
| @@ -186,8 +326,11 @@ class _UpdateSheet extends StatelessWidget { | ||||
|             Column( | ||||
|               crossAxisAlignment: CrossAxisAlignment.start, | ||||
|               children: [ | ||||
|                 Text(release.name, style: theme.textTheme.titleMedium).bold(), | ||||
|                 Text(release.tagName).fontSize(12), | ||||
|                 Text( | ||||
|                   widget.release.name, | ||||
|                   style: theme.textTheme.titleMedium, | ||||
|                 ).bold(), | ||||
|                 Text(widget.release.tagName).fontSize(12), | ||||
|               ], | ||||
|             ).padding(vertical: 16, horizontal: 16), | ||||
|             const Divider(height: 1), | ||||
| @@ -197,21 +340,45 @@ class _UpdateSheet extends StatelessWidget { | ||||
|                   horizontal: 16, | ||||
|                   vertical: 16, | ||||
|                 ), | ||||
|                 child: SelectableText( | ||||
|                   release.body.isEmpty | ||||
|                       ? 'No changelog provided.' | ||||
|                       : release.body, | ||||
|                   style: theme.textTheme.bodyMedium, | ||||
|                 child: MarkdownTextContent( | ||||
|                   content: | ||||
|                       widget.release.body.isEmpty | ||||
|                           ? 'No changelog provided.' | ||||
|                           : widget.release.body, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             if (!kIsWeb && Platform.isAndroid) | ||||
|               SwitchListTile( | ||||
|                 title: const Text('Use GitHub Proxy for Download'), | ||||
|                 value: _useProxy, | ||||
|                 onChanged: (value) { | ||||
|                   setState(() { | ||||
|                     _useProxy = value; | ||||
|                   }); | ||||
|                 }, | ||||
|               ).padding(horizontal: 8), | ||||
|             Column( | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   spacing: 8, | ||||
|                   children: [ | ||||
|                     if (!kIsWeb && | ||||
|                         Platform.isAndroid && | ||||
|                         widget.androidUpdateUrl != null) | ||||
|                       Expanded( | ||||
|                         child: FilledButton.icon( | ||||
|                           onPressed: () { | ||||
|                             log(widget.androidUpdateUrl!); | ||||
|                             _installUpdate(widget.androidUpdateUrl!); | ||||
|                           }, | ||||
|                           icon: const Icon(Symbols.update), | ||||
|                           label: const Text('Install update'), | ||||
|                         ), | ||||
|                       ), | ||||
|                     Expanded( | ||||
|                       child: FilledButton.icon( | ||||
|                         onPressed: onOpen, | ||||
|                         onPressed: widget.onOpen, | ||||
|                         icon: const Icon(Icons.open_in_new), | ||||
|                         label: const Text('Open release page'), | ||||
|                       ), | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| @@ -44,9 +45,8 @@ class AccountPickerSheet extends HookConsumerWidget { | ||||
|     } | ||||
|  | ||||
|     return Container( | ||||
|       constraints: BoxConstraints( | ||||
|         maxHeight: MediaQuery.of(context).size.height * 0.4, | ||||
|       ), | ||||
|       padding: MediaQuery.of(context).viewInsets, | ||||
|       height: MediaQuery.of(context).size.height * 0.6, | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           Padding( | ||||
| @@ -54,8 +54,8 @@ class AccountPickerSheet extends HookConsumerWidget { | ||||
|             child: TextField( | ||||
|               controller: searchController, | ||||
|               onChanged: onSearchChanged, | ||||
|               decoration: const InputDecoration( | ||||
|                 hintText: 'Search accounts...', | ||||
|               decoration: InputDecoration( | ||||
|                 hintText: 'searchAccounts'.tr(), | ||||
|                 contentPadding: EdgeInsets.symmetric( | ||||
|                   horizontal: 18, | ||||
|                   vertical: 16, | ||||
|   | ||||
| @@ -55,7 +55,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget { | ||||
|             'attitude': attitude.value, | ||||
|             'is_invisible': isInvisible.value, | ||||
|             'is_not_disturb': isNotDisturb.value, | ||||
|             'cleared_at': clearedAt.value?.toIso8601String(), | ||||
|             'cleared_at': clearedAt.value?.toUtc().toIso8601String(), | ||||
|             if (labelController.text.isNotEmpty) 'label': labelController.text, | ||||
|           }, | ||||
|           options: Options(method: initialStatus == null ? 'POST' : 'PATCH'), | ||||
|   | ||||
| @@ -331,7 +331,7 @@ class _WebSocketIndicator extends HookConsumerWidget { | ||||
|     final user = ref.watch(userInfoProvider); | ||||
|     final websocketState = ref.watch(websocketStateProvider); | ||||
|     final indicatorHeight = | ||||
|         MediaQuery.of(context).padding.top + (isDesktop ? 27.5 : 20); | ||||
|         MediaQuery.of(context).padding.top + (isDesktop ? 27.5 : 25); | ||||
|  | ||||
|     Color indicatorColor; | ||||
|     String indicatorText; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
| import 'package:island/services/notify.dart'; | ||||
| import 'package:island/services/sharing_intent.dart'; | ||||
| import 'package:island/services/update_service.dart'; | ||||
| import 'package:island/widgets/content/network_status_sheet.dart'; | ||||
| import 'package:island/widgets/tour/tour.dart'; | ||||
|  | ||||
| @@ -21,6 +22,7 @@ class AppWrapper extends HookConsumerWidget { | ||||
|       }); | ||||
|       final sharingService = SharingIntentService(); | ||||
|       sharingService.initialize(context); | ||||
|       UpdateService().checkForUpdates(context); | ||||
|       return () { | ||||
|         sharingService.dispose(); | ||||
|         ntySubs?.cancel(); | ||||
|   | ||||
| @@ -142,7 +142,7 @@ class CloudVideoWidget extends HookConsumerWidget { | ||||
|               mainAxisAlignment: MainAxisAlignment.end, | ||||
|               crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|               children: [ | ||||
|                 Row( | ||||
|                 Wrap( | ||||
|                   spacing: 8, | ||||
|                   children: [ | ||||
|                     if (item.fileMeta?['duration'] != null) | ||||
| @@ -199,8 +199,8 @@ class CloudVideoWidget extends HookConsumerWidget { | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ).padding(horizontal: 16, bottom: 12), | ||||
|             ).padding(horizontal: 16, bottom: 12), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|       onTap: () { | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_highlight/themes/a11y-dark.dart'; | ||||
| import 'package:flutter_highlight/themes/a11y-light.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:google_fonts/google_fonts.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/file.dart'; | ||||
| import 'package:island/pods/config.dart'; | ||||
| @@ -71,7 +72,22 @@ class MarkdownTextContent extends HookConsumerWidget { | ||||
|             textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!, | ||||
|           ), | ||||
|           HrConfig(height: 1, color: Theme.of(context).dividerColor), | ||||
|           PreConfig(theme: isDark ? a11yDarkTheme : a11yLightTheme), | ||||
|           PreConfig( | ||||
|             theme: isDark ? a11yDarkTheme : a11yLightTheme, | ||||
|             textStyle: GoogleFonts.robotoMono(fontSize: 14), | ||||
|             styleNotMatched: GoogleFonts.robotoMono(fontSize: 14), | ||||
|             decoration: BoxDecoration( | ||||
|               color: Theme.of(context).colorScheme.surfaceContainerHighest, | ||||
|               borderRadius: BorderRadius.all(Radius.circular(8.0)), | ||||
|             ), | ||||
|           ), | ||||
|           TableConfig( | ||||
|             wrapper: | ||||
|                 (child) => SingleChildScrollView( | ||||
|                   scrollDirection: Axis.horizontal, | ||||
|                   child: child, | ||||
|                 ), | ||||
|           ), | ||||
|           LinkConfig( | ||||
|             style: | ||||
|                 linkStyle ?? | ||||
| @@ -160,7 +176,7 @@ class MarkdownTextContent extends HookConsumerWidget { | ||||
|                           uri: stickerUri, | ||||
|                           width: size, | ||||
|                           height: size, | ||||
|                           fit: BoxFit.cover, | ||||
|                           fit: BoxFit.contain, | ||||
|                           noCacheOptimization: true, | ||||
|                         ), | ||||
|                       ), | ||||
|   | ||||
							
								
								
									
										76
									
								
								lib/widgets/debug_sheet.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								lib/widgets/debug_sheet.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
| import 'package:flutter_cache_manager/flutter_cache_manager.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/pods/message.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/pods/websocket.dart'; | ||||
| import 'package:island/widgets/content/network_status_sheet.dart'; | ||||
| import 'package:island/widgets/content/sheet.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
|  | ||||
| class DebugSheet extends HookConsumerWidget { | ||||
|   const DebugSheet({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
|     final wsNotifier = ref.watch(websocketStateProvider.notifier); | ||||
|  | ||||
|     return SheetScaffold( | ||||
|       titleText: 'Debug', | ||||
|       child: Column( | ||||
|         children: [ | ||||
|           ListTile( | ||||
|             minTileHeight: 48, | ||||
|             leading: const Icon(Symbols.wifi), | ||||
|             trailing: const Icon(Symbols.chevron_right), | ||||
|             title: Text('Connection Status'), | ||||
|             contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|             onTap: () { | ||||
|               showModalBottomSheet( | ||||
|                 context: context, | ||||
|                 isScrollControlled: true, | ||||
|                 builder: | ||||
|                     (context) => NetworkStatusSheet( | ||||
|                       onReconnect: () => wsNotifier.connect(), | ||||
|                     ), | ||||
|               ); | ||||
|             }, | ||||
|           ), | ||||
|           const Divider(height: 1), | ||||
|           ListTile( | ||||
|             minTileHeight: 48, | ||||
|             leading: const Icon(Symbols.copy_all), | ||||
|             trailing: const Icon(Symbols.chevron_right), | ||||
|             contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|             title: Text('Copy access token'), | ||||
|             onTap: () async { | ||||
|               final tk = ref.watch(tokenProvider); | ||||
|               Clipboard.setData(ClipboardData(text: tk!.token)); | ||||
|             }, | ||||
|           ), | ||||
|           ListTile( | ||||
|             minTileHeight: 48, | ||||
|             leading: const Icon(Symbols.delete), | ||||
|             trailing: const Icon(Symbols.chevron_right), | ||||
|             contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|             title: Text('Reset database'), | ||||
|             onTap: () async { | ||||
|               resetDatabase(ref); | ||||
|             }, | ||||
|           ), | ||||
|           ListTile( | ||||
|             minTileHeight: 48, | ||||
|             leading: const Icon(Symbols.clear), | ||||
|             trailing: const Icon(Symbols.chevron_right), | ||||
|             contentPadding: EdgeInsets.symmetric(horizontal: 24), | ||||
|             title: Text('Clear cache'), | ||||
|             onTap: () async { | ||||
|               DefaultCacheManager().emptyCache(); | ||||
|             }, | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										86
									
								
								lib/widgets/keyboard_navigation.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								lib/widgets/keyboard_navigation.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter/services.dart'; | ||||
|  | ||||
| enum VimMode { normal, insert } | ||||
|  | ||||
| class KeyboardNavigation extends StatefulWidget { | ||||
|   const KeyboardNavigation({super.key, required this.child}); | ||||
|  | ||||
|   final Widget child; | ||||
|  | ||||
|   @override | ||||
|   State<KeyboardNavigation> createState() => _KeyboardNavigationState(); | ||||
| } | ||||
|  | ||||
| class _KeyboardNavigationState extends State<KeyboardNavigation> { | ||||
|   VimMode _mode = VimMode.normal; | ||||
|   final FocusScopeNode _focusScopeNode = FocusScopeNode(); | ||||
|  | ||||
|   @override | ||||
|   void dispose() { | ||||
|     _focusScopeNode.dispose(); | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { | ||||
|     if (event is! KeyDownEvent && event is! KeyRepeatEvent) { | ||||
|       return KeyEventResult.ignored; | ||||
|     } | ||||
|  | ||||
|     if (_mode == VimMode.normal) { | ||||
|       if (event.logicalKey == LogicalKeyboardKey.keyJ) { | ||||
|         node.focusInDirection(TraversalDirection.down); | ||||
|         return KeyEventResult.handled; | ||||
|       } else if (event.logicalKey == LogicalKeyboardKey.keyK) { | ||||
|         node.focusInDirection(TraversalDirection.up); | ||||
|         return KeyEventResult.handled; | ||||
|       } else if (event.logicalKey == LogicalKeyboardKey.keyH) { | ||||
|         final focusNode = FocusManager.instance.primaryFocus; | ||||
|         if (focusNode != null) { | ||||
|           final scrollable = Scrollable.of(focusNode.context!); | ||||
|           if (scrollable.position.axis == Axis.horizontal) { | ||||
|             scrollable.position.moveTo(scrollable.position.pixels - 50); | ||||
|             return KeyEventResult.handled; | ||||
|           } | ||||
|         } | ||||
|         node.focusInDirection(TraversalDirection.left); | ||||
|         return KeyEventResult.handled; | ||||
|       } else if (event.logicalKey == LogicalKeyboardKey.keyL) { | ||||
|         final focusNode = FocusManager.instance.primaryFocus; | ||||
|         if (focusNode != null) { | ||||
|           final scrollable = Scrollable.of(focusNode.context!); | ||||
|           if (scrollable.position.axis == Axis.horizontal) { | ||||
|             scrollable.position.moveTo(scrollable.position.pixels + 50); | ||||
|             return KeyEventResult.handled; | ||||
|           } | ||||
|         } | ||||
|         node.focusInDirection(TraversalDirection.right); | ||||
|         return KeyEventResult.handled; | ||||
|       } else if (event.logicalKey == LogicalKeyboardKey.keyI) { | ||||
|         setState(() { | ||||
|           _mode = VimMode.insert; | ||||
|         }); | ||||
|         return KeyEventResult.handled; | ||||
|       } | ||||
|     } else if (_mode == VimMode.insert) { | ||||
|       if (event.logicalKey == LogicalKeyboardKey.escape) { | ||||
|         setState(() { | ||||
|           _mode = VimMode.normal; | ||||
|         }); | ||||
|         // Unfocus the current widget to prevent typing | ||||
|         node.unfocus(); | ||||
|         return KeyEventResult.handled; | ||||
|       } | ||||
|     } | ||||
|     return KeyEventResult.ignored; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Focus( | ||||
|       focusNode: _focusScopeNode, | ||||
|       onKeyEvent: _handleKeyEvent, | ||||
|       child: widget.child, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -248,7 +248,7 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> { | ||||
|     try { | ||||
|       final client = ref.read(apiClientProvider); | ||||
|       final response = await client.post( | ||||
|         '/orders/${widget.order.id}/pay', | ||||
|         '/id/orders/${widget.order.id}/pay', | ||||
|         data: {'pin_code': pin}, | ||||
|       ); | ||||
|  | ||||
|   | ||||
| @@ -273,7 +273,7 @@ class PostItem extends HookConsumerWidget { | ||||
|             : item.reactionsCount.entries | ||||
|                 .sortedBy((e) => e.value) | ||||
|                 .map((e) => e.key) | ||||
|                 .first; | ||||
|                 .last; | ||||
|  | ||||
|     final postLanguage = | ||||
|         item.content != null | ||||
| @@ -480,7 +480,9 @@ class PostItem extends HookConsumerWidget { | ||||
|               ], | ||||
|             ), | ||||
|           ) | ||||
|         else if (item.content?.isNotEmpty ?? false) | ||||
|         else if ((item.content?.isNotEmpty ?? false) || | ||||
|             (item.title?.isNotEmpty ?? false) || | ||||
|             (item.description?.isNotEmpty ?? false)) | ||||
|           Padding( | ||||
|             padding: EdgeInsets.only( | ||||
|               left: renderingPadding.horizontal, | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:island/models/post.dart'; | ||||
| import 'package:island/models/publisher.dart'; | ||||
| import 'package:island/pods/network.dart'; | ||||
| import 'package:island/screens/creators/publishers.dart'; | ||||
| import 'package:island/screens/posts/compose.dart'; | ||||
| import 'package:island/widgets/alert.dart'; | ||||
| import 'package:island/widgets/content/cloud_files.dart'; | ||||
| import 'package:island/widgets/post/publishers_modal.dart'; | ||||
| @@ -14,8 +16,14 @@ import 'package:styled_widget/styled_widget.dart'; | ||||
|  | ||||
| class PostQuickReply extends HookConsumerWidget { | ||||
|   final SnPost parent; | ||||
|   final Function? onPosted; | ||||
|   const PostQuickReply({super.key, required this.parent, this.onPosted}); | ||||
|   final VoidCallback? onPosted; | ||||
|   final VoidCallback? onLaunch; | ||||
|   const PostQuickReply({ | ||||
|     super.key, | ||||
|     required this.parent, | ||||
|     this.onPosted, | ||||
|     this.onLaunch, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context, WidgetRef ref) { | ||||
| @@ -48,7 +56,7 @@ class PostQuickReply extends HookConsumerWidget { | ||||
|             'content': contentController.text, | ||||
|             'replied_post_id': parent.id, | ||||
|           }, | ||||
|           options: Options(headers: {'X-Pub': currentPublisher.value?.name}), | ||||
|           queryParameters: {'pub': currentPublisher.value?.name}, | ||||
|         ); | ||||
|         contentController.clear(); | ||||
|         onPosted?.call(); | ||||
| @@ -83,9 +91,10 @@ class PostQuickReply extends HookConsumerWidget { | ||||
|                 child: TextField( | ||||
|                   controller: contentController, | ||||
|                   decoration: InputDecoration( | ||||
|                     hintText: 'Post your reply', | ||||
|                     border: const OutlineInputBorder(), | ||||
|                     hintText: 'postReplyPlaceholder'.tr(), | ||||
|                     border: InputBorder.none, | ||||
|                     isDense: true, | ||||
|                     isCollapsed: true, | ||||
|                     contentPadding: EdgeInsets.symmetric( | ||||
|                       horizontal: 12, | ||||
|                       vertical: 8, | ||||
| @@ -97,6 +106,26 @@ class PostQuickReply extends HookConsumerWidget { | ||||
|                       (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|                 ), | ||||
|               ), | ||||
|               IconButton( | ||||
|                 onPressed: () { | ||||
|                   onLaunch?.call(); | ||||
|                   GoRouter.of(context) | ||||
|                       .pushNamed( | ||||
|                         'postCompose', | ||||
|                         extra: PostComposeInitialState( | ||||
|                           content: contentController.text, | ||||
|                           replyingTo: parent, | ||||
|                         ), | ||||
|                       ) | ||||
|                       .then((value) { | ||||
|                         if (value != null) onPosted?.call(); | ||||
|                       }); | ||||
|                 }, | ||||
|                 icon: const Icon(Symbols.launch, size: 20), | ||||
|                 padding: EdgeInsets.zero, | ||||
|                 visualDensity: VisualDensity.compact, | ||||
|                 constraints: const BoxConstraints(), | ||||
|               ), | ||||
|               IconButton( | ||||
|                 padding: EdgeInsets.zero, | ||||
|                 visualDensity: VisualDensity.compact, | ||||
| @@ -110,6 +139,7 @@ class PostQuickReply extends HookConsumerWidget { | ||||
|                         : Icon(Symbols.send, size: 20), | ||||
|                 color: Theme.of(context).colorScheme.primary, | ||||
|                 onPressed: submitting.value ? null : performAction, | ||||
|                 constraints: const BoxConstraints(), | ||||
|               ), | ||||
|             ], | ||||
|           ), | ||||
|   | ||||
| @@ -38,14 +38,18 @@ class PostRepliesSheet extends HookConsumerWidget { | ||||
|           if (user.value != null) | ||||
|             Material( | ||||
|               elevation: 2, | ||||
|               color: Theme.of(context).colorScheme.surfaceContainerHigh, | ||||
|               child: PostQuickReply( | ||||
|                 parent: post, | ||||
|                 onPosted: () { | ||||
|                   ref.invalidate(postRepliesNotifierProvider(post.id)); | ||||
|                 }, | ||||
|                 onLaunch: () { | ||||
|                   Navigator.of(context).pop(); | ||||
|                 }, | ||||
|               ).padding( | ||||
|                 bottom: MediaQuery.of(context).padding.bottom + 16, | ||||
|                 top: 16, | ||||
|                 bottom: MediaQuery.of(context).padding.bottom + 8, | ||||
|                 top: 8, | ||||
|                 horizontal: 16, | ||||
|               ), | ||||
|             ), | ||||
|   | ||||
| @@ -130,25 +130,25 @@ PODS: | ||||
|   - sqflite_darwin (0.0.4): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|   - sqlite3 (3.50.3): | ||||
|     - sqlite3/common (= 3.50.3) | ||||
|   - sqlite3/common (3.50.3) | ||||
|   - sqlite3/dbstatvtab (3.50.3): | ||||
|   - sqlite3 (3.50.4): | ||||
|     - sqlite3/common (= 3.50.4) | ||||
|   - sqlite3/common (3.50.4) | ||||
|   - sqlite3/dbstatvtab (3.50.4): | ||||
|     - sqlite3/common | ||||
|   - sqlite3/fts5 (3.50.3): | ||||
|   - sqlite3/fts5 (3.50.4): | ||||
|     - sqlite3/common | ||||
|   - sqlite3/math (3.50.3): | ||||
|   - sqlite3/math (3.50.4): | ||||
|     - sqlite3/common | ||||
|   - sqlite3/perf-threadsafe (3.50.3): | ||||
|   - sqlite3/perf-threadsafe (3.50.4): | ||||
|     - sqlite3/common | ||||
|   - sqlite3/rtree (3.50.3): | ||||
|   - sqlite3/rtree (3.50.4): | ||||
|     - sqlite3/common | ||||
|   - sqlite3/session (3.50.3): | ||||
|   - sqlite3/session (3.50.4): | ||||
|     - sqlite3/common | ||||
|   - sqlite3_flutter_libs (0.0.1): | ||||
|     - Flutter | ||||
|     - FlutterMacOS | ||||
|     - sqlite3 (~> 3.50.3) | ||||
|     - sqlite3 (~> 3.50.4) | ||||
|     - sqlite3/dbstatvtab | ||||
|     - sqlite3/fts5 | ||||
|     - sqlite3/math | ||||
| @@ -328,8 +328,8 @@ SPEC CHECKSUMS: | ||||
|   shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 | ||||
|   sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e | ||||
|   sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 | ||||
|   sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81 | ||||
|   sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e | ||||
|   sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b | ||||
|   sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 | ||||
|   super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 | ||||
|   url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 | ||||
|   volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd | ||||
|   | ||||
							
								
								
									
										80
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										80
									
								
								pubspec.lock
									
									
									
									
									
								
							| @@ -73,22 +73,6 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.13.0" | ||||
|   auto_route: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: auto_route | ||||
|       sha256: b8c036fa613a98a759cf0fdcba26e62f4985dcbff01a5e760ab411e8554bbaf0 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "10.1.0+1" | ||||
|   auto_route_generator: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: auto_route_generator | ||||
|       sha256: "9e3846fcbeacba5c362557328dd8c8fbc953b6a0cbc3395365e8d8f92eea29c4" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "10.1.0" | ||||
|   avatar_stack: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -205,10 +189,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: built_value | ||||
|       sha256: "0b1b12a0a549605e5f04476031cd0bc91ead1d7c8e830773a18ee54179b3cb62" | ||||
|       sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "8.11.0" | ||||
|     version: "8.11.1" | ||||
|   cached_network_image: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -453,10 +437,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: dio | ||||
|       sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" | ||||
|       sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "5.8.0+1" | ||||
|     version: "5.9.0" | ||||
|   dio_web_adapter: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -573,10 +557,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: file_picker | ||||
|       sha256: "13ba4e627ef24503a465d1d61b32596ce10eb6b8903678d362a528f9939b4aa8" | ||||
|       sha256: "8f9f429998f9232d65bc4757af74475ce44fc80f10704ff5dfa8b1d14fc429b9" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "10.2.1" | ||||
|     version: "10.2.3" | ||||
|   file_selector_linux: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -678,6 +662,14 @@ packages: | ||||
|     description: flutter | ||||
|     source: sdk | ||||
|     version: "0.0.0" | ||||
|   flutter_app_update: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: flutter_app_update | ||||
|       sha256: "09290240949c4651581cd6fc535e52d019e189e694d6019c56b5a56c2e69ba65" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.2.2" | ||||
|   flutter_blurhash: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -911,10 +903,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: flutter_plugin_android_lifecycle | ||||
|       sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e | ||||
|       sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.0.28" | ||||
|     version: "2.0.29" | ||||
|   flutter_popup_card: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1033,10 +1025,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: font_awesome_flutter | ||||
|       sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a | ||||
|       sha256: f50ce90dbe26d977415b9540400d6778bef00894aced6358ae578abd92b14b10 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "10.8.0" | ||||
|     version: "10.9.0" | ||||
|   freezed: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
| @@ -1089,10 +1081,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: go_router | ||||
|       sha256: c489908a54ce2131f1d1b7cc631af9c1a06fac5ca7c449e959192089f9489431 | ||||
|       sha256: "8b1f37dfaf6e958c6b872322db06f946509433bec3de753c3491a42ae9ec2b48" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "16.0.0" | ||||
|     version: "16.1.0" | ||||
|   google_fonts: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1153,10 +1145,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: http | ||||
|       sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" | ||||
|       sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.4.0" | ||||
|     version: "1.5.0" | ||||
|   http_multi_server: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1193,10 +1185,10 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: image_picker_android | ||||
|       sha256: "6fae381e6af2bbe0365a5e4ce1db3959462fa0c4d234facf070746024bb80c8d" | ||||
|       sha256: b08e9a04d0f8d91f4a6e767a745b9871bfbc585410205c311d0492de20a7ccd6 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.8.12+24" | ||||
|     version: "0.8.12+25" | ||||
|   image_picker_for_web: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1361,18 +1353,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: local_auth_android | ||||
|       sha256: "82b2bdeee2199a510d3b7716121e96a6609da86693bb0863edd8566355406b79" | ||||
|       sha256: "316503f6772dea9c0c038bb7aac4f68ab00112d707d258c770f7fc3c250a2d88" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.50" | ||||
|     version: "1.0.51" | ||||
|   local_auth_darwin: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: local_auth_darwin | ||||
|       sha256: "25163ce60a5a6c468cf7a0e3dc8a165f824cabc2aa9e39a5e9fc5c2311b7686f" | ||||
|       sha256: "0e9706a8543a4a2eee60346294d6a633dd7c3ee60fae6b752570457c4ff32055" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.5.0" | ||||
|     version: "1.6.0" | ||||
|   local_auth_platform_interface: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2033,10 +2025,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: shared_preferences_android | ||||
|       sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" | ||||
|       sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.10" | ||||
|     version: "2.4.11" | ||||
|   shared_preferences_foundation: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2206,18 +2198,18 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqlite3 | ||||
|       sha256: dd806fff004a0aeb01e208b858dbc649bc72104670d425a81a6dd17698535f6e | ||||
|       sha256: f393d92c71bdcc118d6203d07c991b9be0f84b1a6f89dd4f7eed348131329924 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.8.0" | ||||
|     version: "2.9.0" | ||||
|   sqlite3_flutter_libs: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: sqlite3_flutter_libs | ||||
|       sha256: fd996da5515a73aacd0a04ae7063db5fe8df42670d974df4c3ee538c652eef2e | ||||
|       sha256: "2b03273e71867a8a4d030861fc21706200debe5c5858a4b9e58f4a1c129586a4" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.5.38" | ||||
|     version: "0.5.39" | ||||
|   sqlparser: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -2424,10 +2416,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: url_launcher_android | ||||
|       sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" | ||||
|       sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.3.16" | ||||
|     version: "6.3.17" | ||||
|   url_launcher_ios: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
							
								
								
									
										12
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								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 | ||||
| # 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. | ||||
| version: 3.1.0+117 | ||||
| version: 3.1.0+123 | ||||
|  | ||||
| environment: | ||||
|   sdk: ^3.7.2 | ||||
| @@ -39,12 +39,12 @@ dependencies: | ||||
|   flutter_hooks: ^0.21.2 | ||||
|   hooks_riverpod: ^2.6.1 | ||||
|   bitsdojo_window: ^0.1.6 | ||||
|   go_router: ^16.0.0 | ||||
|   go_router: ^16.1.0 | ||||
|   styled_widget: ^0.4.1 | ||||
|   shared_preferences: ^2.5.3 | ||||
|   flutter_riverpod: ^2.6.1 | ||||
|   path_provider: ^2.1.5 | ||||
|   dio: ^5.8.0+1 | ||||
|   dio: ^5.9.0 | ||||
|   very_good_infinite_list: ^0.9.0 | ||||
|   freezed_annotation: ^3.1.0 | ||||
|   json_annotation: ^4.9.0 | ||||
| @@ -73,10 +73,10 @@ dependencies: | ||||
|     git: https://github.com/LittleSheep2Code/tus_client.git | ||||
|   cross_file: ^0.3.4+2 | ||||
|   image_picker: ^1.1.2 | ||||
|   file_picker: ^10.2.1 | ||||
|   file_picker: ^10.2.3 | ||||
|   riverpod_annotation: ^2.6.1 | ||||
|   image_picker_platform_interface: ^2.10.1 | ||||
|   image_picker_android: ^0.8.12+24 | ||||
|   image_picker_android: ^0.8.12+25 | ||||
|   super_context_menu: ^0.9.1 | ||||
|   modal_bottom_sheet: ^3.0.0 | ||||
|   firebase_messaging: ^16.0.0 | ||||
| @@ -133,6 +133,7 @@ dependencies: | ||||
|   flutter_typeahead: ^5.2.0 | ||||
|   flutter_langdetect: ^0.0.2 | ||||
|   waveform_flutter: ^1.2.0 | ||||
|   flutter_app_update: ^3.2.2 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
| @@ -144,7 +145,6 @@ dev_dependencies: | ||||
|   # package. See that file for information about deactivating specific lint | ||||
|   # rules and activating additional ones. | ||||
|   flutter_lints: ^6.0.0 | ||||
|   auto_route_generator: ^10.1.0 | ||||
|   build_runner: ^2.5.4 | ||||
|   freezed: ^3.1.0 | ||||
|   json_serializable: ^6.9.5 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user