Compare commits

..

28 Commits

Author SHA1 Message Date
LittleSheep
4e4bd99598 🚀 Launch 3.1.0+123 2025-08-11 02:06:29 +08:00
LittleSheep
d1fbe5f15e 🐛 Dozens of bug fixes 2025-08-11 01:56:19 +08:00
LittleSheep
c061ef2132 🐛 Bug fixes 2025-08-11 01:44:18 +08:00
LittleSheep
c378309bdd 📝 Update localization 2025-08-11 01:44:12 +08:00
LittleSheep
b2c5d64fc5 Keyboard navigation basis 2025-08-10 16:57:11 +08:00
LittleSheep
5371637b16 🔀 Merge pull request #161 from Texas0295/v3
🐛 linux: guard FirebaseMessaging on unsupported platforms
2025-08-10 14:05:48 +08:00
LittleSheep
c5cbf0af37 ⬆️ Upgrade android native project 2025-08-10 13:51:19 +08:00
LittleSheep
1a31e22450 🐛 Fix stickers pack unable to create 2025-08-10 13:23:15 +08:00
Texas0295
49db54529d 🐛 linux: guard FirebaseMessaging on unsupported platforms 2025-08-10 13:17:48 +08:00
LittleSheep
8e0c0c6054 🚀 Launch 3.1.0+122 2025-08-10 04:16:58 +08:00
LittleSheep
f3d1183076 🐛 Fix android update 2025-08-10 04:16:53 +08:00
LittleSheep
a9f7f0cce0 🐛 Fix bugs, ah bugs ha ha, bugs 2025-08-10 04:04:31 +08:00
LittleSheep
f2943f8411 🐛 Fix iOS notify delegate wrong path 2025-08-10 03:30:57 +08:00
LittleSheep
808e7dcffa Option to boost github assets download 2025-08-10 03:25:57 +08:00
LittleSheep
9bed4fa6fb Android install update 2025-08-10 03:21:34 +08:00
LittleSheep
e6255a340b 🐛 Fix background image didn't apply in certain page 2025-08-10 02:59:28 +08:00
LittleSheep
78bf319fb7 💄 Adjust stickers styles 2025-08-10 02:59:11 +08:00
LittleSheep
36a966d582 🐛 Fix profile link parsing 2025-08-10 02:55:00 +08:00
LittleSheep
f72b268d36 💄 Optimize profile page 2025-08-10 02:29:46 +08:00
LittleSheep
44ef31034e 👽 Update the profile links 2025-08-10 02:17:06 +08:00
LittleSheep
229dc2186f 💄 Optimize markdown rendering 2025-08-10 01:53:14 +08:00
LittleSheep
a2f9a1efb4 Optimize post quick reply 2025-08-10 01:45:02 +08:00
LittleSheep
823e3c5de6 🔀 Merge pull request #160 from Texas0295/v3
[FIX] linux_firebase_guard: skip FirebaseMessaging calls on Linux
2025-08-10 01:11:29 +08:00
Texas0295
faac7bac35 🐛 linux: guard FirebaseMessaging calls when Firebase is not initialized 2025-08-10 00:40:49 +08:00
LittleSheep
1fac1bfe02 🐛 Fix post item and payment 2025-08-10 00:26:32 +08:00
LittleSheep
9394b1d9c8 🐛 Serval bug fixes 2025-08-09 23:24:21 +08:00
LittleSheep
43dd13bac4 🐛 Fix status update issue 2025-08-09 22:55:46 +08:00
LittleSheep
65bc372103 🐛 Fix iOS NSE wrong avatar path 2025-08-09 22:55:35 +08:00
45 changed files with 1288 additions and 398 deletions

View File

@@ -51,6 +51,12 @@ android {
buildTypes { buildTypes {
release { release {
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
} }
} }
} }
@@ -58,7 +64,7 @@ android {
dependencies { dependencies {
implementation("com.google.android.material:material:1.12.0") implementation("com.google.android.material:material:1.12.0")
implementation("com.github.bumptech.glide:glide:4.16.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 { flutter {

5
android/app/proguard-rules.pro vendored Normal file
View 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();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

View File

@@ -18,11 +18,11 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" 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 // START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration // 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") include(":app")

View File

@@ -706,6 +706,7 @@
"copyToClipboardTooltip": "Copy to clipboard", "copyToClipboardTooltip": "Copy to clipboard",
"postForwardingTo": "Forwarding to", "postForwardingTo": "Forwarding to",
"postReplyingTo": "Replying to", "postReplyingTo": "Replying to",
"postReplyPlaceholder": "Post your reply",
"postEditing": "You are editing an existing post", "postEditing": "You are editing an existing post",
"postArticle": "Article", "postArticle": "Article",
"aboutDeviceName": "Device Name", "aboutDeviceName": "Device Name",
@@ -787,5 +788,9 @@
"addLink": "Add link", "addLink": "Add link",
"linkKey": "Link Name", "linkKey": "Link Name",
"linkValue": "URL", "linkValue": "URL",
"debugOptions": "Debug Options" "debugOptions": "Debug Options",
"joinedAt": "Joined at {}",
"searchAccounts": "Search accounts...",
"webFeeds": "Web Feeds",
"polls": "Polls"
} }

View File

@@ -46,7 +46,7 @@
"delete": "删除", "delete": "删除",
"deletePublisher": "删除发布者", "deletePublisher": "删除发布者",
"deletePublisherHint": "确定要删除此发布者吗?这也会删除此发布者下的所有帖子和收藏。", "deletePublisherHint": "确定要删除此发布者吗?这也会删除此发布者下的所有帖子和收藏。",
"somethingWentWrong": "发生了一些错误", "somethingWentWrong": "发生了一些错误……",
"deletePost": "删除帖子", "deletePost": "删除帖子",
"deletePostHint": "确定要删除这篇帖子吗?", "deletePostHint": "确定要删除这篇帖子吗?",
"copyLink": "复制链接", "copyLink": "复制链接",
@@ -120,14 +120,9 @@
"other": "{}个附件" "other": "{}个附件"
}, },
"edited": "已编辑", "edited": "已编辑",
"editedAt": "编辑于 {}",
"addVideo": "添加视频", "addVideo": "添加视频",
"addPhoto": "添加照片", "addPhoto": "添加照片",
"addFile": "添加文件", "addFile": "添加文件",
"addAttachmentById": "通过 ID 添加附件",
"enterFileId": "输入文件 ID",
"fileIdCannotBeEmpty": "文件 ID 不能为空",
"failedToFetchFile": "获取文件失败: {}",
"createDirectMessage": "创建新私人消息", "createDirectMessage": "创建新私人消息",
"gotoDirectMessage": "前往私信", "gotoDirectMessage": "前往私信",
"react": "反应", "react": "反应",
@@ -350,7 +345,7 @@
"accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。", "accountSettingsHelpContent": "此页面允许您管理您的帐户安全性、隐私和其他设置。如果您需要帮助,请联系管理员。",
"unauthorized": "未授权", "unauthorized": "未授权",
"unauthorizedHint": "您未登录或会话已过期,请重新登录。", "unauthorizedHint": "您未登录或会话已过期,请重新登录。",
"publisherBelongsTo": "属于 {}", "publisherBelongsTo": "属于",
"postContent": "内容", "postContent": "内容",
"postSettings": "设置", "postSettings": "设置",
"postPublisherUnselected": "未指定发布者", "postPublisherUnselected": "未指定发布者",
@@ -478,7 +473,7 @@
"description": "描述", "description": "描述",
"pinCode": "PIN 码", "pinCode": "PIN 码",
"biometric": "生物识别", "biometric": "生物识别",
"enterPinToConfirm": "请输入您的 6 位数字 PIN 以确认付款", "enterPinToConfirm": "请输入您的6位数字 PIN 以确认付款",
"clearPin": "清除 PIN 码", "clearPin": "清除 PIN 码",
"useBiometricToConfirm": "使用生物特征认证来确认付款", "useBiometricToConfirm": "使用生物特征认证来确认付款",
"touchSensorToAuthenticate": "触摸传感器进行身份验证", "touchSensorToAuthenticate": "触摸传感器进行身份验证",
@@ -495,20 +490,29 @@
"paymentError": "付款失败: {error}", "paymentError": "付款失败: {error}",
"usePinInstead": "使用 PIN 码", "usePinInstead": "使用 PIN 码",
"levelProgress": "等级进度", "levelProgress": "等级进度",
"unlockedFeatures": "已解锁的功能",
"unlockedFeaturesDescription": "在您当前级别上解锁的功能将显示在这里。",
"stellarMembership": "恒星计划", "stellarMembership": "恒星计划",
"upgradeYourPlan": "升级您的计划", "upgradeYourPlan": "升级您的计划",
"chooseYourPlan": "选择你的方案", "chooseYourPlan": "选择你的方案",
"currentMembership": "当前:{}", "currentMembership": "当前:{}",
"currentMembershipMember": "恒星计划「{}」级会员",
"membershipExpires": "过期于:{}", "membershipExpires": "过期于:{}",
"membershipTierStellar": "恒星", "membershipTierStellar": "恒星",
"membershipTierNova": "新星", "membershipTierNova": "新星",
"membershipTierSupernova": "超新星", "membershipTierSupernova": "超新星",
"membershipTierUnknown": "未知", "membershipTierUnknown": "未知",
"membershipPriceStellar": "每月 1200 源点,至少需要 3 级", "membershipPriceStellar": "每月 10 金点",
"membershipPriceNova": "每月 2400 源点,至少需要 6 级", "membershipPriceNova": "每月 20 金点",
"membershipPriceSupernova": "每月 3600 源点,至少需要 9 级", "membershipPriceSupernova": "每月 30 金点",
"membershipFeatureBasic": "基础功能", "membershipFeatureBasic": "基础功能",
"membershipFeaturePrioritySupport": "优先支持",
"membershipFeatureAdFree": "无广告",
"membershipFeatureAllPrimary": "所有主要功能",
"membershipFeatureAdvancedCustomization": "高级自定义",
"membershipFeatureEarlyAccess": "抢先体验",
"membershipFeatureAllNova": "所有「新星」功能",
"membershipFeatureExclusiveContent": "限定内容",
"membershipFeatureVipSupport": "VIP 支持",
"membershipCurrentBadge": "当前", "membershipCurrentBadge": "当前",
"restorePurchase": "恢复购买", "restorePurchase": "恢复购买",
"restorePurchaseDescription": "输入您付款的提供商和订单 ID 以恢复您的购买。", "restorePurchaseDescription": "输入您付款的提供商和订单 ID 以恢复您的购买。",
@@ -518,11 +522,167 @@
"enterOrderId": "输入您的订单 ID", "enterOrderId": "输入您的订单 ID",
"restore": "恢复", "restore": "恢复",
"keyboardShortcuts": "键盘快捷键", "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": "关于", "about": "关于",
"membershipCancel": "取消会员订阅", "membershipCancel": "取消会员资格",
"membershipCancelConfirm": "确定要取消您的会员订阅", "membershipCancelConfirm": "确定要取消会员资格吗",
"membershipCancelHint": "确定要取消您的会员订阅吗?将不会再被收费。的会员资格将在当前计费周期结束前保持有效。并且您在当前订阅结束之前无法重新订阅。", "membershipCancelHint": "确定要取消会员资格吗?将不会再次被扣费。的会员资格将在当前计费周期结束前保持有效。并且你将无法重新订阅,直到当前订阅结束。",
"membershipCancelSuccess": "的会员订阅已成功取消。", "membershipCancelSuccess": "的会员资格已成功取消。",
"aboutScreenTitle": "关于", "aboutScreenTitle": "关于",
"aboutScreenVersionInfo": "版本 {} ({})", "aboutScreenVersionInfo": "版本 {} ({})",
"aboutScreenAppInfoSectionTitle": "应用信息", "aboutScreenAppInfoSectionTitle": "应用信息",
@@ -532,18 +692,103 @@
"aboutScreenLinksSectionTitle": "链接", "aboutScreenLinksSectionTitle": "链接",
"aboutScreenPrivacyPolicyTitle": "隐私政策", "aboutScreenPrivacyPolicyTitle": "隐私政策",
"aboutScreenTermsOfServiceTitle": "服务条款", "aboutScreenTermsOfServiceTitle": "服务条款",
"aboutScreenOpenSourceLicensesTitle": "开源许可", "aboutScreenOpenSourceLicensesTitle": "开源许可",
"aboutScreenDeveloperSectionTitle": "开发者", "aboutScreenDeveloperSectionTitle": "开发者",
"aboutScreenContactUsTitle": "联系我们", "aboutScreenContactUsTitle": "联系我们",
"aboutScreenLicenseTitle": "许可", "aboutScreenLicenseTitle": "许可",
"aboutScreenLicenseContent": "GNU Affero General Public License v3.0", "aboutScreenLicenseContent": "GNU Affero General Public License v3.0",
"aboutScreenCopyright": "版权所有 © 索尔辛茨 {}", "aboutScreenCopyright": "版权所有 © Solsynth {}",
"aboutScreenMadeWith": "由 Solar Network Team 用 ❤︎️ 制作", "aboutScreenMadeWith": "由 Solar Network 团队用 ❤︎️ 制作",
"aboutScreenFailedToLoadPackageInfo": "加载包信息失败{error}", "aboutScreenFailedToLoadPackageInfo": "无法加载包信息:{error}",
"copiedToClipboard": "已复制到剪贴板", "copiedToClipboard": "已复制到剪贴板",
"copyToClipboardTooltip": "复制到剪贴板", "copyToClipboardTooltip": "复制到剪贴板",
"postForwardingTo": "转发", "postForwardingTo": "正在转发",
"postReplyingTo": "回复", "postReplyingTo": "正在回复",
"postEditing": "您正在编辑现有帖子", "postReplyPlaceholder": "发表你的回复",
"postArticle": "文章" "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": "投票"
} }

View File

@@ -73,6 +73,8 @@ PODS:
- GoogleUtilities/UserDefaults (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_app_update (0.0.1):
- Flutter
- flutter_inappwebview_ios (0.0.1): - flutter_inappwebview_ios (0.0.1):
- Flutter - Flutter
- flutter_inappwebview_ios/Core (= 0.0.1) - flutter_inappwebview_ios/Core (= 0.0.1)
@@ -178,25 +180,25 @@ PODS:
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (3.50.3): - sqlite3 (3.50.4):
- sqlite3/common (= 3.50.3) - sqlite3/common (= 3.50.4)
- sqlite3/common (3.50.3) - sqlite3/common (3.50.4)
- sqlite3/dbstatvtab (3.50.3): - sqlite3/dbstatvtab (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/fts5 (3.50.3): - sqlite3/fts5 (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/math (3.50.3): - sqlite3/math (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/perf-threadsafe (3.50.3): - sqlite3/perf-threadsafe (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/rtree (3.50.3): - sqlite3/rtree (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/session (3.50.3): - sqlite3/session (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (~> 3.50.3) - sqlite3 (~> 3.50.4)
- sqlite3/dbstatvtab - sqlite3/dbstatvtab
- sqlite3/fts5 - sqlite3/fts5
- sqlite3/math - sqlite3/math
@@ -223,6 +225,7 @@ DEPENDENCIES:
- firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
@@ -293,6 +296,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_messaging/ios" :path: ".symlinks/plugins/firebase_messaging/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_app_update:
:path: ".symlinks/plugins/flutter_app_update/ios"
flutter_inappwebview_ios: flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios" :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_keyboard_visibility: flutter_keyboard_visibility:
@@ -372,6 +377,7 @@ SPEC CHECKSUMS:
FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988 FirebaseInstallations: d4c7c958f99c8860d7fcece786314ae790e2f988
FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde FirebaseMessaging: af49f8d7c0a3d2a017d9302c80946f45a7777dde
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 816fdb2e30e4832a7c45e3f108d391c42ef040a9
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
@@ -406,8 +412,8 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418 sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81 sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d url_launcher_ios: 694010445543906933d732453a59da0a173ae33d

View File

@@ -34,7 +34,7 @@ class NotifyDelegate: UIResponder, UNUserNotificationCenterDelegate {
} }
let serverUrl = UserDefaults.standard.getServerUrl() 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?] = [ let parameters: [String: Any?] = [
"content": textResponse.userText, "content": textResponse.userText,

View File

@@ -8,7 +8,7 @@
import Foundation import Foundation
func getAttachmentUrl(for identifier: String) -> String { 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)"
} }

View File

@@ -28,9 +28,9 @@ import 'package:relative_time/relative_time.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
import 'package:flutter_native_splash/flutter_native_splash.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:url_launcher/url_launcher_string.dart';
import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect; import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect;
import 'package:island/services/update_service.dart';
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { 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 // Router will be provided through Riverpod
@@ -181,6 +172,9 @@ class IslandApp extends HookConsumerWidget {
} }
useEffect(() { useEffect(() {
if (!kIsWeb && Platform.isLinux) {
return null;
}
const channel = MethodChannel('dev.solsynth.solian/notifications'); const channel = MethodChannel('dev.solsynth.solian/notifications');
Future<void> handleInitialLink() async { Future<void> handleInitialLink() async {
@@ -251,30 +245,32 @@ class IslandApp extends HookConsumerWidget {
final router = ref.watch(routerProvider); final router = ref.watch(routerProvider);
return MaterialApp.router( return KeyboardNavigation(
theme: theme?.light, child: MaterialApp.router(
darkTheme: theme?.dark, theme: theme?.light,
themeMode: ThemeMode.system, darkTheme: theme?.dark,
routerConfig: router, themeMode: ThemeMode.system,
supportedLocales: context.supportedLocales, routerConfig: router,
localizationsDelegates: [ supportedLocales: context.supportedLocales,
...context.localizationDelegates, localizationsDelegates: [
CroppyLocalizations.delegate, ...context.localizationDelegates,
RelativeTimeLocalizations.delegate, CroppyLocalizations.delegate,
], RelativeTimeLocalizations.delegate,
locale: context.locale, ],
builder: (context, child) { locale: context.locale,
return Overlay( builder: (context, child) {
key: globalOverlay, return Overlay(
initialEntries: [ key: globalOverlay,
OverlayEntry( initialEntries: [
builder: OverlayEntry(
(_) => builder:
WindowScaffold(child: child ?? const SizedBox.shrink()), (_) =>
), WindowScaffold(child: child ?? const SizedBox.shrink()),
], ),
); ],
}, );
},
),
); );
} }
} }

View File

@@ -25,6 +25,32 @@ sealed class SnAccount with _$SnAccount {
_$SnAccountFromJson(json); _$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 @freezed
sealed class SnAccountProfile with _$SnAccountProfile { sealed class SnAccountProfile with _$SnAccountProfile {
const factory SnAccountProfile({ const factory SnAccountProfile({
@@ -38,7 +64,7 @@ sealed class SnAccountProfile with _$SnAccountProfile {
@Default('') String location, @Default('') String location,
@Default('') String timeZone, @Default('') String timeZone,
DateTime? birthday, DateTime? birthday,
@Default({}) Map<String, String> links, @ProfileLinkConverter() @Default([]) List<ProfileLink> links,
DateTime? lastSeenAt, DateTime? lastSeenAt,
SnAccountBadge? activeBadge, SnAccountBadge? activeBadge,
required int experience, required int experience,

View File

@@ -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 /// @nodoc
mixin _$SnAccountProfile { 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 /// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -383,7 +643,7 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> {
factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl; factory $SnAccountProfileCopyWith(SnAccountProfile value, $Res Function(SnAccountProfile) _then) = _$SnAccountProfileCopyWithImpl;
@useResult @useResult
$Res call({ $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,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 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 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 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 SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable as int,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) { switch (_that) {
case _SnAccountProfile() when $default != null: case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.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) { switch (_that) {
case _SnAccountProfile(): case _SnAccountProfile():
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);} return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.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) { switch (_that) {
case _SnAccountProfile() when $default != null: case _SnAccountProfile() when $default != null:
return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.levelingProgress,_that.picture,_that.background,_that.verification,_that.createdAt,_that.updatedAt,_that.deletedAt);case _: return $default(_that.id,_that.firstName,_that.middleName,_that.lastName,_that.bio,_that.gender,_that.pronouns,_that.location,_that.timeZone,_that.birthday,_that.links,_that.lastSeenAt,_that.activeBadge,_that.experience,_that.level,_that.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() @JsonSerializable()
class _SnAccountProfile implements SnAccountProfile { 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); factory _SnAccountProfile.fromJson(Map<String, dynamic> json) => _$SnAccountProfileFromJson(json);
@override final String id; @override final String id;
@@ -620,11 +880,11 @@ class _SnAccountProfile implements SnAccountProfile {
@override@JsonKey() final String location; @override@JsonKey() final String location;
@override@JsonKey() final String timeZone; @override@JsonKey() final String timeZone;
@override final DateTime? birthday; @override final DateTime? birthday;
final Map<String, String> _links; final List<ProfileLink> _links;
@override@JsonKey() Map<String, String> get links { @override@JsonKey()@ProfileLinkConverter() List<ProfileLink> get links {
if (_links is EqualUnmodifiableMapView) return _links; if (_links is EqualUnmodifiableListView) return _links;
// ignore: implicit_dynamic_type // ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_links); return EqualUnmodifiableListView(_links);
} }
@override final DateTime? lastSeenAt; @override final DateTime? lastSeenAt;
@@ -672,7 +932,7 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res> implements $SnAccountProfi
factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl; factory _$SnAccountProfileCopyWith(_SnAccountProfile value, $Res Function(_SnAccountProfile) _then) = __$SnAccountProfileCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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,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 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 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 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 SnAccountBadge?,experience: null == experience ? _self.experience : experience // ignore: cast_nullable_to_non_nullable
as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable as int,level: null == level ? _self.level : level // ignore: cast_nullable_to_non_nullable

View File

@@ -47,6 +47,12 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
'deleted_at': instance.deletedAt?.toIso8601String(), '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 _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
_SnAccountProfile( _SnAccountProfile(
id: json['id'] as String, id: json['id'] as String,
@@ -63,10 +69,9 @@ _SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
? null ? null
: DateTime.parse(json['birthday'] as String), : DateTime.parse(json['birthday'] as String),
links: links:
(json['links'] as Map<String, dynamic>?)?.map( json['links'] == null
(k, e) => MapEntry(k, e as String), ? const []
) ?? : const ProfileLinkConverter().fromJson(json['links']),
const {},
lastSeenAt: lastSeenAt:
json['last_seen_at'] == null json['last_seen_at'] == null
? null ? null
@@ -116,7 +121,7 @@ Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
'location': instance.location, 'location': instance.location,
'time_zone': instance.timeZone, 'time_zone': instance.timeZone,
'birthday': instance.birthday?.toIso8601String(), 'birthday': instance.birthday?.toIso8601String(),
'links': instance.links, 'links': const ProfileLinkConverter().toJson(instance.links),
'last_seen_at': instance.lastSeenAt?.toIso8601String(), 'last_seen_at': instance.lastSeenAt?.toIso8601String(),
'active_badge': instance.activeBadge?.toJson(), 'active_badge': instance.activeBadge?.toJson(),
'experience': instance.experience, 'experience': instance.experience,

View File

@@ -7,12 +7,12 @@ import 'package:flutter/services.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/services/udid.native.dart'; import 'package:island/services/udid.native.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:island/services/update_service.dart'; import 'package:island/services/update_service.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@@ -205,33 +205,16 @@ class _AboutScreenState extends ConsumerState<AboutScreen> {
// Fetch latest release and show the unified sheet // Fetch latest release and show the unified sheet
final svc = UpdateService(); final svc = UpdateService();
// Reuse service fetch + compare to decide content // Reuse service fetch + compare to decide content
showLoadingModal(context);
final release = await svc.fetchLatestRelease(); final release = await svc.fetchLatestRelease();
if (!context.mounted) return;
hideLoadingModal(context);
if (release != null) { if (release != null) {
await svc.showUpdateSheet(context, release); await svc.showUpdateSheet(context, release);
} else { } else {
// Fallback: show a simple sheet indicating no info showInfoAlert(
// Use your SheetScaffold for consistent styling 'Currently cannot get update from the GitHub.',
// Show a minimal message 'Unable to check for updates',
// 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.',
),
),
),
),
); );
} }
}, },

View File

@@ -236,7 +236,7 @@ class AccountScreen extends HookConsumerWidget {
), ),
ListTile( ListTile(
minTileHeight: 48, minTileHeight: 48,
title: Text('abuseReports').tr(), title: Text('abuseReport').tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Symbols.gavel), leading: const Icon(Symbols.gavel),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),

View File

@@ -7,6 +7,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
@@ -95,11 +96,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
final usernameController = useTextEditingController(text: user.value!.name); final usernameController = useTextEditingController(text: user.value!.name);
final nicknameController = useTextEditingController(text: user.value!.nick); final nicknameController = useTextEditingController(text: user.value!.nick);
final language = useState(user.value!.language); final language = useState(user.value!.language);
final links = useState<List<Map<String, String>>>( final links = useState<List<ProfileLink>>(user.value!.profile.links);
user.value!.profile.links.entries
.map((e) => {'key': e.key, 'value': e.value})
.toList(),
);
void updateBasicInfo() async { void updateBasicInfo() async {
if (!formKeyBasicInfo.currentState!.validate()) return; if (!formKeyBasicInfo.currentState!.validate()) return;
@@ -171,7 +168,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
'location': locationController.text, 'location': locationController.text,
'time_zone': timeZoneController.text, 'time_zone': timeZoneController.text,
'birthday': birthday.value?.toUtc().toIso8601String(), 'birthday': birthday.value?.toUtc().toIso8601String(),
'links': {for (var e in links.value) e['key']!: e['value']!}, 'links': links.value,
}, },
); );
final userNotifier = ref.read(userInfoProvider.notifier); final userNotifier = ref.read(userInfoProvider.notifier);
@@ -575,13 +572,15 @@ class UpdateProfileScreen extends HookConsumerWidget {
children: [ children: [
Expanded( Expanded(
child: TextFormField( child: TextFormField(
initialValue: links.value[i]['key'], initialValue: links.value[i].name,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'linkKey'.tr(), labelText: 'linkKey'.tr(),
isDense: true, isDense: true,
), ),
onChanged: (value) { onChanged: (value) {
links.value[i]['key'] = value; links.value[i] = links.value[i].copyWith(
name: value,
);
}, },
onTapOutside: onTapOutside:
(_) => (_) =>
@@ -592,13 +591,15 @@ class UpdateProfileScreen extends HookConsumerWidget {
const Gap(8), const Gap(8),
Expanded( Expanded(
child: TextFormField( child: TextFormField(
initialValue: links.value[i]['value'], initialValue: links.value[i].url,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'linkValue'.tr(), labelText: 'linkValue'.tr(),
isDense: true, isDense: true,
), ),
onChanged: (value) { onChanged: (value) {
links.value[i]['value'] = value; links.value[i] = links.value[i].copyWith(
url: value,
);
}, },
onTapOutside: onTapOutside:
(_) => (_) =>
@@ -620,7 +621,7 @@ class UpdateProfileScreen extends HookConsumerWidget {
child: FilledButton.icon( child: FilledButton.icon(
onPressed: () { onPressed: () {
links.value = List.from(links.value) links.value = List.from(links.value)
..add({'key': '', 'value': ''}); ..add(ProfileLink(name: '', url: ''));
}, },
label: Text('addLink').tr(), label: Text('addLink').tr(),
icon: const Icon(Symbols.add), icon: const Icon(Symbols.add),

View File

@@ -1,5 +1,6 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
@@ -196,6 +197,15 @@ class AccountProfileScreen extends HookConsumerWidget {
List<Widget> buildSubcolumn(SnAccount data) { List<Widget> buildSubcolumn(SnAccount data) {
return [ 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) if (data.profile.birthday != null)
Row( Row(
spacing: 6, spacing: 6,
@@ -322,7 +332,7 @@ class AccountProfileScreen extends HookConsumerWidget {
spacing: 2, spacing: 2,
children: buildSubcolumn(data), children: buildSubcolumn(data),
), ),
if (data.profile.timeZone.isNotEmpty) if (data.profile.timeZone.isNotEmpty && !kIsWeb)
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -357,17 +367,21 @@ class AccountProfileScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4), Text('links').tr().bold().padding(horizontal: 24, top: 12, bottom: 4),
for (final link in data.profile.links.entries) for (final link in data.profile.links)
ListTile( ListTile(
title: Text(link.key.capitalizeEachWord()), title: Text(link.name.capitalizeEachWord()),
subtitle: Text(link.value), subtitle: Text(link.url),
contentPadding: EdgeInsets.symmetric(horizontal: 24), contentPadding: EdgeInsets.symmetric(horizontal: 24),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
), ),
onTap: () { onTap: () {
launchUrlString(link.value); if (!link.url.startsWith('http') && !link.url.contains('://')) {
launchUrlString('https://${link.url}');
} else {
launchUrlString(link.url);
}
}, },
), ),
], ],
@@ -561,9 +575,10 @@ class AccountProfileScreen extends HookConsumerWidget {
SliverToBoxAdapter( SliverToBoxAdapter(
child: accountProfileBio(data).padding(top: 4), child: accountProfileBio(data).padding(top: 4),
), ),
SliverToBoxAdapter( if (data.profile.links.isNotEmpty)
child: accountProfileLinks(data), SliverToBoxAdapter(
), child: accountProfileLinks(data),
),
SliverToBoxAdapter( SliverToBoxAdapter(
child: accountProfileDetail(data), child: accountProfileDetail(data),
), ),
@@ -660,11 +675,12 @@ class AccountProfileScreen extends HookConsumerWidget {
SliverToBoxAdapter( SliverToBoxAdapter(
child: accountProfileBio(data).padding(horizontal: 4), child: accountProfileBio(data).padding(horizontal: 4),
), ),
SliverToBoxAdapter( if (data.profile.links.isNotEmpty)
child: accountProfileLinks( SliverToBoxAdapter(
data, child: accountProfileLinks(
).padding(horizontal: 4), data,
), ).padding(horizontal: 4),
),
SliverToBoxAdapter( SliverToBoxAdapter(
child: accountProfileDetail( child: accountProfileDetail(
data, data,

View File

@@ -216,6 +216,7 @@ class RelationshipScreen extends HookConsumerWidget {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
context: context, context: context,
useRootNavigator: true, useRootNavigator: true,
isScrollControlled: true,
builder: (context) => AccountPickerSheet(), builder: (context) => AccountPickerSheet(),
); );
if (result == null) return; if (result == null) return;

View File

@@ -227,6 +227,7 @@ class ChatListScreen extends HookConsumerWidget {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
context: context, context: context,
useRootNavigator: true, useRootNavigator: true,
isScrollControlled: true,
builder: (context) => const AccountPickerSheet(), builder: (context) => const AccountPickerSheet(),
); );
if (result == null) return; if (result == null) return;

View File

@@ -339,7 +339,7 @@ class ChatRoomScreen extends HookConsumerWidget {
} }
await apiClient.post( await apiClient.post(
'/chat/${chatRoom.value!.id}/members/me', '/sphere/chat/${chatRoom.value!.id}/members/me',
); );
ref.invalidate(chatroomIdentityProvider(id)); ref.invalidate(chatroomIdentityProvider(id));
} catch (err) { } catch (err) {
@@ -929,7 +929,7 @@ class ChatRoomScreen extends HookConsumerWidget {
if (attachment.isOnCloud) { if (attachment.isOnCloud) {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
await client.delete( await client.delete(
'/files/${attachment.data.id}', '/drive/files/${attachment.data.id}',
); );
} }
final clone = List.of(attachments.value); final clone = List.of(attachments.value);

View File

@@ -589,6 +589,7 @@ class _ChatMemberListSheet extends HookConsumerWidget {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
context: context, context: context,
useRootNavigator: true, useRootNavigator: true,
isScrollControlled: true,
builder: (context) => const AccountPickerSheet(), builder: (context) => const AccountPickerSheet(),
); );
if (result == null) return; if (result == null) return;
@@ -727,7 +728,7 @@ class _ChatMemberListSheet extends HookConsumerWidget {
apiClientProvider, apiClientProvider,
); );
await apiClient.delete( await apiClient.delete(
'/chat/$roomId/members/${member.accountId}', '/sphere/chat/$roomId/members/${member.accountId}',
); );
// Refresh both providers // Refresh both providers
memberNotifier.reset(); memberNotifier.reset();

View File

@@ -382,7 +382,7 @@ class CreatorHubScreen extends HookConsumerWidget {
), ),
ListTile( ListTile(
minTileHeight: 48, minTileHeight: 48,
title: const Text('Polls'), title: Text('polls').tr(),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
leading: const Icon(Symbols.poll), leading: const Icon(Symbols.poll),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
@@ -419,7 +419,7 @@ class CreatorHubScreen extends HookConsumerWidget {
), ),
ListTile( ListTile(
minTileHeight: 48, minTileHeight: 48,
title: const Text('Web Feeds').tr(), title: const Text('webFeeds').tr(),
trailing: const Icon(Symbols.chevron_right), trailing: const Icon(Symbols.chevron_right),
leading: const Icon(Symbols.rss_feed), leading: const Icon(Symbols.rss_feed),
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
@@ -659,7 +659,7 @@ class PublisherMemberNotifier extends StateNotifier<PublisherMemberState> {
try { try {
final response = await _apiClient.get( final response = await _apiClient.get(
'/publishers/$publisherUname/members', '/sphere/publishers/$publisherUname/members',
queryParameters: {'offset': offset, 'take': take}, queryParameters: {'offset': offset, 'take': take},
); );
@@ -708,6 +708,7 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
Future<void> invitePerson() async { Future<void> invitePerson() async {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: (context) => const AccountPickerSheet(), builder: (context) => const AccountPickerSheet(),
@@ -719,6 +720,9 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
'/publishers/$publisherUname/invites', '/publishers/$publisherUname/invites',
data: {'related_user_id': result.id, 'role': 0}, data: {'related_user_id': result.id, 'role': 0},
); );
// Refresh both providers
memberNotifier.reset();
await memberNotifier.loadMore();
ref.invalidate(memberListProvider); ref.invalidate(memberListProvider);
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
@@ -822,6 +826,9 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
), ),
).then((value) { ).then((value) {
if (value != null) { if (value != null) {
// Refresh both providers
memberNotifier.reset();
memberNotifier.loadMore();
ref.invalidate(memberListProvider); ref.invalidate(memberListProvider);
} }
}); });
@@ -843,6 +850,9 @@ class _PublisherMemberListSheet extends HookConsumerWidget {
await apiClient.delete( await apiClient.delete(
'/publishers/$publisherUname/members/${member.accountId}', '/publishers/$publisherUname/members/${member.accountId}',
); );
// Refresh both providers
memberNotifier.reset();
memberNotifier.loadMore();
ref.invalidate(memberListProvider); ref.invalidate(memberListProvider);
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);

View File

@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/poll.dart'; import 'package:island/models/poll.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/poll/poll_feedback.dart'; import 'package:island/widgets/poll/poll_feedback.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -70,7 +71,7 @@ class CreatorPollListScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Scaffold( return AppScaffold(
appBar: AppBar(title: const Text('Polls')), appBar: AppBar(title: const Text('Polls')),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => _createPoll(context), onPressed: () => _createPoll(context),

View File

@@ -58,7 +58,7 @@ class StickerPackDetailScreen extends HookConsumerWidget {
try { try {
showLoadingModal(context); showLoadingModal(context);
final apiClient = ref.watch(apiClientProvider); 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)); ref.invalidate(stickerPackContentProvider(id));
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
@@ -180,6 +180,7 @@ class StickerPackDetailScreen extends HookConsumerWidget {
.pushNamed( .pushNamed(
'creatorStickerEdit', 'creatorStickerEdit',
pathParameters: { pathParameters: {
'name': pubName,
'packId': id, 'packId': id,
'id': sticker.id, 'id': sticker.id,
}, },
@@ -297,7 +298,7 @@ class _StickerPackActionMenu extends HookConsumerWidget {
).then((confirm) { ).then((confirm) {
if (confirm) { if (confirm) {
final client = ref.watch(apiClientProvider); final client = ref.watch(apiClientProvider);
client.delete('/stickers/$packId'); client.delete('/sphere/stickers/$packId');
ref.invalidate(stickerPacksNotifierProvider); ref.invalidate(stickerPacksNotifierProvider);
if (context.mounted) context.pop(true); if (context.mounted) context.pop(true);
} }
@@ -325,7 +326,7 @@ Future<SnSticker?> stickerPackSticker(
if (query == null) return null; if (query == null) return null;
final apiClient = ref.watch(apiClientProvider); final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get( final resp = await apiClient.get(
'/stickers/${query.packId}/content/${query.id}', '/sphere/stickers/${query.packId}/content/${query.id}',
); );
if (resp.data == null) return null; if (resp.data == null) return null;
return SnSticker.fromJson(resp.data); return SnSticker.fromJson(resp.data);
@@ -379,8 +380,8 @@ class EditStickersScreen extends HookConsumerWidget {
try { try {
final resp = await apiClient.request( final resp = await apiClient.request(
id == null id == null
? '/stickers/$packId/content' ? '/sphere/stickers/$packId/content'
: '/stickers/$packId/content/$id', : '/sphere/stickers/$packId/content/$id',
data: {'slug': slugController.text, 'image_id': imageController.text}, data: {'slug': slugController.text, 'image_id': imageController.text},
options: Options(method: id == null ? 'POST' : 'PATCH'), options: Options(method: id == null ? 'POST' : 'PATCH'),
); );

View File

@@ -151,7 +151,7 @@ class _StickerPackContentProviderElement
} }
String _$stickerPackStickerHash() => String _$stickerPackStickerHash() =>
r'36f524c047e632236d5597aaaa8678ed86599602'; r'5c553666b3a63530bdebae4b7cd52f303c5ab3a0';
/// See also [stickerPackSticker]. /// See also [stickerPackSticker].
@ProviderFor(stickerPackSticker) @ProviderFor(stickerPackSticker)

View File

@@ -31,7 +31,7 @@ class StickersScreen extends HookConsumerWidget {
context context
.pushNamed( .pushNamed(
'creatorStickerPackNew', 'creatorStickerPackNew',
queryParameters: {'name': pubName}, pathParameters: {'name': pubName},
) )
.then((value) { .then((value) {
if (value != null) { if (value != null) {
@@ -187,10 +187,8 @@ class EditStickerPacksScreen extends HookConsumerWidget {
'description': descriptionController.text, 'description': descriptionController.text,
'prefix': prefixController.text, 'prefix': prefixController.text,
}, },
options: Options( queryParameters: {'pub': pubName},
method: packId == null ? 'POST' : 'PATCH', options: Options(method: packId == null ? 'POST' : 'PATCH'),
headers: {'X-Pub': pubName},
),
); );
if (!context.mounted) return; if (!context.mounted) return;
context.pop(SnStickerPack.fromJson(resp.data)); context.pop(SnStickerPack.fromJson(resp.data));

View File

@@ -114,10 +114,11 @@ class WebFeedEditScreen extends HookConsumerWidget {
return feedAsync.when( return feedAsync.when(
loading: loading:
() => () => const AppScaffold(
const Scaffold(body: Center(child: CircularProgressIndicator())), body: Center(child: CircularProgressIndicator()),
),
error: error:
(error, stack) => Scaffold( (error, stack) => AppScaffold(
appBar: AppBar(title: const Text('Error')), appBar: AppBar(title: const Text('Error')),
body: Center(child: Text('Error: $error')), body: Center(child: Text('Error: $error')),
), ),

View File

@@ -9,6 +9,7 @@ import 'package:gap/gap.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart'; import 'package:island/widgets/alert.dart';
import 'package:island/models/poll.dart'; import 'package:island/models/poll.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class PollEditorState { class PollEditorState {
@@ -413,7 +414,7 @@ class PollEditorScreen extends ConsumerWidget {
}); });
} }
return Scaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: Text(model.id == null ? 'Create Poll' : 'Edit Poll'), title: Text(model.id == null ? 'Create Poll' : 'Edit Poll'),
actions: [ actions: [
@@ -428,175 +429,175 @@ class PollEditorScreen extends ConsumerWidget {
const Gap(8), const Gap(8),
], ],
), ),
body: SafeArea( body: Column(
child: Form( children: [
key: ValueKey(model.id), Expanded(
child: ListView( child: Form(
padding: const EdgeInsets.all(16), key: ValueKey(model.id),
children: [ child: ListView(
TextFormField( padding: const EdgeInsets.all(16),
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(
children: [ children: [
Text( TextFormField(
'Questions', initialValue: model.title ?? '',
style: Theme.of(context).textTheme.titleLarge, decoration: const InputDecoration(
), labelText: 'Title',
const Spacer(), border: OutlineInputBorder(
MenuAnchor( borderRadius: BorderRadius.all(Radius.circular(16)),
builder: (context, controller, child) { ),
return FilledButton.icon( ),
onPressed: () { textInputAction: TextInputAction.next,
controller.isOpen maxLength: 256,
? controller.close() onChanged: notifier.setTitle,
: controller.open(); onTapOutside:
}, (_) => FocusManager.instance.primaryFocus?.unfocus(),
icon: const Icon(Icons.add), validator: (v) {
label: const Text('Add question'), 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( Row(
title: 'No questions yet', children: [
subtitle: 'Use "Add question" to start building your poll.', OutlinedButton.icon(
) onPressed: () {
else Navigator.of(context).maybePop();
ReorderableListView.builder( },
shrinkWrap: true, icon: const Icon(Icons.close),
physics: const NeverScrollableScrollPhysics(), label: const Text('Cancel'),
itemCount: model.questions.length, ),
onReorder: (oldIndex, newIndex) { const Spacer(),
// Convert to stepwise moves using provided functions FilledButton.icon(
if (newIndex > oldIndex) newIndex -= 1; onPressed: () {
final steps = newIndex - oldIndex; _submitPoll(context, ref);
if (steps == 0) return; },
if (steps > 0) { icon: const Icon(Icons.cloud_upload_outlined),
for (int i = 0; i < steps; i++) { label: Text(model.id == null ? 'Create' : 'Update'),
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),
], ],
), ),
), ],
),
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'),
),
],
),
), ),
); );
} }

View File

@@ -92,6 +92,7 @@ class PostDetailScreen extends HookConsumerWidget {
right: 0, right: 0,
child: Material( child: Material(
elevation: 2, elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainer,
child: postState child: postState
.when( .when(
data: data:
@@ -107,8 +108,8 @@ class PostDetailScreen extends HookConsumerWidget {
error: (_, _) => const SizedBox.shrink(), error: (_, _) => const SizedBox.shrink(),
) )
.padding( .padding(
bottom: MediaQuery.of(context).padding.bottom + 16, bottom: MediaQuery.of(context).padding.bottom + 8,
top: 16, top: 8,
horizontal: 16, horizontal: 16,
), ),
), ),

View File

@@ -488,6 +488,7 @@ class _RealmMemberListSheet extends HookConsumerWidget {
Future<void> invitePerson() async { Future<void> invitePerson() async {
final result = await showModalBottomSheet( final result = await showModalBottomSheet(
isScrollControlled: true, isScrollControlled: true,
useRootNavigator: true,
context: context, context: context,
builder: (context) => const AccountPickerSheet(), builder: (context) => const AccountPickerSheet(),
); );

View File

@@ -67,6 +67,9 @@ Future<void> subscribePushNotification(
Dio apiClient, { Dio apiClient, {
bool detailedErrors = false, bool detailedErrors = false,
}) async { }) async {
if (Platform.isLinux){
return;
}
await FirebaseMessaging.instance.requestPermission( await FirebaseMessaging.instance.requestPermission(
alert: true, alert: true,
badge: true, badge: true,

View File

@@ -1,19 +1,28 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.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:package_info_plus/package_info_plus.dart';
import 'package:collection/collection.dart'; // Added for firstWhereOrNull
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/content/sheet.dart';
/// Data model for a GitHub release we care about /// Data model for a GitHub release we care about
class GithubReleaseInfo { class GithubReleaseInfo {
final String tagName; // e.g. 3.1.0+118 final String tagName;
final String name; // release title final String name;
final String body; // changelog markdown final String body;
final String htmlUrl; // release page final String htmlUrl;
final DateTime createdAt; final DateTime createdAt;
final List<GithubReleaseAsset> assets;
const GithubReleaseInfo({ const GithubReleaseInfo({
required this.tagName, required this.tagName,
@@ -21,9 +30,28 @@ class GithubReleaseInfo {
required this.body, required this.body,
required this.htmlUrl, required this.htmlUrl,
required this.createdAt, 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" /// Parses version and build number from "x.y.z+build"
class _ParsedVersion implements Comparable<_ParsedVersion> { class _ParsedVersion implements Comparable<_ParsedVersion> {
final int major; final int major;
@@ -62,7 +90,7 @@ class _ParsedVersion implements Comparable<_ParsedVersion> {
} }
class UpdateService { class UpdateService {
UpdateService({Dio? dio}) UpdateService({Dio? dio, this.useProxy = false})
: _dio = : _dio =
dio ?? dio ??
Dio( Dio(
@@ -78,6 +106,9 @@ class UpdateService {
); );
final Dio _dio; final Dio _dio;
final bool useProxy;
static const _proxyBaseUrl = 'https://ghfast.top/';
static const _releasesLatestApi = static const _releasesLatestApi =
'https://api.github.com/repos/solsynth/solian/releases/latest'; '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. /// 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. /// If update is available, shows a bottom sheet with changelog and an action to open release page.
Future<void> checkForUpdates(BuildContext context) async { Future<void> checkForUpdates(BuildContext context) async {
log('[Update] Checking for updates...');
try { try {
final release = await fetchLatestRelease(); 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 info = await PackageInfo.fromPlatform();
final localVersionStr = '${info.version}+${info.buildNumber}'; final localVersionStr = '${info.version}+${info.buildNumber}';
log('[Update] Local app version: $localVersionStr');
final latest = _ParsedVersion.tryParse(release.tagName); final latest = _ParsedVersion.tryParse(release.tagName);
final local = _ParsedVersion.tryParse(localVersionStr); final local = _ParsedVersion.tryParse(localVersionStr);
if (latest == null || local == null) { if (latest == null || local == null) {
log(
'[Update] Failed to parse versions. Latest: ${release.tagName}, Local: $localVersionStr',
);
// If parsing fails, do nothing silently // If parsing fails, do nothing silently
return; return;
} }
log('[Update] Parsed versions. Latest: $latest, Local: $local');
final needsUpdate = latest.compareTo(local) > 0; 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) // Delay to ensure UI is ready (if called at startup)
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
await showUpdateSheet(context, release); if (context.mounted) {
} catch (_) { await showUpdateSheet(context, release);
log('[Update] Update sheet shown.');
}
} catch (e) {
log('[Update] Error checking for updates: $e');
// Ignore errors (network, api, etc.) // Ignore errors (network, api, etc.)
return; return;
} }
@@ -126,25 +178,68 @@ class UpdateService {
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
useRootNavigator: true, useRootNavigator: true,
builder: builder: (ctx) {
(ctx) => _UpdateSheet( String? androidUpdateUrl;
release: release, if (Platform.isAndroid) {
onOpen: () async { androidUpdateUrl = _getAndroidUpdateUrl(release.assets);
final uri = Uri.parse(release.htmlUrl); }
if (await canLaunchUrl(uri)) { return _UpdateSheet(
await launchUrl(uri, mode: LaunchMode.externalApplication); 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. /// Fetch the latest release info from GitHub.
/// Public so other screens (e.g., About) can manually trigger update checks. /// Public so other screens (e.g., About) can manually trigger update checks.
Future<GithubReleaseInfo?> fetchLatestRelease() async { Future<GithubReleaseInfo?> fetchLatestRelease() async {
final resp = await _dio.get(_releasesLatestApi); final apiEndpoint =
if (resp.statusCode != 200) return null; 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>; final data = resp.data as Map<String, dynamic>;
log('[Update] Successfully fetched release data.');
final tagName = (data['tag_name'] ?? '').toString(); final tagName = (data['tag_name'] ?? '').toString();
final name = (data['name'] ?? tagName).toString(); final name = (data['name'] ?? tagName).toString();
@@ -152,25 +247,70 @@ class UpdateService {
final htmlUrl = (data['html_url'] ?? '').toString(); final htmlUrl = (data['html_url'] ?? '').toString();
final createdAtStr = (data['created_at'] ?? '').toString(); final createdAtStr = (data['created_at'] ?? '').toString();
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now(); 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( return GithubReleaseInfo(
tagName: tagName, tagName: tagName,
name: name, name: name,
body: body, body: body,
htmlUrl: htmlUrl, htmlUrl: htmlUrl,
createdAt: createdAt, createdAt: createdAt,
assets: assetsData,
); );
} }
} }
class _UpdateSheet extends StatelessWidget { class _UpdateSheet extends StatefulWidget {
const _UpdateSheet({required this.release, required this.onOpen}); const _UpdateSheet({
required this.release,
required this.onOpen,
this.androidUpdateUrl,
this.useProxy = false,
});
final String? androidUpdateUrl;
final bool useProxy;
final GithubReleaseInfo release; final GithubReleaseInfo release;
final VoidCallback onOpen; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -186,8 +326,11 @@ class _UpdateSheet extends StatelessWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(release.name, style: theme.textTheme.titleMedium).bold(), Text(
Text(release.tagName).fontSize(12), widget.release.name,
style: theme.textTheme.titleMedium,
).bold(),
Text(widget.release.tagName).fontSize(12),
], ],
).padding(vertical: 16, horizontal: 16), ).padding(vertical: 16, horizontal: 16),
const Divider(height: 1), const Divider(height: 1),
@@ -197,21 +340,45 @@ class _UpdateSheet extends StatelessWidget {
horizontal: 16, horizontal: 16,
vertical: 16, vertical: 16,
), ),
child: SelectableText( child: MarkdownTextContent(
release.body.isEmpty content:
? 'No changelog provided.' widget.release.body.isEmpty
: release.body, ? 'No changelog provided.'
style: theme.textTheme.bodyMedium, : 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( Column(
children: [ children: [
Row( Row(
spacing: 8,
children: [ 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( Expanded(
child: FilledButton.icon( child: FilledButton.icon(
onPressed: onOpen, onPressed: widget.onOpen,
icon: const Icon(Icons.open_in_new), icon: const Icon(Icons.open_in_new),
label: const Text('Open release page'), label: const Text('Open release page'),
), ),

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -44,9 +45,8 @@ class AccountPickerSheet extends HookConsumerWidget {
} }
return Container( return Container(
constraints: BoxConstraints( padding: MediaQuery.of(context).viewInsets,
maxHeight: MediaQuery.of(context).size.height * 0.4, height: MediaQuery.of(context).size.height * 0.6,
),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
@@ -54,8 +54,8 @@ class AccountPickerSheet extends HookConsumerWidget {
child: TextField( child: TextField(
controller: searchController, controller: searchController,
onChanged: onSearchChanged, onChanged: onSearchChanged,
decoration: const InputDecoration( decoration: InputDecoration(
hintText: 'Search accounts...', hintText: 'searchAccounts'.tr(),
contentPadding: EdgeInsets.symmetric( contentPadding: EdgeInsets.symmetric(
horizontal: 18, horizontal: 18,
vertical: 16, vertical: 16,

View File

@@ -55,7 +55,7 @@ class AccountStatusCreationSheet extends HookConsumerWidget {
'attitude': attitude.value, 'attitude': attitude.value,
'is_invisible': isInvisible.value, 'is_invisible': isInvisible.value,
'is_not_disturb': isNotDisturb.value, 'is_not_disturb': isNotDisturb.value,
'cleared_at': clearedAt.value?.toIso8601String(), 'cleared_at': clearedAt.value?.toUtc().toIso8601String(),
if (labelController.text.isNotEmpty) 'label': labelController.text, if (labelController.text.isNotEmpty) 'label': labelController.text,
}, },
options: Options(method: initialStatus == null ? 'POST' : 'PATCH'), options: Options(method: initialStatus == null ? 'POST' : 'PATCH'),

View File

@@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/services/notify.dart'; import 'package:island/services/notify.dart';
import 'package:island/services/sharing_intent.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/content/network_status_sheet.dart';
import 'package:island/widgets/tour/tour.dart'; import 'package:island/widgets/tour/tour.dart';
@@ -21,6 +22,7 @@ class AppWrapper extends HookConsumerWidget {
}); });
final sharingService = SharingIntentService(); final sharingService = SharingIntentService();
sharingService.initialize(context); sharingService.initialize(context);
UpdateService().checkForUpdates(context);
return () { return () {
sharingService.dispose(); sharingService.dispose();
ntySubs?.cancel(); ntySubs?.cancel();

View File

@@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_highlight/themes/a11y-dark.dart'; import 'package:flutter_highlight/themes/a11y-dark.dart';
import 'package:flutter_highlight/themes/a11y-light.dart'; import 'package:flutter_highlight/themes/a11y-light.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
@@ -71,7 +72,22 @@ class MarkdownTextContent extends HookConsumerWidget {
textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!, textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium!,
), ),
HrConfig(height: 1, color: Theme.of(context).dividerColor), 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( LinkConfig(
style: style:
linkStyle ?? linkStyle ??
@@ -160,7 +176,7 @@ class MarkdownTextContent extends HookConsumerWidget {
uri: stickerUri, uri: stickerUri,
width: size, width: size,
height: size, height: size,
fit: BoxFit.cover, fit: BoxFit.contain,
noCacheOptimization: true, noCacheOptimization: true,
), ),
), ),

View 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,
);
}
}

View File

@@ -248,7 +248,7 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> {
try { try {
final client = ref.read(apiClientProvider); final client = ref.read(apiClientProvider);
final response = await client.post( final response = await client.post(
'/orders/${widget.order.id}/pay', '/id/orders/${widget.order.id}/pay',
data: {'pin_code': pin}, data: {'pin_code': pin},
); );

View File

@@ -273,7 +273,7 @@ class PostItem extends HookConsumerWidget {
: item.reactionsCount.entries : item.reactionsCount.entries
.sortedBy((e) => e.value) .sortedBy((e) => e.value)
.map((e) => e.key) .map((e) => e.key)
.first; .last;
final postLanguage = final postLanguage =
item.content != null 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(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: renderingPadding.horizontal, left: renderingPadding.horizontal,

View File

@@ -1,11 +1,13 @@
import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart'; import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart'; import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.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/alert.dart';
import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/publishers_modal.dart'; import 'package:island/widgets/post/publishers_modal.dart';
@@ -14,8 +16,14 @@ import 'package:styled_widget/styled_widget.dart';
class PostQuickReply extends HookConsumerWidget { class PostQuickReply extends HookConsumerWidget {
final SnPost parent; final SnPost parent;
final Function? onPosted; final VoidCallback? onPosted;
const PostQuickReply({super.key, required this.parent, this.onPosted}); final VoidCallback? onLaunch;
const PostQuickReply({
super.key,
required this.parent,
this.onPosted,
this.onLaunch,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -48,7 +56,7 @@ class PostQuickReply extends HookConsumerWidget {
'content': contentController.text, 'content': contentController.text,
'replied_post_id': parent.id, 'replied_post_id': parent.id,
}, },
options: Options(headers: {'X-Pub': currentPublisher.value?.name}), queryParameters: {'pub': currentPublisher.value?.name},
); );
contentController.clear(); contentController.clear();
onPosted?.call(); onPosted?.call();
@@ -83,9 +91,10 @@ class PostQuickReply extends HookConsumerWidget {
child: TextField( child: TextField(
controller: contentController, controller: contentController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Post your reply', hintText: 'postReplyPlaceholder'.tr(),
border: const OutlineInputBorder(), border: InputBorder.none,
isDense: true, isDense: true,
isCollapsed: true,
contentPadding: EdgeInsets.symmetric( contentPadding: EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
vertical: 8, vertical: 8,
@@ -97,6 +106,26 @@ class PostQuickReply extends HookConsumerWidget {
(_) => FocusManager.instance.primaryFocus?.unfocus(), (_) => 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( IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
@@ -110,6 +139,7 @@ class PostQuickReply extends HookConsumerWidget {
: Icon(Symbols.send, size: 20), : Icon(Symbols.send, size: 20),
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
onPressed: submitting.value ? null : performAction, onPressed: submitting.value ? null : performAction,
constraints: const BoxConstraints(),
), ),
], ],
), ),

View File

@@ -38,14 +38,18 @@ class PostRepliesSheet extends HookConsumerWidget {
if (user.value != null) if (user.value != null)
Material( Material(
elevation: 2, elevation: 2,
color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: PostQuickReply( child: PostQuickReply(
parent: post, parent: post,
onPosted: () { onPosted: () {
ref.invalidate(postRepliesNotifierProvider(post.id)); ref.invalidate(postRepliesNotifierProvider(post.id));
}, },
onLaunch: () {
Navigator.of(context).pop();
},
).padding( ).padding(
bottom: MediaQuery.of(context).padding.bottom + 16, bottom: MediaQuery.of(context).padding.bottom + 8,
top: 16, top: 8,
horizontal: 16, horizontal: 16,
), ),
), ),

View File

@@ -130,25 +130,25 @@ PODS:
- sqflite_darwin (0.0.4): - sqflite_darwin (0.0.4):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (3.50.3): - sqlite3 (3.50.4):
- sqlite3/common (= 3.50.3) - sqlite3/common (= 3.50.4)
- sqlite3/common (3.50.3) - sqlite3/common (3.50.4)
- sqlite3/dbstatvtab (3.50.3): - sqlite3/dbstatvtab (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/fts5 (3.50.3): - sqlite3/fts5 (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/math (3.50.3): - sqlite3/math (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/perf-threadsafe (3.50.3): - sqlite3/perf-threadsafe (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/rtree (3.50.3): - sqlite3/rtree (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3/session (3.50.3): - sqlite3/session (3.50.4):
- sqlite3/common - sqlite3/common
- sqlite3_flutter_libs (0.0.1): - sqlite3_flutter_libs (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- sqlite3 (~> 3.50.3) - sqlite3 (~> 3.50.4)
- sqlite3/dbstatvtab - sqlite3/dbstatvtab
- sqlite3/fts5 - sqlite3/fts5
- sqlite3/math - sqlite3/math
@@ -328,8 +328,8 @@ SPEC CHECKSUMS:
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 83105acd294c9137c026e2da1931c30b4588ab81 sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 616267f2fca40e9c6af8c5d82324e05667040b6e sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd

View File

@@ -662,6 +662,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_blurhash:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 3.1.0+120 version: 3.1.0+123
environment: environment:
sdk: ^3.7.2 sdk: ^3.7.2
@@ -133,6 +133,7 @@ dependencies:
flutter_typeahead: ^5.2.0 flutter_typeahead: ^5.2.0
flutter_langdetect: ^0.0.2 flutter_langdetect: ^0.0.2
waveform_flutter: ^1.2.0 waveform_flutter: ^1.2.0
flutter_app_update: ^3.2.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: