Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
153c15e5c9 | |||
6a0f42cdc9 | |||
01aaa5455e | |||
f3ceb5f967 | |||
b5e2fa4c25 | |||
8378024490 | |||
6d40d6bba3 | |||
77075c8dab | |||
dec34e297d | |||
358677ade0 | |||
d2f37ae45d | |||
e4b741ff0c | |||
e69abb7f9d | |||
565a8e41cc | |||
c9fbe47337 | |||
01db63e297 | |||
d87e67bd17 | |||
06aa1fb359 | |||
62733bf29f | |||
ce16de9c71 | |||
47eb6cbc66 | |||
029e72fb0b | |||
152efd97a0 | |||
ad1dc064e6 | |||
675b5dea5d | |||
5941cb9fd5 | |||
e11bf204af | |||
8a2d94cedf | |||
780f7c22bc | |||
c18ce88993 | |||
73456fcff6 | |||
8e8be52658 | |||
df22b65777 | |||
1437414b7f | |||
c1ff317c66 |
@ -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.autofocus" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
|
||||
|
@ -18,7 +18,7 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
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.firebase.crashlytics" version "2.8.1" apply false
|
||||
id "org.jetbrains.kotlin.android" version '2.0.0' apply false
|
||||
|
@ -3,6 +3,7 @@
|
||||
"hide": "Hide",
|
||||
"okay": "Okay",
|
||||
"next": "Next",
|
||||
"prev": "Previous",
|
||||
"reset": "Reset",
|
||||
"page": "Page",
|
||||
"home": "Home",
|
||||
@ -54,7 +55,7 @@
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"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.",
|
||||
"search": "Search",
|
||||
"post": "Post",
|
||||
@ -67,11 +68,20 @@
|
||||
"notificationUnreadCount": "@count unread notifications",
|
||||
"errorHappened": "An error occurred",
|
||||
"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",
|
||||
"email": "Email",
|
||||
"username": "Username",
|
||||
"usernameInputHint": "Also supports email and phone number",
|
||||
"nickname": "Nickname",
|
||||
"password": "Password",
|
||||
"passwordOneTime": "One-time-password",
|
||||
"passwordInputHint": "Forgot your password? Go back to the first step to reset your password",
|
||||
"passwordOneTimeInputHint": "Check your inbox or authorizer for a verification code",
|
||||
"title": "Title",
|
||||
"description": "Description",
|
||||
"birthday": "Birthday",
|
||||
@ -103,6 +113,11 @@
|
||||
"signinRiskDetected": "Risk detected, click Next to open a webpage and signin through it to pass security check.",
|
||||
"signinResetPasswordHint": "Please enter username to request reset password.",
|
||||
"signinResetPasswordSent": "Reset password request sent, check your inbox!",
|
||||
"signinPickFactor": "Pick a way\nfor verification",
|
||||
"signinEnterPassword": "Enter your\npassword",
|
||||
"signinMultiFactor": "@n step(s) verifications",
|
||||
"authFactorEmail": "Email One-time-password",
|
||||
"authFactorPassword": "Password",
|
||||
"signup": "Sign up",
|
||||
"signupGreeting": "Welcome onboard",
|
||||
"signupCaption": "Create an account on Solarpass and then get the access of entire Solar Network!",
|
||||
@ -147,6 +162,9 @@
|
||||
"postListNews": "News",
|
||||
"postListFriends": "Friends",
|
||||
"postListShuffle": "Random",
|
||||
"attachmentThumbnail": "Thumbnail",
|
||||
"attachmentThumbnailAttachmentNew": "Upload thumbnail",
|
||||
"attachmentThumbnailAttachment": "Attachment serial number",
|
||||
"postEditorModeStory": "Post a post",
|
||||
"postEditorModeArticle": "Post an article",
|
||||
"postEditor": "Post editor",
|
||||
@ -215,6 +233,8 @@
|
||||
"realmDescription": "Description",
|
||||
"realmPublic": "Public Realm",
|
||||
"realmCommunity": "Community Realm",
|
||||
"realmAvatar": "Realm avatar",
|
||||
"realmBanner": "Realm banner",
|
||||
"realmDetail": "Realm detail",
|
||||
"realmMember": "Realm member",
|
||||
"realmMembers": "Realm members",
|
||||
@ -240,7 +260,8 @@
|
||||
"channelName": "Name",
|
||||
"channelDescription": "Description",
|
||||
"channelDirectDescription": "Direct message with @username",
|
||||
"channelEncrypted": "Encrypted Channel",
|
||||
"channelPublic": "Public channel",
|
||||
"channelCommunity": "Community channel",
|
||||
"channelMember": "Channel member",
|
||||
"channelMembers": "Channel members",
|
||||
"channelMembersAdd": "Add channel members",
|
||||
@ -334,8 +355,7 @@
|
||||
"bsCheckForUpdate": "Checking For Updates",
|
||||
"bsCheckForUpdateFailed": "Unable to Check Updates",
|
||||
"bsCheckForUpdateNew": "Found New Version",
|
||||
"bsCheckForUpdateDescApple": "Please head to TestFlight 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.",
|
||||
"bsCheckForUpdateDesc": "Please head to app store and update your app to latest version to prevent error happens and get latest functions.",
|
||||
"bsCheckingServer": "Checking Server Status",
|
||||
"bsCheckingServerFail": "Unable connect to server, check your network connection",
|
||||
"bsCheckingServerDown": "Server currently unavailable, please retry later",
|
||||
@ -397,5 +417,20 @@
|
||||
"userLevel13": "Immortal",
|
||||
"postBrowsingIn": "Browsing in @region",
|
||||
"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."
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
"okay": "确认",
|
||||
"home": "首页",
|
||||
"next": "下一步",
|
||||
"prev": "上一步",
|
||||
"reset": "重置",
|
||||
"cancel": "取消",
|
||||
"confirm": "确认",
|
||||
@ -75,8 +76,12 @@
|
||||
"forgotPassword": "忘记密码",
|
||||
"email": "邮件地址",
|
||||
"username": "用户名",
|
||||
"usernameInputHint": "同时支持邮箱 / 电话号码",
|
||||
"nickname": "显示名",
|
||||
"password": "密码",
|
||||
"passwordOneTime": "一次性验证码",
|
||||
"passwordInputHint": "忘记密码了?回到第一步以重置密码",
|
||||
"passwordOneTimeInputHint": "检查你的收件箱或是授权器获得以验证码",
|
||||
"title": "标题",
|
||||
"description": "简介",
|
||||
"birthday": "生日",
|
||||
@ -108,6 +113,11 @@
|
||||
"signinRiskDetected": "检测到风险,点击下一步按钮来打开一个网页,并通过在其上面登录来通过安全检查。",
|
||||
"signinResetPasswordHint": "请先填写用户名以发送重置密码请求。",
|
||||
"signinResetPasswordSent": "重置密码请求已发送,在绑定邮件收件箱可收取一份包含重置密码链接的邮件。",
|
||||
"signinPickFactor": "选择一个\n验证方式",
|
||||
"signinEnterPassword": "输入密码\n或验证码",
|
||||
"signinMultiFactor": "@n 步验证",
|
||||
"authFactorEmail": "邮箱一次性密码",
|
||||
"authFactorPassword": "账户密码",
|
||||
"signup": "注册",
|
||||
"signupGreeting": "欢迎加入\nSolar Network",
|
||||
"signupCaption": "在 Solarpass 注册一个账号以获得整个 Solar Network 的存取权!",
|
||||
@ -158,6 +168,9 @@
|
||||
"postListNews": "新鲜事",
|
||||
"postListFriends": "好友圈",
|
||||
"postListShuffle": "打乱看",
|
||||
"attachmentThumbnail": "附件缩略图",
|
||||
"attachmentThumbnailAttachmentNew": "上传附件作为缩略图",
|
||||
"attachmentThumbnailAttachment": "附件序列号",
|
||||
"postNew": "创建新帖子",
|
||||
"postNewInRealmHint": "在领域 @realm 里发表新帖子",
|
||||
"postAction": "发表",
|
||||
@ -216,6 +229,8 @@
|
||||
"realmDescription": "领域简介",
|
||||
"realmPublic": "公开领域",
|
||||
"realmCommunity": "社区领域",
|
||||
"realmAvatar": "领域头像",
|
||||
"realmBanner": "领域横幅",
|
||||
"realmDetail": "领域详情",
|
||||
"realmMember": "领域成员",
|
||||
"realmMembers": "领域成员",
|
||||
@ -241,7 +256,8 @@
|
||||
"channelName": "显示名称",
|
||||
"channelDescription": "频道简介",
|
||||
"channelDirectDescription": "与 @username 的私聊",
|
||||
"channelEncrypted": "加密频道",
|
||||
"channelPublic": "公开频道",
|
||||
"channelCommunity": "社区频道",
|
||||
"channelMember": "频道成员",
|
||||
"channelMembers": "频道成员",
|
||||
"channelMembersAdd": "添加频道成员",
|
||||
@ -335,8 +351,7 @@
|
||||
"bsCheckForUpdate": "正在检查更新",
|
||||
"bsCheckForUpdateFailed": "无法检查更新",
|
||||
"bsCheckForUpdateNew": "发现新版本",
|
||||
"bsCheckForUpdateDescApple": "请前往 TestFlight 并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。",
|
||||
"bsCheckForUpdateDescCommon": "请前往我们的网站下载并安装最新版本的应用程序,以防止出现错误并获取最新功能。",
|
||||
"bsCheckForUpdateDesc": "请前往应用商店并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。",
|
||||
"bsCheckingServer": "检查服务器状态中",
|
||||
"bsCheckingServerFail": "无法连接至服务器,请检查你的网络连接状态",
|
||||
"bsCheckingServerDown": "当前服务器不可用,请稍后重试",
|
||||
@ -398,5 +413,20 @@
|
||||
"userLevel13": "万古流芳",
|
||||
"postBrowsingIn": "浏览 @region 内的帖子中",
|
||||
"needRestartToApply": "需要重启应用来生效",
|
||||
"holdToSeeDetail": "长按 / 鼠标悬浮来查看详情"
|
||||
"holdToSeeDetail": "长按 / 鼠标悬浮来查看详情",
|
||||
"subscribe": "订阅",
|
||||
"subscribed": "已订阅",
|
||||
"unsubscribe": "取消订阅",
|
||||
"preferences": "偏好设置",
|
||||
"notificationPreferences": "通知偏好设置",
|
||||
"notificationTopicPostFeedback": "帖子反馈",
|
||||
"notificationTopicPostSubscription": "订阅源",
|
||||
"preferencesApplied": "偏好设置已应用",
|
||||
"save": "保存",
|
||||
"updateAvailable": "有可用更新",
|
||||
"updateAvailableDesc": "有可用更新 (@from 到 @to) 你想现在下载安装吗?在等待下载期间你仍可以正常使用。",
|
||||
"update": "更新",
|
||||
"updateCheckStrictly": "严格模式",
|
||||
"updateCheckStrictlyDesc": "如果启用,应用程序将会在本地版本与远程版本不同时询问更新,而不会检查版本号大小。",
|
||||
"updateMayAvailable": "版本 @version 现已可用,你可以前往应用商店或是我们的官网下载更新。"
|
||||
}
|
||||
|
@ -54,26 +54,26 @@ PODS:
|
||||
- Firebase/Performance (11.0.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebasePerformance (~> 11.0.0)
|
||||
- firebase_analytics (11.3.1):
|
||||
- firebase_analytics (11.3.2):
|
||||
- Firebase/Analytics (= 11.0.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_core (3.4.1):
|
||||
- firebase_core (3.5.0):
|
||||
- Firebase/CoreOnly (= 11.0.0)
|
||||
- Flutter
|
||||
- firebase_crashlytics (4.1.1):
|
||||
- firebase_crashlytics (4.1.2):
|
||||
- Firebase/Crashlytics (= 11.0.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_messaging (15.1.1):
|
||||
- firebase_messaging (15.1.2):
|
||||
- Firebase/Messaging (= 11.0.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_performance (0.10.0-6):
|
||||
- firebase_performance (0.10.0-7):
|
||||
- Firebase/Performance (= 11.0.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseABTesting (11.1.0):
|
||||
- FirebaseABTesting (11.2.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseAnalytics (11.0.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 11.0.0)
|
||||
@ -97,9 +97,9 @@ PODS:
|
||||
- FirebaseCoreInternal (~> 11.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- FirebaseCoreExtension (11.1.0):
|
||||
- FirebaseCoreExtension (11.2.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseCoreInternal (11.1.0):
|
||||
- FirebaseCoreInternal (11.2.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseCrashlytics (11.0.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
@ -110,7 +110,7 @@ PODS:
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseInstallations (11.1.0):
|
||||
- FirebaseInstallations (11.2.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
@ -134,7 +134,7 @@ PODS:
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseRemoteConfig (11.1.0):
|
||||
- FirebaseRemoteConfig (11.2.0):
|
||||
- FirebaseABTesting (~> 11.0)
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
@ -142,8 +142,8 @@ PODS:
|
||||
- FirebaseSharedSwift (~> 11.0)
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.0)"
|
||||
- FirebaseRemoteConfigInterop (11.1.0)
|
||||
- FirebaseSessions (11.1.0):
|
||||
- FirebaseRemoteConfigInterop (11.2.0)
|
||||
- FirebaseSessions (11.2.0):
|
||||
- FirebaseCore (~> 11.0)
|
||||
- FirebaseCoreExtension (~> 11.0)
|
||||
- FirebaseInstallations (~> 11.0)
|
||||
@ -152,8 +152,10 @@ PODS:
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesSwift (~> 2.1)
|
||||
- FirebaseSharedSwift (11.1.0)
|
||||
- FirebaseSharedSwift (11.2.0)
|
||||
- Flutter (1.0.0)
|
||||
- flutter_app_update (0.0.1):
|
||||
- Flutter
|
||||
- flutter_background_service_ios (0.0.3):
|
||||
- Flutter
|
||||
- flutter_keyboard_visibility (0.0.1):
|
||||
@ -306,6 +308,7 @@ DEPENDENCIES:
|
||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||
- firebase_performance (from `.symlinks/plugins/firebase_performance/ios`)
|
||||
- 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_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
@ -383,6 +386,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/firebase_performance/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_app_update:
|
||||
:path: ".symlinks/plugins/flutter_app_update/ios"
|
||||
flutter_background_service_ios:
|
||||
:path: ".symlinks/plugins/flutter_background_service_ios/ios"
|
||||
flutter_keyboard_visibility:
|
||||
@ -445,25 +450,26 @@ SPEC CHECKSUMS:
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
|
||||
firebase_analytics: b8ce6c2c4b245d3c3bb3a147965d09da0f455959
|
||||
firebase_core: ba84e940cf5cbbc601095f86556560937419195c
|
||||
firebase_crashlytics: 4111f8198b78c99471c955af488cecd8224967e6
|
||||
firebase_messaging: c40f84e7a98da956d5262fada373b5c458edcf13
|
||||
firebase_performance: 8b7b9ca5adf3a9b3afa12b4eb96b9cabefc2c248
|
||||
FirebaseABTesting: c2e22c3aab99afa81d0561708b2c1c356c556976
|
||||
firebase_analytics: 4fd10182fd08bb8358f26ac8aca8dad7b6d0f592
|
||||
firebase_core: 2ec6b789859c7c24766344ec71fdf78639402d56
|
||||
firebase_crashlytics: 60630a0f91ee432275fa1660fd8593079761448a
|
||||
firebase_messaging: a18e1e02b2e8e69097c8173e0c851be223b21c50
|
||||
firebase_performance: 12d45fdf120992fa879d990929bf73d4a5ced053
|
||||
FirebaseABTesting: 2104d957ce33888a3d6f3bde298cdee376dde8f1
|
||||
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
|
||||
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
|
||||
FirebaseCoreExtension: aa5c9779c2d0d39d83f1ceb3fdbafe80c4feecfa
|
||||
FirebaseCoreInternal: adefedc9a88dbe393c4884640a73ec9e8e790f8c
|
||||
FirebaseCoreExtension: cda74ddfb001224bd8fd1d6e74698b4ed07803de
|
||||
FirebaseCoreInternal: 0c569513412da9f3b31bd0b340013bbee8f295c5
|
||||
FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b
|
||||
FirebaseInstallations: d0a8fea5a6fa91abc661591cf57c0f0d70863e57
|
||||
FirebaseInstallations: 771177d89d6c451dc6e50085ec82e2fc77ed0a4a
|
||||
FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742
|
||||
FirebasePerformance: efdc02bacb1b4710588c9f867011605c081cdf79
|
||||
FirebaseRemoteConfig: 05521e937b72e01847a7128da5a492327364c705
|
||||
FirebaseRemoteConfigInterop: abf8b1bbc0bf1b84abd22b66746926410bf91a87
|
||||
FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a
|
||||
FirebaseSharedSwift: 260a35e08943ec810d820a70bc0359136351d0c5
|
||||
FirebaseRemoteConfig: fca0b2d017fc1de52b28a4e5bcf2007c1a840457
|
||||
FirebaseRemoteConfigInterop: 477b26fdeb8fb5fbaf22fa9db5343b42289dc7db
|
||||
FirebaseSessions: adcec8b72d0066a385e3affcd1bcb1ebb3908ce6
|
||||
FirebaseSharedSwift: 7a0d78d155ede78407f0fdc89fbc914014c7c540
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc
|
||||
flutter_background_service_ios: e30e0d3ee69e4cee66272d0c78eacd48c2e94aac
|
||||
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
|
@ -616,6 +616,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
@ -920,6 +921,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
@ -947,6 +949,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
|
@ -1,19 +1,23 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/platform.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/stickers.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/services.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 {
|
||||
final Widget child;
|
||||
@ -35,6 +39,8 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
|
||||
int _periodCursor = 0;
|
||||
|
||||
final Completer _bootCompleter = Completer();
|
||||
|
||||
late final List<({String label, Future<void> Function() action})> _periods = [
|
||||
(
|
||||
label: 'bsLoadingTheme',
|
||||
@ -47,31 +53,67 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
action: () async {
|
||||
if (PlatformInfo.isWeb) return;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
final localVersionString = '${info.version}+${info.buildNumber}';
|
||||
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(() {
|
||||
_isErrored = true;
|
||||
_subtitle = PlatformInfo.isIOS || PlatformInfo.isMacOS
|
||||
? 'bsCheckForUpdateDescApple'.tr
|
||||
: 'bsCheckForUpdateDescCommon'.tr;
|
||||
_subtitle = 'bsCheckForUpdateDesc'.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) {
|
||||
setState(() {
|
||||
_isErrored = true;
|
||||
_subtitle = 'bsCheckForUpdateFailed'.tr;
|
||||
});
|
||||
context.showErrorDialog('Unable to check update: $e');
|
||||
}
|
||||
},
|
||||
),
|
||||
(
|
||||
label: 'bsCheckingServer',
|
||||
action: () async {
|
||||
final client = ServiceFinder.configureClient('dealer');
|
||||
final client = await ServiceFinder.configureClient('dealer');
|
||||
final resp = await client.get('/.well-known');
|
||||
if (resp.statusCode != null && resp.statusCode != 200) {
|
||||
setState(() {
|
||||
@ -115,7 +157,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
final AuthProvider auth = Get.find();
|
||||
try {
|
||||
await Future.wait([
|
||||
Get.find<StickerProvider>().refreshAvailableStickers(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
Get.find<ChannelProvider>().refreshAvailableChannel(),
|
||||
if (auth.isAuthorized.isTrue)
|
||||
@ -156,6 +197,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
}
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_bootCompleter.complete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,6 +297,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
||||
_isBusy = false;
|
||||
_isErrored = false;
|
||||
});
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_bootCompleter.complete();
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_isBusy = true;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/event.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/database/database.dart';
|
||||
import 'package:solian/providers/database/services/messages.dart';
|
||||
|
||||
@ -31,80 +32,56 @@ class ChatEventController {
|
||||
this.channel = channel;
|
||||
this.scope = scope;
|
||||
|
||||
syncLocal(channel);
|
||||
const firstTake = 20;
|
||||
const furtherTake = 100;
|
||||
|
||||
isLoading.value = true;
|
||||
if (PlatformInfo.isWeb) {
|
||||
final result = await src.fetchRemoteEvents(
|
||||
channel,
|
||||
scope,
|
||||
depth: 1,
|
||||
offset: 0,
|
||||
);
|
||||
totalEvents.value = result?.$2 ?? 0;
|
||||
if (result != null) {
|
||||
for (final x in result.$1.reversed) {
|
||||
final entry = LocalMessageEventTableData(
|
||||
id: x.id,
|
||||
channelId: x.channelId,
|
||||
createdAt: x.createdAt,
|
||||
data: x,
|
||||
);
|
||||
insertEvent(entry);
|
||||
applyEvent(entry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final result = await src.pullRemoteEvents(
|
||||
channel,
|
||||
scope: scope,
|
||||
depth: 1,
|
||||
);
|
||||
totalEvents.value = result?.$2 ?? 0;
|
||||
await syncLocal(channel);
|
||||
}
|
||||
await syncLocal(channel, take: firstTake);
|
||||
isLoading.value = false;
|
||||
|
||||
// Take a small range of messages to check is local database up to date
|
||||
var isUpToDate = true;
|
||||
final result =
|
||||
await src.pullRemoteEvents(channel, scope: scope, take: firstTake);
|
||||
totalEvents.value = result?.$2 ?? 0;
|
||||
if ((result?.$1.length ?? 0) > 0) {
|
||||
final minId = result!.$1.map((x) => x.id).reduce(math.min);
|
||||
isUpToDate = await src.getEventFromLocal(minId) != null;
|
||||
}
|
||||
syncLocal(channel, take: firstTake);
|
||||
|
||||
if (!isUpToDate) {
|
||||
// Loading more content due to isn't up to date
|
||||
final result =
|
||||
await src.pullRemoteEvents(channel, scope: scope, take: furtherTake);
|
||||
totalEvents.value = result?.$2 ?? 0;
|
||||
syncLocal(channel, take: furtherTake);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadEvents(Channel channel, String scope) async {
|
||||
const take = 20;
|
||||
final offset = currentEvents.length;
|
||||
|
||||
isLoading.value = true;
|
||||
if (PlatformInfo.isWeb) {
|
||||
final result = await src.fetchRemoteEvents(
|
||||
channel,
|
||||
scope,
|
||||
depth: 3,
|
||||
offset: currentEvents.length,
|
||||
);
|
||||
if (result != null) {
|
||||
totalEvents.value = result.$2;
|
||||
for (final x in result.$1.reversed) {
|
||||
final entry = LocalMessageEventTableData(
|
||||
id: x.id,
|
||||
channelId: x.channelId,
|
||||
createdAt: x.createdAt,
|
||||
data: x,
|
||||
);
|
||||
currentEvents.add(entry);
|
||||
applyEvent(entry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final result = await src.pullRemoteEvents(
|
||||
channel,
|
||||
depth: 3,
|
||||
scope: scope,
|
||||
offset: currentEvents.length,
|
||||
);
|
||||
await syncLocal(channel, take: take, offset: offset);
|
||||
src
|
||||
.pullRemoteEvents(channel, scope: scope, take: take, offset: offset)
|
||||
.then((result) {
|
||||
totalEvents.value = result?.$2 ?? 0;
|
||||
await syncLocal(channel);
|
||||
}
|
||||
syncLocal(channel, take: take, offset: offset);
|
||||
});
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
Future<bool> syncLocal(Channel channel) async {
|
||||
if (PlatformInfo.isWeb) return false;
|
||||
final data = await src.listEvents(channel);
|
||||
currentEvents.replaceRange(0, currentEvents.length, data);
|
||||
Future<bool> syncLocal(Channel channel,
|
||||
{required int take, int offset = 0}) async {
|
||||
final data = await src.listEvents(channel, take: take, offset: offset);
|
||||
if (currentEvents.length >= offset + take) {
|
||||
currentEvents.replaceRange(offset, offset + take, data);
|
||||
} else {
|
||||
currentEvents.insertAll(currentEvents.length, data);
|
||||
}
|
||||
for (final x in data.reversed) {
|
||||
applyEvent(x);
|
||||
}
|
||||
@ -113,16 +90,7 @@ class ChatEventController {
|
||||
|
||||
receiveEvent(Event remote) async {
|
||||
LocalMessageEventTableData entry;
|
||||
if (PlatformInfo.isWeb) {
|
||||
entry = LocalMessageEventTableData(
|
||||
id: remote.id,
|
||||
channelId: remote.channelId,
|
||||
createdAt: remote.createdAt,
|
||||
data: remote,
|
||||
);
|
||||
} else {
|
||||
entry = await src.receiveEvent(remote);
|
||||
}
|
||||
|
||||
totalEvents.value++;
|
||||
insertEvent(entry);
|
||||
|
@ -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) {
|
||||
Widget content = Text(exception.toString().capitalize!);
|
||||
if (exception is UnauthorizedException) {
|
||||
|
@ -9,7 +9,6 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:protocol_handler/protocol_handler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/background.dart';
|
||||
import 'package:solian/bootstrapper.dart';
|
||||
import 'package:solian/firebase_options.dart';
|
||||
import 'package:solian/platform.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/navigation.dart';
|
||||
import 'package:solian/providers/stickers.dart';
|
||||
import 'package:solian/providers/subscription.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
@ -122,10 +122,8 @@ class SolianApp extends StatelessWidget {
|
||||
builder: (context, child) {
|
||||
return SystemShell(
|
||||
child: ScaffoldMessenger(
|
||||
child: BootstrapperShell(
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -151,6 +149,7 @@ class SolianApp extends StatelessWidget {
|
||||
Get.lazyPut(() => LinkExpandProvider());
|
||||
Get.lazyPut(() => DailySignProvider());
|
||||
Get.lazyPut(() => LastReadProvider());
|
||||
Get.lazyPut(() => SubscriptionProvider());
|
||||
|
||||
Get.find<WebSocketProvider>().requestPermissions();
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'account.g.dart';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'account_status.g.dart';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
|
||||
part 'attachment.g.dart';
|
||||
|
103
lib/models/auth.dart
Normal file
103
lib/models/auth.dart
Normal file
@ -0,0 +1,103 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
|
||||
part 'auth.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class AuthResult {
|
||||
bool isFinished;
|
||||
AuthTicket ticket;
|
||||
|
||||
AuthResult({
|
||||
required this.isFinished,
|
||||
required this.ticket,
|
||||
});
|
||||
|
||||
factory AuthResult.fromJson(Map<String, dynamic> json) =>
|
||||
_$AuthResultFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AuthResultToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class AuthTicket {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
String location;
|
||||
String ipAddress;
|
||||
String userAgent;
|
||||
int stepRemain;
|
||||
List<String> claims;
|
||||
List<String> audiences;
|
||||
@JsonKey(defaultValue: [])
|
||||
List<int> factorTrail;
|
||||
String? grantToken;
|
||||
String? accessToken;
|
||||
String? refreshToken;
|
||||
DateTime? expiredAt;
|
||||
DateTime? availableAt;
|
||||
DateTime? lastGrantAt;
|
||||
String? nonce;
|
||||
int? clientId;
|
||||
Account account;
|
||||
int accountId;
|
||||
|
||||
AuthTicket({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.location,
|
||||
required this.ipAddress,
|
||||
required this.userAgent,
|
||||
required this.stepRemain,
|
||||
required this.claims,
|
||||
required this.audiences,
|
||||
required this.factorTrail,
|
||||
required this.grantToken,
|
||||
required this.accessToken,
|
||||
required this.refreshToken,
|
||||
required this.expiredAt,
|
||||
required this.availableAt,
|
||||
required this.lastGrantAt,
|
||||
required this.nonce,
|
||||
required this.clientId,
|
||||
required this.account,
|
||||
required this.accountId,
|
||||
});
|
||||
|
||||
factory AuthTicket.fromJson(Map<String, dynamic> json) =>
|
||||
_$AuthTicketFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AuthTicketToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class AuthFactor {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
int type;
|
||||
Map<String, dynamic>? config;
|
||||
Account account;
|
||||
int accountId;
|
||||
|
||||
AuthFactor({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.type,
|
||||
required this.config,
|
||||
required this.account,
|
||||
required this.accountId,
|
||||
});
|
||||
|
||||
factory AuthFactor.fromJson(Map<String, dynamic> json) =>
|
||||
_$AuthFactorFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AuthFactorToJson(this);
|
||||
}
|
105
lib/models/auth.g.dart
Normal file
105
lib/models/auth.g.dart
Normal file
@ -0,0 +1,105 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'auth.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
AuthResult _$AuthResultFromJson(Map<String, dynamic> json) => AuthResult(
|
||||
isFinished: json['is_finished'] as bool,
|
||||
ticket: AuthTicket.fromJson(json['ticket'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AuthResultToJson(AuthResult instance) =>
|
||||
<String, dynamic>{
|
||||
'is_finished': instance.isFinished,
|
||||
'ticket': instance.ticket.toJson(),
|
||||
};
|
||||
|
||||
AuthTicket _$AuthTicketFromJson(Map<String, dynamic> json) => AuthTicket(
|
||||
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),
|
||||
location: json['location'] as String,
|
||||
ipAddress: json['ip_address'] as String,
|
||||
userAgent: json['user_agent'] as String,
|
||||
stepRemain: (json['step_remain'] as num).toInt(),
|
||||
claims:
|
||||
(json['claims'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
audiences:
|
||||
(json['audiences'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
factorTrail: (json['factor_trail'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList() ??
|
||||
[],
|
||||
grantToken: json['grant_token'] as String?,
|
||||
accessToken: json['access_token'] as String?,
|
||||
refreshToken: json['refresh_token'] as String?,
|
||||
expiredAt: json['expired_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['expired_at'] as String),
|
||||
availableAt: json['available_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['available_at'] as String),
|
||||
lastGrantAt: json['last_grant_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['last_grant_at'] as String),
|
||||
nonce: json['nonce'] as String?,
|
||||
clientId: (json['client_id'] as num?)?.toInt(),
|
||||
account: Account.fromJson(json['account'] as Map<String, dynamic>),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AuthTicketToJson(AuthTicket instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'location': instance.location,
|
||||
'ip_address': instance.ipAddress,
|
||||
'user_agent': instance.userAgent,
|
||||
'step_remain': instance.stepRemain,
|
||||
'claims': instance.claims,
|
||||
'audiences': instance.audiences,
|
||||
'factor_trail': instance.factorTrail,
|
||||
'grant_token': instance.grantToken,
|
||||
'access_token': instance.accessToken,
|
||||
'refresh_token': instance.refreshToken,
|
||||
'expired_at': instance.expiredAt?.toIso8601String(),
|
||||
'available_at': instance.availableAt?.toIso8601String(),
|
||||
'last_grant_at': instance.lastGrantAt?.toIso8601String(),
|
||||
'nonce': instance.nonce,
|
||||
'client_id': instance.clientId,
|
||||
'account': instance.account.toJson(),
|
||||
'account_id': instance.accountId,
|
||||
};
|
||||
|
||||
AuthFactor _$AuthFactorFromJson(Map<String, dynamic> json) => AuthFactor(
|
||||
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),
|
||||
type: (json['type'] as num).toInt(),
|
||||
config: json['config'] as Map<String, dynamic>?,
|
||||
account: Account.fromJson(json['account'] as Map<String, dynamic>),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AuthFactorToJson(AuthFactor instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'type': instance.type,
|
||||
'config': instance.config,
|
||||
'account': instance.account.toJson(),
|
||||
'account_id': instance.accountId,
|
||||
};
|
@ -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:solian/models/channel.dart';
|
||||
|
||||
|
@ -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/realm.dart';
|
||||
|
||||
@ -19,7 +19,8 @@ class Channel {
|
||||
int accountId;
|
||||
Realm? realm;
|
||||
int? realmId;
|
||||
bool isEncrypted;
|
||||
bool isPublic;
|
||||
bool isCommunity;
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: true)
|
||||
bool isAvailable = false;
|
||||
@ -36,7 +37,8 @@ class Channel {
|
||||
required this.members,
|
||||
required this.account,
|
||||
required this.accountId,
|
||||
required this.isEncrypted,
|
||||
required this.isPublic,
|
||||
required this.isCommunity,
|
||||
required this.realm,
|
||||
required this.realmId,
|
||||
});
|
||||
|
@ -22,7 +22,8 @@ Channel _$ChannelFromJson(Map<String, dynamic> json) => Channel(
|
||||
.toList(),
|
||||
account: Account.fromJson(json['account'] as Map<String, dynamic>),
|
||||
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
|
||||
? null
|
||||
: Realm.fromJson(json['realm'] as Map<String, dynamic>),
|
||||
@ -43,7 +44,8 @@ Map<String, dynamic> _$ChannelToJson(Channel instance) => <String, dynamic>{
|
||||
'account_id': instance.accountId,
|
||||
'realm': instance.realm?.toJson(),
|
||||
'realm_id': instance.realmId,
|
||||
'is_encrypted': instance.isEncrypted,
|
||||
'is_public': instance.isPublic,
|
||||
'is_community': instance.isCommunity,
|
||||
'is_available': instance.isAvailable,
|
||||
};
|
||||
|
||||
|
@ -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:solian/models/account.dart';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
|
||||
part 'event.g.dart';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'link.g.dart';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'notification.g.dart';
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'packet.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class NetworkPackage {
|
||||
@JsonKey(name: 'w')
|
||||
@JsonKey(name: 'w', defaultValue: 'unknown')
|
||||
String method;
|
||||
@JsonKey(name: 'e')
|
||||
String? endpoint;
|
||||
|
@ -8,7 +8,7 @@ part of 'packet.dart';
|
||||
|
||||
NetworkPackage _$NetworkPackageFromJson(Map<String, dynamic> json) =>
|
||||
NetworkPackage(
|
||||
method: json['w'] as String,
|
||||
method: json['w'] as String? ?? 'unknown',
|
||||
endpoint: json['e'] as String?,
|
||||
message: json['m'] as String?,
|
||||
payload: json['p'] as Map<String, dynamic>?,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'pagination.g.dart';
|
||||
|
||||
|
@ -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/post_categories.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'post_categories.g.dart';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
|
||||
part 'realm.g.dart';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
|
||||
part 'relations.g.dart';
|
||||
|
@ -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/attachment.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
41
lib/models/subscription.dart
Normal file
41
lib/models/subscription.dart
Normal 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);
|
||||
}
|
46
lib/models/subscription.g.dart
Normal file
46
lib/models/subscription.g.dart
Normal 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(),
|
||||
};
|
@ -37,7 +37,7 @@ class StatusProvider extends GetConnect {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
return await client.get('/users/me/status');
|
||||
}
|
||||
@ -56,7 +56,7 @@ class StatusProvider extends GetConnect {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final payload = {
|
||||
'type': type,
|
||||
@ -85,7 +85,7 @@ class StatusProvider extends GetConnect {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.delete('/users/me/status');
|
||||
if (resp.statusCode != 200) {
|
||||
|
@ -9,6 +9,7 @@ import 'package:get/get_connect/http/src/request/request.dart';
|
||||
import 'package:solian/background.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:solian/models/auth.dart';
|
||||
import 'package:solian/providers/database/database.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/services.dart';
|
||||
@ -114,14 +115,14 @@ class AuthProvider extends GetConnect {
|
||||
return request;
|
||||
}
|
||||
|
||||
GetConnect configureClient(
|
||||
Future<GetConnect> configureClient(
|
||||
String service, {
|
||||
timeout = const Duration(seconds: 5),
|
||||
}) {
|
||||
}) async {
|
||||
final client = GetConnect(
|
||||
maxAuthRetries: 3,
|
||||
timeout: timeout,
|
||||
userAgent: 'Solian/1.1',
|
||||
userAgent: await ServiceFinder.getUserAgent(),
|
||||
sendUserAgent: true,
|
||||
);
|
||||
client.httpClient.addAuthenticator(requestAuthenticator);
|
||||
@ -148,27 +149,13 @@ class AuthProvider extends GetConnect {
|
||||
|
||||
Future<TokenSet> signin(
|
||||
BuildContext context,
|
||||
String username,
|
||||
String password,
|
||||
AuthTicket ticket,
|
||||
) async {
|
||||
userProfile.value = null;
|
||||
|
||||
final client = ServiceFinder.configureClient('auth');
|
||||
|
||||
// Create ticket
|
||||
final resp = await client.post('/auth', {
|
||||
'username': username,
|
||||
'password': password,
|
||||
});
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
} else if (resp.body['is_finished'] == false) {
|
||||
throw RiskyAuthenticateException(resp.body['ticket']['id']);
|
||||
}
|
||||
|
||||
// Assign token
|
||||
final tokenResp = await post('/auth/token', {
|
||||
'code': resp.body['ticket']['grant_token'],
|
||||
'code': ticket.grantToken!,
|
||||
'grant_type': 'grant_token',
|
||||
});
|
||||
if (tokenResp.statusCode != 200) {
|
||||
@ -217,7 +204,7 @@ class AuthProvider extends GetConnect {
|
||||
|
||||
Future<void> refreshUserProfile() async {
|
||||
if (!isAuthorized.value) return;
|
||||
final client = configureClient('auth');
|
||||
final client = await configureClient('auth');
|
||||
final resp = await client.get('/users/me');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
|
@ -92,7 +92,7 @@ class ChatCallProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.post(
|
||||
'/channels/global/${channel.value!.alias}/calls/ongoing/token',
|
||||
|
@ -93,7 +93,7 @@ class AttachmentProvider extends GetConnect {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient(
|
||||
final client = await auth.configureClient(
|
||||
'uc',
|
||||
timeout: const Duration(minutes: 3),
|
||||
);
|
||||
@ -135,7 +135,7 @@ class AttachmentProvider extends GetConnect {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('uc');
|
||||
final client = await auth.configureClient('uc');
|
||||
|
||||
final fileAlt = basename(path).contains('.')
|
||||
? basename(path).substring(0, basename(path).lastIndexOf('.'))
|
||||
@ -173,7 +173,7 @@ class AttachmentProvider extends GetConnect {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient(
|
||||
final client = await auth.configureClient(
|
||||
'uc',
|
||||
timeout: const Duration(minutes: 3),
|
||||
);
|
||||
@ -198,7 +198,7 @@ class AttachmentProvider extends GetConnect {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('files');
|
||||
final client = await auth.configureClient('files');
|
||||
|
||||
var resp = await client.put('/attachments/$id', {
|
||||
'alt': alt,
|
||||
@ -217,7 +217,7 @@ class AttachmentProvider extends GetConnect {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('files');
|
||||
final client = await auth.configureClient('files');
|
||||
|
||||
var resp = await client.delete('/attachments/$id');
|
||||
if (resp.statusCode != 200) {
|
||||
|
@ -33,7 +33,7 @@ class ChannelProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get('/channels/$realm/$alias');
|
||||
if (resp.statusCode != 200) {
|
||||
@ -48,7 +48,7 @@ class ChannelProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get('/channels/$realm/$alias/me');
|
||||
if (resp.statusCode != 200) {
|
||||
@ -63,7 +63,7 @@ class ChannelProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get('/channels/$realm/$alias/calls/ongoing');
|
||||
if (resp.statusCode == 404) {
|
||||
@ -79,7 +79,7 @@ class ChannelProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get('/channels/$scope');
|
||||
if (resp.statusCode != 200) {
|
||||
@ -89,13 +89,13 @@ class ChannelProvider extends GetxController {
|
||||
return resp;
|
||||
}
|
||||
|
||||
Future<Response> listAvailableChannel({String realm = 'global'}) async {
|
||||
Future<Response> listAvailableChannel({String scope = 'global'}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = 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) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
@ -107,7 +107,7 @@ class ChannelProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.post('/channels/$scope', payload);
|
||||
if (resp.statusCode != 200) {
|
||||
@ -132,7 +132,7 @@ class ChannelProvider extends GetxController {
|
||||
if (related == null) return null;
|
||||
|
||||
final prof = auth.userProfile.value!;
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.post('/channels/$scope/dm', {
|
||||
'alias': const Uuid().v4().replaceAll('-', '').substring(0, 12),
|
||||
@ -153,7 +153,7 @@ class ChannelProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.put('/channels/$scope/$id', payload);
|
||||
if (resp.statusCode != 200) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
@ -14,9 +15,9 @@ class PostProvider extends GetConnect {
|
||||
GetConnect client;
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.value) {
|
||||
client = auth.configureClient('co');
|
||||
client = await auth.configureClient('co');
|
||||
} else {
|
||||
client = ServiceFinder.configureClient('co');
|
||||
client = await ServiceFinder.configureClient('co');
|
||||
}
|
||||
final resp = await client.get('/whats-new?pivot=$pivot');
|
||||
if (resp.statusCode != 200) {
|
||||
@ -36,9 +37,9 @@ class PostProvider extends GetConnect {
|
||||
if (realm != null) 'realm=$realm',
|
||||
];
|
||||
if (auth.isAuthorized.value) {
|
||||
client = auth.configureClient('co');
|
||||
client = await auth.configureClient('co');
|
||||
} else {
|
||||
client = ServiceFinder.configureClient('co');
|
||||
client = await ServiceFinder.configureClient('co');
|
||||
}
|
||||
final resp = await client.get(
|
||||
channel == null
|
||||
@ -60,7 +61,7 @@ class PostProvider extends GetConnect {
|
||||
'take=${10}',
|
||||
'offset=$page',
|
||||
];
|
||||
final client = auth.configureClient('interactive');
|
||||
final client = await auth.configureClient('interactive');
|
||||
final resp = await client.get('/posts/drafts?${queries.join('&')}');
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
@ -96,6 +97,15 @@ class PostProvider extends GetConnect {
|
||||
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 {
|
||||
final resp = await get('/posts/$alias');
|
||||
if (resp.statusCode != 200) {
|
||||
|
@ -25,7 +25,7 @@ class RealmProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.get('/realms/$alias');
|
||||
if (resp.statusCode != 200) {
|
||||
@ -39,7 +39,7 @@ class RealmProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.get('/realms/me/available');
|
||||
if (resp.statusCode != 200) {
|
||||
|
@ -10,7 +10,7 @@ class DailySignProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('id');
|
||||
final client = await auth.configureClient('id');
|
||||
|
||||
final resp = await client.get('/daily?take=$take');
|
||||
if (resp.statusCode != 200 && resp.statusCode != 404) {
|
||||
@ -30,7 +30,7 @@ class DailySignProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('id');
|
||||
final client = await auth.configureClient('id');
|
||||
|
||||
final resp = await client.get('/daily/today');
|
||||
if (resp.statusCode != 200 && resp.statusCode != 404) {
|
||||
@ -46,7 +46,7 @@ class DailySignProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('id');
|
||||
final client = await auth.configureClient('id');
|
||||
|
||||
final resp = await client.post('/daily', {});
|
||||
if (resp.statusCode != 200) {
|
||||
|
@ -20,7 +20,13 @@ class AppDatabase extends _$AppDatabase {
|
||||
int get schemaVersion => 1;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
return driftDatabase(name: 'solar_network_local_db');
|
||||
return driftDatabase(
|
||||
name: 'solar_network_local_db',
|
||||
web: DriftWebOptions(
|
||||
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
|
||||
driftWorker: Uri.parse('drift_worker.dart.js'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<int> getDatabaseSize() async {
|
||||
|
@ -12,7 +12,7 @@ class MessagesFetchingProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return null;
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get(
|
||||
'/whats-new?pivot=$pivot&take=$take',
|
||||
@ -33,7 +33,7 @@ class MessagesFetchingProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return null;
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get(
|
||||
'/channels/$scope/${channel.alias}/events/$id',
|
||||
@ -51,19 +51,13 @@ class MessagesFetchingProvider extends GetxController {
|
||||
Future<(List<Event>, int)?> fetchRemoteEvents(
|
||||
Channel channel,
|
||||
String scope, {
|
||||
required int depth,
|
||||
bool Function(List<Event> items)? onBrake,
|
||||
take = 10,
|
||||
offset = 0,
|
||||
}) async {
|
||||
if (depth <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return null;
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get(
|
||||
'/channels/$scope/${channel.alias}/events?take=$take&offset=$offset',
|
||||
@ -77,23 +71,9 @@ class MessagesFetchingProvider extends GetxController {
|
||||
final result =
|
||||
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
|
||||
|
||||
if (onBrake != null && onBrake(result)) {
|
||||
return (result, response.count);
|
||||
}
|
||||
|
||||
final expandResult = (await fetchRemoteEvents(
|
||||
channel,
|
||||
scope,
|
||||
depth: depth - 1,
|
||||
take: take,
|
||||
offset: offset + result.length,
|
||||
))
|
||||
?.$1 ??
|
||||
List.empty();
|
||||
|
||||
return ([...result, ...expandResult], response.count);
|
||||
}
|
||||
|
||||
Future<LocalMessageEventTableData> receiveEvent(Event remote) async {
|
||||
// Insert record
|
||||
final database = Get.find<DatabaseProvider>().database;
|
||||
@ -149,24 +129,24 @@ class MessagesFetchingProvider extends GetxController {
|
||||
return await receiveEvent(remoteRecord);
|
||||
}
|
||||
|
||||
Future<LocalMessageEventTableData?> getEventFromLocal(int id) async {
|
||||
final database = Get.find<DatabaseProvider>().database;
|
||||
final localRecord = await (database.select(database.localMessageEventTable)
|
||||
..where((x) => x.id.equals(id)))
|
||||
.getSingleOrNull();
|
||||
return localRecord;
|
||||
}
|
||||
|
||||
/// Pull the remote events to local database
|
||||
Future<(List<Event>, int)?> pullRemoteEvents(Channel channel,
|
||||
{String scope = 'global', depth = 10, offset = 0}) async {
|
||||
{String scope = 'global', take = 10, offset = 0}) async {
|
||||
final database = Get.find<DatabaseProvider>().database;
|
||||
final lastOne = await (database.select(database.localMessageEventTable)
|
||||
..where((x) => x.channelId.equals(channel.id))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.id)])
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
|
||||
final data = await fetchRemoteEvents(
|
||||
channel,
|
||||
scope,
|
||||
depth: depth,
|
||||
offset: offset,
|
||||
onBrake: (items) {
|
||||
return items.any((x) => x.id == lastOne?.id);
|
||||
},
|
||||
take: take,
|
||||
);
|
||||
if (data != null) {
|
||||
await database.batch((batch) {
|
||||
@ -185,11 +165,13 @@ class MessagesFetchingProvider extends GetxController {
|
||||
return data;
|
||||
}
|
||||
|
||||
Future<List<LocalMessageEventTableData>> listEvents(Channel channel) async {
|
||||
Future<List<LocalMessageEventTableData>> listEvents(Channel channel,
|
||||
{required int take, int offset = 0}) async {
|
||||
final database = Get.find<DatabaseProvider>().database;
|
||||
return await (database.select(database.localMessageEventTable)
|
||||
..where((x) => x.channelId.equals(channel.id))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.id)]))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.id)])
|
||||
..limit(take, offset: offset))
|
||||
.get();
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@ class LinkExpandProvider extends GetxController {
|
||||
log('[LinkExpander] Expanding link... $url');
|
||||
final target = utf8.fuse(base64).encode(url);
|
||||
if (_cachedResponse.containsKey(target)) return _cachedResponse[target];
|
||||
final client = ServiceFinder.configureClient('dealer');
|
||||
final client = await ServiceFinder.configureClient('dealer');
|
||||
final resp = await client.get('/api/links/$target');
|
||||
if (resp.statusCode != 200) {
|
||||
log('Unable to expand link ($url), status: ${resp.statusCode}, response: ${resp.body}');
|
||||
|
@ -26,21 +26,21 @@ class RelationshipProvider extends GetxController {
|
||||
return _friends.any((x) => x.relatedId == account.id);
|
||||
}
|
||||
|
||||
Future<Response> listRelation() {
|
||||
Future<Response> listRelation() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
return client.get('/users/me/relations');
|
||||
}
|
||||
|
||||
Future<Response> listRelationWithStatus(int status) {
|
||||
Future<Response> listRelationWithStatus(int status) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
return client.get('/users/me/relations?status=$status');
|
||||
}
|
||||
|
||||
Future<Response> makeFriend(String username) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
final resp = await client.post('/users/me/relations?related=$username', {});
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
@ -52,7 +52,7 @@ class RelationshipProvider extends GetxController {
|
||||
Future<Response> handleRelation(
|
||||
Relationship relationship, bool doAccept) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
final resp = await client.post(
|
||||
'/users/me/relations/${relationship.relatedId}/${doAccept ? 'accept' : 'decline'}',
|
||||
{},
|
||||
@ -66,7 +66,7 @@ class RelationshipProvider extends GetxController {
|
||||
|
||||
Future<Response> editRelation(Relationship relationship, int status) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
final resp = await client.patch(
|
||||
'/users/me/relations/${relationship.relatedId}',
|
||||
{'status': status},
|
||||
|
@ -1,34 +1,48 @@
|
||||
import 'dart:async';
|
||||
|
||||
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/services.dart';
|
||||
|
||||
class StickerProvider extends GetxController {
|
||||
final RxMap<String, String> aliasImageMapping = RxMap();
|
||||
final RxList<Sticker> availableStickers = RxList.empty(growable: true);
|
||||
final RxMap<String, FutureOr<Sticker?>> stickerCache = RxMap();
|
||||
|
||||
Future<void> refreshAvailableStickers() async {
|
||||
availableStickers.clear();
|
||||
aliasImageMapping.clear();
|
||||
Future<Sticker?> getStickerByAlias(String alias) {
|
||||
if (stickerCache.containsKey(alias)) {
|
||||
return Future.value(stickerCache[alias]);
|
||||
}
|
||||
|
||||
final client = ServiceFinder.configureClient('files');
|
||||
stickerCache[alias] = Future(() async {
|
||||
final client = await ServiceFinder.configureClient('files');
|
||||
final resp = await client.get(
|
||||
'/stickers/manifest?take=100',
|
||||
'/stickers/lookup/$alias',
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
|
||||
if (out == null) return;
|
||||
if (resp.statusCode != 200) {
|
||||
if (resp.statusCode == 404) {
|
||||
stickerCache[alias] = null;
|
||||
}
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
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);
|
||||
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 resp = await client.get(
|
||||
'/stickers/lookup?probe=$alias',
|
||||
);
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
}
|
||||
availableStickers.refresh();
|
||||
|
||||
return List<Sticker>.from(resp.body.map((x) => Sticker.fromJson(x)));
|
||||
}
|
||||
}
|
||||
|
46
lib/providers/subscription.dart
Normal file
46
lib/providers/subscription.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -138,7 +138,7 @@ class WebSocketProvider extends GetxController {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.get('/notifications?skip=0&take=100');
|
||||
if (resp.statusCode == 200) {
|
||||
@ -152,6 +152,8 @@ class WebSocketProvider extends GetxController {
|
||||
}
|
||||
|
||||
Future<void> registerPushNotifications() async {
|
||||
if (PlatformInfo.isWeb) return;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.getBool('service_background_notification') == true) {
|
||||
log('Background notification service has been enabled, skip register push notifications');
|
||||
@ -180,7 +182,7 @@ class WebSocketProvider extends GetxController {
|
||||
}
|
||||
log('Device Push Token is $token');
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.post('/notifications/subscribe', {
|
||||
'provider': provider,
|
||||
|
@ -1,13 +1,16 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:solian/bootstrapper.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
import 'package:solian/screens/about.dart';
|
||||
import 'package:solian/screens/account.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/stickers.dart';
|
||||
import 'package:solian/screens/auth/signin.dart';
|
||||
import 'package:solian/screens/auth/signup.dart';
|
||||
import 'package:solian/screens/channel/channel_chat.dart';
|
||||
import 'package:solian/screens/channel/channel_detail.dart';
|
||||
import 'package:solian/screens/channel/channel_organize.dart';
|
||||
@ -30,10 +33,13 @@ abstract class AppRouter {
|
||||
static GoRouter instance = GoRouter(
|
||||
routes: [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) => RootShell(
|
||||
builder: (context, state, child) => BootstrapperShell(
|
||||
key: const Key('global-bootstrapper'),
|
||||
child: RootShell(
|
||||
state: state,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
@ -236,14 +242,6 @@ abstract class AppRouter {
|
||||
name: 'accountFriend',
|
||||
builder: (context, state) => const FriendScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/stickers',
|
||||
name: 'accountStickers',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: const StickerScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/personalize',
|
||||
name: 'accountProfile',
|
||||
@ -252,6 +250,14 @@ abstract class AppRouter {
|
||||
child: const PersonalizeScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/preferences/notifications',
|
||||
name: 'notificationPreferences',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
child: const NotificationPreferencesScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/account/view/:name',
|
||||
name: 'accountProfilePage',
|
||||
@ -259,6 +265,24 @@ abstract class AppRouter {
|
||||
name: state.pathParameters['name']!,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth/sign-in',
|
||||
name: 'signin',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
isCenteredTitle: true,
|
||||
child: const SignInScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/auth/sign-up',
|
||||
name: 'signup',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
isCenteredTitle: true,
|
||||
child: const SignUpScreen(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -6,8 +6,6 @@ import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/account_status.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/auth/signin.dart';
|
||||
import 'package:solian/screens/auth/signup.dart';
|
||||
import 'package:solian/widgets/account/account_heading.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
@ -47,11 +45,6 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
'accountFriend'.tr,
|
||||
'accountFriend',
|
||||
),
|
||||
(
|
||||
const Icon(Icons.emoji_symbols),
|
||||
'accountStickers'.tr,
|
||||
'accountStickers',
|
||||
),
|
||||
];
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
@ -73,13 +66,7 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
title: 'signin'.tr,
|
||||
caption: 'signinCaption'.tr,
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isDismissible: false,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const SignInPopup(),
|
||||
).then((val) async {
|
||||
AppRouter.instance.pushNamed('signin').then((val) async {
|
||||
if (val == true) {
|
||||
await auth.refreshUserProfile();
|
||||
}
|
||||
@ -94,13 +81,7 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
title: 'signup'.tr,
|
||||
caption: 'signupCaption'.tr,
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isDismissible: false,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const SignUpPopup(),
|
||||
).then((_) {
|
||||
AppRouter.instance.pushNamed('signup').then((_) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
@ -150,6 +131,15 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
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)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
ListTile(
|
||||
@ -219,7 +209,6 @@ class _ActionCard extends StatelessWidget {
|
||||
final Function onTap;
|
||||
|
||||
const _ActionCard({
|
||||
super.key,
|
||||
required this.onTap,
|
||||
required this.title,
|
||||
required this.caption,
|
||||
|
@ -31,7 +31,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
}
|
||||
|
||||
if (markList.isNotEmpty) {
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
await client.put('/notifications/read', {'messages': markList});
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
await client.put('/notifications/read/${element.id}', {});
|
||||
|
||||
|
118
lib/screens/account/preferences/notifications.dart
Normal file
118
lib/screens/account/preferences/notifications.dart
Normal 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;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -126,7 +126,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.put(
|
||||
'/users/me/$position',
|
||||
@ -148,7 +148,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
_birthday?.toIso8601String();
|
||||
final resp = await client.put(
|
@ -8,8 +8,10 @@ import 'package:solian/models/account.dart';
|
||||
import 'package:solian/models/attachment.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/models/subscription.dart';
|
||||
import 'package:solian/providers/account_status.dart';
|
||||
import 'package:solian/providers/relation.dart';
|
||||
import 'package:solian/providers/subscription.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/account/account_avatar.dart';
|
||||
@ -37,16 +39,25 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
|
||||
bool _isBusy = true;
|
||||
bool _isMakingFriend = false;
|
||||
bool _isSubscribing = false;
|
||||
bool _showMature = false;
|
||||
|
||||
Account? _userinfo;
|
||||
Subscription? _subscription;
|
||||
List<Post> _pinnedPosts = List.empty();
|
||||
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 {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
var client = ServiceFinder.configureClient('auth');
|
||||
var client = await ServiceFinder.configureClient('auth');
|
||||
var resp = await client.get('/users/${widget.name}');
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(resp.bodyString).then((_) {
|
||||
@ -56,7 +67,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
_userinfo = Account.fromJson(resp.body);
|
||||
}
|
||||
|
||||
client = ServiceFinder.configureClient('interactive');
|
||||
client = await ServiceFinder.configureClient('interactive');
|
||||
resp = await client.get('/users/${widget.name}');
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(resp.bodyString).then((_) {
|
||||
@ -70,8 +81,8 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
Future<void> getPinnedPosts() async {
|
||||
final client = ServiceFinder.configureClient('interactive');
|
||||
Future<void> _getPinnedPosts() async {
|
||||
final client = await ServiceFinder.configureClient('interactive');
|
||||
final resp = await client.get('/users/${widget.name}/pin');
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(resp.bodyString).then((_) {
|
||||
@ -95,7 +106,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
_relationshipProvider = Get.find();
|
||||
_postController = PostListController(author: widget.name);
|
||||
_albumPagingController.addPageRequestListener((pageKey) async {
|
||||
final client = ServiceFinder.configureClient('files');
|
||||
final client = await ServiceFinder.configureClient('files');
|
||||
final resp = await client.get(
|
||||
'/attachments?take=10&offset=$pageKey&author=${widget.name}&original=true',
|
||||
);
|
||||
@ -115,8 +126,10 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
}
|
||||
});
|
||||
|
||||
_getUserinfo();
|
||||
getPinnedPosts();
|
||||
_getUserinfo().then((_) {
|
||||
_getSubscription();
|
||||
_getPinnedPosts();
|
||||
});
|
||||
}
|
||||
|
||||
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 &&
|
||||
!_relationshipProvider.hasFriend(_userinfo!))
|
||||
IconButton(
|
||||
@ -245,7 +292,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
RefreshIndicator(
|
||||
onRefresh: () => Future.wait([
|
||||
_postController.reloadAllOver(),
|
||||
getPinnedPosts(),
|
||||
_getPinnedPosts(),
|
||||
]),
|
||||
child: CustomScrollView(slivers: [
|
||||
SliverToBoxAdapter(
|
||||
@ -302,6 +349,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
isClickable: true,
|
||||
isNestedClickable: true,
|
||||
isShowEmbed: true,
|
||||
showFeaturedReply: true,
|
||||
onUpdate: () {
|
||||
_postController.reloadAllOver();
|
||||
},
|
||||
@ -352,7 +400,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
child: AttachmentListEntry(
|
||||
item: item,
|
||||
isDense: true,
|
||||
parentId: 'album',
|
||||
parentId: 'album-$index',
|
||||
showMature: _showMature,
|
||||
onReveal: (value) {
|
||||
setState(() => _showMature = value);
|
||||
|
@ -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 = 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 = 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(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,28 +1,48 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:protocol_handler/protocol_handler.dart';
|
||||
import 'package:solian/background.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/models/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/services.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
|
||||
class SignInPopup extends StatefulWidget {
|
||||
const SignInPopup({super.key});
|
||||
class SignInScreen extends StatefulWidget {
|
||||
const SignInScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SignInPopup> createState() => _SignInPopupState();
|
||||
State<SignInScreen> createState() => _SignInScreenState();
|
||||
}
|
||||
|
||||
class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
|
||||
class _SignInScreenState extends State<SignInScreen> {
|
||||
bool _isBusy = false;
|
||||
|
||||
AuthTicket? _currentTicket;
|
||||
|
||||
List<AuthFactor>? _factors;
|
||||
int? _factorPicked;
|
||||
int? _factorPickedType;
|
||||
|
||||
int _period = 0;
|
||||
|
||||
final _usernameController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
|
||||
final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
|
||||
0: ('authFactorPassword'.tr, Icons.password, false),
|
||||
1: ('authFactorEmail'.tr, Icons.email, true),
|
||||
};
|
||||
|
||||
Color get _unFocusColor =>
|
||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||
|
||||
void _requestResetPassword() async {
|
||||
final username = _usernameController.value.text;
|
||||
if (username.isEmpty) {
|
||||
@ -32,7 +52,7 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = ServiceFinder.configureClient('auth');
|
||||
final client = await ServiceFinder.configureClient('auth');
|
||||
final lookupResp = await client.get('/users/lookup?probe=$username');
|
||||
if (lookupResp.statusCode != 200) {
|
||||
context.showErrorDialog(lookupResp.bodyString);
|
||||
@ -53,97 +73,339 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
|
||||
context.showModalDialog('done'.tr, 'signinResetPasswordSent'.tr);
|
||||
}
|
||||
|
||||
void _performAction() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
void _performNewTicket() async {
|
||||
final username = _usernameController.value.text;
|
||||
final password = _passwordController.value.text;
|
||||
if (username.isEmpty || password.isEmpty) return;
|
||||
if (username.isEmpty) return;
|
||||
|
||||
final client = await ServiceFinder.configureClient('auth');
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
await auth.signin(context, username, password);
|
||||
await Future.delayed(const Duration(milliseconds: 250), () async {
|
||||
await auth.refreshAuthorizeStatus();
|
||||
await auth.refreshUserProfile();
|
||||
// Create ticket
|
||||
final resp = await client.post('/auth', {
|
||||
'username': username,
|
||||
});
|
||||
} on RiskyAuthenticateException catch (e) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('riskDetection'.tr),
|
||||
content: Text('signinRiskDetected'.tr),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text('next'.tr),
|
||||
onPressed: () {
|
||||
const redirect = 'solink://auth?status=done';
|
||||
launchUrlString(
|
||||
ServiceFinder.buildUrl('capital',
|
||||
'/auth/mfa?redirect_uri=$redirect&ticketId=${e.ticketId}'),
|
||||
mode: LaunchMode.inAppWebView,
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
} else {
|
||||
final result = AuthResult.fromJson(resp.body);
|
||||
_currentTicket = result.ticket;
|
||||
}
|
||||
|
||||
// Pull factors
|
||||
final factorResp = await client.get('/auth/factors',
|
||||
query: {'ticketId': _currentTicket!.id.toString()});
|
||||
if (factorResp.statusCode != 200) {
|
||||
throw RequestException(factorResp);
|
||||
} else {
|
||||
final result = List<AuthFactor>.from(
|
||||
factorResp.body.map((x) => AuthFactor.fromJson(x)),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
return;
|
||||
_factors = result;
|
||||
}
|
||||
|
||||
setState(() => _period++);
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
return;
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _performGetFactorCode() async {
|
||||
if (_factorPicked == null) return;
|
||||
|
||||
final client = await ServiceFinder.configureClient('auth');
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
// Request one-time-password code
|
||||
final resp = await client.post('/auth/factors/$_factorPicked', {});
|
||||
if (resp.statusCode != 200 && resp.statusCode != 204) {
|
||||
throw RequestException(resp);
|
||||
} else {
|
||||
_factorPickedType = _factors!
|
||||
.where(
|
||||
(x) => x.id == _factorPicked,
|
||||
)
|
||||
.first
|
||||
.type;
|
||||
}
|
||||
|
||||
setState(() => _period++);
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
return;
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _performCheckTicket() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
final password = _passwordController.value.text;
|
||||
if (password.isEmpty) return;
|
||||
|
||||
final client = await ServiceFinder.configureClient('auth');
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
// Check ticket
|
||||
final resp = await client.request('/auth', 'PATCH', body: {
|
||||
'ticket_id': _currentTicket!.id,
|
||||
'factor_id': _factorPicked!,
|
||||
'code': password,
|
||||
});
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
final result = AuthResult.fromJson(resp.body);
|
||||
_currentTicket = result.ticket;
|
||||
_passwordController.clear();
|
||||
|
||||
// Finish sign in if possible
|
||||
if (result.isFinished) {
|
||||
await auth.signin(context, _currentTicket!);
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 250), () async {
|
||||
await auth.refreshAuthorizeStatus();
|
||||
await auth.refreshUserProfile();
|
||||
|
||||
Get.find<ChannelProvider>().refreshAvailableChannel();
|
||||
Get.find<RealmProvider>().refreshAvailableRealms();
|
||||
Get.find<RelationshipProvider>().refreshRelativeList();
|
||||
Get.find<WebSocketProvider>().registerPushNotifications();
|
||||
autoConfigureBackgroundNotificationService();
|
||||
autoStartBackgroundNotificationService();
|
||||
|
||||
Navigator.pop(context, true);
|
||||
});
|
||||
} else {
|
||||
// Skip the first step
|
||||
_factorPicked = null;
|
||||
_factorPickedType = null;
|
||||
setState(() => _period += 2);
|
||||
}
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
return;
|
||||
} finally {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
protocolHandler.addListener(this);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
protocolHandler.removeListener(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void onProtocolUrlReceived(String url) {
|
||||
final uri = url.replaceFirst('solink://', '');
|
||||
if (uri == 'auth?status=done') {
|
||||
closeInAppWebView();
|
||||
_performAction();
|
||||
void _previousStep() {
|
||||
assert(_period > 0);
|
||||
switch (_period % 3) {
|
||||
case 1:
|
||||
_currentTicket = null;
|
||||
_factors = null;
|
||||
_factorPicked = null;
|
||||
case 2:
|
||||
_passwordController.clear();
|
||||
_factorPickedType = null;
|
||||
default:
|
||||
setState(() => _period--);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.9,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.6,
|
||||
constraints: const BoxConstraints(maxWidth: 360),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: CenteredContainer(
|
||||
maxWidth: 360,
|
||||
child: PageTransitionSwitcher(
|
||||
transitionBuilder: (
|
||||
Widget child,
|
||||
Animation<double> primaryAnimation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return SharedAxisTransition(
|
||||
animation: primaryAnimation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: switch (_period % 3) {
|
||||
1 => Column(
|
||||
key: const ValueKey<int>(1),
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Image.asset('assets/logo.png', width: 64, height: 64),
|
||||
).paddingOnly(bottom: 4),
|
||||
child:
|
||||
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||
).paddingOnly(bottom: 8, left: 4),
|
||||
Text(
|
||||
'signinPickFactor'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
).paddingOnly(left: 4, bottom: 16),
|
||||
Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
children: _factors
|
||||
?.map(
|
||||
(x) => CheckboxListTile(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(8),
|
||||
),
|
||||
),
|
||||
secondary: Icon(
|
||||
_factorLabelMap[x.type]?.$2 ??
|
||||
Icons.question_mark,
|
||||
),
|
||||
title: Text(
|
||||
_factorLabelMap[x.type]?.$1 ?? 'unknown'.tr,
|
||||
),
|
||||
enabled: !_currentTicket!.factorTrail
|
||||
.contains(x.id),
|
||||
value: _factorPicked == x.id,
|
||||
onChanged: (value) {
|
||||
if (value == true) {
|
||||
setState(() => _factorPicked = x.id);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
List.empty(),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'signinMultiFactor'.trParams(
|
||||
{'n': _currentTicket!.stepRemain.toString()},
|
||||
),
|
||||
style: TextStyle(color: _unFocusColor, fontSize: 12),
|
||||
).paddingOnly(left: 16, right: 16),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: (_isBusy || _period > 1)
|
||||
? null
|
||||
: () => _previousStep(),
|
||||
style:
|
||||
TextButton.styleFrom(foregroundColor: Colors.grey),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.chevron_left),
|
||||
Text('prev'.tr),
|
||||
],
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed:
|
||||
_isBusy ? null : () => _performGetFactorCode(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('next'.tr),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
2 => Column(
|
||||
key: const ValueKey<int>(2),
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child:
|
||||
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||
).paddingOnly(bottom: 8, left: 4),
|
||||
Text(
|
||||
'signinEnterPassword'.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
).paddingOnly(left: 4, bottom: 16),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
controller: _passwordController,
|
||||
obscureText: true,
|
||||
autofillHints: [
|
||||
(_factorLabelMap[_factorPickedType]?.$3 ?? true)
|
||||
? AutofillHints.password
|
||||
: AutofillHints.oneTimeCode
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText:
|
||||
(_factorLabelMap[_factorPickedType]?.$3 ?? true)
|
||||
? 'passwordOneTime'.tr
|
||||
: 'password'.tr,
|
||||
helperText:
|
||||
(_factorLabelMap[_factorPickedType]?.$3 ?? true)
|
||||
? 'passwordOneTimeInputHint'.tr
|
||||
: 'passwordInputHint'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onSubmitted: _isBusy ? null : (_) => _performCheckTicket(),
|
||||
),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _previousStep(),
|
||||
style:
|
||||
TextButton.styleFrom(foregroundColor: Colors.grey),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.chevron_left),
|
||||
Text('prev'.tr),
|
||||
],
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _performCheckTicket(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('next'.tr),
|
||||
const Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
_ => Column(
|
||||
key: const ValueKey<int>(0),
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child:
|
||||
Image.asset('assets/logo.png', width: 64, height: 64),
|
||||
).paddingOnly(bottom: 8, left: 4),
|
||||
Text(
|
||||
'signinGreeting'.tr,
|
||||
style: const TextStyle(
|
||||
@ -160,37 +422,25 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'username'.tr,
|
||||
helperText: 'usernameInputHint'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
const Gap(12),
|
||||
TextField(
|
||||
obscureText: true,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
controller: _passwordController,
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: 'password'.tr,
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
onSubmitted: (_) => _performAction(),
|
||||
onSubmitted: _isBusy ? null : (_) => _performNewTicket(),
|
||||
),
|
||||
const Gap(12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _requestResetPassword(),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.grey),
|
||||
onPressed:
|
||||
_isBusy ? null : () => _requestResetPassword(),
|
||||
style:
|
||||
TextButton.styleFrom(foregroundColor: Colors.grey),
|
||||
child: Text('forgotPassword'.tr),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => _performAction(),
|
||||
onPressed: _isBusy ? null : () => _performNewTicket(),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -203,6 +453,7 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
|
||||
),
|
||||
],
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -3,15 +3,16 @@ import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
|
||||
class SignUpPopup extends StatefulWidget {
|
||||
const SignUpPopup({super.key});
|
||||
class SignUpScreen extends StatefulWidget {
|
||||
const SignUpScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SignUpPopup> createState() => _SignUpPopupState();
|
||||
State<SignUpScreen> createState() => _SignUpScreenState();
|
||||
}
|
||||
|
||||
class _SignUpPopupState extends State<SignUpPopup> {
|
||||
class _SignUpScreenState extends State<SignUpScreen> {
|
||||
final _emailController = TextEditingController();
|
||||
final _usernameController = TextEditingController();
|
||||
final _nicknameController = TextEditingController();
|
||||
@ -27,7 +28,7 @@ class _SignUpPopupState extends State<SignUpPopup> {
|
||||
nickname.isEmpty ||
|
||||
password.isEmpty) return;
|
||||
|
||||
final client = ServiceFinder.configureClient('auth');
|
||||
final client = await ServiceFinder.configureClient('auth');
|
||||
|
||||
final resp = await client.post('/users', {
|
||||
'name': username,
|
||||
@ -61,12 +62,10 @@ class _SignUpPopupState extends State<SignUpPopup> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(context).size.height * 0.9,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.6,
|
||||
constraints: const BoxConstraints(maxWidth: 360),
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: CenteredContainer(
|
||||
maxWidth: 360,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -74,7 +73,7 @@ class _SignUpPopupState extends State<SignUpPopup> {
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: Image.asset('assets/logo.png', width: 64, height: 64),
|
||||
).paddingOnly(bottom: 4),
|
||||
).paddingOnly(bottom: 8, left: 4),
|
||||
Text(
|
||||
'signupGreeting'.tr,
|
||||
style: const TextStyle(
|
||||
@ -156,7 +155,6 @@ class _SignUpPopupState extends State<SignUpPopup> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
setState(() => _ongoingCall = Call.fromJson(resp.body));
|
||||
}
|
||||
} catch (e) {
|
||||
print((e as dynamic).stackTrace);
|
||||
context.showErrorDialog(e);
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client
|
||||
.put('/channels/${widget.realm}/${widget.channel.alias}/members/me', {
|
||||
@ -114,7 +114,8 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('channelSettings'.tr.capitalize!),
|
||||
title: Text('channelSettings'.tr),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () async {
|
||||
AppRouter.instance
|
||||
.pushNamed(
|
||||
@ -173,7 +174,8 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.notifications_active),
|
||||
title: Text('channelNotifyLevel'.tr.capitalize!),
|
||||
title: Text('channelNotifyLevel'.tr),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
trailing: DropdownButtonHideUnderline(
|
||||
child: DropdownButton2<int>(
|
||||
isExpanded: true,
|
||||
@ -206,14 +208,16 @@ class _ChannelDetailScreenState extends State<ChannelDetailScreen> {
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.supervisor_account),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('channelMembers'.tr.capitalize!),
|
||||
title: Text('channelMembers'.tr),
|
||||
onTap: () => showMemberList(),
|
||||
),
|
||||
...(_isOwned ? ownerActions : List.empty()),
|
||||
const Divider(thickness: 0.3),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: _isOwned
|
||||
? const Icon(Icons.delete)
|
||||
: const Icon(Icons.exit_to_app),
|
||||
|
@ -35,13 +35,14 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
final _nameController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
|
||||
bool _isEncrypted = false;
|
||||
bool _isPublic = false;
|
||||
bool _isCommunity = false;
|
||||
|
||||
void applyChannel() async {
|
||||
void _applyChannel() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
if (_aliasController.value.text.isEmpty) randomizeAlias();
|
||||
if (_aliasController.value.text.isEmpty) _randomizeAlias();
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
@ -52,7 +53,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
'alias': _aliasController.value.text.toLowerCase(),
|
||||
'name': _nameController.value.text,
|
||||
'description': _descriptionController.value.text,
|
||||
'is_encrypted': _isEncrypted,
|
||||
'is_encrypted': _isPublic,
|
||||
};
|
||||
|
||||
Response? resp;
|
||||
@ -71,35 +72,44 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
setState(() => _isBusy = false);
|
||||
}
|
||||
|
||||
void randomizeAlias() {
|
||||
void _randomizeAlias() {
|
||||
_aliasController.text =
|
||||
const Uuid().v4().replaceAll('-', '').substring(0, 12);
|
||||
}
|
||||
|
||||
void syncWidget() {
|
||||
void _syncWidget() {
|
||||
if (widget.edit != null) {
|
||||
_aliasController.text = widget.edit!.alias;
|
||||
_nameController.text = widget.edit!.name;
|
||||
_descriptionController.text = widget.edit!.description;
|
||||
_isEncrypted = widget.edit!.isEncrypted;
|
||||
_isPublic = widget.edit!.isPublic;
|
||||
_isCommunity = widget.edit!.isCommunity;
|
||||
}
|
||||
}
|
||||
|
||||
void cancelAction() {
|
||||
void _cancelAction() {
|
||||
AppRouter.instance.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
syncWidget();
|
||||
_syncWidget();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_aliasController.dispose();
|
||||
_nameController.dispose();
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final notifyBannerActions = [
|
||||
TextButton(
|
||||
onPressed: cancelAction,
|
||||
onPressed: _cancelAction,
|
||||
child: Text('cancel'.tr),
|
||||
),
|
||||
];
|
||||
@ -113,7 +123,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => applyChannel(),
|
||||
onPressed: _isBusy ? null : () => _applyChannel(),
|
||||
child: Text('apply'.tr.toUpperCase()),
|
||||
)
|
||||
],
|
||||
@ -164,7 +174,7 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -2, vertical: -2),
|
||||
),
|
||||
onPressed: () => randomizeAlias(),
|
||||
onPressed: () => _randomizeAlias(),
|
||||
child: const Icon(Icons.refresh),
|
||||
)
|
||||
],
|
||||
@ -196,12 +206,17 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
),
|
||||
const Divider(thickness: 0.3),
|
||||
CheckboxListTile(
|
||||
title: Text('channelEncrypted'.tr),
|
||||
value: _isEncrypted,
|
||||
onChanged: (widget.edit?.isEncrypted ?? false)
|
||||
? null
|
||||
: (newValue) =>
|
||||
setState(() => _isEncrypted = newValue ?? false),
|
||||
title: Text('channelPublic'.tr),
|
||||
value: _isPublic,
|
||||
onChanged: (value) =>
|
||||
setState(() => _isPublic = value ?? false),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: Text('channelCommunity'.tr),
|
||||
value: _isCommunity,
|
||||
onChanged: (value) =>
|
||||
setState(() => _isCommunity = value ?? false),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
),
|
||||
],
|
||||
|
@ -102,7 +102,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
body: Obx(() {
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return SigninRequiredOverlay(
|
||||
onSignedIn: () => _channels.refreshAvailableChannel(),
|
||||
onDone: () => _channels.refreshAvailableChannel(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -125,7 +125,11 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
child: Obx(
|
||||
() => ChannelListWidget(
|
||||
noCategory: true,
|
||||
channels: _channels.directChannels,
|
||||
channels: List.from([
|
||||
..._channels.groupChannels
|
||||
.where((x) => x.realmId == null),
|
||||
..._channels.directChannels
|
||||
]),
|
||||
selfId: selfId,
|
||||
useReplace: true,
|
||||
),
|
||||
|
@ -379,6 +379,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
isClickable: true,
|
||||
isShowEmbed: true,
|
||||
isNestedClickable: true,
|
||||
showFeaturedReply: true,
|
||||
onUpdate: (_) {
|
||||
_pullPosts();
|
||||
},
|
||||
|
@ -151,7 +151,7 @@ class _FeedScreenState extends State<FeedScreen>
|
||||
);
|
||||
} else {
|
||||
return SigninRequiredOverlay(
|
||||
onSignedIn: () => _postController.reloadAllOver(),
|
||||
onDone: () => _postController.reloadAllOver(),
|
||||
);
|
||||
}
|
||||
}),
|
||||
|
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/post.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_replies.dart';
|
||||
|
||||
@ -26,6 +27,7 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
Future<Post?> getDetail() async {
|
||||
if (widget.post != null) {
|
||||
item = widget.post;
|
||||
Get.find<LastReadProvider>().feedLastReadAt = item?.id;
|
||||
return widget.post;
|
||||
}
|
||||
|
||||
@ -38,6 +40,8 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
||||
context.showErrorDialog(e).then((_) => Navigator.pop(context));
|
||||
}
|
||||
|
||||
Get.find<LastReadProvider>().feedLastReadAt = item?.id;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
|
@ -75,7 +75,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = auth.configureClient('interactive');
|
||||
final client = await auth.configureClient('interactive');
|
||||
|
||||
Response resp;
|
||||
if (widget.edit != null) {
|
||||
@ -183,18 +183,18 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
|
||||
children: [
|
||||
ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
title: Row(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_editorController.title ?? 'title'.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const Gap(6),
|
||||
if (_editorController.aliasController.text.isNotEmpty)
|
||||
Badge(
|
||||
label: Text('#${_editorController.aliasController.text}'),
|
||||
),
|
||||
).paddingOnly(bottom: 2),
|
||||
],
|
||||
),
|
||||
subtitle: Text(
|
||||
|
@ -7,10 +7,13 @@ import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/realm.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/account/notification.dart';
|
||||
import 'package:solian/services.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/app_bar_leading.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/sized_container.dart';
|
||||
|
||||
@ -84,7 +87,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
body: Obx(() {
|
||||
if (auth.isAuthorized.isFalse) {
|
||||
return SigninRequiredOverlay(
|
||||
onSignedIn: () => _getRealms(),
|
||||
onDone: () => _getRealms(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -128,18 +131,33 @@ class _RealmListScreenState extends State<RealmListScreen> {
|
||||
children: [
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: (element.banner?.isEmpty ?? true)
|
||||
? const SizedBox.shrink()
|
||||
: AutoCacheImage(
|
||||
ServiceFinder.buildUrl(
|
||||
'uc',
|
||||
'/attachments/${element.banner}',
|
||||
),
|
||||
const Positioned(
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -30,
|
||||
left: 18,
|
||||
child: CircleAvatar(
|
||||
child: (element.avatar?.isEmpty ?? true)
|
||||
? CircleAvatar(
|
||||
radius: 24,
|
||||
backgroundColor: Colors.indigo,
|
||||
child: FaIcon(
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons.globe,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
)
|
||||
: AccountAvatar(
|
||||
content: element.avatar!,
|
||||
bgColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -69,7 +69,8 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('realmSettings'.tr.capitalize!),
|
||||
title: Text('realmSettings'.tr),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
onTap: () async {
|
||||
AppRouter.instance
|
||||
.pushNamed(
|
||||
@ -120,14 +121,16 @@ class _RealmDetailScreenState extends State<RealmDetailScreen> {
|
||||
child: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: const Icon(Icons.supervisor_account),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text('realmMembers'.tr.capitalize!),
|
||||
title: Text('realmMembers'.tr),
|
||||
onTap: () => showMemberList(),
|
||||
),
|
||||
...(_isOwned ? ownerActions : List.empty()),
|
||||
const Divider(thickness: 0.3),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
leading: _isOwned
|
||||
? const Icon(Icons.delete)
|
||||
: const Icon(Icons.exit_to_app),
|
||||
|
@ -1,9 +1,15 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.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/models/attachment.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/attachment.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
import 'package:solian/widgets/app_bar_leading.dart';
|
||||
@ -29,26 +35,30 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
bool _isBusy = false;
|
||||
|
||||
final _aliasController = TextEditingController();
|
||||
final _avatarController = TextEditingController();
|
||||
final _bannerController = TextEditingController();
|
||||
final _nameController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
|
||||
bool _isCommunity = false;
|
||||
bool _isPublic = false;
|
||||
|
||||
void applyRealm() async {
|
||||
void _applyRealm() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
if (_aliasController.value.text.isEmpty) randomizeAlias();
|
||||
if (_aliasController.value.text.isEmpty) _randomizeAlias();
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final payload = {
|
||||
'alias': _aliasController.value.text.toLowerCase(),
|
||||
'name': _nameController.value.text,
|
||||
'description': _descriptionController.value.text,
|
||||
'avatar': _avatarController.value.text,
|
||||
'banner': _bannerController.value.text,
|
||||
'is_public': _isPublic,
|
||||
'is_community': _isCommunity,
|
||||
};
|
||||
@ -68,31 +78,110 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
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 =
|
||||
const Uuid().v4().replaceAll('-', '').substring(0, 12);
|
||||
}
|
||||
|
||||
void syncWidget() {
|
||||
void _syncWidget() {
|
||||
if (widget.edit != null) {
|
||||
_aliasController.text = widget.edit!.alias;
|
||||
_nameController.text = widget.edit!.name;
|
||||
_descriptionController.text = widget.edit!.description;
|
||||
_avatarController.text = widget.edit!.avatar ?? '';
|
||||
_bannerController.text = widget.edit!.banner ?? '';
|
||||
_isPublic = widget.edit!.isPublic;
|
||||
_isCommunity = widget.edit!.isCommunity;
|
||||
}
|
||||
}
|
||||
|
||||
void cancelAction() {
|
||||
void _cancelAction() {
|
||||
AppRouter.instance.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
syncWidget();
|
||||
_syncWidget();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_aliasController.dispose();
|
||||
_avatarController.dispose();
|
||||
_bannerController.dispose();
|
||||
_nameController.dispose();
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
@ -105,7 +194,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isBusy ? null : () => applyRealm(),
|
||||
onPressed: _isBusy ? null : () => _applyRealm(),
|
||||
child: Text('apply'.tr.toUpperCase()),
|
||||
)
|
||||
],
|
||||
@ -126,7 +215,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: cancelAction,
|
||||
onPressed: _cancelAction,
|
||||
child: Text('cancel'.tr),
|
||||
),
|
||||
],
|
||||
@ -150,7 +239,7 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
visualDensity:
|
||||
const VisualDensity(horizontal: -2, vertical: -2),
|
||||
),
|
||||
onPressed: () => randomizeAlias(),
|
||||
onPressed: () => _randomizeAlias(),
|
||||
child: const Icon(Icons.refresh),
|
||||
)
|
||||
],
|
||||
@ -166,6 +255,55 @@ class _RealmOrganizeScreenState extends State<RealmOrganizeScreen> {
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
).paddingSymmetric(horizontal: 16, vertical: 8),
|
||||
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(
|
||||
child: TextField(
|
||||
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';
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
||||
final List<Channel> _channels = List.empty(growable: true);
|
||||
|
||||
getRealm({String? overrideAlias}) async {
|
||||
final RealmProvider provider = Get.find();
|
||||
final RealmProvider realm = Get.find();
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
@ -43,7 +43,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
||||
}
|
||||
|
||||
try {
|
||||
final resp = await provider.getRealm(_overrideAlias ?? widget.alias);
|
||||
final resp = await realm.getRealm(_overrideAlias ?? widget.alias);
|
||||
setState(() => _realm = Realm.fromJson(resp.body));
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
@ -55,14 +55,26 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
|
||||
getChannels() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final ChannelProvider provider = Get.find();
|
||||
final resp = await provider.listChannel(scope: _realm!.alias);
|
||||
final ChannelProvider channel = Get.find();
|
||||
final resp = await channel.listChannel(scope: _realm!.alias);
|
||||
final availableResp = await channel.listAvailableChannel(
|
||||
scope: _realm!.alias,
|
||||
);
|
||||
|
||||
final Set<int> channelIdx = {};
|
||||
|
||||
setState(() {
|
||||
_channels.clear();
|
||||
_channels.addAll(
|
||||
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);
|
||||
|
@ -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),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_sweep),
|
||||
|
@ -1,28 +1,58 @@
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
|
||||
abstract class ServiceFinder {
|
||||
static const bool devFlag = false;
|
||||
|
||||
static const String dealerUrl =
|
||||
devFlag ? 'http://localhost:8442' : 'https://api.sn.solsynth.dev';
|
||||
static const String capitalUrl =
|
||||
devFlag ? 'http://localhost:8444' : 'https://solsynth.dev';
|
||||
|
||||
static String buildUrl(String serviceName, String? append) {
|
||||
append ??= '';
|
||||
if (serviceName == 'dealer') {
|
||||
return '$dealerUrl$append';
|
||||
} else if (serviceName == 'capital') {
|
||||
return '$capitalUrl$append';
|
||||
}
|
||||
return '$dealerUrl/cgi/$serviceName$append';
|
||||
}
|
||||
|
||||
static GetConnect configureClient(String serviceName,
|
||||
{timeout = const Duration(seconds: 5)}) {
|
||||
static Future<String> getUserAgent() async {
|
||||
final String platformInfo;
|
||||
if (PlatformInfo.isAndroid) {
|
||||
final deviceInfo = await DeviceInfoPlugin().androidInfo;
|
||||
platformInfo =
|
||||
'Android; ${deviceInfo.brand} ${deviceInfo.model}; ${deviceInfo.id}';
|
||||
} else if (PlatformInfo.isIOS) {
|
||||
final deviceInfo = await DeviceInfoPlugin().iosInfo;
|
||||
platformInfo = 'iOS; ${deviceInfo.model}; ${deviceInfo.name}';
|
||||
} else if (PlatformInfo.isMacOS) {
|
||||
final deviceInfo = await DeviceInfoPlugin().macOsInfo;
|
||||
platformInfo = 'MacOS; ${deviceInfo.model}; ${deviceInfo.hostName}';
|
||||
} else if (PlatformInfo.isWindows) {
|
||||
final deviceInfo = await DeviceInfoPlugin().windowsInfo;
|
||||
platformInfo =
|
||||
'Windows NT; ${deviceInfo.productName}; ${deviceInfo.computerName}';
|
||||
} else if (PlatformInfo.isLinux) {
|
||||
final deviceInfo = await DeviceInfoPlugin().linuxInfo;
|
||||
platformInfo = 'Linux; ${deviceInfo.prettyName}';
|
||||
} else if (PlatformInfo.isWeb) {
|
||||
final deviceInfo = await DeviceInfoPlugin().webBrowserInfo;
|
||||
platformInfo = 'Web; ${deviceInfo.vendor}';
|
||||
} else {
|
||||
platformInfo = 'Unknown';
|
||||
}
|
||||
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
return 'Solian/${packageInfo.version}+${packageInfo.buildNumber} ($platformInfo)';
|
||||
}
|
||||
|
||||
static Future<GetConnect> configureClient(String serviceName,
|
||||
{timeout = const Duration(seconds: 5)}) async {
|
||||
final client = GetConnect(
|
||||
timeout: timeout,
|
||||
userAgent: 'Solian/1.1',
|
||||
userAgent: await getUserAgent(),
|
||||
sendUserAgent: true,
|
||||
);
|
||||
client.httpClient.baseUrl = buildUrl(serviceName, null);
|
||||
|
@ -24,6 +24,8 @@ class TitleShell extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(state != null || title != null);
|
||||
|
||||
return Scaffold(
|
||||
appBar: showAppBar
|
||||
? AppBar(
|
||||
|
@ -26,7 +26,7 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
try {
|
||||
final client = ServiceFinder.configureClient('auth');
|
||||
final client = await ServiceFinder.configureClient('auth');
|
||||
final resp = await client.get('/users/${widget.name}');
|
||||
if (resp.statusCode == 200) {
|
||||
setState(() {
|
||||
|
@ -36,16 +36,13 @@ class _AccountSelectorState extends State<AccountSelector> {
|
||||
|
||||
_revertSelectedUsers() async {
|
||||
if (widget.initialSelection?.isEmpty ?? true) return;
|
||||
final client = ServiceFinder.configureClient('auth');
|
||||
final client = await ServiceFinder.configureClient('auth');
|
||||
final idQuery = widget.initialSelection!.join(',');
|
||||
final resp = await client.get('/users?id=$idQuery');
|
||||
|
||||
setState(() {
|
||||
_selectedUsers.addAll(
|
||||
resp.body
|
||||
.map((e) => Account.fromJson(e))
|
||||
.toList()
|
||||
.cast<Account>(),
|
||||
resp.body.map((e) => Account.fromJson(e)).toList().cast<Account>(),
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -73,7 +70,7 @@ class _AccountSelectorState extends State<AccountSelector> {
|
||||
|
||||
if (_probeController.text.isEmpty) return;
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
final resp = await client.get(
|
||||
'/users/search?probe=${_probeController.text}',
|
||||
);
|
||||
@ -156,7 +153,8 @@ class _AccountSelectorState extends State<AccountSelector> {
|
||||
}
|
||||
|
||||
setState(() {
|
||||
final idx = _selectedUsers.indexWhere((x) => x.id == element.id);
|
||||
final idx = _selectedUsers
|
||||
.indexWhere((x) => x.id == element.id);
|
||||
if (idx != -1) {
|
||||
_selectedUsers.removeAt(idx);
|
||||
} else {
|
||||
|
@ -1,19 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/screens/auth/signin.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/widgets/sized_container.dart';
|
||||
|
||||
class SigninRequiredOverlay extends StatelessWidget {
|
||||
final Function onSignedIn;
|
||||
final Function onDone;
|
||||
|
||||
const SigninRequiredOverlay({super.key, required this.onSignedIn});
|
||||
const SigninRequiredOverlay({super.key, required this.onDone});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 280),
|
||||
child: CenteredContainer(
|
||||
maxWidth: 280,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -35,15 +35,9 @@ class SigninRequiredOverlay extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
context: context,
|
||||
builder: (context) => const SignInPopup(),
|
||||
).then((value) {
|
||||
if (value != null) onSignedIn();
|
||||
AppRouter.instance.pushNamed('signin').then((value) {
|
||||
if (value != null) onDone();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
@ -312,7 +312,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
}
|
||||
|
||||
Widget _buildQueueEntry(AttachmentUploadTask element, int index) {
|
||||
final extName = extension(element.file.path).substring(1);
|
||||
final extName = element.file.name.contains('.')
|
||||
? extension(element.file.name).substring(1)
|
||||
: '';
|
||||
final canBeCrop = ['png', 'jpg', 'jpeg', 'gif'].contains(extName);
|
||||
|
||||
return Container(
|
||||
@ -482,7 +484,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${fileType[0].toUpperCase()}${fileType.substring(1)} · ${element.size.formatBytes()}',
|
||||
'${fileType.isNotEmpty ? fileType.capitalize : 'unknown'.tr} · ${element.size.formatBytes()}',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
|
@ -93,14 +93,14 @@ class _AttachmentEditorThumbnailDialogState
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('postThumbnail'.tr),
|
||||
title: Text('attachmentThumbnail'.tr),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
title: Text('postThumbnailAttachmentNew'.tr),
|
||||
title: Text('attachmentThumbnailAttachmentNew'.tr),
|
||||
contentPadding: const EdgeInsets.only(left: 12, right: 9),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
shape: const RoundedRectangleBorder(
|
||||
@ -122,7 +122,7 @@ class _AttachmentEditorThumbnailDialogState
|
||||
isDense: true,
|
||||
border: const OutlineInputBorder(),
|
||||
prefixText: '#',
|
||||
labelText: 'postThumbnailAttachment'.tr,
|
||||
labelText: 'attachmentThumbnailAttachment'.tr,
|
||||
),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
),
|
||||
|
@ -233,6 +233,7 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
|
||||
final ratio = widget.item.metadata?['ratio'] ?? 16 / 9;
|
||||
if (!_showContent) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (widget.item.metadata?['thumbnail'] != null)
|
||||
@ -282,6 +283,8 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
|
||||
children: [
|
||||
Text(
|
||||
widget.item.alt,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
shadows: labelShadows,
|
||||
color: Colors.white,
|
||||
@ -398,6 +401,7 @@ class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
|
||||
const ratio = 16 / 9;
|
||||
if (!_showContent) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (widget.item.metadata?['thumbnail'] != null)
|
||||
@ -447,6 +451,8 @@ class _AttachmentItemAudioState extends State<_AttachmentItemAudio> {
|
||||
children: [
|
||||
Text(
|
||||
widget.item.alt,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
shadows: labelShadows,
|
||||
color: Colors.white,
|
||||
|
@ -10,6 +10,7 @@ class AutoCacheImage extends StatelessWidget {
|
||||
final BoxFit? fit;
|
||||
final bool noProgressIndicator;
|
||||
final bool noErrorWidget;
|
||||
final bool isDense;
|
||||
|
||||
const AutoCacheImage(
|
||||
this.url, {
|
||||
@ -19,6 +20,7 @@ class AutoCacheImage extends StatelessWidget {
|
||||
this.fit,
|
||||
this.noProgressIndicator = false,
|
||||
this.noErrorWidget = false,
|
||||
this.isDense = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -46,9 +48,10 @@ class AutoCacheImage extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.close, size: 32)
|
||||
Icon(Icons.close, size: isDense ? 24 : 32)
|
||||
.animate(onPlay: (e) => e.repeat(reverse: true))
|
||||
.fade(duration: 500.ms),
|
||||
if (!isDense)
|
||||
Text(
|
||||
error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
@ -89,9 +92,10 @@ class AutoCacheImage extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.close, size: 32)
|
||||
Icon(Icons.close, size: isDense ? 24 : 32)
|
||||
.animate(onPlay: (e) => e.repeat(reverse: true))
|
||||
.fade(duration: 500.ms),
|
||||
if (!isDense)
|
||||
Text(
|
||||
error.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
|
@ -29,10 +29,10 @@ class _ChannelDeletionDialogState extends State<ChannelDeletionDialog> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client
|
||||
.delete('/channels/${widget.realm}/${widget.channel.id}');
|
||||
final resp =
|
||||
await client.delete('/channels/${widget.realm}/${widget.channel.id}');
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
} else if (Navigator.canPop(context)) {
|
||||
@ -48,7 +48,7 @@ class _ChannelDeletionDialogState extends State<ChannelDeletionDialog> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.delete(
|
||||
'/channels/${widget.realm}/${widget.channel.alias}/members/me',
|
||||
@ -69,10 +69,10 @@ class _ChannelDeletionDialogState extends State<ChannelDeletionDialog> {
|
||||
? 'channelDeletionConfirm'.tr
|
||||
: 'channelLeaveConfirm'.tr),
|
||||
content: Text(
|
||||
widget.isOwned ?
|
||||
'channelDeletionConfirmCaption'
|
||||
.trParams({'channel': '#${widget.channel.alias}'}) :
|
||||
'channelLeaveConfirmCaption'
|
||||
widget.isOwned
|
||||
? 'channelDeletionConfirmCaption'
|
||||
.trParams({'channel': '#${widget.channel.alias}'})
|
||||
: 'channelLeaveConfirmCaption'
|
||||
.trParams({'channel': '#${widget.channel.alias}'}),
|
||||
),
|
||||
actions: <Widget>[
|
||||
|
@ -1,5 +1,8 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/controllers/chat_events_controller.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
@ -213,6 +216,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
|
||||
return _buildEntry(element);
|
||||
},
|
||||
),
|
||||
SliverGap(max(16, MediaQuery.of(context).padding.bottom)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
|
||||
void getMembers() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = ServiceFinder.configureClient('messaging');
|
||||
final client = await ServiceFinder.configureClient('messaging');
|
||||
|
||||
final resp = await client
|
||||
.get('/channels/${widget.realm}/${widget.channel.alias}/members');
|
||||
@ -75,7 +75,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.post(
|
||||
'/channels/${widget.realm}/${widget.channel.alias}/members',
|
||||
@ -96,7 +96,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.request(
|
||||
'/channels/${widget.realm}/${widget.channel.alias}/members',
|
||||
|
@ -33,7 +33,7 @@ class _ChatCallButtonState extends State<ChatCallButton> {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
@ -57,7 +57,7 @@ class _ChatCallButtonState extends State<ChatCallButton> {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
|
@ -30,7 +30,7 @@ class _ChatEventDeletionDialogState extends State<ChatEventDeletionDialog> {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
|
@ -92,10 +92,13 @@ class ChatEventList extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
onFetchData: () {
|
||||
if (chatController.currentEvents.length <
|
||||
chatController.totalEvents.value) {
|
||||
chatController.loadEvents(
|
||||
chatController.channel!,
|
||||
chatController.scope!,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
@ -118,7 +118,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
final mentionedUserNames = _findMentionedUsers(_textController.text);
|
||||
final mentionedUserIds = List<int>.empty(growable: true);
|
||||
|
||||
var client = auth.configureClient('auth');
|
||||
var client = await auth.configureClient('auth');
|
||||
if (mentionedUserNames.isNotEmpty) {
|
||||
resp = await client.get('/users?name=${mentionedUserNames.join(',')}');
|
||||
if (resp.statusCode != 200) {
|
||||
@ -131,7 +131,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
}
|
||||
}
|
||||
|
||||
client = auth.configureClient('messaging');
|
||||
client = await auth.configureClient('messaging');
|
||||
|
||||
if (_textController.text.trim().isEmpty && _attachments.isEmpty) return;
|
||||
|
||||
@ -405,12 +405,9 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
if (emojiMatch != null) {
|
||||
final StickerProvider stickers = Get.find();
|
||||
final emoteSearch = emojiMatch[2]!;
|
||||
return stickers.availableStickers
|
||||
.where(
|
||||
(x) => x.textWarpedPlaceholder
|
||||
.toUpperCase()
|
||||
.contains(emoteSearch.toUpperCase()),
|
||||
)
|
||||
final result = await stickers
|
||||
.searchStickerByAlias(emoteSearch.substring(1));
|
||||
return result
|
||||
.map(
|
||||
(x) => ChatMessageSuggestion(
|
||||
type: 'emotes',
|
||||
@ -418,6 +415,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
x.imageUrl,
|
||||
width: 28,
|
||||
height: 28,
|
||||
isDense: true,
|
||||
),
|
||||
display: x.name,
|
||||
content: x.textWarpedPlaceholder,
|
||||
@ -432,7 +430,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
||||
final userSearch = userMatch[1]!.toLowerCase();
|
||||
final AuthProvider auth = Get.find();
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
final resp = await client.get(
|
||||
'/users/search?probe=$userSearch',
|
||||
);
|
||||
|
@ -14,12 +14,14 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
final String content;
|
||||
final String parentId;
|
||||
final bool isSelectable;
|
||||
final bool isLargeText;
|
||||
|
||||
const MarkdownTextContent({
|
||||
super.key,
|
||||
required this.content,
|
||||
required this.parentId,
|
||||
this.isSelectable = false,
|
||||
this.isLargeText = false,
|
||||
});
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
@ -35,6 +37,14 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
||||
Theme.of(context),
|
||||
).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(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
@ -89,7 +99,6 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
case 'stickers':
|
||||
double radius = 8;
|
||||
final StickerProvider sticker = Get.find();
|
||||
url = sticker.aliasImageMapping[segments[1].toUpperCase()]!;
|
||||
if (emojiMatch.length <= 1 && isOnlyEmoji) {
|
||||
width = 128;
|
||||
height = 128;
|
||||
@ -106,11 +115,20 @@ class MarkdownTextContent extends StatelessWidget {
|
||||
borderRadius: BorderRadius.all(Radius.circular(radius)),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: AutoCacheImage(
|
||||
url,
|
||||
child: FutureBuilder(
|
||||
future: sticker.getStickerByAlias(segments[1]),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return AutoCacheImage(
|
||||
snapshot.data!.imageUrl,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
noErrorWidget: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
).paddingSymmetric(vertical: 4);
|
||||
@ -172,7 +190,8 @@ class _CustomEmoteInlineSyntax extends InlineSyntax {
|
||||
bool onMatch(markdown.InlineParser parser, Match match) {
|
||||
final StickerProvider sticker = Get.find();
|
||||
final alias = match[1]!.toUpperCase();
|
||||
if (sticker.aliasImageMapping[alias] == null) {
|
||||
if (sticker.stickerCache.containsKey(alias) &&
|
||||
sticker.stickerCache[alias] == null) {
|
||||
parser.advanceBy(1);
|
||||
return false;
|
||||
}
|
||||
|
@ -138,7 +138,14 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer>
|
||||
Builder(
|
||||
builder: (context) {
|
||||
if (_accountStatus == null) {
|
||||
return Text('loading'.tr).paddingOnly(left: 16);
|
||||
return Text(
|
||||
'loading'.tr,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
style: TextStyle(
|
||||
color: _unFocusColor,
|
||||
),
|
||||
).paddingOnly(left: 16);
|
||||
}
|
||||
final info = StatusProvider.determineStatus(
|
||||
_accountStatus!,
|
||||
@ -274,6 +281,9 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer>
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: AppNavigationRegion(
|
||||
isCollapsed: _isCollapsed,
|
||||
onSelected: () {
|
||||
_closeDrawer();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
@ -5,15 +6,19 @@ import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/content/channel.dart';
|
||||
import 'package:solian/providers/content/realm.dart';
|
||||
import 'package:solian/providers/navigation.dart';
|
||||
import 'package:solian/services.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';
|
||||
|
||||
class AppNavigationRegion extends StatefulWidget {
|
||||
final bool isCollapsed;
|
||||
final Function onSelected;
|
||||
|
||||
const AppNavigationRegion({
|
||||
super.key,
|
||||
this.isCollapsed = false,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -124,16 +129,12 @@ class _AppNavigationRegionState extends State<AppNavigationRegion> {
|
||||
final NavigationStateProvider navState = Get.find();
|
||||
|
||||
return Obx(
|
||||
() => AnimatedSwitcher(
|
||||
switchInCurve: Curves.fastOutSlowIn,
|
||||
switchOutCurve: Curves.fastOutSlowIn,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(1.0, 0.0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
() => PageTransitionSwitcher(
|
||||
transitionBuilder: (child, animation, secondaryAnimation) {
|
||||
return SharedAxisTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: child,
|
||||
@ -170,6 +171,19 @@ class _AppNavigationRegionState extends State<AppNavigationRegion> {
|
||||
)
|
||||
: Column(
|
||||
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)
|
||||
Tooltip(
|
||||
message: navState.focusedRealm.value!.name,
|
||||
@ -204,6 +218,7 @@ class _AppNavigationRegionState extends State<AppNavigationRegion> {
|
||||
isCollapsed: widget.isCollapsed,
|
||||
selfId: auth.userProfile.value!['id'],
|
||||
noCategory: true,
|
||||
onSelected: (_) => widget.onSelected(),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/controllers/post_editor_controller.dart';
|
||||
import 'package:solian/widgets/attachments/attachment_editor.dart';
|
||||
@ -58,9 +57,11 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: ListTile(
|
||||
title: Text('postThumbnailAttachmentNew'.tr),
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 13),
|
||||
contentPadding: const EdgeInsets.only(left: 12, right: 9),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
@ -69,7 +70,12 @@ class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
|
||||
_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(
|
||||
controller: _attachmentController,
|
||||
decoration: InputDecoration(
|
||||
|
@ -35,7 +35,7 @@ class _TagsFieldState extends State<TagsField> {
|
||||
Future<List<String>?> _searchTags(String probe) async {
|
||||
_currentSearchProbe = probe;
|
||||
|
||||
final client = ServiceFinder.configureClient('interactive');
|
||||
final client = await ServiceFinder.configureClient('interactive');
|
||||
final resp = await client.get(
|
||||
'/tags?take=10&probe=$_currentSearchProbe',
|
||||
);
|
||||
|
@ -192,7 +192,7 @@ class _PostActionState extends State<PostAction> {
|
||||
: 'unpinPost'.tr,
|
||||
),
|
||||
onTap: () async {
|
||||
final client = Get.find<AuthProvider>()
|
||||
final client = await Get.find<AuthProvider>()
|
||||
.configureClient('interactive');
|
||||
await client.post('/posts/${widget.item.id}/pin', {});
|
||||
Navigator.pop(context, true);
|
||||
@ -254,7 +254,7 @@ class _PostDeletionDialogState extends State<PostDeletionDialog> {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final client = auth.configureClient('interactive');
|
||||
final client = await auth.configureClient('interactive');
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
final resp = await client.delete('/posts/${widget.item.id}');
|
||||
|
@ -1,11 +1,13 @@
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.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:solian/models/post.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/screens/posts/post_detail.dart';
|
||||
import 'package:solian/shells/title_shell.dart';
|
||||
import 'package:solian/theme.dart';
|
||||
@ -30,6 +32,7 @@ class PostItem extends StatefulWidget {
|
||||
final bool isFullDate;
|
||||
final bool isFullContent;
|
||||
final bool isContentSelectable;
|
||||
final bool showFeaturedReply;
|
||||
final String? attachmentParent;
|
||||
final Color? backgroundColor;
|
||||
|
||||
@ -45,6 +48,7 @@ class PostItem extends StatefulWidget {
|
||||
this.isFullDate = false,
|
||||
this.isFullContent = false,
|
||||
this.isContentSelectable = false,
|
||||
this.showFeaturedReply = false,
|
||||
this.attachmentParent,
|
||||
this.backgroundColor,
|
||||
});
|
||||
@ -103,7 +107,7 @@ class _PostItemState extends State<PostItem> {
|
||||
children: [
|
||||
if (widget.isCompact)
|
||||
AccountAvatar(
|
||||
content: item.author.avatar.toString(),
|
||||
content: item.author.avatar,
|
||||
radius: 10,
|
||||
).paddingOnly(left: 2, top: 1),
|
||||
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;
|
||||
|
||||
@override
|
||||
@ -417,7 +520,7 @@ class _PostItemState extends State<PostItem> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
child: AccountAvatar(content: item.author.avatar.toString()),
|
||||
child: AccountAvatar(content: item.author.avatar),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
@ -450,6 +553,8 @@ class _PostItemState extends State<PostItem> {
|
||||
parentId: 'p${item.id}-embed',
|
||||
content: item.body['content'],
|
||||
isSelectable: widget.isContentSelectable,
|
||||
isLargeText: item.type == 'article' &&
|
||||
widget.isFullContent,
|
||||
).paddingOnly(left: 12, right: 8),
|
||||
),
|
||||
),
|
||||
@ -506,6 +611,7 @@ class _PostItemState extends State<PostItem> {
|
||||
left: 16,
|
||||
),
|
||||
_buildAttachments(),
|
||||
if (widget.showFeaturedReply) _buildFeaturedReply(),
|
||||
if (widget.isShowReply || widget.isReactable)
|
||||
PostQuickAction(
|
||||
isShowReply: widget.isShowReply,
|
||||
|
@ -33,6 +33,7 @@ class PostListWidget extends StatelessWidget {
|
||||
isShowEmbed: isShowEmbed,
|
||||
isNestedClickable: isNestedClickable,
|
||||
isClickable: isClickable,
|
||||
showFeaturedReply: true,
|
||||
item: item,
|
||||
backgroundColor: backgroundColor,
|
||||
onUpdate: () {
|
||||
@ -51,6 +52,7 @@ class PostListEntryWidget extends StatelessWidget {
|
||||
final bool isShowEmbed;
|
||||
final bool isNestedClickable;
|
||||
final bool isClickable;
|
||||
final bool showFeaturedReply;
|
||||
final Post item;
|
||||
final Function onUpdate;
|
||||
final Color? backgroundColor;
|
||||
@ -61,6 +63,7 @@ class PostListEntryWidget extends StatelessWidget {
|
||||
required this.isShowEmbed,
|
||||
required this.isNestedClickable,
|
||||
required this.isClickable,
|
||||
required this.showFeaturedReply,
|
||||
required this.item,
|
||||
required this.onUpdate,
|
||||
this.backgroundColor,
|
||||
@ -74,6 +77,7 @@ class PostListEntryWidget extends StatelessWidget {
|
||||
item: item,
|
||||
isShowEmbed: isShowEmbed,
|
||||
isClickable: isNestedClickable,
|
||||
showFeaturedReply: showFeaturedReply,
|
||||
backgroundColor: backgroundColor,
|
||||
).paddingSymmetric(vertical: 8),
|
||||
onLongPress: () {
|
||||
|
@ -49,7 +49,7 @@ class _PostQuickActionState extends State<PostQuickAction> {
|
||||
if (_isSubmitting) return;
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
final client = auth.configureClient('interactive');
|
||||
final client = await auth.configureClient('interactive');
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
|
||||
|
@ -23,6 +23,7 @@ class PostSingleDisplay extends StatelessWidget {
|
||||
isClickable: true,
|
||||
isShowEmbed: true,
|
||||
isNestedClickable: true,
|
||||
showFeaturedReply: true,
|
||||
onUpdate: onUpdate,
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
),
|
||||
|
@ -36,6 +36,7 @@ class PostWarpedListWidget extends StatelessWidget {
|
||||
isShowEmbed: isShowEmbed,
|
||||
isNestedClickable: isNestedClickable,
|
||||
isClickable: isClickable,
|
||||
showFeaturedReply: true,
|
||||
item: item,
|
||||
onUpdate: onUpdate ?? () {},
|
||||
);
|
||||
|
@ -27,7 +27,7 @@ class _RealmDeletionDialogState extends State<RealmDeletionDialog> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.delete('/realms/${widget.realm.id}');
|
||||
if (resp.statusCode != 200) {
|
||||
@ -45,10 +45,9 @@ class _RealmDeletionDialogState extends State<RealmDeletionDialog> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp =
|
||||
await client.delete('/realms/${widget.realm.id}/members/me');
|
||||
final resp = await client.delete('/realms/${widget.realm.id}/members/me');
|
||||
if (resp.statusCode != 200) {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
} else if (Navigator.canPop(context)) {
|
||||
|
@ -37,7 +37,7 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
|
||||
void getMembers() async {
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = ServiceFinder.configureClient('auth');
|
||||
final client = await ServiceFinder.configureClient('auth');
|
||||
|
||||
final resp = await client.get('/realms/${widget.realm.alias}/members');
|
||||
if (resp.statusCode == 200) {
|
||||
@ -72,7 +72,7 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.post(
|
||||
'/realms/${widget.realm.alias}/members',
|
||||
@ -93,7 +93,7 @@ class _RealmMemberListPopupState extends State<RealmMemberListPopup> {
|
||||
|
||||
setState(() => _isBusy = true);
|
||||
|
||||
final client = auth.configureClient('auth');
|
||||
final client = await auth.configureClient('auth');
|
||||
|
||||
final resp = await client.request(
|
||||
'/realms/${widget.realm.alias}/members',
|
||||
|
@ -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 = 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user