Compare commits

...

23 Commits

Author SHA1 Message Date
153c15e5c9 🚀 Launch 1.2.2+2 2024-09-18 13:05:08 +08:00
6a0f42cdc9 🐛 Fix realm view won't show channels 2024-09-18 13:03:40 +08:00
01aaa5455e 💄 Fix content padding mis-match 2024-09-18 00:14:16 +08:00
f3ceb5f967 🚀 Launch 1.2.2+1 2024-09-17 23:50:49 +08:00
b5e2fa4c25 🐛 Fix post editor alias overflow 2024-09-17 23:08:00 +08:00
8378024490 🚀 Launch 1.2.1+41 2024-09-17 22:31:37 +08:00
6d40d6bba3 💄 Optimize content 2024-09-17 21:48:20 +08:00
77075c8dab Optimize updater 2024-09-17 21:37:20 +08:00
dec34e297d 🐛 Bug fixes on attachments and related things 2024-09-17 20:59:01 +08:00
358677ade0 Android self-update 2024-09-17 20:40:44 +08:00
d2f37ae45d 🐛 Fix fileType render error 2024-09-17 18:28:53 +08:00
e4b741ff0c 🚀 Launch 1.2.1+40 2024-09-17 16:02:13 +08:00
e69abb7f9d Notification preferences 2024-09-17 15:59:17 +08:00
565a8e41cc Realm avatar, banner 2024-09-17 14:21:37 +08:00
c9fbe47337 Channel isPublic and isCommunity 2024-09-17 13:50:04 +08:00
01db63e297 🐛 Fix compability on iOS 18 and macOS 15 2024-09-17 13:39:08 +08:00
d87e67bd17 Subscriptions 2024-09-17 02:14:23 +08:00
06aa1fb359 🐛 Fix post last read at 2024-09-17 01:23:49 +08:00
62733bf29f 💄 Optimize featured reply style 2024-09-16 23:39:15 +08:00
ce16de9c71 Featured replies on post 2024-09-16 23:35:44 +08:00
47eb6cbc66 Chat list will also show wild group channel 2024-09-16 21:09:19 +08:00
029e72fb0b Improve sticker loading 2024-09-16 21:00:19 +08:00
152efd97a0 💄 Unified design of single attachment uploader 2024-09-16 20:33:34 +08:00
70 changed files with 1133 additions and 669 deletions

View File

@ -1,4 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="dev.solsynth.solian">
<uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" /> <uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />

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.6-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip

View File

@ -18,7 +18,7 @@ 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.4.0' apply false id "com.android.application" version '8.6.0' apply false
id "com.google.gms.google-services" version "4.3.15" apply false id "com.google.gms.google-services" version "4.3.15" apply false
id "com.google.firebase.crashlytics" version "2.8.1" apply false id "com.google.firebase.crashlytics" version "2.8.1" apply false
id "org.jetbrains.kotlin.android" version '2.0.0' apply false id "org.jetbrains.kotlin.android" version '2.0.0' apply false

View File

@ -55,7 +55,7 @@
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "delete": "Delete",
"settings": "Settings", "settings": "Settings",
"settingsNotificationBgService": "Background Notification Service", "settingsNotificationBgService": "Background notification service",
"settingsNotificationBgServiceDesc": "A notification service is always installed on the device, so that some devices that do not support push notifications can receive notifications in the background. When this feature is enabled, push notifications will not be registered with the server, and you will always appear to be online in the eyes of others (except for invisible). You may need to turn off power and traffic optimization in the settings.", "settingsNotificationBgServiceDesc": "A notification service is always installed on the device, so that some devices that do not support push notifications can receive notifications in the background. When this feature is enabled, push notifications will not be registered with the server, and you will always appear to be online in the eyes of others (except for invisible). You may need to turn off power and traffic optimization in the settings.",
"search": "Search", "search": "Search",
"post": "Post", "post": "Post",
@ -68,6 +68,11 @@
"notificationUnreadCount": "@count unread notifications", "notificationUnreadCount": "@count unread notifications",
"errorHappened": "An error occurred", "errorHappened": "An error occurred",
"errorHappenedUnauthorized": "Unauthorized request, please sign in or try resign in.", "errorHappenedUnauthorized": "Unauthorized request, please sign in or try resign in.",
"errorHappenedRequestBad": "Request error, the server refused to process the request. Please check your request data.",
"errorHappenedRequestForbidden": "Request error, insufficient permissions.",
"errorHappenedRequestNotFound": "Request error, the requested data does not exist.",
"errorHappenedRequestConnection": "Network request failed. Please check the connection status and service status, then try again.",
"errorHappenedRequestUnknown": "Request error, unknown type. Please take a full screenshot of this message and submit feedback.",
"forgotPassword": "Forgot password", "forgotPassword": "Forgot password",
"email": "Email", "email": "Email",
"username": "Username", "username": "Username",
@ -157,6 +162,9 @@
"postListNews": "News", "postListNews": "News",
"postListFriends": "Friends", "postListFriends": "Friends",
"postListShuffle": "Random", "postListShuffle": "Random",
"attachmentThumbnail": "Thumbnail",
"attachmentThumbnailAttachmentNew": "Upload thumbnail",
"attachmentThumbnailAttachment": "Attachment serial number",
"postEditorModeStory": "Post a post", "postEditorModeStory": "Post a post",
"postEditorModeArticle": "Post an article", "postEditorModeArticle": "Post an article",
"postEditor": "Post editor", "postEditor": "Post editor",
@ -225,6 +233,8 @@
"realmDescription": "Description", "realmDescription": "Description",
"realmPublic": "Public Realm", "realmPublic": "Public Realm",
"realmCommunity": "Community Realm", "realmCommunity": "Community Realm",
"realmAvatar": "Realm avatar",
"realmBanner": "Realm banner",
"realmDetail": "Realm detail", "realmDetail": "Realm detail",
"realmMember": "Realm member", "realmMember": "Realm member",
"realmMembers": "Realm members", "realmMembers": "Realm members",
@ -250,7 +260,8 @@
"channelName": "Name", "channelName": "Name",
"channelDescription": "Description", "channelDescription": "Description",
"channelDirectDescription": "Direct message with @username", "channelDirectDescription": "Direct message with @username",
"channelEncrypted": "Encrypted Channel", "channelPublic": "Public channel",
"channelCommunity": "Community channel",
"channelMember": "Channel member", "channelMember": "Channel member",
"channelMembers": "Channel members", "channelMembers": "Channel members",
"channelMembersAdd": "Add channel members", "channelMembersAdd": "Add channel members",
@ -344,8 +355,7 @@
"bsCheckForUpdate": "Checking For Updates", "bsCheckForUpdate": "Checking For Updates",
"bsCheckForUpdateFailed": "Unable to Check Updates", "bsCheckForUpdateFailed": "Unable to Check Updates",
"bsCheckForUpdateNew": "Found New Version", "bsCheckForUpdateNew": "Found New Version",
"bsCheckForUpdateDescApple": "Please head to TestFlight and update your app to latest version to prevent error happens and get latest functions.", "bsCheckForUpdateDesc": "Please head to app store and update your app to latest version to prevent error happens and get latest functions.",
"bsCheckForUpdateDescCommon": "Please head to our website download and install latest version of application to prevent error happens and get latest functions.",
"bsCheckingServer": "Checking Server Status", "bsCheckingServer": "Checking Server Status",
"bsCheckingServerFail": "Unable connect to server, check your network connection", "bsCheckingServerFail": "Unable connect to server, check your network connection",
"bsCheckingServerDown": "Server currently unavailable, please retry later", "bsCheckingServerDown": "Server currently unavailable, please retry later",
@ -407,5 +417,20 @@
"userLevel13": "Immortal", "userLevel13": "Immortal",
"postBrowsingIn": "Browsing in @region", "postBrowsingIn": "Browsing in @region",
"needRestartToApply": "Restart the application to take effect", "needRestartToApply": "Restart the application to take effect",
"holdToSeeDetail": "Long press / Mouse hover to see detail" "holdToSeeDetail": "Long press / Mouse hover to see detail",
"subscribe": "Subscribe",
"subscribed": "Subscribed",
"unsubscribe": "Unsubscribe",
"preferences": "Preferences",
"notificationPreferences": "Notification preferences",
"notificationTopicPostFeedback": "Post feedbacks",
"notificationTopicPostSubscription": "Post subscriptions",
"preferencesApplied": "Preferences has been applied.",
"save": "Save",
"updateAvailable": "Update available",
"updateAvailableDesc": "There is an update available (@version). Do you want to download and install it now? You can still use the app normally while waiting for the download to complete.",
"update": "Update",
"updateCheckStrictly": "Strict mode",
"updateCheckStrictlyDesc": "If enabled, the app will ask for updating once the local version is different from remote one.",
"updateMayAvailable": "App version @version is available, you can update from app store or our website."
} }

View File

@ -168,6 +168,9 @@
"postListNews": "新鲜事", "postListNews": "新鲜事",
"postListFriends": "好友圈", "postListFriends": "好友圈",
"postListShuffle": "打乱看", "postListShuffle": "打乱看",
"attachmentThumbnail": "附件缩略图",
"attachmentThumbnailAttachmentNew": "上传附件作为缩略图",
"attachmentThumbnailAttachment": "附件序列号",
"postNew": "创建新帖子", "postNew": "创建新帖子",
"postNewInRealmHint": "在领域 @realm 里发表新帖子", "postNewInRealmHint": "在领域 @realm 里发表新帖子",
"postAction": "发表", "postAction": "发表",
@ -226,6 +229,8 @@
"realmDescription": "领域简介", "realmDescription": "领域简介",
"realmPublic": "公开领域", "realmPublic": "公开领域",
"realmCommunity": "社区领域", "realmCommunity": "社区领域",
"realmAvatar": "领域头像",
"realmBanner": "领域横幅",
"realmDetail": "领域详情", "realmDetail": "领域详情",
"realmMember": "领域成员", "realmMember": "领域成员",
"realmMembers": "领域成员", "realmMembers": "领域成员",
@ -251,7 +256,8 @@
"channelName": "显示名称", "channelName": "显示名称",
"channelDescription": "频道简介", "channelDescription": "频道简介",
"channelDirectDescription": "与 @username 的私聊", "channelDirectDescription": "与 @username 的私聊",
"channelEncrypted": "加密频道", "channelPublic": "公开频道",
"channelCommunity": "社区频道",
"channelMember": "频道成员", "channelMember": "频道成员",
"channelMembers": "频道成员", "channelMembers": "频道成员",
"channelMembersAdd": "添加频道成员", "channelMembersAdd": "添加频道成员",
@ -345,8 +351,7 @@
"bsCheckForUpdate": "正在检查更新", "bsCheckForUpdate": "正在检查更新",
"bsCheckForUpdateFailed": "无法检查更新", "bsCheckForUpdateFailed": "无法检查更新",
"bsCheckForUpdateNew": "发现新版本", "bsCheckForUpdateNew": "发现新版本",
"bsCheckForUpdateDescApple": "请前往 TestFlight 并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。", "bsCheckForUpdateDesc": "请前往应用商店并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。",
"bsCheckForUpdateDescCommon": "请前往我们的网站下载并安装最新版本的应用程序,以防止出现错误并获取最新功能。",
"bsCheckingServer": "检查服务器状态中", "bsCheckingServer": "检查服务器状态中",
"bsCheckingServerFail": "无法连接至服务器,请检查你的网络连接状态", "bsCheckingServerFail": "无法连接至服务器,请检查你的网络连接状态",
"bsCheckingServerDown": "当前服务器不可用,请稍后重试", "bsCheckingServerDown": "当前服务器不可用,请稍后重试",
@ -408,5 +413,20 @@
"userLevel13": "万古流芳", "userLevel13": "万古流芳",
"postBrowsingIn": "浏览 @region 内的帖子中", "postBrowsingIn": "浏览 @region 内的帖子中",
"needRestartToApply": "需要重启应用来生效", "needRestartToApply": "需要重启应用来生效",
"holdToSeeDetail": "长按 / 鼠标悬浮来查看详情" "holdToSeeDetail": "长按 / 鼠标悬浮来查看详情",
"subscribe": "订阅",
"subscribed": "已订阅",
"unsubscribe": "取消订阅",
"preferences": "偏好设置",
"notificationPreferences": "通知偏好设置",
"notificationTopicPostFeedback": "帖子反馈",
"notificationTopicPostSubscription": "订阅源",
"preferencesApplied": "偏好设置已应用",
"save": "保存",
"updateAvailable": "有可用更新",
"updateAvailableDesc": "有可用更新 (@from 到 @to) 你想现在下载安装吗?在等待下载期间你仍可以正常使用。",
"update": "更新",
"updateCheckStrictly": "严格模式",
"updateCheckStrictlyDesc": "如果启用,应用程序将会在本地版本与远程版本不同时询问更新,而不会检查版本号大小。",
"updateMayAvailable": "版本 @version 现已可用,你可以前往应用商店或是我们的官网下载更新。"
} }

View File

@ -54,26 +54,26 @@ PODS:
- Firebase/Performance (11.0.0): - Firebase/Performance (11.0.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebasePerformance (~> 11.0.0) - FirebasePerformance (~> 11.0.0)
- firebase_analytics (11.3.1): - firebase_analytics (11.3.2):
- Firebase/Analytics (= 11.0.0) - Firebase/Analytics (= 11.0.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_core (3.4.1): - firebase_core (3.5.0):
- Firebase/CoreOnly (= 11.0.0) - Firebase/CoreOnly (= 11.0.0)
- Flutter - Flutter
- firebase_crashlytics (4.1.1): - firebase_crashlytics (4.1.2):
- Firebase/Crashlytics (= 11.0.0) - Firebase/Crashlytics (= 11.0.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_messaging (15.1.1): - firebase_messaging (15.1.2):
- Firebase/Messaging (= 11.0.0) - Firebase/Messaging (= 11.0.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_performance (0.10.0-6): - firebase_performance (0.10.0-7):
- Firebase/Performance (= 11.0.0) - Firebase/Performance (= 11.0.0)
- firebase_core - firebase_core
- Flutter - Flutter
- FirebaseABTesting (11.1.0): - FirebaseABTesting (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseAnalytics (11.0.0): - FirebaseAnalytics (11.0.0):
- FirebaseAnalytics/AdIdSupport (= 11.0.0) - FirebaseAnalytics/AdIdSupport (= 11.0.0)
@ -97,9 +97,9 @@ PODS:
- FirebaseCoreInternal (~> 11.0) - FirebaseCoreInternal (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0) - GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.1.0): - FirebaseCoreExtension (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseCoreInternal (11.1.0): - FirebaseCoreInternal (11.2.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseCrashlytics (11.0.0): - FirebaseCrashlytics (11.0.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
@ -110,7 +110,7 @@ PODS:
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- FirebaseInstallations (11.1.0): - FirebaseInstallations (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
@ -134,7 +134,7 @@ PODS:
- GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseRemoteConfig (11.1.0): - FirebaseRemoteConfig (11.2.0):
- FirebaseABTesting (~> 11.0) - FirebaseABTesting (~> 11.0)
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
@ -142,8 +142,8 @@ PODS:
- FirebaseSharedSwift (~> 11.0) - FirebaseSharedSwift (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseRemoteConfigInterop (11.1.0) - FirebaseRemoteConfigInterop (11.2.0)
- FirebaseSessions (11.1.0): - FirebaseSessions (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseCoreExtension (~> 11.0) - FirebaseCoreExtension (~> 11.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
@ -152,8 +152,10 @@ PODS:
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- PromisesSwift (~> 2.1) - PromisesSwift (~> 2.1)
- FirebaseSharedSwift (11.1.0) - FirebaseSharedSwift (11.2.0)
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_app_update (0.0.1):
- Flutter
- flutter_background_service_ios (0.0.3): - flutter_background_service_ios (0.0.3):
- Flutter - Flutter
- flutter_keyboard_visibility (0.0.1): - flutter_keyboard_visibility (0.0.1):
@ -306,6 +308,7 @@ DEPENDENCIES:
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- firebase_performance (from `.symlinks/plugins/firebase_performance/ios`) - firebase_performance (from `.symlinks/plugins/firebase_performance/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`)
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`) - flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
@ -383,6 +386,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_performance/ios" :path: ".symlinks/plugins/firebase_performance/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_app_update:
:path: ".symlinks/plugins/flutter_app_update/ios"
flutter_background_service_ios: flutter_background_service_ios:
:path: ".symlinks/plugins/flutter_background_service_ios/ios" :path: ".symlinks/plugins/flutter_background_service_ios/ios"
flutter_keyboard_visibility: flutter_keyboard_visibility:
@ -445,25 +450,26 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9 Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
firebase_analytics: b8ce6c2c4b245d3c3bb3a147965d09da0f455959 firebase_analytics: 4fd10182fd08bb8358f26ac8aca8dad7b6d0f592
firebase_core: ba84e940cf5cbbc601095f86556560937419195c firebase_core: 2ec6b789859c7c24766344ec71fdf78639402d56
firebase_crashlytics: 4111f8198b78c99471c955af488cecd8224967e6 firebase_crashlytics: 60630a0f91ee432275fa1660fd8593079761448a
firebase_messaging: c40f84e7a98da956d5262fada373b5c458edcf13 firebase_messaging: a18e1e02b2e8e69097c8173e0c851be223b21c50
firebase_performance: 8b7b9ca5adf3a9b3afa12b4eb96b9cabefc2c248 firebase_performance: 12d45fdf120992fa879d990929bf73d4a5ced053
FirebaseABTesting: c2e22c3aab99afa81d0561708b2c1c356c556976 FirebaseABTesting: 2104d957ce33888a3d6f3bde298cdee376dde8f1
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383 FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
FirebaseCoreExtension: aa5c9779c2d0d39d83f1ceb3fdbafe80c4feecfa FirebaseCoreExtension: cda74ddfb001224bd8fd1d6e74698b4ed07803de
FirebaseCoreInternal: adefedc9a88dbe393c4884640a73ec9e8e790f8c FirebaseCoreInternal: 0c569513412da9f3b31bd0b340013bbee8f295c5
FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b
FirebaseInstallations: d0a8fea5a6fa91abc661591cf57c0f0d70863e57 FirebaseInstallations: 771177d89d6c451dc6e50085ec82e2fc77ed0a4a
FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742 FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742
FirebasePerformance: efdc02bacb1b4710588c9f867011605c081cdf79 FirebasePerformance: efdc02bacb1b4710588c9f867011605c081cdf79
FirebaseRemoteConfig: 05521e937b72e01847a7128da5a492327364c705 FirebaseRemoteConfig: fca0b2d017fc1de52b28a4e5bcf2007c1a840457
FirebaseRemoteConfigInterop: abf8b1bbc0bf1b84abd22b66746926410bf91a87 FirebaseRemoteConfigInterop: 477b26fdeb8fb5fbaf22fa9db5343b42289dc7db
FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a FirebaseSessions: adcec8b72d0066a385e3affcd1bcb1ebb3908ce6
FirebaseSharedSwift: 260a35e08943ec810d820a70bc0359136351d0c5 FirebaseSharedSwift: 7a0d78d155ede78407f0fdc89fbc914014c7c540
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086

View File

@ -616,6 +616,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@ -920,6 +921,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@ -947,6 +949,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";

View File

@ -1,19 +1,23 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
import 'package:flutter_app_update/flutter_app_update.dart';
import 'package:version/version.dart';
class BootstrapperShell extends StatefulWidget { class BootstrapperShell extends StatefulWidget {
final Widget child; final Widget child;
@ -35,6 +39,8 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
int _periodCursor = 0; int _periodCursor = 0;
final Completer _bootCompleter = Completer();
late final List<({String label, Future<void> Function() action})> _periods = [ late final List<({String label, Future<void> Function() action})> _periods = [
( (
label: 'bsLoadingTheme', label: 'bsLoadingTheme',
@ -47,24 +53,60 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
action: () async { action: () async {
if (PlatformInfo.isWeb) return; if (PlatformInfo.isWeb) return;
try { try {
final prefs = await SharedPreferences.getInstance();
final info = await PackageInfo.fromPlatform(); final info = await PackageInfo.fromPlatform();
final localVersionString = '${info.version}+${info.buildNumber}'; final localVersionString = '${info.version}+${info.buildNumber}';
final resp = await GetConnect().get( final resp = await GetConnect().get(
'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?limit=1', 'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?page=1&limit=1',
); );
if (resp.body[0]['name'] != localVersionString) { final remoteVersionString =
(resp.body as List).firstOrNull?['name'] ?? '0.0.0';
final remoteVersion = Version.parse(remoteVersionString ?? '0.0.0');
final localVersion =
Version.parse(localVersionString.split('+').first);
final strictUpdate = prefs.getBool('check_update_strictly') ?? false;
if (remoteVersion > localVersion ||
(remoteVersionString != localVersionString && strictUpdate)) {
setState(() { setState(() {
_isErrored = true; _isErrored = true;
_subtitle = PlatformInfo.isIOS || PlatformInfo.isMacOS _subtitle = 'bsCheckForUpdateDesc'.tr;
? 'bsCheckForUpdateDescApple'.tr });
: 'bsCheckForUpdateDescCommon'.tr;
if (PlatformInfo.isAndroid) {
context
.showConfirmDialog(
'updateAvailable'.tr,
'updateAvailableDesc'.trParams({
'from': localVersionString,
'to': remoteVersionString,
}),
)
.then((result) {
if (result) {
final model = UpdateModel(
'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk',
'solian-app-arm64-v8a-release.apk',
'ic_launcher',
'https://testflight.apple.com/join/YJ0lmN6O',
);
AzhonAppUpdate.update(model);
}
});
} else {
setState(() {
_isErrored = true;
_subtitle = 'bsCheckForUpdateDesc'.tr;
});
}
} else if (remoteVersionString != localVersionString) {
_bootCompleter.future.then((_) {
context.showSnackbar('updateMayAvailable'.trParams({
'version': remoteVersionString,
}));
}); });
} }
} catch (e) { } catch (e) {
setState(() { context.showErrorDialog('Unable to check update: $e');
_isErrored = true;
_subtitle = 'bsCheckForUpdateFailed'.tr;
});
} }
}, },
), ),
@ -115,7 +157,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
try { try {
await Future.wait([ await Future.wait([
Get.find<StickerProvider>().refreshAvailableStickers(),
if (auth.isAuthorized.isTrue) if (auth.isAuthorized.isTrue)
Get.find<ChannelProvider>().refreshAvailableChannel(), Get.find<ChannelProvider>().refreshAvailableChannel(),
if (auth.isAuthorized.isTrue) if (auth.isAuthorized.isTrue)
@ -156,6 +197,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
} }
} finally { } finally {
setState(() => _isBusy = false); setState(() => _isBusy = false);
Future.delayed(const Duration(milliseconds: 100), () {
_bootCompleter.complete();
});
} }
} }
@ -253,6 +297,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
_isBusy = false; _isBusy = false;
_isErrored = false; _isErrored = false;
}); });
Future.delayed(const Duration(milliseconds: 100), () {
_bootCompleter.complete();
});
} else { } else {
setState(() { setState(() {
_isBusy = true; _isBusy = true;

View File

@ -51,6 +51,28 @@ extension AppExtensions on BuildContext {
); );
} }
Future<bool> showConfirmDialog(String title, body) async {
return await showDialog<bool>(
useRootNavigator: true,
context: this,
builder: (ctx) => AlertDialog(
title: Text(title),
content: Text(body),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text('cancel'.tr),
),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: Text('okay'.tr),
)
],
),
) ??
false;
}
Future<void> showErrorDialog(dynamic exception) { Future<void> showErrorDialog(dynamic exception) {
Widget content = Text(exception.toString().capitalize!); Widget content = Text(exception.toString().capitalize!);
if (exception is UnauthorizedException) { if (exception is UnauthorizedException) {

View File

@ -9,7 +9,6 @@ import 'package:go_router/go_router.dart';
import 'package:protocol_handler/protocol_handler.dart'; import 'package:protocol_handler/protocol_handler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:solian/background.dart'; import 'package:solian/background.dart';
import 'package:solian/bootstrapper.dart';
import 'package:solian/firebase_options.dart'; import 'package:solian/firebase_options.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/attachment_uploader.dart'; import 'package:solian/providers/attachment_uploader.dart';
@ -20,6 +19,7 @@ import 'package:solian/providers/last_read.dart';
import 'package:solian/providers/link_expander.dart'; import 'package:solian/providers/link_expander.dart';
import 'package:solian/providers/navigation.dart'; import 'package:solian/providers/navigation.dart';
import 'package:solian/providers/stickers.dart'; import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/subscription.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
@ -122,9 +122,7 @@ class SolianApp extends StatelessWidget {
builder: (context, child) { builder: (context, child) {
return SystemShell( return SystemShell(
child: ScaffoldMessenger( child: ScaffoldMessenger(
child: BootstrapperShell( child: child ?? const SizedBox.shrink(),
child: child ?? const SizedBox.shrink(),
),
), ),
); );
}, },
@ -151,6 +149,7 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => LinkExpandProvider()); Get.lazyPut(() => LinkExpandProvider());
Get.lazyPut(() => DailySignProvider()); Get.lazyPut(() => DailySignProvider());
Get.lazyPut(() => LastReadProvider()); Get.lazyPut(() => LastReadProvider());
Get.lazyPut(() => SubscriptionProvider());
Get.find<WebSocketProvider>().requestPermissions(); Get.find<WebSocketProvider>().requestPermissions();
} }

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'account.g.dart'; part 'account.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'account_status.g.dart'; part 'account_status.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
part 'attachment.g.dart'; part 'attachment.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
part 'auth.g.dart'; part 'auth.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
@ -19,7 +19,8 @@ class Channel {
int accountId; int accountId;
Realm? realm; Realm? realm;
int? realmId; int? realmId;
bool isEncrypted; bool isPublic;
bool isCommunity;
@JsonKey(includeFromJson: false, includeToJson: true) @JsonKey(includeFromJson: false, includeToJson: true)
bool isAvailable = false; bool isAvailable = false;
@ -36,7 +37,8 @@ class Channel {
required this.members, required this.members,
required this.account, required this.account,
required this.accountId, required this.accountId,
required this.isEncrypted, required this.isPublic,
required this.isCommunity,
required this.realm, required this.realm,
required this.realmId, required this.realmId,
}); });

View File

@ -22,7 +22,8 @@ Channel _$ChannelFromJson(Map<String, dynamic> json) => Channel(
.toList(), .toList(),
account: Account.fromJson(json['account'] as Map<String, dynamic>), account: Account.fromJson(json['account'] as Map<String, dynamic>),
accountId: (json['account_id'] as num).toInt(), accountId: (json['account_id'] as num).toInt(),
isEncrypted: json['is_encrypted'] as bool, isPublic: json['is_public'] as bool,
isCommunity: json['is_community'] as bool,
realm: json['realm'] == null realm: json['realm'] == null
? null ? null
: Realm.fromJson(json['realm'] as Map<String, dynamic>), : Realm.fromJson(json['realm'] as Map<String, dynamic>),
@ -43,7 +44,8 @@ Map<String, dynamic> _$ChannelToJson(Channel instance) => <String, dynamic>{
'account_id': instance.accountId, 'account_id': instance.accountId,
'realm': instance.realm?.toJson(), 'realm': instance.realm?.toJson(),
'realm_id': instance.realmId, 'realm_id': instance.realmId,
'is_encrypted': instance.isEncrypted, 'is_public': instance.isPublic,
'is_community': instance.isCommunity,
'is_available': instance.isAvailable, 'is_available': instance.isAvailable,
}; };

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
part 'event.g.dart'; part 'event.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'link.g.dart'; part 'link.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'notification.g.dart'; part 'notification.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'packet.g.dart'; part 'packet.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'pagination.g.dart'; part 'pagination.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/post_categories.dart'; import 'package:solian/models/post_categories.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'post_categories.g.dart'; part 'post_categories.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
part 'realm.g.dart'; part 'realm.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
part 'relations.g.dart'; part 'relations.g.dart';

View File

@ -1,4 +1,4 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';

View File

@ -0,0 +1,41 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/post_categories.dart';
part 'subscription.g.dart';
@JsonSerializable()
class Subscription {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
int followerId;
Account follower;
int? accountId;
Account? account;
int? tagId;
Tag? tag;
int? categoryId;
Category? category;
Subscription({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.followerId,
required this.follower,
required this.accountId,
required this.account,
required this.tagId,
required this.tag,
required this.categoryId,
required this.category,
});
factory Subscription.fromJson(Map<String, dynamic> json) =>
_$SubscriptionFromJson(json);
Map<String, dynamic> toJson() => _$SubscriptionToJson(this);
}

View File

@ -0,0 +1,46 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'subscription.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Subscription _$SubscriptionFromJson(Map<String, dynamic> json) => Subscription(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
followerId: (json['follower_id'] as num).toInt(),
follower: Account.fromJson(json['follower'] as Map<String, dynamic>),
accountId: (json['account_id'] as num?)?.toInt(),
account: json['account'] == null
? null
: Account.fromJson(json['account'] as Map<String, dynamic>),
tagId: (json['tag_id'] as num?)?.toInt(),
tag: json['tag'] == null
? null
: Tag.fromJson(json['tag'] as Map<String, dynamic>),
categoryId: (json['category_id'] as num?)?.toInt(),
category: json['category'] == null
? null
: Category.fromJson(json['category'] as Map<String, dynamic>),
);
Map<String, dynamic> _$SubscriptionToJson(Subscription instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'follower_id': instance.followerId,
'follower': instance.follower.toJson(),
'account_id': instance.accountId,
'account': instance.account?.toJson(),
'tag_id': instance.tagId,
'tag': instance.tag?.toJson(),
'category_id': instance.categoryId,
'category': instance.category?.toJson(),
};

View File

@ -89,13 +89,13 @@ class ChannelProvider extends GetxController {
return resp; return resp;
} }
Future<Response> listAvailableChannel({String realm = 'global'}) async { Future<Response> listAvailableChannel({String scope = 'global'}) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw const UnauthorizedException(); if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = await auth.configureClient('messaging'); final client = await auth.configureClient('messaging');
final resp = await client.get('/channels/$realm/me/available'); final resp = await client.get('/channels/$scope/me/available');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw RequestException(resp); throw RequestException(resp);
} }

View File

@ -1,6 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart'; import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart'; import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@ -96,6 +97,15 @@ class PostProvider extends GetConnect {
return resp; return resp;
} }
Future<List<Post>> listPostFeaturedReply(String alias, {int take = 1}) async {
final resp = await get('/posts/$alias/replies/featured?take=$take');
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return List<Post>.from(resp.body.map((x) => Post.fromJson(x)));
}
Future<Response> getPost(String alias) async { Future<Response> getPost(String alias) async {
final resp = await get('/posts/$alias'); final resp = await get('/posts/$alias');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {

View File

@ -1,34 +1,48 @@
import 'dart:async';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/exceptions/request.dart';
import 'package:solian/models/stickers.dart'; import 'package:solian/models/stickers.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
class StickerProvider extends GetxController { class StickerProvider extends GetxController {
final RxMap<String, String> aliasImageMapping = RxMap(); final RxMap<String, FutureOr<Sticker?>> stickerCache = RxMap();
final RxList<Sticker> availableStickers = RxList.empty(growable: true);
Future<void> refreshAvailableStickers() async { Future<Sticker?> getStickerByAlias(String alias) {
availableStickers.clear(); if (stickerCache.containsKey(alias)) {
aliasImageMapping.clear(); return Future.value(stickerCache[alias]);
}
stickerCache[alias] = Future(() async {
final client = await ServiceFinder.configureClient('files');
final resp = await client.get(
'/stickers/lookup/$alias',
);
if (resp.statusCode != 200) {
if (resp.statusCode == 404) {
stickerCache[alias] = null;
}
throw RequestException(resp);
}
return Sticker.fromJson(resp.body);
}).then((result) {
stickerCache[alias] = result;
return result;
});
return Future.value(stickerCache[alias]);
}
Future<List<Sticker>> searchStickerByAlias(String alias) async {
final client = await ServiceFinder.configureClient('files'); final client = await ServiceFinder.configureClient('files');
final resp = await client.get( final resp = await client.get(
'/stickers/manifest?take=100', '/stickers/lookup?probe=$alias',
); );
if (resp.statusCode == 200) { if (resp.statusCode != 200) {
final result = PaginationResult.fromJson(resp.body); throw RequestException(resp);
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
if (out == null) return;
for (final pack in out) {
for (final sticker in (pack.stickers ?? List<Sticker>.empty())) {
sticker.pack = pack;
aliasImageMapping[sticker.textPlaceholder.toUpperCase()] =
sticker.imageUrl;
availableStickers.add(sticker);
}
}
} }
availableStickers.refresh();
return List<Sticker>.from(resp.body.map((x) => Sticker.fromJson(x)));
} }
} }

View File

@ -0,0 +1,46 @@
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/models/subscription.dart';
import 'package:solian/providers/auth.dart';
class SubscriptionProvider extends GetxController {
Future<Subscription?> getSubscriptionOnUser(int userId) async {
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw const UnauthorizedException();
final client = await auth.configureClient('co');
final resp = await client.get('/subscriptions/users/$userId');
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Subscription.fromJson(resp.body);
}
Future<Subscription> subscribeToUser(int userId) async {
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw const UnauthorizedException();
final client = await auth.configureClient('co');
final resp = await client.post('/subscriptions/users/$userId', {});
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Subscription.fromJson(resp.body);
}
Future<void> unsubscribeFromUser(int userId) async {
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw const UnauthorizedException();
final client = await auth.configureClient('co');
final resp = await client.delete('/subscriptions/users/$userId');
if (resp.statusCode != 200) {
throw RequestException(resp);
}
}
}

View File

@ -1,13 +1,14 @@
import 'package:animations/animations.dart'; import 'package:animations/animations.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:solian/bootstrapper.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
import 'package:solian/screens/about.dart'; import 'package:solian/screens/about.dart';
import 'package:solian/screens/account.dart'; import 'package:solian/screens/account.dart';
import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/friend.dart';
import 'package:solian/screens/account/personalize.dart'; import 'package:solian/screens/account/preferences/notifications.dart';
import 'package:solian/screens/account/profile_edit.dart';
import 'package:solian/screens/account/profile_page.dart'; import 'package:solian/screens/account/profile_page.dart';
import 'package:solian/screens/account/stickers.dart';
import 'package:solian/screens/auth/signin.dart'; import 'package:solian/screens/auth/signin.dart';
import 'package:solian/screens/auth/signup.dart'; import 'package:solian/screens/auth/signup.dart';
import 'package:solian/screens/channel/channel_chat.dart'; import 'package:solian/screens/channel/channel_chat.dart';
@ -32,9 +33,12 @@ abstract class AppRouter {
static GoRouter instance = GoRouter( static GoRouter instance = GoRouter(
routes: [ routes: [
ShellRoute( ShellRoute(
builder: (context, state, child) => RootShell( builder: (context, state, child) => BootstrapperShell(
state: state, key: const Key('global-bootstrapper'),
child: child, child: RootShell(
state: state,
child: child,
),
), ),
routes: [ routes: [
GoRoute( GoRoute(
@ -238,14 +242,6 @@ abstract class AppRouter {
name: 'accountFriend', name: 'accountFriend',
builder: (context, state) => const FriendScreen(), builder: (context, state) => const FriendScreen(),
), ),
GoRoute(
path: '/account/stickers',
name: 'accountStickers',
builder: (context, state) => TitleShell(
state: state,
child: const StickerScreen(),
),
),
GoRoute( GoRoute(
path: '/account/personalize', path: '/account/personalize',
name: 'accountProfile', name: 'accountProfile',
@ -254,6 +250,14 @@ abstract class AppRouter {
child: const PersonalizeScreen(), child: const PersonalizeScreen(),
), ),
), ),
GoRoute(
path: '/account/preferences/notifications',
name: 'notificationPreferences',
builder: (context, state) => TitleShell(
state: state,
child: const NotificationPreferencesScreen(),
),
),
GoRoute( GoRoute(
path: '/account/view/:name', path: '/account/view/:name',
name: 'accountProfilePage', name: 'accountProfilePage',

View File

@ -45,11 +45,6 @@ class _AccountScreenState extends State<AccountScreen> {
'accountFriend'.tr, 'accountFriend'.tr,
'accountFriend', 'accountFriend',
), ),
(
const Icon(Icons.emoji_symbols),
'accountStickers'.tr,
'accountStickers',
),
]; ];
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
@ -136,6 +131,15 @@ class _AccountScreenState extends State<AccountScreen> {
AppRouter.instance.pushNamed('settings'); AppRouter.instance.pushNamed('settings');
}, },
), ),
if (auth.isAuthorized.value)
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
leading: const Icon(Icons.edit_notifications),
title: Text('notificationPreferences'.tr),
onTap: () {
AppRouter.instance.pushNamed('notificationPreferences');
},
),
const Divider(thickness: 0.3, height: 1) const Divider(thickness: 0.3, height: 1)
.paddingSymmetric(vertical: 4), .paddingSymmetric(vertical: 4),
ListTile( ListTile(

View File

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:get/get_connect/http/src/exceptions/exceptions.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart';
import 'package:solian/providers/auth.dart';
class NotificationPreferencesScreen extends StatefulWidget {
const NotificationPreferencesScreen({super.key});
@override
State<NotificationPreferencesScreen> createState() =>
_NotificationPreferencesScreenState();
}
class _NotificationPreferencesScreenState
extends State<NotificationPreferencesScreen> {
bool _isBusy = true;
Map<String, bool> _config = {};
final Map<String, String> _topicMap = {
'interactive.feedback': 'notificationTopicPostFeedback'.tr,
'interactive.subscription': 'notificationTopicPostSubscription'.tr,
};
Future<void> _getPreferences() async {
setState(() => _isBusy = true);
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw UnauthorizedException();
final client = await auth.configureClient('id');
final resp = await client.get('/preferences/notifications');
if (resp.statusCode != 200 && resp.statusCode != 404) {
context.showErrorDialog(RequestException(resp));
}
if (resp.statusCode == 200) {
_config = resp.body['config']
.map((k, v) => MapEntry(k, v as bool))
.cast<String, bool>();
}
setState(() => _isBusy = false);
}
Future<void> _savePreferences() async {
setState(() => _isBusy = true);
final auth = Get.find<AuthProvider>();
if (!auth.isAuthorized.value) throw UnauthorizedException();
final client = await auth.configureClient('id');
final resp = await client.put('/preferences/notifications', {
'config': _config,
});
if (resp.statusCode != 200) {
context.showErrorDialog(RequestException(resp));
}
context.showSnackbar('preferencesApplied'.tr);
setState(() => _isBusy = false);
}
@override
void initState() {
super.initState();
_getPreferences();
}
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: Column(
children: [
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainer,
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.save),
title: Text('save'.tr),
enabled: !_isBusy,
onTap: () {
_savePreferences();
},
),
Expanded(
child: ListView.builder(
itemCount: _topicMap.length,
itemBuilder: (context, index) {
final element = _topicMap.entries.elementAt(index);
return CheckboxListTile(
title: Text(element.value),
subtitle: Text(
element.key,
style: GoogleFonts.robotoMono(fontSize: 12),
),
value: _config[element.key] ?? true,
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onChanged: (value) {
setState(() {
_config[element.key] = value ?? false;
});
},
);
},
),
),
],
),
);
}
}

View File

@ -8,8 +8,10 @@ import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/models/subscription.dart';
import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/providers/subscription.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
@ -37,12 +39,21 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
bool _isBusy = true; bool _isBusy = true;
bool _isMakingFriend = false; bool _isMakingFriend = false;
bool _isSubscribing = false;
bool _showMature = false; bool _showMature = false;
Account? _userinfo; Account? _userinfo;
Subscription? _subscription;
List<Post> _pinnedPosts = List.empty(); List<Post> _pinnedPosts = List.empty();
int _totalUpvote = 0, _totalDownvote = 0; int _totalUpvote = 0, _totalDownvote = 0;
Future<void> _getSubscription() async {
setState(() => _isSubscribing = true);
_subscription = await Get.find<SubscriptionProvider>()
.getSubscriptionOnUser(_userinfo!.id);
setState(() => _isSubscribing = false);
}
Future<void> _getUserinfo() async { Future<void> _getUserinfo() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
@ -70,7 +81,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
Future<void> getPinnedPosts() async { Future<void> _getPinnedPosts() async {
final client = await ServiceFinder.configureClient('interactive'); final client = await ServiceFinder.configureClient('interactive');
final resp = await client.get('/users/${widget.name}/pin'); final resp = await client.get('/users/${widget.name}/pin');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
@ -115,8 +126,10 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
} }
}); });
_getUserinfo(); _getUserinfo().then((_) {
getPinnedPosts(); _getSubscription();
_getPinnedPosts();
});
} }
Widget _buildStatisticsEntry(String label, String content) { Widget _buildStatisticsEntry(String label, String content) {
@ -180,6 +193,40 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
], ],
), ),
), ),
if (_userinfo != null && _subscription == null)
OutlinedButton(
style: const ButtonStyle(
visualDensity:
VisualDensity(horizontal: -4, vertical: -2),
),
onPressed: _isSubscribing
? null
: () async {
setState(() => _isSubscribing = true);
_subscription =
await Get.find<SubscriptionProvider>()
.subscribeToUser(_userinfo!.id);
setState(() => _isSubscribing = false);
},
child: Text('subscribe'.tr),
)
else if (_userinfo != null)
OutlinedButton(
style: const ButtonStyle(
visualDensity:
VisualDensity(horizontal: -4, vertical: -2),
),
onPressed: _isSubscribing
? null
: () async {
setState(() => _isSubscribing = true);
await Get.find<SubscriptionProvider>()
.unsubscribeFromUser(_userinfo!.id);
_subscription = null;
setState(() => _isSubscribing = false);
},
child: Text('unsubscribe'.tr),
),
if (_userinfo != null && if (_userinfo != null &&
!_relationshipProvider.hasFriend(_userinfo!)) !_relationshipProvider.hasFriend(_userinfo!))
IconButton( IconButton(
@ -245,7 +292,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
RefreshIndicator( RefreshIndicator(
onRefresh: () => Future.wait([ onRefresh: () => Future.wait([
_postController.reloadAllOver(), _postController.reloadAllOver(),
getPinnedPosts(), _getPinnedPosts(),
]), ]),
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
@ -302,6 +349,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
isClickable: true, isClickable: true,
isNestedClickable: true, isNestedClickable: true,
isShowEmbed: true, isShowEmbed: true,
showFeaturedReply: true,
onUpdate: () { onUpdate: () {
_postController.reloadAllOver(); _postController.reloadAllOver();
}, },
@ -352,7 +400,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
child: AttachmentListEntry( child: AttachmentListEntry(
item: item, item: item,
isDense: true, isDense: true,
parentId: 'album', parentId: 'album-$index',
showMature: _showMature, showMature: _showMature,
onReveal: (value) { onReveal: (value) {
setState(() => _showMature = value); setState(() => _showMature = value);

View File

@ -1,186 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/stickers.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/stickers/sticker_uploader.dart';
class StickerScreen extends StatefulWidget {
const StickerScreen({super.key});
@override
State<StickerScreen> createState() => _StickerScreenState();
}
class _StickerScreenState extends State<StickerScreen> {
final PagingController<int, StickerPack> _pagingController =
PagingController(firstPageKey: 0);
Future<bool> _promptDelete(Sticker item, String prefix) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return false;
final confirm = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('stickerDeletionConfirm'.tr),
content: Text(
'stickerDeletionConfirmCaption'.trParams({
'name': ':${'$prefix${item.alias}'.camelCase}:',
}),
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('cancel'.tr),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('confirm'.tr),
),
],
),
);
if (confirm != true) return false;
final client = await auth.configureClient('files');
final resp = await client.delete('/stickers/${item.id}');
return resp.statusCode == 200;
}
Future<bool?> _promptUploadSticker({Sticker? edit}) {
return showDialog(
context: context,
builder: (context) => StickerUploadDialog(
edit: edit,
),
);
}
Widget _buildEmoteEntry(Sticker item, String prefix) {
final imageUrl = ServiceFinder.buildUrl(
'files',
'/attachments/${item.attachment.rid}',
);
return ListTile(
title: Text(item.name),
subtitle: Text(item.textWarpedPlaceholder),
contentPadding: const EdgeInsets.only(left: 16, right: 14),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit_square),
onPressed: () {
_promptUploadSticker(edit: item).then((value) {
if (value == true) _pagingController.refresh();
});
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
_promptDelete(item, prefix).then((value) {
if (value == true) _pagingController.refresh();
});
},
),
],
),
leading: AutoCacheImage(
imageUrl,
width: 28,
height: 28,
noErrorWidget: true,
),
);
}
@override
void initState() {
final AuthProvider auth = Get.find();
final name = auth.userProfile.value!['name'];
_pagingController.addPageRequestListener((pageKey) async {
final client = await ServiceFinder.configureClient('files');
final resp = await client.get(
'/stickers/manifest?take=10&offset=$pageKey&author=$name',
);
if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body);
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
if (out != null && result.data!.length >= 10) {
_pagingController.appendPage(out, pageKey + out.length);
} else if (out != null) {
_pagingController.appendLastPage(out);
}
} else {
_pagingController.error = resp.bodyString;
}
});
super.initState();
}
@override
void dispose() {
final StickerProvider sticker = Get.find();
sticker.refreshAvailableStickers();
_pagingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
_promptUploadSticker().then((value) {
if (value == true) _pagingController.refresh();
});
},
),
body: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView(
slivers: [
PagedSliverList<int, StickerPack>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate(
itemBuilder: (BuildContext context, item, int index) {
return ExpansionTile(
title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(item.name),
const Gap(6),
Badge(
label: Text('#${item.id}'),
)
],
),
subtitle: Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
children: item.stickers?.map((x) {
x.pack = item;
return _buildEmoteEntry(x, item.prefix);
}).toList() ??
List.empty(),
);
},
),
),
],
),
),
);
}
}

View File

@ -7,6 +7,9 @@ import 'package:solian/exceptions/request.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/auth.dart'; import 'package:solian/models/auth.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
@ -164,6 +167,7 @@ class _SignInScreenState extends State<SignInScreen> {
final result = AuthResult.fromJson(resp.body); final result = AuthResult.fromJson(resp.body);
_currentTicket = result.ticket; _currentTicket = result.ticket;
_passwordController.clear();
// Finish sign in if possible // Finish sign in if possible
if (result.isFinished) { if (result.isFinished) {
@ -173,6 +177,9 @@ class _SignInScreenState extends State<SignInScreen> {
await auth.refreshAuthorizeStatus(); await auth.refreshAuthorizeStatus();
await auth.refreshUserProfile(); await auth.refreshUserProfile();
Get.find<ChannelProvider>().refreshAvailableChannel();
Get.find<RealmProvider>().refreshAvailableRealms();
Get.find<RelationshipProvider>().refreshRelativeList();
Get.find<WebSocketProvider>().registerPushNotifications(); Get.find<WebSocketProvider>().registerPushNotifications();
autoConfigureBackgroundNotificationService(); autoConfigureBackgroundNotificationService();
autoStartBackgroundNotificationService(); autoStartBackgroundNotificationService();

View File

@ -114,7 +114,8 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
ListTile( ListTile(
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
title: Text('channelSettings'.tr.capitalize!), title: Text('channelSettings'.tr),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () async { onTap: () async {
AppRouter.instance AppRouter.instance
.pushNamed( .pushNamed(
@ -173,7 +174,8 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.notifications_active), leading: const Icon(Icons.notifications_active),
title: Text('channelNotifyLevel'.tr.capitalize!), title: Text('channelNotifyLevel'.tr),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
trailing: DropdownButtonHideUnderline( trailing: DropdownButtonHideUnderline(
child: DropdownButton2<int>( child: DropdownButton2<int>(
isExpanded: true, isExpanded: true,
@ -206,14 +208,16 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
), ),
), ),
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.supervisor_account), leading: const Icon(Icons.supervisor_account),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
title: Text('channelMembers'.tr.capitalize!), title: Text('channelMembers'.tr),
onTap: () => showMemberList(), onTap: () => showMemberList(),
), ),
...(_isOwned ? ownerActions : List.empty()), ...(_isOwned ? ownerActions : List.empty()),
const Divider(thickness: 0.3), const Divider(thickness: 0.3),
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: _isOwned leading: _isOwned
? const Icon(Icons.delete) ? const Icon(Icons.delete)
: const Icon(Icons.exit_to_app), : const Icon(Icons.exit_to_app),

View File

@ -35,13 +35,14 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
final _nameController = TextEditingController(); final _nameController = TextEditingController();
final _descriptionController = TextEditingController(); final _descriptionController = TextEditingController();
bool _isEncrypted = false; bool _isPublic = false;
bool _isCommunity = false;
void applyChannel() async { void _applyChannel() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
if (_aliasController.value.text.isEmpty) randomizeAlias(); if (_aliasController.value.text.isEmpty) _randomizeAlias();
setState(() => _isBusy = true); setState(() => _isBusy = true);
@ -52,7 +53,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
'alias': _aliasController.value.text.toLowerCase(), 'alias': _aliasController.value.text.toLowerCase(),
'name': _nameController.value.text, 'name': _nameController.value.text,
'description': _descriptionController.value.text, 'description': _descriptionController.value.text,
'is_encrypted': _isEncrypted, 'is_encrypted': _isPublic,
}; };
Response? resp; Response? resp;
@ -71,35 +72,44 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void randomizeAlias() { void _randomizeAlias() {
_aliasController.text = _aliasController.text =
const Uuid().v4().replaceAll('-', '').substring(0, 12); const Uuid().v4().replaceAll('-', '').substring(0, 12);
} }
void syncWidget() { void _syncWidget() {
if (widget.edit != null) { if (widget.edit != null) {
_aliasController.text = widget.edit!.alias; _aliasController.text = widget.edit!.alias;
_nameController.text = widget.edit!.name; _nameController.text = widget.edit!.name;
_descriptionController.text = widget.edit!.description; _descriptionController.text = widget.edit!.description;
_isEncrypted = widget.edit!.isEncrypted; _isPublic = widget.edit!.isPublic;
_isCommunity = widget.edit!.isCommunity;
} }
} }
void cancelAction() { void _cancelAction() {
AppRouter.instance.pop(); AppRouter.instance.pop();
} }
@override @override
void initState() { void initState() {
syncWidget(); _syncWidget();
super.initState(); super.initState();
} }
@override
void dispose() {
_aliasController.dispose();
_nameController.dispose();
_descriptionController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final notifyBannerActions = [ final notifyBannerActions = [
TextButton( TextButton(
onPressed: cancelAction, onPressed: _cancelAction,
child: Text('cancel'.tr), child: Text('cancel'.tr),
), ),
]; ];
@ -113,7 +123,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
toolbarHeight: AppTheme.toolbarHeight(context), toolbarHeight: AppTheme.toolbarHeight(context),
actions: [ actions: [
TextButton( TextButton(
onPressed: _isBusy ? null : () => applyChannel(), onPressed: _isBusy ? null : () => _applyChannel(),
child: Text('apply'.tr.toUpperCase()), child: Text('apply'.tr.toUpperCase()),
) )
], ],
@ -164,7 +174,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
visualDensity: visualDensity:
const VisualDensity(horizontal: -2, vertical: -2), const VisualDensity(horizontal: -2, vertical: -2),
), ),
onPressed: () => randomizeAlias(), onPressed: () => _randomizeAlias(),
child: const Icon(Icons.refresh), child: const Icon(Icons.refresh),
) )
], ],
@ -196,12 +206,17 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
), ),
const Divider(thickness: 0.3), const Divider(thickness: 0.3),
CheckboxListTile( CheckboxListTile(
title: Text('channelEncrypted'.tr), title: Text('channelPublic'.tr),
value: _isEncrypted, value: _isPublic,
onChanged: (widget.edit?.isEncrypted ?? false) onChanged: (value) =>
? null setState(() => _isPublic = value ?? false),
: (newValue) => controlAffinity: ListTileControlAffinity.leading,
setState(() => _isEncrypted = newValue ?? false), ),
CheckboxListTile(
title: Text('channelCommunity'.tr),
value: _isCommunity,
onChanged: (value) =>
setState(() => _isCommunity = value ?? false),
controlAffinity: ListTileControlAffinity.leading, controlAffinity: ListTileControlAffinity.leading,
), ),
], ],

View File

@ -125,7 +125,11 @@ class _ChatScreenState extends State<ChatScreen> {
child: Obx( child: Obx(
() => ChannelListWidget( () => ChannelListWidget(
noCategory: true, noCategory: true,
channels: _channels.directChannels, channels: List.from([
..._channels.groupChannels
.where((x) => x.realmId == null),
..._channels.directChannels
]),
selfId: selfId, selfId: selfId,
useReplace: true, useReplace: true,
), ),

View File

@ -379,6 +379,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
isClickable: true, isClickable: true,
isShowEmbed: true, isShowEmbed: true,
isNestedClickable: true, isNestedClickable: true,
showFeaturedReply: true,
onUpdate: (_) { onUpdate: (_) {
_pullPosts(); _pullPosts();
}, },

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/last_read.dart';
import 'package:solian/widgets/posts/post_item.dart'; import 'package:solian/widgets/posts/post_item.dart';
import 'package:solian/widgets/posts/post_replies.dart'; import 'package:solian/widgets/posts/post_replies.dart';
@ -26,6 +27,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
Future<Post?> getDetail() async { Future<Post?> getDetail() async {
if (widget.post != null) { if (widget.post != null) {
item = widget.post; item = widget.post;
Get.find<LastReadProvider>().feedLastReadAt = item?.id;
return widget.post; return widget.post;
} }
@ -38,6 +40,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
context.showErrorDialog(e).then((_) => Navigator.pop(context)); context.showErrorDialog(e).then((_) => Navigator.pop(context));
} }
Get.find<LastReadProvider>().feedLastReadAt = item?.id;
return item; return item;
} }

View File

@ -183,18 +183,18 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
children: [ children: [
ListTile( ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow, tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
title: Row( title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
_editorController.title ?? 'title'.tr, _editorController.title ?? 'title'.tr,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const Gap(6),
if (_editorController.aliasController.text.isNotEmpty) if (_editorController.aliasController.text.isNotEmpty)
Badge( Badge(
label: Text('#${_editorController.aliasController.text}'), label: Text('#${_editorController.aliasController.text}'),
), ).paddingOnly(bottom: 2),
], ],
), ),
subtitle: Text( subtitle: Text(

View File

@ -7,10 +7,13 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart'; import 'package:solian/screens/account/notification.dart';
import 'package:solian/services.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/signin_required_overlay.dart'; import 'package:solian/widgets/account/signin_required_overlay.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
@ -128,19 +131,34 @@ class _RealmListScreenState extends State<RealmListScreen> {
children: [ children: [
Container( Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: (element.banner?.isEmpty ?? true)
? const SizedBox.shrink()
: AutoCacheImage(
ServiceFinder.buildUrl(
'uc',
'/attachments/${element.banner}',
),
fit: BoxFit.cover,
),
), ),
const Positioned( Positioned(
bottom: -30, bottom: -30,
left: 18, left: 18,
child: CircleAvatar( child: (element.avatar?.isEmpty ?? true)
radius: 24, ? CircleAvatar(
backgroundColor: Colors.indigo, radius: 24,
child: FaIcon( backgroundColor:
FontAwesomeIcons.globe, Theme.of(context).colorScheme.primary,
color: Colors.white, child: const FaIcon(
size: 18, FontAwesomeIcons.globe,
), color: Colors.white,
), size: 18,
),
)
: AccountAvatar(
content: element.avatar!,
bgColor: Theme.of(context).colorScheme.primary,
),
), ),
], ],
), ),

View File

@ -69,7 +69,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
ListTile( ListTile(
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
title: Text('realmSettings'.tr.capitalize!), title: Text('realmSettings'.tr),
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
onTap: () async { onTap: () async {
AppRouter.instance AppRouter.instance
.pushNamed( .pushNamed(
@ -120,14 +121,16 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
child: ListView( child: ListView(
children: [ children: [
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.supervisor_account), leading: const Icon(Icons.supervisor_account),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
title: Text('realmMembers'.tr.capitalize!), title: Text('realmMembers'.tr),
onTap: () => showMemberList(), onTap: () => showMemberList(),
), ),
...(_isOwned ? ownerActions : List.empty()), ...(_isOwned ? ownerActions : List.empty()),
const Divider(thickness: 0.3), const Divider(thickness: 0.3),
ListTile( ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: _isOwned leading: _isOwned
? const Icon(Icons.delete) ? const Icon(Icons.delete)
: const Icon(Icons.exit_to_app), : const Icon(Icons.exit_to_app),

View File

@ -1,9 +1,15 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
@ -29,17 +35,19 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
bool _isBusy = false; bool _isBusy = false;
final _aliasController = TextEditingController(); final _aliasController = TextEditingController();
final _avatarController = TextEditingController();
final _bannerController = TextEditingController();
final _nameController = TextEditingController(); final _nameController = TextEditingController();
final _descriptionController = TextEditingController(); final _descriptionController = TextEditingController();
bool _isCommunity = false; bool _isCommunity = false;
bool _isPublic = false; bool _isPublic = false;
void applyRealm() async { void _applyRealm() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
if (_aliasController.value.text.isEmpty) randomizeAlias(); if (_aliasController.value.text.isEmpty) _randomizeAlias();
setState(() => _isBusy = true); setState(() => _isBusy = true);
@ -49,6 +57,8 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
'alias': _aliasController.value.text.toLowerCase(), 'alias': _aliasController.value.text.toLowerCase(),
'name': _nameController.value.text, 'name': _nameController.value.text,
'description': _descriptionController.value.text, 'description': _descriptionController.value.text,
'avatar': _avatarController.value.text,
'banner': _bannerController.value.text,
'is_public': _isPublic, 'is_public': _isPublic,
'is_community': _isCommunity, 'is_community': _isCommunity,
}; };
@ -68,31 +78,110 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void randomizeAlias() { final _imagePicker = ImagePicker();
Future<void> _editImage(String position) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
final image = await _imagePicker.pickImage(source: ImageSource.gallery);
if (image == null) return;
CroppedFile? croppedFile = await ImageCropper().cropImage(
sourcePath: image.path,
uiSettings: [
AndroidUiSettings(
toolbarTitle: 'cropImage'.tr,
toolbarColor: Theme.of(context).colorScheme.primary,
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
),
IOSUiSettings(
title: 'cropImage'.tr,
aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
),
WebUiSettings(
context: context,
),
],
);
if (croppedFile == null) return;
final file = File(croppedFile.path);
setState(() => _isBusy = true);
final AttachmentProvider attach = Get.find();
Attachment? attachResult;
try {
attachResult = await attach.createAttachmentDirectly(
await file.readAsBytes(),
file.path,
'avatar',
null,
);
} catch (e) {
setState(() => _isBusy = false);
context.showErrorDialog(e);
return;
}
switch (position) {
case 'avatar':
_avatarController.text = attachResult.rid;
break;
case 'banner':
_bannerController.text = attachResult.rid;
break;
}
setState(() => _isBusy = false);
}
void _randomizeAlias() {
_aliasController.text = _aliasController.text =
const Uuid().v4().replaceAll('-', '').substring(0, 12); const Uuid().v4().replaceAll('-', '').substring(0, 12);
} }
void syncWidget() { void _syncWidget() {
if (widget.edit != null) { if (widget.edit != null) {
_aliasController.text = widget.edit!.alias; _aliasController.text = widget.edit!.alias;
_nameController.text = widget.edit!.name; _nameController.text = widget.edit!.name;
_descriptionController.text = widget.edit!.description; _descriptionController.text = widget.edit!.description;
_avatarController.text = widget.edit!.avatar ?? '';
_bannerController.text = widget.edit!.banner ?? '';
_isPublic = widget.edit!.isPublic; _isPublic = widget.edit!.isPublic;
_isCommunity = widget.edit!.isCommunity; _isCommunity = widget.edit!.isCommunity;
} }
} }
void cancelAction() { void _cancelAction() {
AppRouter.instance.pop(); AppRouter.instance.pop();
} }
@override @override
void initState() { void initState() {
syncWidget(); _syncWidget();
super.initState(); super.initState();
} }
@override
void dispose() {
_aliasController.dispose();
_avatarController.dispose();
_bannerController.dispose();
_nameController.dispose();
_descriptionController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return Material(
@ -105,7 +194,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
toolbarHeight: AppTheme.toolbarHeight(context), toolbarHeight: AppTheme.toolbarHeight(context),
actions: [ actions: [
TextButton( TextButton(
onPressed: _isBusy ? null : () => applyRealm(), onPressed: _isBusy ? null : () => _applyRealm(),
child: Text('apply'.tr.toUpperCase()), child: Text('apply'.tr.toUpperCase()),
) )
], ],
@ -126,7 +215,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: cancelAction, onPressed: _cancelAction,
child: Text('cancel'.tr), child: Text('cancel'.tr),
), ),
], ],
@ -150,7 +239,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
visualDensity: visualDensity:
const VisualDensity(horizontal: -2, vertical: -2), const VisualDensity(horizontal: -2, vertical: -2),
), ),
onPressed: () => randomizeAlias(), onPressed: () => _randomizeAlias(),
child: const Icon(Icons.refresh), child: const Icon(Icons.refresh),
) )
], ],
@ -166,6 +255,55 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
).paddingSymmetric(horizontal: 16, vertical: 8), ).paddingSymmetric(horizontal: 16, vertical: 8),
const Divider(thickness: 0.3), const Divider(thickness: 0.3),
Row(
children: [
Expanded(
child: TextField(
autofocus: true,
controller: _avatarController,
decoration: InputDecoration.collapsed(
hintText: 'realmAvatar'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
TextButton(
style: TextButton.styleFrom(
shape: const CircleBorder(),
visualDensity:
const VisualDensity(horizontal: -2, vertical: -2),
),
onPressed: _isBusy ? null : () => _editImage('avatar'),
child: const Icon(Icons.upload),
)
],
).paddingSymmetric(horizontal: 16, vertical: 2),
Row(
children: [
Expanded(
child: TextField(
autofocus: true,
controller: _bannerController,
decoration: InputDecoration.collapsed(
hintText: 'realmBanner'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
TextButton(
style: TextButton.styleFrom(
shape: const CircleBorder(),
visualDensity:
const VisualDensity(horizontal: -2, vertical: -2),
),
onPressed: _isBusy ? null : () => _editImage('banner'),
child: const Icon(Icons.upload),
)
],
).paddingSymmetric(horizontal: 16, vertical: 2),
const Divider(thickness: 0.3),
Expanded( Expanded(
child: TextField( child: TextField(
minLines: 5, minLines: 5,
@ -202,3 +340,11 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
); );
} }
} }
class _BannerCropAspectRatioPreset extends CropAspectRatioPresetData {
@override
(int, int)? get data => (16, 7);
@override
String get name => '16x7';
}

View File

@ -34,7 +34,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
final List<Channel> _channels = List.empty(growable: true); final List<Channel> _channels = List.empty(growable: true);
getRealm({String? overrideAlias}) async { getRealm({String? overrideAlias}) async {
final RealmProvider provider = Get.find(); final RealmProvider realm = Get.find();
setState(() => _isBusy = true); setState(() => _isBusy = true);
@ -43,7 +43,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
} }
try { try {
final resp = await provider.getRealm(_overrideAlias ?? widget.alias); final resp = await realm.getRealm(_overrideAlias ?? widget.alias);
setState(() => _realm = Realm.fromJson(resp.body)); setState(() => _realm = Realm.fromJson(resp.body));
} catch (e) { } catch (e) {
context.showErrorDialog(e); context.showErrorDialog(e);
@ -55,14 +55,26 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
getChannels() async { getChannels() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final ChannelProvider provider = Get.find(); final ChannelProvider channel = Get.find();
final resp = await provider.listChannel(scope: _realm!.alias); final resp = await channel.listChannel(scope: _realm!.alias);
final availableResp = await channel.listAvailableChannel(
scope: _realm!.alias,
);
final Set<int> channelIdx = {};
setState(() { setState(() {
_channels.clear(); _channels.clear();
_channels.addAll( _channels.addAll(
resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(), resp.body.map((e) => Channel.fromJson(e)).toList().cast<Channel>(),
); );
_channels.addAll(
availableResp.body
.map((e) => Channel.fromJson(e))
.toList()
.cast<Channel>(),
);
_channels.retainWhere((x) => channelIdx.add(x.id));
}); });
setState(() => _isBusy = false); setState(() => _isBusy = false);

View File

@ -114,6 +114,21 @@ class _SettingScreenState extends State<SettingScreen> {
}, },
), ),
), ),
_buildCaptionHeader('update'.tr),
CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 22),
secondary: const Icon(Icons.sync_alt),
title: Text('updateCheckStrictly'.tr),
subtitle: Text('updateCheckStrictlyDesc'.tr),
value: _prefs?.getBool('check_update_strictly') ?? false,
onChanged: (value) {
_prefs
?.setBool('check_update_strictly', value ?? false)
.then((_) {
setState(() {});
});
},
),
_buildCaptionHeader('more'.tr), _buildCaptionHeader('more'.tr),
ListTile( ListTile(
leading: const Icon(Icons.delete_sweep), leading: const Icon(Icons.delete_sweep),

View File

@ -484,7 +484,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
), ),
), ),
Text( Text(
'${fileType[0].toUpperCase()}${fileType.substring(1)} · ${element.size.formatBytes()}', '${fileType.isNotEmpty ? fileType.capitalize : 'unknown'.tr} · ${element.size.formatBytes()}',
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
), ),
], ],

View File

@ -93,14 +93,14 @@ class _AttachmentEditorThumbnailDialogState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text('postThumbnail'.tr), title: Text('attachmentThumbnail'.tr),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Card( Card(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
child: ListTile( child: ListTile(
title: Text('postThumbnailAttachmentNew'.tr), title: Text('attachmentThumbnailAttachmentNew'.tr),
contentPadding: const EdgeInsets.only(left: 12, right: 9), contentPadding: const EdgeInsets.only(left: 12, right: 9),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
@ -122,7 +122,7 @@ class _AttachmentEditorThumbnailDialogState
isDense: true, isDense: true,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
prefixText: '#', prefixText: '#',
labelText: 'postThumbnailAttachment'.tr, labelText: 'attachmentThumbnailAttachment'.tr,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
), ),

View File

@ -233,6 +233,7 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
final ratio = widget.item.metadata?['ratio'] ?? 16 / 9; final ratio = widget.item.metadata?['ratio'] ?? 16 / 9;
if (!_showContent) { if (!_showContent) {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque,
child: Stack( child: Stack(
children: [ children: [
if (widget.item.metadata?['thumbnail'] != null) if (widget.item.metadata?['thumbnail'] != null)
@ -282,6 +283,8 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
children: [ children: [
Text( Text(
widget.item.alt, widget.item.alt,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
shadows: labelShadows, shadows: labelShadows,
color: Colors.white, color: Colors.white,
@ -398,6 +401,7 @@ class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
const ratio = 16 / 9; const ratio = 16 / 9;
if (!_showContent) { if (!_showContent) {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque,
child: Stack( child: Stack(
children: [ children: [
if (widget.item.metadata?['thumbnail'] != null) if (widget.item.metadata?['thumbnail'] != null)
@ -447,6 +451,8 @@ class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
children: [ children: [
Text( Text(
widget.item.alt, widget.item.alt,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
shadows: labelShadows, shadows: labelShadows,
color: Colors.white, color: Colors.white,

View File

@ -10,6 +10,7 @@ class AutoCacheImage extends StatelessWidget {
final BoxFit? fit; final BoxFit? fit;
final bool noProgressIndicator; final bool noProgressIndicator;
final bool noErrorWidget; final bool noErrorWidget;
final bool isDense;
const AutoCacheImage( const AutoCacheImage(
this.url, { this.url, {
@ -19,6 +20,7 @@ class AutoCacheImage extends StatelessWidget {
this.fit, this.fit,
this.noProgressIndicator = false, this.noProgressIndicator = false,
this.noErrorWidget = false, this.noErrorWidget = false,
this.isDense = false,
}); });
@override @override
@ -46,13 +48,14 @@ class AutoCacheImage extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.close, size: 32) Icon(Icons.close, size: isDense ? 24 : 32)
.animate(onPlay: (e) => e.repeat(reverse: true)) .animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms), .fade(duration: 500.ms),
Text( if (!isDense)
error.toString(), Text(
textAlign: TextAlign.center, error.toString(),
), textAlign: TextAlign.center,
),
], ],
), ),
), ),
@ -89,13 +92,14 @@ class AutoCacheImage extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.close, size: 32) Icon(Icons.close, size: isDense ? 24 : 32)
.animate(onPlay: (e) => e.repeat(reverse: true)) .animate(onPlay: (e) => e.repeat(reverse: true))
.fade(duration: 500.ms), .fade(duration: 500.ms),
Text( if (!isDense)
error.toString(), Text(
textAlign: TextAlign.center, error.toString(),
), textAlign: TextAlign.center,
),
], ],
), ),
), ),

View File

@ -1,5 +1,8 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/controllers/chat_events_controller.dart'; import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
@ -213,6 +216,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
return _buildEntry(element); return _buildEntry(element);
}, },
), ),
SliverGap(max(16, MediaQuery.of(context).padding.bottom)),
], ],
); );
} }

View File

@ -405,12 +405,9 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
if (emojiMatch != null) { if (emojiMatch != null) {
final StickerProvider stickers = Get.find(); final StickerProvider stickers = Get.find();
final emoteSearch = emojiMatch[2]!; final emoteSearch = emojiMatch[2]!;
return stickers.availableStickers final result = await stickers
.where( .searchStickerByAlias(emoteSearch.substring(1));
(x) => x.textWarpedPlaceholder return result
.toUpperCase()
.contains(emoteSearch.toUpperCase()),
)
.map( .map(
(x) => ChatMessageSuggestion( (x) => ChatMessageSuggestion(
type: 'emotes', type: 'emotes',
@ -418,6 +415,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
x.imageUrl, x.imageUrl,
width: 28, width: 28,
height: 28, height: 28,
isDense: true,
), ),
display: x.name, display: x.name,
content: x.textWarpedPlaceholder, content: x.textWarpedPlaceholder,

View File

@ -14,12 +14,14 @@ class MarkdownTextContent extends StatelessWidget {
final String content; final String content;
final String parentId; final String parentId;
final bool isSelectable; final bool isSelectable;
final bool isLargeText;
const MarkdownTextContent({ const MarkdownTextContent({
super.key, super.key,
required this.content, required this.content,
required this.parentId, required this.parentId,
this.isSelectable = false, this.isSelectable = false,
this.isLargeText = false,
}); });
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
@ -35,6 +37,14 @@ class MarkdownTextContent extends StatelessWidget {
styleSheet: MarkdownStyleSheet.fromTheme( styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context), Theme.of(context),
).copyWith( ).copyWith(
textScaleFactor: isLargeText ? 1.1 : 1,
blockquote: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
blockquoteDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
horizontalRuleDecoration: BoxDecoration( horizontalRuleDecoration: BoxDecoration(
border: Border( border: Border(
top: BorderSide( top: BorderSide(
@ -89,7 +99,6 @@ class MarkdownTextContent extends StatelessWidget {
case 'stickers': case 'stickers':
double radius = 8; double radius = 8;
final StickerProvider sticker = Get.find(); final StickerProvider sticker = Get.find();
url = sticker.aliasImageMapping[segments[1].toUpperCase()]!;
if (emojiMatch.length <= 1 && isOnlyEmoji) { if (emojiMatch.length <= 1 && isOnlyEmoji) {
width = 128; width = 128;
height = 128; height = 128;
@ -106,11 +115,20 @@ class MarkdownTextContent extends StatelessWidget {
borderRadius: BorderRadius.all(Radius.circular(radius)), borderRadius: BorderRadius.all(Radius.circular(radius)),
child: Container( child: Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: AutoCacheImage( child: FutureBuilder(
url, future: sticker.getStickerByAlias(segments[1]),
width: width, builder: (context, snapshot) {
height: height, if (!snapshot.hasData) {
fit: fit, return const Center(child: CircularProgressIndicator());
}
return AutoCacheImage(
snapshot.data!.imageUrl,
width: width,
height: height,
fit: fit,
noErrorWidget: true,
);
},
), ),
), ),
).paddingSymmetric(vertical: 4); ).paddingSymmetric(vertical: 4);
@ -172,7 +190,8 @@ class _CustomEmoteInlineSyntax extends InlineSyntax {
bool onMatch(markdown.InlineParser parser, Match match) { bool onMatch(markdown.InlineParser parser, Match match) {
final StickerProvider sticker = Get.find(); final StickerProvider sticker = Get.find();
final alias = match[1]!.toUpperCase(); final alias = match[1]!.toUpperCase();
if (sticker.aliasImageMapping[alias] == null) { if (sticker.stickerCache.containsKey(alias) &&
sticker.stickerCache[alias] == null) {
parser.advanceBy(1); parser.advanceBy(1);
return false; return false;
} }

View File

@ -6,7 +6,9 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/navigation.dart'; import 'package:solian/providers/navigation.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/channel/channel_list.dart'; import 'package:solian/widgets/channel/channel_list.dart';
class AppNavigationRegion extends StatefulWidget { class AppNavigationRegion extends StatefulWidget {
@ -169,6 +171,19 @@ class _AppNavigationRegionState extends State<AppNavigationRegion> {
) )
: Column( : Column(
children: [ children: [
if (!widget.isCollapsed &&
(navState.focusedRealm.value!.banner?.isNotEmpty ??
false))
AspectRatio(
aspectRatio: 16 / 7,
child: AutoCacheImage(
ServiceFinder.buildUrl(
'uc',
'/attachments/${navState.focusedRealm.value!.banner}',
),
fit: BoxFit.cover,
),
),
if (widget.isCollapsed) if (widget.isCollapsed)
Tooltip( Tooltip(
message: navState.focusedRealm.value!.name, message: navState.focusedRealm.value!.name,

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/controllers/post_editor_controller.dart'; import 'package:solian/controllers/post_editor_controller.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart'; import 'package:solian/widgets/attachments/attachment_editor.dart';
@ -58,18 +57,25 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ListTile( Card(
title: Text('postThumbnailAttachmentNew'.tr), margin: EdgeInsets.zero,
contentPadding: const EdgeInsets.only(left: 16, right: 13), child: ListTile(
trailing: const Icon(Icons.chevron_right), title: Text('postThumbnailAttachmentNew'.tr),
shape: const RoundedRectangleBorder( contentPadding: const EdgeInsets.only(left: 12, right: 9),
borderRadius: BorderRadius.all(Radius.circular(8)), trailing: const Icon(Icons.chevron_right),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
onTap: () {
_promptUploadNewAttachment();
},
), ),
onTap: () {
_promptUploadNewAttachment();
},
), ),
const Gap(8), const Row(children: <Widget>[
Expanded(child: Divider()),
Text('OR'),
Expanded(child: Divider()),
]).paddingOnly(top: 12, bottom: 16, left: 16, right: 16),
TextField( TextField(
controller: _attachmentController, controller: _attachmentController,
decoration: InputDecoration( decoration: InputDecoration(

View File

@ -1,11 +1,13 @@
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:get/get_utils/get_utils.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/screens/posts/post_detail.dart'; import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/shells/title_shell.dart'; import 'package:solian/shells/title_shell.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
@ -30,6 +32,7 @@ class PostItem extends StatefulWidget {
final bool isFullDate; final bool isFullDate;
final bool isFullContent; final bool isFullContent;
final bool isContentSelectable; final bool isContentSelectable;
final bool showFeaturedReply;
final String? attachmentParent; final String? attachmentParent;
final Color? backgroundColor; final Color? backgroundColor;
@ -45,6 +48,7 @@ class PostItem extends StatefulWidget {
this.isFullDate = false, this.isFullDate = false,
this.isFullContent = false, this.isFullContent = false,
this.isContentSelectable = false, this.isContentSelectable = false,
this.showFeaturedReply = false,
this.attachmentParent, this.attachmentParent,
this.backgroundColor, this.backgroundColor,
}); });
@ -103,7 +107,7 @@ class _PostItemState extends State<PostItem> {
children: [ children: [
if (widget.isCompact) if (widget.isCompact)
AccountAvatar( AccountAvatar(
content: item.author.avatar.toString(), content: item.author.avatar,
radius: 10, radius: 10,
).paddingOnly(left: 2, top: 1), ).paddingOnly(left: 2, top: 1),
Expanded( Expanded(
@ -320,6 +324,105 @@ class _PostItemState extends State<PostItem> {
} }
} }
Widget _buildFeaturedReply() {
if ((widget.item.metric?.replyCount ?? 0) == 0) {
return const SizedBox.shrink();
}
final List<String> attachments = item.body['attachments'] is List
? List.from(item.body['attachments']?.whereType<String>())
: List.empty();
final unFocusColor =
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
return FutureBuilder(
future: Get.find<PostProvider>().listPostFeaturedReply(
widget.item.id.toString(),
),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const SizedBox.shrink();
}
return Container(
constraints: const BoxConstraints(maxWidth: 480),
child: Card(
margin: EdgeInsets.zero,
child: Column(
children: snapshot.data!
.map(
(x) => Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AccountAvatar(content: x.author.avatar, radius: 10),
const Gap(6),
Text(
x.author.nick,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const Gap(6),
Text(
format(
x.publishedAt?.toLocal() ?? DateTime.now(),
locale: 'en_short',
),
).paddingOnly(top: 0.5),
const Gap(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownTextContent(
content: x.body['content'],
parentId: 'p${item.id}-featured-reply${x.id}',
),
if (x.body['attachments'] is List &&
x.body['attachments'].length > 0)
Row(
children: [
Icon(
Icons.file_copy,
size: 15,
color: unFocusColor,
).paddingOnly(right: 5),
Text(
'attachmentHint'.trParams(
{
'count': x.body['attachments'].length
.toString()
},
),
style: TextStyle(color: unFocusColor),
)
],
),
],
),
),
],
).paddingSymmetric(horizontal: 12, vertical: 8),
)
.toList(),
),
),
)
.animate()
.fadeIn(
duration: 300.ms,
curve: Curves.easeIn,
)
.paddingOnly(
top: (attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 10
: 6,
left:
(attachments.length == 1 && !AppTheme.isLargeScreen(context))
? 24
: 60,
right: 16,
);
},
);
}
double _contentHeight = 0; double _contentHeight = 0;
@override @override
@ -417,7 +520,7 @@ class _PostItemState extends State<PostItem> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GestureDetector( GestureDetector(
child: AccountAvatar(content: item.author.avatar.toString()), child: AccountAvatar(content: item.author.avatar),
onTap: () { onTap: () {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
@ -450,6 +553,8 @@ class _PostItemState extends State<PostItem> {
parentId: 'p${item.id}-embed', parentId: 'p${item.id}-embed',
content: item.body['content'], content: item.body['content'],
isSelectable: widget.isContentSelectable, isSelectable: widget.isContentSelectable,
isLargeText: item.type == 'article' &&
widget.isFullContent,
).paddingOnly(left: 12, right: 8), ).paddingOnly(left: 12, right: 8),
), ),
), ),
@ -506,6 +611,7 @@ class _PostItemState extends State<PostItem> {
left: 16, left: 16,
), ),
_buildAttachments(), _buildAttachments(),
if (widget.showFeaturedReply) _buildFeaturedReply(),
if (widget.isShowReply || widget.isReactable) if (widget.isShowReply || widget.isReactable)
PostQuickAction( PostQuickAction(
isShowReply: widget.isShowReply, isShowReply: widget.isShowReply,

View File

@ -33,6 +33,7 @@ class PostListWidget extends StatelessWidget {
isShowEmbed: isShowEmbed, isShowEmbed: isShowEmbed,
isNestedClickable: isNestedClickable, isNestedClickable: isNestedClickable,
isClickable: isClickable, isClickable: isClickable,
showFeaturedReply: true,
item: item, item: item,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
onUpdate: () { onUpdate: () {
@ -51,6 +52,7 @@ class PostListEntryWidget extends StatelessWidget {
final bool isShowEmbed; final bool isShowEmbed;
final bool isNestedClickable; final bool isNestedClickable;
final bool isClickable; final bool isClickable;
final bool showFeaturedReply;
final Post item; final Post item;
final Function onUpdate; final Function onUpdate;
final Color? backgroundColor; final Color? backgroundColor;
@ -61,6 +63,7 @@ class PostListEntryWidget extends StatelessWidget {
required this.isShowEmbed, required this.isShowEmbed,
required this.isNestedClickable, required this.isNestedClickable,
required this.isClickable, required this.isClickable,
required this.showFeaturedReply,
required this.item, required this.item,
required this.onUpdate, required this.onUpdate,
this.backgroundColor, this.backgroundColor,
@ -74,6 +77,7 @@ class PostListEntryWidget extends StatelessWidget {
item: item, item: item,
isShowEmbed: isShowEmbed, isShowEmbed: isShowEmbed,
isClickable: isNestedClickable, isClickable: isNestedClickable,
showFeaturedReply: showFeaturedReply,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
).paddingSymmetric(vertical: 8), ).paddingSymmetric(vertical: 8),
onLongPress: () { onLongPress: () {

View File

@ -23,6 +23,7 @@ class PostSingleDisplay extends StatelessWidget {
isClickable: true, isClickable: true,
isShowEmbed: true, isShowEmbed: true,
isNestedClickable: true, isNestedClickable: true,
showFeaturedReply: true,
onUpdate: onUpdate, onUpdate: onUpdate,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow, backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
), ),

View File

@ -36,6 +36,7 @@ class PostWarpedListWidget extends StatelessWidget {
isShowEmbed: isShowEmbed, isShowEmbed: isShowEmbed,
isNestedClickable: isNestedClickable, isNestedClickable: isNestedClickable,
isClickable: isClickable, isClickable: isClickable,
showFeaturedReply: true,
item: item, item: item,
onUpdate: onUpdate ?? () {}, onUpdate: onUpdate ?? () {},
); );

View File

@ -1,212 +0,0 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/stickers.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart';
class StickerUploadDialog extends StatefulWidget {
final Sticker? edit;
const StickerUploadDialog({super.key, this.edit});
@override
State<StickerUploadDialog> createState() => _StickerUploadDialogState();
}
class _StickerUploadDialogState extends State<StickerUploadDialog> {
final TextEditingController _attachmentController = TextEditingController();
final TextEditingController _packController = TextEditingController();
final TextEditingController _aliasController = TextEditingController();
final TextEditingController _nameController = TextEditingController();
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
bool _isBusy = false;
void _promptUploadNewAttachment() {
showModalBottomSheet(
context: context,
builder: (context) => AttachmentEditorPopup(
pool: 'sticker',
singleMode: true,
imageOnly: true,
autoUpload: true,
imageMaxHeight: 28,
imageMaxWidth: 28,
onAdd: (value) {
setState(() {
_attachmentController.text = value.toString();
});
},
initialAttachments: const [],
onRemove: (_) {},
),
);
}
Future<void> _applySticker() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
if ([
_nameController.text.isEmpty,
_aliasController.text.isEmpty,
_packController.text.isEmpty,
_attachmentController.text.isEmpty,
].any((x) => x)) {
return;
}
setState(() => _isBusy = true);
Response resp;
final client = await auth.configureClient('files');
if (widget.edit == null) {
resp = await client.post('/stickers', {
'name': _nameController.text,
'alias': _aliasController.text,
'pack_id': int.tryParse(_packController.text),
'attachment_id': int.tryParse(_attachmentController.text),
});
} else {
resp = await client.put('/stickers/${widget.edit!.id}', {
'name': _nameController.text,
'alias': _aliasController.text,
'pack_id': int.tryParse(_packController.text),
'attachment_id': int.tryParse(_attachmentController.text),
});
}
setState(() => _isBusy = false);
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString);
} else {
Navigator.pop(context, true);
}
}
@override
void initState() {
super.initState();
if (widget.edit != null) {
_attachmentController.text = widget.edit!.attachmentId.toString();
_packController.text = widget.edit!.packId.toString();
_aliasController.text = widget.edit!.alias;
_nameController.text = widget.edit!.name;
}
}
@override
void dispose() {
_attachmentController.dispose();
_packController.dispose();
_aliasController.dispose();
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('stickerUploader'.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
title: Text('stickerUploaderAttachmentNew'.tr),
contentPadding: const EdgeInsets.only(left: 16, right: 13),
trailing: const Icon(Icons.chevron_right),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
onTap: () {
_promptUploadNewAttachment();
},
),
const Gap(8),
TextField(
controller: _attachmentController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
prefixText: '#',
labelText: 'stickerUploaderAttachment'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(8),
TextField(
controller: _packController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
prefixText: '#',
labelText: 'stickerUploaderPack'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Container(
padding:
const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 6),
child: Text(
'stickerUploaderPackHint'.tr,
style: TextStyle(color: _unFocusColor),
),
),
TextField(
controller: _aliasController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: 'stickerUploaderAlias'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Container(
padding:
const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 6),
child: Text(
'stickerUploaderAliasHint'.tr,
style: TextStyle(color: _unFocusColor),
),
),
TextField(
controller: _nameController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: 'stickerUploaderName'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Container(
padding: const EdgeInsets.only(left: 8, right: 8, top: 4),
child: Text(
'stickerUploaderNameHint'.tr,
style: TextStyle(color: _unFocusColor),
),
),
],
),
actions: [
TextButton(
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
),
onPressed: _isBusy ? null : () => Navigator.pop(context),
child: Text('cancel'.tr),
),
TextButton(
onPressed: _isBusy ? null : () => _applySticker(),
child: Text('apply'.tr),
),
],
);
}
}

View File

@ -21,19 +21,19 @@ PODS:
- Firebase/Messaging (11.0.0): - Firebase/Messaging (11.0.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 11.0.0) - FirebaseMessaging (~> 11.0.0)
- firebase_analytics (11.3.1): - firebase_analytics (11.3.2):
- Firebase/Analytics (= 11.0.0) - Firebase/Analytics (= 11.0.0)
- firebase_core - firebase_core
- FlutterMacOS - FlutterMacOS
- firebase_core (3.4.1): - firebase_core (3.5.0):
- Firebase/CoreOnly (~> 11.0.0) - Firebase/CoreOnly (~> 11.0.0)
- FlutterMacOS - FlutterMacOS
- firebase_crashlytics (4.1.1): - firebase_crashlytics (4.1.2):
- Firebase/CoreOnly (~> 11.0.0) - Firebase/CoreOnly (~> 11.0.0)
- Firebase/Crashlytics (~> 11.0.0) - Firebase/Crashlytics (~> 11.0.0)
- firebase_core - firebase_core
- FlutterMacOS - FlutterMacOS
- firebase_messaging (15.1.1): - firebase_messaging (15.1.2):
- Firebase/CoreOnly (~> 11.0.0) - Firebase/CoreOnly (~> 11.0.0)
- Firebase/Messaging (~> 11.0.0) - Firebase/Messaging (~> 11.0.0)
- firebase_core - firebase_core
@ -60,9 +60,9 @@ PODS:
- FirebaseCoreInternal (~> 11.0) - FirebaseCoreInternal (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0) - GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.1.0): - FirebaseCoreExtension (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseCoreInternal (11.1.0): - FirebaseCoreInternal (11.2.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)" - "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseCrashlytics (11.0.0): - FirebaseCrashlytics (11.0.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
@ -73,7 +73,7 @@ PODS:
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4) - PromisesObjC (~> 2.4)
- FirebaseInstallations (11.1.0): - FirebaseInstallations (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
@ -87,8 +87,8 @@ PODS:
- GoogleUtilities/Reachability (~> 8.0) - GoogleUtilities/Reachability (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0) - nanopb (~> 3.30910.0)
- FirebaseRemoteConfigInterop (11.1.0) - FirebaseRemoteConfigInterop (11.2.0)
- FirebaseSessions (11.1.0): - FirebaseSessions (11.2.0):
- FirebaseCore (~> 11.0) - FirebaseCore (~> 11.0)
- FirebaseCoreExtension (~> 11.0) - FirebaseCoreExtension (~> 11.0)
- FirebaseInstallations (~> 11.0) - FirebaseInstallations (~> 11.0)
@ -338,19 +338,19 @@ SPEC CHECKSUMS:
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2 file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9 Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
firebase_analytics: 2169e28bb3ee1f765efe0fd4f5b5f625d92fda13 firebase_analytics: a2d0d907566e4a48e27745317f05b4b7db85edd9
firebase_core: 3f80bec72646b26618f0497e74ce8bcd608f03ca firebase_core: c55630cdb8a01cf49eae741dd4bc8c93bdd546b8
firebase_crashlytics: 6b1e511297406a6efd4405dc6301da8843b9dfe1 firebase_crashlytics: a359f1f75a23e560c8c97b743ab684ba795d7688
firebase_messaging: ce70e6615f0cd906d80b7a651b960d76dad6de56 firebase_messaging: 12726b352752420d073ff075e328cc2f0ca14c47
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383 FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
FirebaseCoreExtension: aa5c9779c2d0d39d83f1ceb3fdbafe80c4feecfa FirebaseCoreExtension: cda74ddfb001224bd8fd1d6e74698b4ed07803de
FirebaseCoreInternal: adefedc9a88dbe393c4884640a73ec9e8e790f8c FirebaseCoreInternal: 0c569513412da9f3b31bd0b340013bbee8f295c5
FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b
FirebaseInstallations: d0a8fea5a6fa91abc661591cf57c0f0d70863e57 FirebaseInstallations: 771177d89d6c451dc6e50085ec82e2fc77ed0a4a
FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742 FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742
FirebaseRemoteConfigInterop: abf8b1bbc0bf1b84abd22b66746926410bf91a87 FirebaseRemoteConfigInterop: 477b26fdeb8fb5fbaf22fa9db5343b42289dc7db
FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a FirebaseSessions: adcec8b72d0066a385e3affcd1bcb1ebb3908ce6
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9 flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9

View File

@ -701,6 +701,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
@ -843,6 +844,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
@ -870,6 +872,7 @@
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _flutterfire_internals name: _flutterfire_internals
sha256: ddc6f775260b89176d329dee26f88b9469ef46aa3228ff6a0b91caf2b2989692 sha256: "5fdcea390499dd26c808a3c662df5f4208d6bbc0643072eee94f1476249e2818"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.42" version: "1.3.43"
_macros: _macros:
dependency: transitive dependency: transitive
description: dart description: dart
@ -490,114 +490,114 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_analytics name: firebase_analytics
sha256: "7b5ae39d853ead76f9d030dc23389bfec4ea826d7cccb4eea4873dcb0cdd172b" sha256: "9c52c099e9cbb852c7f1d2302c7eb34a15758834eca1877f7a779e6082f9882d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.3.1" version: "11.3.2"
firebase_analytics_platform_interface: firebase_analytics_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_analytics_platform_interface name: firebase_analytics_platform_interface
sha256: "0205e05bb37abd29d5dec5cd89aeb04f3f58bf849aad21dd938be0507d52a40c" sha256: "4ec57aee951832fdbf10ca722bbb83fe0001d6168d6c4cfea9ccee0df6afb1e0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.3" version: "4.2.4"
firebase_analytics_web: firebase_analytics_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_analytics_web name: firebase_analytics_web
sha256: "434807f8b30526e21cc062410c28ee5c6680a13626c4443b5ffede29f84b0c74" sha256: "95c594fb1e8960992a607b135459e2f9ea3683dd8d01e6b845cace7c6665ec7e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.10" version: "0.5.10+1"
firebase_core: firebase_core:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_core name: firebase_core
sha256: "40921de9795fbf5887ed5c0adfdf4972d5a8d7ae7e1b2bb98dea39bc02626a88" sha256: c7de9354eb2cd8bfe8059e1112174c9a58beda7051807207306bc48283277cfb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.1" version: "3.5.0"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_core_platform_interface name: firebase_core_platform_interface
sha256: f7d7180c7f99babd4b4c517754d41a09a4943a0f7a69b65c894ca5c68ba66315 sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.2.1" version: "5.3.0"
firebase_core_web: firebase_core_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_core_web name: firebase_core_web
sha256: f4ee170441ca141c5f9ee5ad8737daba3ee9c8e7efb6902aee90b4fbd178ce25 sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.18.0" version: "2.18.1"
firebase_crashlytics: firebase_crashlytics:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_crashlytics name: firebase_crashlytics
sha256: c4fdbb14ba6f36794f89dc27fb5c759c9cc67ecbaeb079edc4dba515bbf9f555 sha256: "7821f9d8373b91f2a5ca8214226891d5870e196a7376f66350f65204387e9c15"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.1" version: "4.1.2"
firebase_crashlytics_platform_interface: firebase_crashlytics_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_crashlytics_platform_interface name: firebase_crashlytics_platform_interface
sha256: "891d6f7ba4b93672d0e1265f27b6a9dccd56ba2cc30ce6496586b32d1d8770ac" sha256: "8ed539fd9e9b6c07905f9f44c5f6d4785ac841a5a8195bd35586c8b1d54ec26d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.6.42" version: "3.6.43"
firebase_messaging: firebase_messaging:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_messaging name: firebase_messaging
sha256: cc02c4afd6510cd84586020670140c4a23fbe52af16cd260ccf8ede101bb8d1b sha256: "32ce60b747e755b48d7112d728d4f736ba82acd98ec825626558d444d385fa3a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.1.1" version: "15.1.2"
firebase_messaging_platform_interface: firebase_messaging_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_platform_interface name: firebase_messaging_platform_interface
sha256: d8a4984635f09213302243ea670fe5c42f3261d7d8c7c0a5f7dcd5d6c84be459 sha256: "69671a0f1a40c7b7c46ad0283e6f34ca2a59a0362ca14a240a4ea01c46e8a521"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.44" version: "4.5.45"
firebase_messaging_web: firebase_messaging_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_web name: firebase_messaging_web
sha256: "258b9d637965db7855299b123533609ed95e52350746a723dfd1d8d6f3fac678" sha256: "6890111a9d01d7b13d0f6fe74850812c334e903d2c80a2d9356a3abb8c3a9e9a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.9.0" version: "3.9.1"
firebase_performance: firebase_performance:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_performance name: firebase_performance
sha256: "879ce4d83242cb7d1ec67a8b45daa351f230211778e34eeea979894839c4832a" sha256: ed9a408b6d1f221fc0e2890dcf0733b604d1aea6cd3b897f97dd3f889f01ddfc
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.10.0+6" version: "0.10.0+7"
firebase_performance_platform_interface: firebase_performance_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_performance_platform_interface name: firebase_performance_platform_interface
sha256: ac68eba644f593903a931ba7f26f0677b725d5a60f8f7bc0fed01d88a11d1463 sha256: bfcfbfcefeaf3853a72602675b786e13a609d49ac70fc325d302b5794b8b0c06
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.4+42" version: "0.1.4+43"
firebase_performance_web: firebase_performance_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_performance_web name: firebase_performance_web
sha256: ff53b9c5d8601fc983d0173b88fdfd8abcc74948f0a3753f3c1ec276b680f23c sha256: "2ac9e44a1be7b20f1a7a3912d84bf2e1ec76398f2dadc07b6b7c3173d590e329"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.7" version: "0.1.7+1"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -635,6 +635,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.0" version: "4.5.0"
flutter_app_update:
dependency: "direct main"
description:
name: flutter_app_update
sha256: "2b83278d5cc807f543e623d5b466216316104335a4918d9cc4556f39985fe84a"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
flutter_background_service: flutter_background_service:
dependency: "direct main" dependency: "direct main"
description: description:
@ -909,14 +917,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.7.0" version: "10.7.0"
freezed_annotation:
dependency: "direct main"
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.4"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -1894,10 +1894,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqflite_common name: sqflite_common
sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e" sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.4+2" version: "2.5.4+3"
sqlite3: sqlite3:
dependency: transitive dependency: transitive
description: description:
@ -2138,6 +2138,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
version:
dependency: "direct main"
description:
name: version
sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
very_good_infinite_list: very_good_infinite_list:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App" description: "The Solar Network App"
publish_to: "none" publish_to: "none"
version: 1.2.1+39 version: 1.2.2+2
environment: environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
@ -68,7 +68,6 @@ dependencies:
flutter_svg: ^2.0.10+1 flutter_svg: ^2.0.10+1
cross_file: ^0.3.4+2 cross_file: ^0.3.4+2
google_fonts: ^6.2.1 google_fonts: ^6.2.1
freezed_annotation: ^2.4.4
json_annotation: ^4.9.0 json_annotation: ^4.9.0
gap: ^3.0.1 gap: ^3.0.1
fl_chart: ^0.69.0 fl_chart: ^0.69.0
@ -81,6 +80,8 @@ dependencies:
path_provider: ^2.1.4 path_provider: ^2.1.4
flutter_background_service: ^5.0.10 flutter_background_service: ^5.0.10
flutter_local_notifications: ^17.2.2 flutter_local_notifications: ^17.2.2
flutter_app_update: ^3.1.0
version: ^3.0.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: