Compare commits
51 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 | |||
f3375070a0 | |||
204df3306e | |||
aeaade9590 | |||
306ce9e2b4 | |||
a487924300 | |||
ad66c11593 | |||
40b885b27b | |||
2183a2ca55 | |||
00449f3f83 | |||
b14e55355f | |||
db808650e3 | |||
c1cbcbe734 | |||
2c4040096f | |||
b449735bf5 | |||
dd01f964d4 | |||
6daa04c208 |
@ -1,8 +1,9 @@
|
||||
<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" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
@ -51,6 +52,14 @@
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="solink" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@ -61,14 +70,6 @@
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="solink" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
@ -84,6 +85,11 @@
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
|
||||
|
||||
<service
|
||||
android:name="id.flutter.flutter_background_service.BackgroundService"
|
||||
android:foregroundServiceType="remoteMessaging"
|
||||
/>
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
After Width: | Height: | Size: 125 KiB |
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 5.7 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 3.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 8.7 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 9.4 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 16 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 8.2 KiB |
After Width: | Height: | Size: 18 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 24 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 26 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 17 KiB |
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
@ -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,6 +55,8 @@
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"settings": "Settings",
|
||||
"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",
|
||||
"article": "Article",
|
||||
@ -65,19 +68,28 @@
|
||||
"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",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"account": "Account",
|
||||
"accountPersonalize": "Personalize",
|
||||
"accountPersonalizeApplied": "Account personalize settings has been saved.",
|
||||
"accountProfile": "Your profile",
|
||||
"accountProfileApplied": "Account profile has been saved.",
|
||||
"accountStickers": "Stickers",
|
||||
"accountFriend": "Friend",
|
||||
"accountFriendNew": "New friend",
|
||||
@ -101,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!",
|
||||
@ -145,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",
|
||||
@ -213,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",
|
||||
@ -238,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",
|
||||
@ -332,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",
|
||||
@ -373,7 +395,8 @@
|
||||
"callStatusReconnected": "Reconnecting",
|
||||
"messageOutOfSync": "May Out of Sync with Server",
|
||||
"messageOutOfSyncCaption": "Since the App has entered the background, there may be a time difference between the message list and the server. Click to Refresh.",
|
||||
"messageHistoryWipe": "Wipe local message history",
|
||||
"localDatabaseWipe": "Wipe local database",
|
||||
"localDatabaseSize": "Overall database size: @size",
|
||||
"unknown": "Unknown",
|
||||
"collapse": "Collapse",
|
||||
"expand": "Expand",
|
||||
@ -391,5 +414,23 @@
|
||||
"userLevel10": "Grandmaster",
|
||||
"userLevel11": "Legend",
|
||||
"userLevel12": "Mythic",
|
||||
"userLevel13": "Immortal"
|
||||
"userLevel13": "Immortal",
|
||||
"postBrowsingIn": "Browsing in @region",
|
||||
"needRestartToApply": "Restart the application to take effect",
|
||||
"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": "确认",
|
||||
@ -14,6 +15,8 @@
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"settings": "设置",
|
||||
"settingsNotificationBgService": "常驻通知服务",
|
||||
"settingsNotificationBgServiceDesc": "在设备常驻一个通知服务,使得部分不支持推送通知的设备可以在后台收到通知;启用该功能的情况下不会向服务器注册推送通知,并且你会始终在他人眼中成为在线(隐身除外);可能需要在设置中关闭电量与流量优化。",
|
||||
"page": "页面",
|
||||
"draft": "草稿",
|
||||
"draftSave": "存为草稿",
|
||||
@ -73,16 +76,20 @@
|
||||
"forgotPassword": "忘记密码",
|
||||
"email": "邮件地址",
|
||||
"username": "用户名",
|
||||
"usernameInputHint": "同时支持邮箱 / 电话号码",
|
||||
"nickname": "显示名",
|
||||
"password": "密码",
|
||||
"passwordOneTime": "一次性验证码",
|
||||
"passwordInputHint": "忘记密码了?回到第一步以重置密码",
|
||||
"passwordOneTimeInputHint": "检查你的收件箱或是授权器获得以验证码",
|
||||
"title": "标题",
|
||||
"description": "简介",
|
||||
"birthday": "生日",
|
||||
"firstName": "名称",
|
||||
"lastName": "姓氏",
|
||||
"account": "账号",
|
||||
"accountPersonalize": "个性化",
|
||||
"accountPersonalizeApplied": "账户的个性化设置已保存。",
|
||||
"accountProfile": "个人资料",
|
||||
"accountProfileApplied": "账户的资料已保存。",
|
||||
"accountStickers": "贴图",
|
||||
"accountFriend": "好友",
|
||||
"accountFriendNew": "添加好友",
|
||||
@ -106,6 +113,11 @@
|
||||
"signinRiskDetected": "检测到风险,点击下一步按钮来打开一个网页,并通过在其上面登录来通过安全检查。",
|
||||
"signinResetPasswordHint": "请先填写用户名以发送重置密码请求。",
|
||||
"signinResetPasswordSent": "重置密码请求已发送,在绑定邮件收件箱可收取一份包含重置密码链接的邮件。",
|
||||
"signinPickFactor": "选择一个\n验证方式",
|
||||
"signinEnterPassword": "输入密码\n或验证码",
|
||||
"signinMultiFactor": "@n 步验证",
|
||||
"authFactorEmail": "邮箱一次性密码",
|
||||
"authFactorPassword": "账户密码",
|
||||
"signup": "注册",
|
||||
"signupGreeting": "欢迎加入\nSolar Network",
|
||||
"signupCaption": "在 Solarpass 注册一个账号以获得整个 Solar Network 的存取权!",
|
||||
@ -156,6 +168,9 @@
|
||||
"postListNews": "新鲜事",
|
||||
"postListFriends": "好友圈",
|
||||
"postListShuffle": "打乱看",
|
||||
"attachmentThumbnail": "附件缩略图",
|
||||
"attachmentThumbnailAttachmentNew": "上传附件作为缩略图",
|
||||
"attachmentThumbnailAttachment": "附件序列号",
|
||||
"postNew": "创建新帖子",
|
||||
"postNewInRealmHint": "在领域 @realm 里发表新帖子",
|
||||
"postAction": "发表",
|
||||
@ -214,6 +229,8 @@
|
||||
"realmDescription": "领域简介",
|
||||
"realmPublic": "公开领域",
|
||||
"realmCommunity": "社区领域",
|
||||
"realmAvatar": "领域头像",
|
||||
"realmBanner": "领域横幅",
|
||||
"realmDetail": "领域详情",
|
||||
"realmMember": "领域成员",
|
||||
"realmMembers": "领域成员",
|
||||
@ -239,7 +256,8 @@
|
||||
"channelName": "显示名称",
|
||||
"channelDescription": "频道简介",
|
||||
"channelDirectDescription": "与 @username 的私聊",
|
||||
"channelEncrypted": "加密频道",
|
||||
"channelPublic": "公开频道",
|
||||
"channelCommunity": "社区频道",
|
||||
"channelMember": "频道成员",
|
||||
"channelMembers": "频道成员",
|
||||
"channelMembersAdd": "添加频道成员",
|
||||
@ -333,8 +351,7 @@
|
||||
"bsCheckForUpdate": "正在检查更新",
|
||||
"bsCheckForUpdateFailed": "无法检查更新",
|
||||
"bsCheckForUpdateNew": "发现新版本",
|
||||
"bsCheckForUpdateDescApple": "请前往 TestFlight 并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。",
|
||||
"bsCheckForUpdateDescCommon": "请前往我们的网站下载并安装最新版本的应用程序,以防止出现错误并获取最新功能。",
|
||||
"bsCheckForUpdateDesc": "请前往应用商店并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。",
|
||||
"bsCheckingServer": "检查服务器状态中",
|
||||
"bsCheckingServerFail": "无法连接至服务器,请检查你的网络连接状态",
|
||||
"bsCheckingServerDown": "当前服务器不可用,请稍后重试",
|
||||
@ -374,7 +391,8 @@
|
||||
"callStatusReconnected": "重连中",
|
||||
"messageOutOfSync": "消息可能与服务器脱节",
|
||||
"messageOutOfSyncCaption": "由于 App 进入后台,消息列表可能与服务器存在时差,点击刷新。",
|
||||
"messageHistoryWipe": "清除消息记录",
|
||||
"localDatabaseWipe": "清除本地数据库",
|
||||
"localDatabaseSize": "本地数据库大小:@size",
|
||||
"unknown": "未知",
|
||||
"collapse": "折叠",
|
||||
"expand": "展开",
|
||||
@ -392,5 +410,23 @@
|
||||
"userLevel10": "出神入化",
|
||||
"userLevel11": "名垂千古",
|
||||
"userLevel12": "独占鳌头",
|
||||
"userLevel13": "万古流芳"
|
||||
"userLevel13": "万古流芳",
|
||||
"postBrowsingIn": "浏览 @region 内的帖子中",
|
||||
"needRestartToApply": "需要重启应用来生效",
|
||||
"holdToSeeDetail": "长按 / 鼠标悬浮来查看详情",
|
||||
"subscribe": "订阅",
|
||||
"subscribed": "已订阅",
|
||||
"unsubscribe": "取消订阅",
|
||||
"preferences": "偏好设置",
|
||||
"notificationPreferences": "通知偏好设置",
|
||||
"notificationTopicPostFeedback": "帖子反馈",
|
||||
"notificationTopicPostSubscription": "订阅源",
|
||||
"preferencesApplied": "偏好设置已应用",
|
||||
"save": "保存",
|
||||
"updateAvailable": "有可用更新",
|
||||
"updateAvailableDesc": "有可用更新 (@from 到 @to) 你想现在下载安装吗?在等待下载期间你仍可以正常使用。",
|
||||
"update": "更新",
|
||||
"updateCheckStrictly": "严格模式",
|
||||
"updateCheckStrictlyDesc": "如果启用,应用程序将会在本地版本与远程版本不同时询问更新,而不会检查版本号大小。",
|
||||
"updateMayAvailable": "版本 @version 现已可用,你可以前往应用商店或是我们的官网下载更新。"
|
||||
}
|
||||
|
@ -2,3 +2,4 @@ description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
- provider: true
|
||||
- drift: true
|
@ -54,26 +54,26 @@ PODS:
|
||||
- Firebase/Performance (11.0.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebasePerformance (~> 11.0.0)
|
||||
- firebase_analytics (11.3.0):
|
||||
- firebase_analytics (11.3.2):
|
||||
- Firebase/Analytics (= 11.0.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_core (3.4.0):
|
||||
- firebase_core (3.5.0):
|
||||
- Firebase/CoreOnly (= 11.0.0)
|
||||
- Flutter
|
||||
- firebase_crashlytics (4.1.0):
|
||||
- firebase_crashlytics (4.1.2):
|
||||
- Firebase/Crashlytics (= 11.0.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_messaging (15.1.0):
|
||||
- firebase_messaging (15.1.2):
|
||||
- Firebase/Messaging (= 11.0.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- firebase_performance (0.10.0-5):
|
||||
- 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,10 +152,16 @@ 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):
|
||||
- Flutter
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- flutter_native_splash (0.0.1):
|
||||
- Flutter
|
||||
- flutter_secure_storage (6.0.0):
|
||||
@ -221,7 +227,7 @@ PODS:
|
||||
- TOCropViewController (~> 2.7.4)
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- livekit_client (2.2.4):
|
||||
- livekit_client (2.2.5):
|
||||
- Flutter
|
||||
- WebRTC-SDK (= 125.6422.04)
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
@ -264,6 +270,24 @@ PODS:
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- "sqlite3 (3.46.1+1)":
|
||||
- "sqlite3/common (= 3.46.1+1)"
|
||||
- "sqlite3/common (3.46.1+1)"
|
||||
- "sqlite3/dbstatvtab (3.46.1+1)":
|
||||
- sqlite3/common
|
||||
- "sqlite3/fts5 (3.46.1+1)":
|
||||
- sqlite3/common
|
||||
- "sqlite3/perf-threadsafe (3.46.1+1)":
|
||||
- sqlite3/common
|
||||
- "sqlite3/rtree (3.46.1+1)":
|
||||
- sqlite3/common
|
||||
- sqlite3_flutter_libs (0.0.1):
|
||||
- Flutter
|
||||
- "sqlite3 (~> 3.46.0+1)"
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- SwiftyGif (5.4.5)
|
||||
- TOCropViewController (2.7.4)
|
||||
- url_launcher_ios (0.0.1):
|
||||
@ -284,7 +308,10 @@ 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`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||
@ -305,6 +332,7 @@ DEPENDENCIES:
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
@ -334,6 +362,7 @@ SPEC REPOS:
|
||||
- PromisesObjC
|
||||
- PromisesSwift
|
||||
- SDWebImage
|
||||
- sqlite3
|
||||
- SwiftyGif
|
||||
- TOCropViewController
|
||||
- WebRTC-SDK
|
||||
@ -357,8 +386,14 @@ 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:
|
||||
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_secure_storage:
|
||||
@ -399,6 +434,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/darwin"
|
||||
sqlite3_flutter_libs:
|
||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
volume_controller:
|
||||
@ -413,26 +450,29 @@ SPEC CHECKSUMS:
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
|
||||
firebase_analytics: 1a66fe8d4375eccff44671ea37897683a78b2675
|
||||
firebase_core: ceec591a66629daaee82d3321551692c4a871493
|
||||
firebase_crashlytics: e4f04180f443d5a8b56fbc0685bdbd7d90dd26f0
|
||||
firebase_messaging: 15d8b557010f3bb7b98d0302e1c7c8fbcd244425
|
||||
firebase_performance: d373c742649e2d85d92cc223b4511c3d132887ef
|
||||
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
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
|
||||
@ -442,7 +482,7 @@ SPEC CHECKSUMS:
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
livekit_client: d079c5f040d4bf2b80440ff0ae997725a183e4bc
|
||||
livekit_client: 9c8080879256a0fb16da13c9be4845248209d896
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
@ -460,6 +500,8 @@ SPEC CHECKSUMS:
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
|
@ -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)";
|
||||
|
109
lib/background.dart
Normal file
@ -0,0 +1,109 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart' hide Notification;
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/models/notification.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
|
||||
FlutterBackgroundService? bgNotificationService;
|
||||
|
||||
void autoConfigureBackgroundNotificationService() async {
|
||||
if (bgNotificationService != null) return;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.getBool('service_background_notification') != true) return;
|
||||
|
||||
bgNotificationService = FlutterBackgroundService();
|
||||
|
||||
await bgNotificationService!.configure(
|
||||
androidConfiguration: AndroidConfiguration(
|
||||
onStart: onBackgroundNotificationServiceStart,
|
||||
autoStart: true,
|
||||
autoStartOnBoot: true,
|
||||
isForegroundMode: false,
|
||||
),
|
||||
// This feature won't be able to use on iOS
|
||||
// We got APNs support covered
|
||||
iosConfiguration: IosConfiguration(
|
||||
autoStart: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void autoStartBackgroundNotificationService() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (prefs.getBool('service_background_notification') != true) return;
|
||||
if (bgNotificationService == null) return;
|
||||
bgNotificationService!.startService();
|
||||
}
|
||||
|
||||
void autoStopBackgroundNotificationService() async {
|
||||
if (bgNotificationService == null) return;
|
||||
if (await bgNotificationService!.isRunning()) {
|
||||
bgNotificationService?.invoke('stopService');
|
||||
}
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
void onBackgroundNotificationServiceStart(ServiceInstance service) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
|
||||
Get.put(AuthProvider());
|
||||
Get.put(WebSocketProvider());
|
||||
|
||||
service.on('stopService').listen((event) {
|
||||
service.stopSelf();
|
||||
});
|
||||
|
||||
final auth = Get.find<AuthProvider>();
|
||||
await auth.refreshAuthorizeStatus();
|
||||
await auth.ensureCredentials();
|
||||
if (!auth.isAuthorized.value) {
|
||||
debugPrint(
|
||||
'Background notification do nothing due to user didn\'t sign in.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const notificationChannelId = 'solian_notification_service';
|
||||
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
final ws = Get.find<WebSocketProvider>();
|
||||
await ws.connect();
|
||||
debugPrint('Background notification has been started');
|
||||
ws.stream.stream.listen(
|
||||
(event) {
|
||||
debugPrint(
|
||||
'Background notification service incoming message: ${event.method} ${event.message}',
|
||||
);
|
||||
|
||||
if (event.method == 'notifications.new' && event.payload != null) {
|
||||
final data = Notification.fromJson(event.payload!);
|
||||
debugPrint(
|
||||
'Background notification service got a notification id=${data.id}',
|
||||
);
|
||||
flutterLocalNotificationsPlugin.show(
|
||||
data.id,
|
||||
data.title,
|
||||
[data.subtitle, data.body].where((x) => x != null).join('\n'),
|
||||
const NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
notificationChannelId,
|
||||
'Solian Notification Service',
|
||||
channelDescription: 'Notifications that sent via Solar Network',
|
||||
importance: Importance.high,
|
||||
icon: 'mipmap/ic_launcher',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
@ -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,112 +1,87 @@
|
||||
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/message/adaptor.dart';
|
||||
import 'package:solian/providers/message/events.dart';
|
||||
import 'package:solian/providers/database/database.dart';
|
||||
import 'package:solian/providers/database/services/messages.dart';
|
||||
|
||||
class ChatEventController {
|
||||
late final MessageHistoryDb database;
|
||||
late final MessagesFetchingProvider src;
|
||||
|
||||
final RxList<LocalEvent> currentEvents = RxList.empty(growable: true);
|
||||
final RxList<LocalMessageEventTableData> currentEvents =
|
||||
RxList.empty(growable: true);
|
||||
final RxInt totalEvents = 0.obs;
|
||||
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxBool isLoading = true.obs;
|
||||
|
||||
Channel? channel;
|
||||
String? scope;
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (!PlatformInfo.isWeb) {
|
||||
database = await createHistoryDb();
|
||||
}
|
||||
src = Get.find();
|
||||
currentEvents.clear();
|
||||
}
|
||||
|
||||
Future<LocalEvent?> getEvent(int id) async {
|
||||
Future<LocalMessageEventTableData?> getEvent(int id) async {
|
||||
if (channel == null || scope == null) return null;
|
||||
|
||||
if (PlatformInfo.isWeb) {
|
||||
final remoteRecord = await getRemoteEvent(id, channel!, scope!);
|
||||
if (remoteRecord == null) return null;
|
||||
return LocalEvent(
|
||||
remoteRecord.id,
|
||||
remoteRecord,
|
||||
remoteRecord.channelId,
|
||||
remoteRecord.createdAt,
|
||||
);
|
||||
} else {
|
||||
return await database.getEvent(id, channel!, scope: scope!);
|
||||
}
|
||||
return await src.getEvent(id, channel!, scope: scope!);
|
||||
}
|
||||
|
||||
Future<void> getEvents(Channel channel, String scope) async {
|
||||
Future<void> getInitialEvents(Channel channel, String scope) async {
|
||||
this.channel = channel;
|
||||
this.scope = scope;
|
||||
|
||||
syncLocal(channel);
|
||||
const firstTake = 20;
|
||||
const furtherTake = 100;
|
||||
|
||||
isLoading.value = true;
|
||||
if (PlatformInfo.isWeb) {
|
||||
final result = await getRemoteEvents(
|
||||
channel,
|
||||
scope,
|
||||
remainDepth: 3,
|
||||
offset: 0,
|
||||
);
|
||||
totalEvents.value = result?.$2 ?? 0;
|
||||
if (result != null) {
|
||||
for (final x in result.$1.reversed) {
|
||||
final entry = LocalEvent(x.id, x, x.channelId, x.createdAt);
|
||||
insertEvent(entry);
|
||||
applyEvent(entry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final result = await database.syncRemoteEvents(
|
||||
channel,
|
||||
scope: scope,
|
||||
);
|
||||
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 getRemoteEvents(
|
||||
channel,
|
||||
scope,
|
||||
remainDepth: 3,
|
||||
offset: currentEvents.length,
|
||||
);
|
||||
if (result != null) {
|
||||
totalEvents.value = result.$2;
|
||||
for (final x in result.$1.reversed) {
|
||||
final entry = LocalEvent(x.id, x, x.channelId, x.createdAt);
|
||||
currentEvents.add(entry);
|
||||
applyEvent(entry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
final result = await database.syncRemoteEvents(
|
||||
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 database.localEvents.findAllByChannel(channel.id);
|
||||
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);
|
||||
}
|
||||
@ -114,26 +89,20 @@ class ChatEventController {
|
||||
}
|
||||
|
||||
receiveEvent(Event remote) async {
|
||||
LocalEvent entry;
|
||||
if (PlatformInfo.isWeb) {
|
||||
entry = LocalEvent(
|
||||
remote.id,
|
||||
remote,
|
||||
remote.channelId,
|
||||
remote.createdAt,
|
||||
);
|
||||
} else {
|
||||
entry = await database.receiveEvent(remote);
|
||||
}
|
||||
LocalMessageEventTableData entry;
|
||||
entry = await src.receiveEvent(remote);
|
||||
|
||||
totalEvents.value++;
|
||||
insertEvent(entry);
|
||||
applyEvent(entry);
|
||||
}
|
||||
|
||||
insertEvent(LocalEvent entry) {
|
||||
void insertEvent(LocalMessageEventTableData entry) {
|
||||
if (entry.channelId != channel?.id) return;
|
||||
|
||||
final idx = currentEvents.indexWhere((x) => x.data.uuid == entry.data.uuid);
|
||||
final idx = currentEvents.indexWhere(
|
||||
(x) => x.data!.uuid == entry.data!.uuid,
|
||||
);
|
||||
if (idx != -1) {
|
||||
currentEvents[idx] = entry;
|
||||
} else {
|
||||
@ -141,36 +110,36 @@ class ChatEventController {
|
||||
}
|
||||
}
|
||||
|
||||
applyEvent(LocalEvent entry) {
|
||||
void applyEvent(LocalMessageEventTableData entry) {
|
||||
if (entry.channelId != channel?.id) return;
|
||||
|
||||
switch (entry.data.type) {
|
||||
switch (entry.data!.type) {
|
||||
case 'messages.edit':
|
||||
final body = EventMessageBody.fromJson(entry.data.body);
|
||||
final body = EventMessageBody.fromJson(entry.data!.body);
|
||||
if (body.relatedEvent != null) {
|
||||
final idx =
|
||||
currentEvents.indexWhere((x) => x.data.id == body.relatedEvent);
|
||||
currentEvents.indexWhere((x) => x.data!.id == body.relatedEvent);
|
||||
if (idx != -1) {
|
||||
currentEvents[idx].data.body = entry.data.body;
|
||||
currentEvents[idx].data.updatedAt = entry.data.updatedAt;
|
||||
currentEvents[idx].data!.body = entry.data!.body;
|
||||
currentEvents[idx].data!.updatedAt = entry.data!.updatedAt;
|
||||
}
|
||||
}
|
||||
case 'messages.delete':
|
||||
final body = EventMessageBody.fromJson(entry.data.body);
|
||||
final body = EventMessageBody.fromJson(entry.data!.body);
|
||||
if (body.relatedEvent != null) {
|
||||
currentEvents.removeWhere((x) => x.id == body.relatedEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addPendingEvent(Event info) async {
|
||||
Future<void> addPendingEvent(Event info) async {
|
||||
currentEvents.insert(
|
||||
0,
|
||||
LocalEvent(
|
||||
info.id,
|
||||
info,
|
||||
info.channelId,
|
||||
DateTime.now(),
|
||||
LocalMessageEventTableData(
|
||||
id: info.id,
|
||||
channelId: info.channelId,
|
||||
createdAt: DateTime.now(),
|
||||
data: info,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -155,13 +155,14 @@ class PostEditorController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
void localRead() {
|
||||
SharedPreferences.getInstance().then((inst) {
|
||||
Future<bool> localRead() async {
|
||||
final inst = await SharedPreferences.getInstance();
|
||||
if (inst.containsKey('post_editor_local_save')) {
|
||||
isRestoreFromLocal.value = true;
|
||||
payload = jsonDecode(inst.getString('post_editor_local_save')!);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
void localClear() {
|
||||
|
@ -9,6 +9,7 @@ import 'package:solian/providers/last_read.dart';
|
||||
|
||||
class PostListController extends GetxController {
|
||||
String? author;
|
||||
String? realm;
|
||||
|
||||
/// The polling source modifier.
|
||||
/// - `0`: default recommendations
|
||||
@ -99,8 +100,10 @@ class PostListController extends GetxController {
|
||||
final idx = <dynamic>{};
|
||||
postList.retainWhere((x) => idx.add(x.id));
|
||||
|
||||
if (postList.isNotEmpty) {
|
||||
var lastId = postList.map((x) => x.id).reduce(max);
|
||||
Get.find<LastReadProvider>().feedLastReadAt = lastId;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -123,16 +126,21 @@ class PostListController extends GetxController {
|
||||
resp = await provider.listRecommendations(
|
||||
pageKey,
|
||||
channel: 'shuffle',
|
||||
realm: realm,
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
resp = await provider.listRecommendations(
|
||||
pageKey,
|
||||
channel: 'friends',
|
||||
realm: realm,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
resp = await provider.listRecommendations(pageKey);
|
||||
resp = await provider.listRecommendations(
|
||||
pageKey,
|
||||
realm: realm,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
|
||||
extension SolianExtenions on BuildContext {
|
||||
extension AppExtensions on BuildContext {
|
||||
void showSnackbar(String content, {SnackBarAction? action}) {
|
||||
ScaffoldMessenger.of(this).showSnackBar(SnackBar(
|
||||
content: Text(content),
|
||||
@ -49,6 +51,28 @@ extension SolianExtenions 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) {
|
||||
@ -102,3 +126,24 @@ extension SolianExtenions on BuildContext {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension ByteFormatter on int {
|
||||
String formatBytes({int decimals = 2}) {
|
||||
if (this == 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
final dm = decimals < 0 ? 0 : decimals;
|
||||
final sizes = [
|
||||
'Bytes',
|
||||
'KiB',
|
||||
'MiB',
|
||||
'GiB',
|
||||
'TiB',
|
||||
'PiB',
|
||||
'EiB',
|
||||
'ZiB',
|
||||
'YiB'
|
||||
];
|
||||
final i = (math.log(this) / math.log(k)).floor().toInt();
|
||||
return '${(this / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
|
||||
}
|
||||
}
|
||||
|
@ -2,20 +2,24 @@ import 'dart:ui';
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/material.dart' hide Notification;
|
||||
import 'package:flutter_acrylic/flutter_acrylic.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:protocol_handler/protocol_handler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/bootstrapper.dart';
|
||||
import 'package:solian/background.dart';
|
||||
import 'package:solian/firebase_options.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/attachment_uploader.dart';
|
||||
import 'package:solian/providers/daily_sign.dart';
|
||||
import 'package:solian/providers/database/database.dart';
|
||||
import 'package:solian/providers/database/services/messages.dart';
|
||||
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';
|
||||
@ -38,10 +42,12 @@ void main() async {
|
||||
await Future.wait([
|
||||
_initializeFirebase(),
|
||||
_initializePlatformComponents(),
|
||||
_initializeBackgroundNotificationService(),
|
||||
]);
|
||||
|
||||
GoRouter.optionURLReflectsImperativeAPIs = true;
|
||||
|
||||
Get.put(DatabaseProvider());
|
||||
Get.put(AppTranslations());
|
||||
await AppTranslations.init();
|
||||
|
||||
@ -60,6 +66,11 @@ Future<void> _initializeFirebase() async {
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _initializeBackgroundNotificationService() async {
|
||||
autoConfigureBackgroundNotificationService();
|
||||
autoStartBackgroundNotificationService();
|
||||
}
|
||||
|
||||
Future<void> _initializePlatformComponents() async {
|
||||
if (!PlatformInfo.isWeb) {
|
||||
await protocolHandler.register('solink');
|
||||
@ -79,8 +90,8 @@ Future<void> _initializePlatformComponents() async {
|
||||
}
|
||||
|
||||
final themeSwitcher = ThemeSwitcher(
|
||||
lightThemeData: SolianTheme.build(Brightness.light),
|
||||
darkThemeData: SolianTheme.build(Brightness.dark),
|
||||
lightThemeData: AppTheme.build(Brightness.light),
|
||||
darkThemeData: AppTheme.build(Brightness.dark),
|
||||
);
|
||||
|
||||
class SolianApp extends StatelessWidget {
|
||||
@ -111,10 +122,8 @@ class SolianApp extends StatelessWidget {
|
||||
builder: (context, child) {
|
||||
return SystemShell(
|
||||
child: ScaffoldMessenger(
|
||||
child: BootstrapperShell(
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -123,6 +132,8 @@ class SolianApp extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _initializeProviders(BuildContext context) async {
|
||||
Get.put(NavigationStateProvider());
|
||||
|
||||
Get.lazyPut(() => AuthProvider());
|
||||
Get.lazyPut(() => RelationshipProvider());
|
||||
Get.lazyPut(() => PostProvider());
|
||||
@ -132,10 +143,14 @@ class SolianApp extends StatelessWidget {
|
||||
Get.lazyPut(() => StatusProvider());
|
||||
Get.lazyPut(() => ChannelProvider());
|
||||
Get.lazyPut(() => RealmProvider());
|
||||
Get.lazyPut(() => MessagesFetchingProvider());
|
||||
Get.lazyPut(() => ChatCallProvider());
|
||||
Get.lazyPut(() => AttachmentUploaderController());
|
||||
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
@ -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
@ -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';
|
||||
@ -12,9 +12,12 @@ class Realm {
|
||||
String alias;
|
||||
String name;
|
||||
String description;
|
||||
String? avatar;
|
||||
String? banner;
|
||||
bool isPublic;
|
||||
bool isCommunity;
|
||||
int? accountId;
|
||||
int? externalId;
|
||||
|
||||
Realm({
|
||||
required this.id,
|
||||
@ -24,9 +27,12 @@ class Realm {
|
||||
required this.alias,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.avatar,
|
||||
required this.banner,
|
||||
required this.isPublic,
|
||||
required this.isCommunity,
|
||||
this.accountId,
|
||||
this.externalId,
|
||||
});
|
||||
|
||||
factory Realm.fromJson(Map<String, dynamic> json) => _$RealmFromJson(json);
|
||||
|
@ -16,9 +16,12 @@ Realm _$RealmFromJson(Map<String, dynamic> json) => Realm(
|
||||
alias: json['alias'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
avatar: json['avatar'] as String?,
|
||||
banner: json['banner'] as String?,
|
||||
isPublic: json['is_public'] as bool,
|
||||
isCommunity: json['is_community'] as bool,
|
||||
accountId: (json['account_id'] as num?)?.toInt(),
|
||||
externalId: (json['external_id'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RealmToJson(Realm instance) => <String, dynamic>{
|
||||
@ -29,9 +32,12 @@ Map<String, dynamic> _$RealmToJson(Realm instance) => <String, dynamic>{
|
||||
'alias': instance.alias,
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'avatar': instance.avatar,
|
||||
'banner': instance.banner,
|
||||
'is_public': instance.isPublic,
|
||||
'is_community': instance.isCommunity,
|
||||
'account_id': instance.accountId,
|
||||
'external_id': instance.externalId,
|
||||
};
|
||||
|
||||
RealmMember _$RealmMemberFromJson(Map<String, dynamic> json) => RealmMember(
|
||||
|
@ -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
@ -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
@ -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) {
|
||||
|
@ -6,9 +6,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_connect/http/src/request/request.dart';
|
||||
import 'package:solian/controllers/chat_events_controller.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';
|
||||
|
||||
@ -113,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);
|
||||
@ -147,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) {
|
||||
@ -199,10 +187,8 @@ class AuthProvider extends GetConnect {
|
||||
Get.find<WebSocketProvider>().notifications.clear();
|
||||
Get.find<WebSocketProvider>().notificationUnread.value = 0;
|
||||
|
||||
final chatHistory = ChatEventController();
|
||||
chatHistory.initialize().then((_) async {
|
||||
await chatHistory.database.localEvents.wipeLocalEvents();
|
||||
});
|
||||
AppDatabase.removeDatabase();
|
||||
autoStopBackgroundNotificationService();
|
||||
|
||||
storage.deleteAll();
|
||||
}
|
||||
@ -217,7 +203,8 @@ class AuthProvider extends GetConnect {
|
||||
}
|
||||
|
||||
Future<void> refreshUserProfile() async {
|
||||
final client = configureClient('auth');
|
||||
if (!isAuthorized.value) return;
|
||||
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) {
|
||||
|
51
lib/providers/database/database.dart
Normal file
@ -0,0 +1,51 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
import 'package:get/get.dart' hide Value;
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/database/tables/messages.dart';
|
||||
|
||||
import 'package:solian/models/event.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@DriftDatabase(tables: [LocalMessageEventTable])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
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 {
|
||||
if (PlatformInfo.isWeb) return 0;
|
||||
final basepath = await getApplicationDocumentsDirectory();
|
||||
return await File(join(basepath.path, 'solar_network_local_db.sqlite'))
|
||||
.length();
|
||||
}
|
||||
|
||||
static Future<void> removeDatabase() async {
|
||||
if (PlatformInfo.isWeb) return;
|
||||
final basepath = await getApplicationDocumentsDirectory();
|
||||
final file = File(join(basepath.path, 'solar_network_local_db.sqlite'));
|
||||
await Get.find<DatabaseProvider>().database.close();
|
||||
await file.delete();
|
||||
Get.find<DatabaseProvider>().database = AppDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseProvider extends GetxController {
|
||||
var database = AppDatabase();
|
||||
}
|
429
lib/providers/database/database.g.dart
Normal file
@ -0,0 +1,429 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'database.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
class $LocalMessageEventTableTable extends LocalMessageEventTable
|
||||
with TableInfo<$LocalMessageEventTableTable, LocalMessageEventTableData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$LocalMessageEventTableTable(this.attachedDatabase, [this._alias]);
|
||||
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
@override
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
static const VerificationMeta _channelIdMeta =
|
||||
const VerificationMeta('channelId');
|
||||
@override
|
||||
late final GeneratedColumn<int> channelId = GeneratedColumn<int>(
|
||||
'channel_id', aliasedName, false,
|
||||
type: DriftSqlType.int, requiredDuringInsert: true);
|
||||
static const VerificationMeta _dataMeta = const VerificationMeta('data');
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<Event?, String> data =
|
||||
GeneratedColumn<String>('data', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true)
|
||||
.withConverter<Event?>($LocalMessageEventTableTable.$converterdata);
|
||||
static const VerificationMeta _createdAtMeta =
|
||||
const VerificationMeta('createdAt');
|
||||
@override
|
||||
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
|
||||
'created_at', aliasedName, false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: Constant(DateTime.now()));
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [id, channelId, data, createdAt];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'local_message_event_table';
|
||||
@override
|
||||
VerificationContext validateIntegrity(
|
||||
Insertable<LocalMessageEventTableData> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
if (data.containsKey('channel_id')) {
|
||||
context.handle(_channelIdMeta,
|
||||
channelId.isAcceptableOrUnknown(data['channel_id']!, _channelIdMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_channelIdMeta);
|
||||
}
|
||||
context.handle(_dataMeta, const VerificationResult.success());
|
||||
if (data.containsKey('created_at')) {
|
||||
context.handle(_createdAtMeta,
|
||||
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
LocalMessageEventTableData map(Map<String, dynamic> data,
|
||||
{String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return LocalMessageEventTableData(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
channelId: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}channel_id'])!,
|
||||
data: $LocalMessageEventTableTable.$converterdata.fromSql(attachedDatabase
|
||||
.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}data'])!),
|
||||
createdAt: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$LocalMessageEventTableTable createAlias(String alias) {
|
||||
return $LocalMessageEventTableTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static TypeConverter<Event?, String> $converterdata =
|
||||
const MessageEventConverter();
|
||||
}
|
||||
|
||||
class LocalMessageEventTableData extends DataClass
|
||||
implements Insertable<LocalMessageEventTableData> {
|
||||
final int id;
|
||||
final int channelId;
|
||||
final Event? data;
|
||||
final DateTime createdAt;
|
||||
const LocalMessageEventTableData(
|
||||
{required this.id,
|
||||
required this.channelId,
|
||||
this.data,
|
||||
required this.createdAt});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<int>(id);
|
||||
map['channel_id'] = Variable<int>(channelId);
|
||||
if (!nullToAbsent || data != null) {
|
||||
map['data'] = Variable<String>(
|
||||
$LocalMessageEventTableTable.$converterdata.toSql(data));
|
||||
}
|
||||
map['created_at'] = Variable<DateTime>(createdAt);
|
||||
return map;
|
||||
}
|
||||
|
||||
LocalMessageEventTableCompanion toCompanion(bool nullToAbsent) {
|
||||
return LocalMessageEventTableCompanion(
|
||||
id: Value(id),
|
||||
channelId: Value(channelId),
|
||||
data: data == null && nullToAbsent ? const Value.absent() : Value(data),
|
||||
createdAt: Value(createdAt),
|
||||
);
|
||||
}
|
||||
|
||||
factory LocalMessageEventTableData.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return LocalMessageEventTableData(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
channelId: serializer.fromJson<int>(json['channelId']),
|
||||
data: serializer.fromJson<Event?>(json['data']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'channelId': serializer.toJson<int>(channelId),
|
||||
'data': serializer.toJson<Event?>(data),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
LocalMessageEventTableData copyWith(
|
||||
{int? id,
|
||||
int? channelId,
|
||||
Value<Event?> data = const Value.absent(),
|
||||
DateTime? createdAt}) =>
|
||||
LocalMessageEventTableData(
|
||||
id: id ?? this.id,
|
||||
channelId: channelId ?? this.channelId,
|
||||
data: data.present ? data.value : this.data,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
LocalMessageEventTableData copyWithCompanion(
|
||||
LocalMessageEventTableCompanion data) {
|
||||
return LocalMessageEventTableData(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
channelId: data.channelId.present ? data.channelId.value : this.channelId,
|
||||
data: data.data.present ? data.data.value : this.data,
|
||||
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('LocalMessageEventTableData(')
|
||||
..write('id: $id, ')
|
||||
..write('channelId: $channelId, ')
|
||||
..write('data: $data, ')
|
||||
..write('createdAt: $createdAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, channelId, data, createdAt);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is LocalMessageEventTableData &&
|
||||
other.id == this.id &&
|
||||
other.channelId == this.channelId &&
|
||||
other.data == this.data &&
|
||||
other.createdAt == this.createdAt);
|
||||
}
|
||||
|
||||
class LocalMessageEventTableCompanion
|
||||
extends UpdateCompanion<LocalMessageEventTableData> {
|
||||
final Value<int> id;
|
||||
final Value<int> channelId;
|
||||
final Value<Event?> data;
|
||||
final Value<DateTime> createdAt;
|
||||
const LocalMessageEventTableCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.channelId = const Value.absent(),
|
||||
this.data = const Value.absent(),
|
||||
this.createdAt = const Value.absent(),
|
||||
});
|
||||
LocalMessageEventTableCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
required int channelId,
|
||||
required Event? data,
|
||||
this.createdAt = const Value.absent(),
|
||||
}) : channelId = Value(channelId),
|
||||
data = Value(data);
|
||||
static Insertable<LocalMessageEventTableData> custom({
|
||||
Expression<int>? id,
|
||||
Expression<int>? channelId,
|
||||
Expression<String>? data,
|
||||
Expression<DateTime>? createdAt,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (channelId != null) 'channel_id': channelId,
|
||||
if (data != null) 'data': data,
|
||||
if (createdAt != null) 'created_at': createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
LocalMessageEventTableCompanion copyWith(
|
||||
{Value<int>? id,
|
||||
Value<int>? channelId,
|
||||
Value<Event?>? data,
|
||||
Value<DateTime>? createdAt}) {
|
||||
return LocalMessageEventTableCompanion(
|
||||
id: id ?? this.id,
|
||||
channelId: channelId ?? this.channelId,
|
||||
data: data ?? this.data,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<int>(id.value);
|
||||
}
|
||||
if (channelId.present) {
|
||||
map['channel_id'] = Variable<int>(channelId.value);
|
||||
}
|
||||
if (data.present) {
|
||||
map['data'] = Variable<String>(
|
||||
$LocalMessageEventTableTable.$converterdata.toSql(data.value));
|
||||
}
|
||||
if (createdAt.present) {
|
||||
map['created_at'] = Variable<DateTime>(createdAt.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('LocalMessageEventTableCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('channelId: $channelId, ')
|
||||
..write('data: $data, ')
|
||||
..write('createdAt: $createdAt')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$AppDatabase extends GeneratedDatabase {
|
||||
_$AppDatabase(QueryExecutor e) : super(e);
|
||||
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
||||
late final $LocalMessageEventTableTable localMessageEventTable =
|
||||
$LocalMessageEventTableTable(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [localMessageEventTable];
|
||||
}
|
||||
|
||||
typedef $$LocalMessageEventTableTableCreateCompanionBuilder
|
||||
= LocalMessageEventTableCompanion Function({
|
||||
Value<int> id,
|
||||
required int channelId,
|
||||
required Event? data,
|
||||
Value<DateTime> createdAt,
|
||||
});
|
||||
typedef $$LocalMessageEventTableTableUpdateCompanionBuilder
|
||||
= LocalMessageEventTableCompanion Function({
|
||||
Value<int> id,
|
||||
Value<int> channelId,
|
||||
Value<Event?> data,
|
||||
Value<DateTime> createdAt,
|
||||
});
|
||||
|
||||
class $$LocalMessageEventTableTableFilterComposer
|
||||
extends FilterComposer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||
$$LocalMessageEventTableTableFilterComposer(super.$state);
|
||||
ColumnFilters<int> get id => $state.composableBuilder(
|
||||
column: $state.table.id,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnFilters<int> get channelId => $state.composableBuilder(
|
||||
column: $state.table.channelId,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnWithTypeConverterFilters<Event?, Event, String> get data =>
|
||||
$state.composableBuilder(
|
||||
column: $state.table.data,
|
||||
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
|
||||
column,
|
||||
joinBuilders: joinBuilders));
|
||||
|
||||
ColumnFilters<DateTime> get createdAt => $state.composableBuilder(
|
||||
column: $state.table.createdAt,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
}
|
||||
|
||||
class $$LocalMessageEventTableTableOrderingComposer
|
||||
extends OrderingComposer<_$AppDatabase, $LocalMessageEventTableTable> {
|
||||
$$LocalMessageEventTableTableOrderingComposer(super.$state);
|
||||
ColumnOrderings<int> get id => $state.composableBuilder(
|
||||
column: $state.table.id,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnOrderings<int> get channelId => $state.composableBuilder(
|
||||
column: $state.table.channelId,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnOrderings<String> get data => $state.composableBuilder(
|
||||
column: $state.table.data,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnOrderings<DateTime> get createdAt => $state.composableBuilder(
|
||||
column: $state.table.createdAt,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
}
|
||||
|
||||
class $$LocalMessageEventTableTableTableManager extends RootTableManager<
|
||||
_$AppDatabase,
|
||||
$LocalMessageEventTableTable,
|
||||
LocalMessageEventTableData,
|
||||
$$LocalMessageEventTableTableFilterComposer,
|
||||
$$LocalMessageEventTableTableOrderingComposer,
|
||||
$$LocalMessageEventTableTableCreateCompanionBuilder,
|
||||
$$LocalMessageEventTableTableUpdateCompanionBuilder,
|
||||
(
|
||||
LocalMessageEventTableData,
|
||||
BaseReferences<_$AppDatabase, $LocalMessageEventTableTable,
|
||||
LocalMessageEventTableData>
|
||||
),
|
||||
LocalMessageEventTableData,
|
||||
PrefetchHooks Function()> {
|
||||
$$LocalMessageEventTableTableTableManager(
|
||||
_$AppDatabase db, $LocalMessageEventTableTable table)
|
||||
: super(TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
filteringComposer: $$LocalMessageEventTableTableFilterComposer(
|
||||
ComposerState(db, table)),
|
||||
orderingComposer: $$LocalMessageEventTableTableOrderingComposer(
|
||||
ComposerState(db, table)),
|
||||
updateCompanionCallback: ({
|
||||
Value<int> id = const Value.absent(),
|
||||
Value<int> channelId = const Value.absent(),
|
||||
Value<Event?> data = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
}) =>
|
||||
LocalMessageEventTableCompanion(
|
||||
id: id,
|
||||
channelId: channelId,
|
||||
data: data,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
createCompanionCallback: ({
|
||||
Value<int> id = const Value.absent(),
|
||||
required int channelId,
|
||||
required Event? data,
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
}) =>
|
||||
LocalMessageEventTableCompanion.insert(
|
||||
id: id,
|
||||
channelId: channelId,
|
||||
data: data,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||
.toList(),
|
||||
prefetchHooksCallback: null,
|
||||
));
|
||||
}
|
||||
|
||||
typedef $$LocalMessageEventTableTableProcessedTableManager
|
||||
= ProcessedTableManager<
|
||||
_$AppDatabase,
|
||||
$LocalMessageEventTableTable,
|
||||
LocalMessageEventTableData,
|
||||
$$LocalMessageEventTableTableFilterComposer,
|
||||
$$LocalMessageEventTableTableOrderingComposer,
|
||||
$$LocalMessageEventTableTableCreateCompanionBuilder,
|
||||
$$LocalMessageEventTableTableUpdateCompanionBuilder,
|
||||
(
|
||||
LocalMessageEventTableData,
|
||||
BaseReferences<_$AppDatabase, $LocalMessageEventTableTable,
|
||||
LocalMessageEventTableData>
|
||||
),
|
||||
LocalMessageEventTableData,
|
||||
PrefetchHooks Function()>;
|
||||
|
||||
class $AppDatabaseManager {
|
||||
final _$AppDatabase _db;
|
||||
$AppDatabaseManager(this._db);
|
||||
$$LocalMessageEventTableTableTableManager get localMessageEventTable =>
|
||||
$$LocalMessageEventTableTableTableManager(
|
||||
_db, _db.localMessageEventTable);
|
||||
}
|
185
lib/providers/database/services/messages.dart
Normal file
@ -0,0 +1,185 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:get/get.dart' hide Value;
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/event.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/database/database.dart';
|
||||
|
||||
class MessagesFetchingProvider extends GetxController {
|
||||
Future<(List<Event>, int)?> getWhatsNewEvents(int pivot, {take = 10}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return null;
|
||||
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get(
|
||||
'/whats-new?pivot=$pivot&take=$take',
|
||||
);
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
final PaginationResult response = PaginationResult.fromJson(resp.body);
|
||||
final result =
|
||||
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
|
||||
|
||||
return (result, response.count);
|
||||
}
|
||||
|
||||
Future<Event?> fetchRemoteEvent(int id, Channel channel, String scope) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return null;
|
||||
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get(
|
||||
'/channels/$scope/${channel.alias}/events/$id',
|
||||
);
|
||||
|
||||
if (resp.statusCode == 404) {
|
||||
return null;
|
||||
} else if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return Event.fromJson(resp.body);
|
||||
}
|
||||
|
||||
Future<(List<Event>, int)?> fetchRemoteEvents(
|
||||
Channel channel,
|
||||
String scope, {
|
||||
take = 10,
|
||||
offset = 0,
|
||||
}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return null;
|
||||
|
||||
final client = await auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get(
|
||||
'/channels/$scope/${channel.alias}/events?take=$take&offset=$offset',
|
||||
);
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
final PaginationResult response = PaginationResult.fromJson(resp.body);
|
||||
final result =
|
||||
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
|
||||
|
||||
return (result, response.count);
|
||||
}
|
||||
|
||||
Future<LocalMessageEventTableData> receiveEvent(Event remote) async {
|
||||
// Insert record
|
||||
final database = Get.find<DatabaseProvider>().database;
|
||||
final entry = await database
|
||||
.into(database.localMessageEventTable)
|
||||
.insertReturning(LocalMessageEventTableCompanion.insert(
|
||||
id: Value(remote.id),
|
||||
channelId: remote.channelId,
|
||||
data: remote,
|
||||
createdAt: Value(remote.createdAt),
|
||||
));
|
||||
|
||||
// Handle side-effect like editing and deleting
|
||||
switch (remote.type) {
|
||||
case 'messages.edit':
|
||||
final body = EventMessageBody.fromJson(remote.body);
|
||||
if (body.relatedEvent != null) {
|
||||
final target = await (database.select(database.localMessageEventTable)
|
||||
..where((x) => x.id.equals(body.relatedEvent!)))
|
||||
.getSingleOrNull();
|
||||
if (target != null) {
|
||||
target.data!.body = remote.body;
|
||||
target.data!.updatedAt = remote.updatedAt;
|
||||
await (database.update(database.localMessageEventTable)
|
||||
..where((x) => x.id.equals(target.id)))
|
||||
.write(
|
||||
LocalMessageEventTableCompanion(data: Value(target.data)),
|
||||
);
|
||||
}
|
||||
}
|
||||
case 'messages.delete':
|
||||
final body = EventMessageBody.fromJson(remote.body);
|
||||
if (body.relatedEvent != null) {
|
||||
await (database.delete(database.localMessageEventTable)
|
||||
..where((x) => x.id.equals(body.relatedEvent!)))
|
||||
.go();
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
Future<LocalMessageEventTableData?> getEvent(int id, Channel channel,
|
||||
{String scope = 'global'}) async {
|
||||
final database = Get.find<DatabaseProvider>().database;
|
||||
final localRecord = await (database.select(database.localMessageEventTable)
|
||||
..where((x) => x.id.equals(id)))
|
||||
.getSingleOrNull();
|
||||
if (localRecord != null) return localRecord;
|
||||
|
||||
final remoteRecord = await fetchRemoteEvent(id, channel, scope);
|
||||
if (remoteRecord == null) return null;
|
||||
|
||||
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', take = 10, offset = 0}) async {
|
||||
final database = Get.find<DatabaseProvider>().database;
|
||||
|
||||
final data = await fetchRemoteEvents(
|
||||
channel,
|
||||
scope,
|
||||
offset: offset,
|
||||
take: take,
|
||||
);
|
||||
if (data != null) {
|
||||
await database.batch((batch) {
|
||||
batch.insertAllOnConflictUpdate(
|
||||
database.localMessageEventTable,
|
||||
data.$1.map((x) => LocalMessageEventTableCompanion(
|
||||
id: Value(x.id),
|
||||
channelId: Value(x.channelId),
|
||||
data: Value(x),
|
||||
createdAt: Value(x.createdAt),
|
||||
)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
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)])
|
||||
..limit(take, offset: offset))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<LocalMessageEventTableData?> getLastInChannel(Channel channel) 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)]))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
}
|
13
lib/providers/database/tables/json.dart
Normal file
@ -0,0 +1,13 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class JsonConverter extends TypeConverter<Object?, String> {
|
||||
const JsonConverter();
|
||||
|
||||
@override
|
||||
Object? fromSql(String fromDb) => jsonDecode(fromDb);
|
||||
|
||||
@override
|
||||
String toSql(Object? value) => jsonEncode(value);
|
||||
}
|
22
lib/providers/database/tables/messages.dart
Normal file
@ -0,0 +1,22 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:solian/models/event.dart';
|
||||
|
||||
class LocalMessageEventTable extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get channelId => integer()();
|
||||
TextColumn get data => text().map(const MessageEventConverter())();
|
||||
DateTimeColumn get createdAt =>
|
||||
dateTime().withDefault(Constant(DateTime.now()))();
|
||||
}
|
||||
|
||||
class MessageEventConverter extends TypeConverter<Event?, String> {
|
||||
const MessageEventConverter();
|
||||
|
||||
@override
|
||||
Event? fromSql(String fromDb) => Event.fromJson(jsonDecode(fromDb));
|
||||
|
||||
@override
|
||||
String toSql(Event? value) => jsonEncode(value?.toJson());
|
||||
}
|
@ -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}');
|
||||
|
@ -1,173 +0,0 @@
|
||||
import 'package:floor/floor.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/models/channel.dart';
|
||||
import 'package:solian/models/event.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/message/events.dart';
|
||||
|
||||
Future<MessageHistoryDb> createHistoryDb() async {
|
||||
final migration1to2 = Migration(1, 2, (database) async {
|
||||
await database.execute('DROP TABLE IF EXISTS LocalMessage');
|
||||
});
|
||||
|
||||
return await $FloorMessageHistoryDb
|
||||
.databaseBuilder('messaging_data.dart')
|
||||
.addMigrations([migration1to2]).build();
|
||||
}
|
||||
|
||||
Future<(List<Event>, int)?> getWhatsNewEvents(int pivot, {take = 10}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return null;
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get(
|
||||
'/whats-new?pivot=$pivot&take=$take',
|
||||
);
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
final PaginationResult response = PaginationResult.fromJson(resp.body);
|
||||
final result =
|
||||
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
|
||||
|
||||
return (result, response.count);
|
||||
}
|
||||
|
||||
Future<Event?> getRemoteEvent(int id, Channel channel, String scope) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return null;
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get(
|
||||
'/channels/$scope/${channel.alias}/events/$id',
|
||||
);
|
||||
|
||||
if (resp.statusCode == 404) {
|
||||
return null;
|
||||
} else if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return Event.fromJson(resp.body);
|
||||
}
|
||||
|
||||
Future<(List<Event>, int)?> getRemoteEvents(
|
||||
Channel channel,
|
||||
String scope, {
|
||||
required int remainDepth,
|
||||
bool Function(List<Event> items)? onBrake,
|
||||
take = 10,
|
||||
offset = 0,
|
||||
}) async {
|
||||
if (remainDepth <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return null;
|
||||
|
||||
final client = auth.configureClient('messaging');
|
||||
|
||||
final resp = await client.get(
|
||||
'/channels/$scope/${channel.alias}/events?take=$take&offset=$offset',
|
||||
);
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
final PaginationResult response = PaginationResult.fromJson(resp.body);
|
||||
final result =
|
||||
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
|
||||
|
||||
if (onBrake != null && onBrake(result)) {
|
||||
return (result, response.count);
|
||||
}
|
||||
|
||||
final expandResult = (await getRemoteEvents(
|
||||
channel,
|
||||
scope,
|
||||
remainDepth: remainDepth - 1,
|
||||
take: take,
|
||||
offset: offset + result.length,
|
||||
))
|
||||
?.$1 ??
|
||||
List.empty();
|
||||
|
||||
return ([...result, ...expandResult], response.count);
|
||||
}
|
||||
|
||||
extension MessageHistoryAdaptor on MessageHistoryDb {
|
||||
Future<LocalEvent> receiveEvent(Event remote) async {
|
||||
final entry = LocalEvent(
|
||||
remote.id,
|
||||
remote,
|
||||
remote.channelId,
|
||||
remote.createdAt,
|
||||
);
|
||||
await localEvents.insert(entry);
|
||||
switch (remote.type) {
|
||||
case 'messages.edit':
|
||||
final body = EventMessageBody.fromJson(remote.body);
|
||||
if (body.relatedEvent != null) {
|
||||
final target = await localEvents.findById(body.relatedEvent!);
|
||||
if (target != null) {
|
||||
target.data.body = remote.body;
|
||||
target.data.updatedAt = remote.updatedAt;
|
||||
await localEvents.update(target);
|
||||
}
|
||||
}
|
||||
case 'messages.delete':
|
||||
final body = EventMessageBody.fromJson(remote.body);
|
||||
if (body.relatedEvent != null) {
|
||||
await localEvents.delete(body.relatedEvent!);
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
Future<LocalEvent?> getEvent(int id, Channel channel,
|
||||
{String scope = 'global'}) async {
|
||||
final localRecord = await localEvents.findById(id);
|
||||
if (localRecord != null) return localRecord;
|
||||
|
||||
final remoteRecord = await getRemoteEvent(id, channel, scope);
|
||||
if (remoteRecord == null) return null;
|
||||
|
||||
return await receiveEvent(remoteRecord);
|
||||
}
|
||||
|
||||
Future<(List<Event>, int)?> syncRemoteEvents(Channel channel,
|
||||
{String scope = 'global', depth = 10, offset = 0}) async {
|
||||
final lastOne = await localEvents.findLastByChannel(channel.id);
|
||||
|
||||
final data = await getRemoteEvents(
|
||||
channel,
|
||||
scope,
|
||||
remainDepth: depth,
|
||||
offset: offset,
|
||||
onBrake: (items) {
|
||||
return items.any((x) => x.id == lastOne?.id);
|
||||
},
|
||||
);
|
||||
if (data != null) {
|
||||
await localEvents.insertBulk(
|
||||
data.$1
|
||||
.map((x) => LocalEvent(x.id, x, x.channelId, x.createdAt))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
Future<List<LocalEvent>> listEvents(Channel channel) async {
|
||||
return await localEvents.findAllByChannel(channel.id);
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:floor/floor.dart';
|
||||
import 'package:solian/models/event.dart';
|
||||
import 'package:sqflite/sqflite.dart' as sqflite;
|
||||
|
||||
part 'events.g.dart';
|
||||
|
||||
@entity
|
||||
class LocalEvent {
|
||||
@primaryKey
|
||||
final int id;
|
||||
|
||||
final Event data;
|
||||
final int channelId;
|
||||
|
||||
final DateTime createdAt;
|
||||
|
||||
LocalEvent(this.id, this.data, this.channelId, this.createdAt);
|
||||
}
|
||||
|
||||
class DateTimeConverter extends TypeConverter<DateTime, int> {
|
||||
@override
|
||||
DateTime decode(int databaseValue) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(databaseValue);
|
||||
}
|
||||
|
||||
@override
|
||||
int encode(DateTime value) {
|
||||
return value.millisecondsSinceEpoch;
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteEventConverter extends TypeConverter<Event, String> {
|
||||
@override
|
||||
Event decode(String databaseValue) {
|
||||
return Event.fromJson(jsonDecode(databaseValue));
|
||||
}
|
||||
|
||||
@override
|
||||
String encode(Event value) {
|
||||
return jsonEncode(value.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
@dao
|
||||
abstract class LocalEventDao {
|
||||
@Query('SELECT COUNT(id) FROM LocalEvent WHERE channelId = :channelId')
|
||||
Future<int?> countByChannel(int channelId);
|
||||
|
||||
@Query('SELECT * FROM LocalEvent WHERE id = :id')
|
||||
Future<LocalEvent?> findById(int id);
|
||||
|
||||
@Query('SELECT * FROM LocalEvent WHERE channelId = :channelId ORDER BY createdAt DESC')
|
||||
Future<List<LocalEvent>> findAllByChannel(int channelId);
|
||||
|
||||
@Query('SELECT * FROM LocalEvent WHERE channelId = :channelId ORDER BY createdAt DESC LIMIT 1')
|
||||
Future<LocalEvent?> findLastByChannel(int channelId);
|
||||
|
||||
@Insert(onConflict: OnConflictStrategy.replace)
|
||||
Future<void> insert(LocalEvent m);
|
||||
|
||||
@Insert(onConflict: OnConflictStrategy.replace)
|
||||
Future<void> insertBulk(List<LocalEvent> m);
|
||||
|
||||
@Update(onConflict: OnConflictStrategy.replace)
|
||||
Future<void> update(LocalEvent m);
|
||||
|
||||
@Query('DELETE FROM LocalEvent WHERE id = :id')
|
||||
Future<void> delete(int id);
|
||||
|
||||
@Query('DELETE FROM LocalEvent WHERE channelId = :channelId')
|
||||
Future<List<LocalEvent>> deleteByChannel(int channelId);
|
||||
|
||||
@Query('DELETE FROM LocalEvent')
|
||||
Future<void> wipeLocalEvents();
|
||||
}
|
||||
|
||||
@TypeConverters([DateTimeConverter, RemoteEventConverter])
|
||||
@Database(version: 2, entities: [LocalEvent])
|
||||
abstract class MessageHistoryDb extends FloorDatabase {
|
||||
LocalEventDao get localEvents;
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'events.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FloorGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class $MessageHistoryDbBuilderContract {
|
||||
/// Adds migrations to the builder.
|
||||
$MessageHistoryDbBuilderContract addMigrations(List<Migration> migrations);
|
||||
|
||||
/// Adds a database [Callback] to the builder.
|
||||
$MessageHistoryDbBuilderContract addCallback(Callback callback);
|
||||
|
||||
/// Creates the database and initializes it.
|
||||
Future<MessageHistoryDb> build();
|
||||
}
|
||||
|
||||
// ignore: avoid_classes_with_only_static_members
|
||||
class $FloorMessageHistoryDb {
|
||||
/// Creates a database builder for a persistent database.
|
||||
/// Once a database is built, you should keep a reference to it and re-use it.
|
||||
static $MessageHistoryDbBuilderContract databaseBuilder(String name) =>
|
||||
_$MessageHistoryDbBuilder(name);
|
||||
|
||||
/// Creates a database builder for an in memory database.
|
||||
/// Information stored in an in memory database disappears when the process is killed.
|
||||
/// Once a database is built, you should keep a reference to it and re-use it.
|
||||
static $MessageHistoryDbBuilderContract inMemoryDatabaseBuilder() =>
|
||||
_$MessageHistoryDbBuilder(null);
|
||||
}
|
||||
|
||||
class _$MessageHistoryDbBuilder implements $MessageHistoryDbBuilderContract {
|
||||
_$MessageHistoryDbBuilder(this.name);
|
||||
|
||||
final String? name;
|
||||
|
||||
final List<Migration> _migrations = [];
|
||||
|
||||
Callback? _callback;
|
||||
|
||||
@override
|
||||
$MessageHistoryDbBuilderContract addMigrations(List<Migration> migrations) {
|
||||
_migrations.addAll(migrations);
|
||||
return this;
|
||||
}
|
||||
|
||||
@override
|
||||
$MessageHistoryDbBuilderContract addCallback(Callback callback) {
|
||||
_callback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MessageHistoryDb> build() async {
|
||||
final path = name != null
|
||||
? await sqfliteDatabaseFactory.getDatabasePath(name!)
|
||||
: ':memory:';
|
||||
final database = _$MessageHistoryDb();
|
||||
database.database = await database.open(
|
||||
path,
|
||||
_migrations,
|
||||
_callback,
|
||||
);
|
||||
return database;
|
||||
}
|
||||
}
|
||||
|
||||
class _$MessageHistoryDb extends MessageHistoryDb {
|
||||
_$MessageHistoryDb([StreamController<String>? listener]) {
|
||||
changeListener = listener ?? StreamController<String>.broadcast();
|
||||
}
|
||||
|
||||
LocalEventDao? _localEventsInstance;
|
||||
|
||||
Future<sqflite.Database> open(
|
||||
String path,
|
||||
List<Migration> migrations, [
|
||||
Callback? callback,
|
||||
]) async {
|
||||
final databaseOptions = sqflite.OpenDatabaseOptions(
|
||||
version: 2,
|
||||
onConfigure: (database) async {
|
||||
await database.execute('PRAGMA foreign_keys = ON');
|
||||
await callback?.onConfigure?.call(database);
|
||||
},
|
||||
onOpen: (database) async {
|
||||
await callback?.onOpen?.call(database);
|
||||
},
|
||||
onUpgrade: (database, startVersion, endVersion) async {
|
||||
await MigrationAdapter.runMigrations(
|
||||
database, startVersion, endVersion, migrations);
|
||||
|
||||
await callback?.onUpgrade?.call(database, startVersion, endVersion);
|
||||
},
|
||||
onCreate: (database, version) async {
|
||||
await database.execute(
|
||||
'CREATE TABLE IF NOT EXISTS `LocalEvent` (`id` INTEGER NOT NULL, `data` TEXT NOT NULL, `channelId` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, PRIMARY KEY (`id`))');
|
||||
|
||||
await callback?.onCreate?.call(database, version);
|
||||
},
|
||||
);
|
||||
return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions);
|
||||
}
|
||||
|
||||
@override
|
||||
LocalEventDao get localEvents {
|
||||
return _localEventsInstance ??= _$LocalEventDao(database, changeListener);
|
||||
}
|
||||
}
|
||||
|
||||
class _$LocalEventDao extends LocalEventDao {
|
||||
_$LocalEventDao(
|
||||
this.database,
|
||||
this.changeListener,
|
||||
) : _queryAdapter = QueryAdapter(database),
|
||||
_localEventInsertionAdapter = InsertionAdapter(
|
||||
database,
|
||||
'LocalEvent',
|
||||
(LocalEvent item) => <String, Object?>{
|
||||
'id': item.id,
|
||||
'data': _remoteEventConverter.encode(item.data),
|
||||
'channelId': item.channelId,
|
||||
'createdAt': _dateTimeConverter.encode(item.createdAt)
|
||||
}),
|
||||
_localEventUpdateAdapter = UpdateAdapter(
|
||||
database,
|
||||
'LocalEvent',
|
||||
['id'],
|
||||
(LocalEvent item) => <String, Object?>{
|
||||
'id': item.id,
|
||||
'data': _remoteEventConverter.encode(item.data),
|
||||
'channelId': item.channelId,
|
||||
'createdAt': _dateTimeConverter.encode(item.createdAt)
|
||||
});
|
||||
|
||||
final sqflite.DatabaseExecutor database;
|
||||
|
||||
final StreamController<String> changeListener;
|
||||
|
||||
final QueryAdapter _queryAdapter;
|
||||
|
||||
final InsertionAdapter<LocalEvent> _localEventInsertionAdapter;
|
||||
|
||||
final UpdateAdapter<LocalEvent> _localEventUpdateAdapter;
|
||||
|
||||
@override
|
||||
Future<int?> countByChannel(int channelId) async {
|
||||
return _queryAdapter.query(
|
||||
'SELECT COUNT(id) FROM LocalEvent WHERE channelId = ?1',
|
||||
mapper: (Map<String, Object?> row) => row.values.first as int,
|
||||
arguments: [channelId]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LocalEvent?> findById(int id) async {
|
||||
return _queryAdapter.query('SELECT * FROM LocalEvent WHERE id = ?1',
|
||||
mapper: (Map<String, Object?> row) => LocalEvent(
|
||||
row['id'] as int,
|
||||
_remoteEventConverter.decode(row['data'] as String),
|
||||
row['channelId'] as int,
|
||||
_dateTimeConverter.decode(row['createdAt'] as int)),
|
||||
arguments: [id]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<LocalEvent>> findAllByChannel(int channelId) async {
|
||||
return _queryAdapter.queryList(
|
||||
'SELECT * FROM LocalEvent WHERE channelId = ?1 ORDER BY createdAt DESC',
|
||||
mapper: (Map<String, Object?> row) => LocalEvent(
|
||||
row['id'] as int,
|
||||
_remoteEventConverter.decode(row['data'] as String),
|
||||
row['channelId'] as int,
|
||||
_dateTimeConverter.decode(row['createdAt'] as int)),
|
||||
arguments: [channelId]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LocalEvent?> findLastByChannel(int channelId) async {
|
||||
return _queryAdapter.query(
|
||||
'SELECT * FROM LocalEvent WHERE channelId = ?1 ORDER BY createdAt DESC LIMIT 1',
|
||||
mapper: (Map<String, Object?> row) => LocalEvent(row['id'] as int, _remoteEventConverter.decode(row['data'] as String), row['channelId'] as int, _dateTimeConverter.decode(row['createdAt'] as int)),
|
||||
arguments: [channelId]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(int id) async {
|
||||
await _queryAdapter
|
||||
.queryNoReturn('DELETE FROM LocalEvent WHERE id = ?1', arguments: [id]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<LocalEvent>> deleteByChannel(int channelId) async {
|
||||
return _queryAdapter.queryList(
|
||||
'DELETE FROM LocalEvent WHERE channelId = ?1',
|
||||
mapper: (Map<String, Object?> row) => LocalEvent(
|
||||
row['id'] as int,
|
||||
_remoteEventConverter.decode(row['data'] as String),
|
||||
row['channelId'] as int,
|
||||
_dateTimeConverter.decode(row['createdAt'] as int)),
|
||||
arguments: [channelId]);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> wipeLocalEvents() async {
|
||||
await _queryAdapter.queryNoReturn('DELETE FROM LocalEvent');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> insert(LocalEvent m) async {
|
||||
await _localEventInsertionAdapter.insert(m, OnConflictStrategy.replace);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> insertBulk(List<LocalEvent> m) async {
|
||||
await _localEventInsertionAdapter.insertList(m, OnConflictStrategy.replace);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(LocalEvent m) async {
|
||||
await _localEventUpdateAdapter.update(m, OnConflictStrategy.replace);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore_for_file: unused_element
|
||||
final _dateTimeConverter = DateTimeConverter();
|
||||
final _remoteEventConverter = RemoteEventConverter();
|
6
lib/providers/navigation.dart
Normal file
@ -0,0 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/realm.dart';
|
||||
|
||||
class NavigationStateProvider extends GetxController {
|
||||
final Rx<Realm?> focusedRealm = Rx(null);
|
||||
}
|
@ -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
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -16,8 +16,8 @@ class ThemeSwitcher extends ChangeNotifier {
|
||||
if (prefs.containsKey('global_theme_color')) {
|
||||
final value = prefs.getInt('global_theme_color')!;
|
||||
final color = Color(value);
|
||||
lightThemeData = SolianTheme.build(Brightness.light, seedColor: color);
|
||||
darkThemeData = SolianTheme.build(Brightness.dark, seedColor: color);
|
||||
lightThemeData = AppTheme.build(Brightness.light, seedColor: color);
|
||||
darkThemeData = AppTheme.build(Brightness.dark, seedColor: color);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/models/notification.dart';
|
||||
import 'package:solian/models/packet.dart';
|
||||
@ -29,18 +31,44 @@ class WebSocketProvider extends GetxController {
|
||||
|
||||
@override
|
||||
onInit() {
|
||||
FirebaseMessaging.instance
|
||||
.requestPermission(
|
||||
notifyPrefetch();
|
||||
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void requestPermissions() {
|
||||
try {
|
||||
FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
announcement: true,
|
||||
carPlay: true,
|
||||
badge: true,
|
||||
sound: true)
|
||||
.then((status) {
|
||||
notifyPrefetch();
|
||||
});
|
||||
|
||||
super.onInit();
|
||||
sound: true);
|
||||
} catch (_) {
|
||||
// When firebase isn't initialized (background service)
|
||||
FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.requestNotificationsPermission();
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
IOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
MacOSFlutterLocalNotificationsPlugin>()
|
||||
?.requestPermissions(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> connect({noRetry = false}) async {
|
||||
@ -90,6 +118,10 @@ class WebSocketProvider extends GetxController {
|
||||
final packet = NetworkPackage.fromJson(jsonDecode(event));
|
||||
log('Websocket incoming message: ${packet.method} ${packet.message}');
|
||||
stream.sink.add(packet);
|
||||
if (packet.method == 'notifications.new') {
|
||||
notifications.add(Notification.fromJson(packet.payload!));
|
||||
notificationUnread.value++;
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
isConnected.value = false;
|
||||
@ -106,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) {
|
||||
@ -120,6 +152,14 @@ 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');
|
||||
return;
|
||||
}
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) return;
|
||||
|
||||
@ -142,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: '/',
|
||||
@ -154,6 +160,7 @@ abstract class AppRouter {
|
||||
name: 'channelChat',
|
||||
builder: (context, state) {
|
||||
return ChannelChatScreen(
|
||||
key: UniqueKey(),
|
||||
alias: state.pathParameters['alias']!,
|
||||
realm: state.uri.queryParameters['realm'] ?? 'global',
|
||||
);
|
||||
@ -235,22 +242,22 @@ 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: 'accountPersonalize',
|
||||
name: 'accountProfile',
|
||||
builder: (context, state) => TitleShell(
|
||||
state: state,
|
||||
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',
|
||||
@ -258,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(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
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;
|
||||
@ -23,9 +22,9 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final actionItems = [
|
||||
(
|
||||
const Icon(Icons.color_lens),
|
||||
'accountPersonalize'.tr,
|
||||
'accountPersonalize',
|
||||
const Icon(Icons.face),
|
||||
'accountProfile'.tr,
|
||||
'accountProfile',
|
||||
),
|
||||
(
|
||||
Obx(() {
|
||||
@ -46,11 +45,6 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
'accountFriend'.tr,
|
||||
'accountFriend',
|
||||
),
|
||||
(
|
||||
const Icon(Icons.emoji_symbols),
|
||||
'accountStickers'.tr,
|
||||
'accountStickers',
|
||||
),
|
||||
];
|
||||
|
||||
final AuthProvider auth = Get.find();
|
||||
@ -64,7 +58,7 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ActionCard(
|
||||
_ActionCard(
|
||||
icon: Icon(
|
||||
Icons.login,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
@ -72,20 +66,14 @@ 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();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
ActionCard(
|
||||
_ActionCard(
|
||||
icon: Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
@ -93,17 +81,24 @@ 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(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
const Gap(4),
|
||||
TextButton(
|
||||
style: const ButtonStyle(
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: -4,
|
||||
vertical: -2,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
AppRouter.instance.pushNamed('settings');
|
||||
},
|
||||
child: Text('settings'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -126,6 +121,27 @@ class _AccountScreenState extends State<AccountScreen> {
|
||||
},
|
||||
),
|
||||
)),
|
||||
const Divider(thickness: 0.3, height: 1)
|
||||
.paddingSymmetric(vertical: 4),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
leading: const Icon(Icons.settings),
|
||||
title: Text('settings'.tr),
|
||||
onTap: () {
|
||||
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(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 34),
|
||||
leading: const Icon(Icons.logout),
|
||||
@ -186,14 +202,13 @@ class _AccountHeadingState extends State<AccountHeading> {
|
||||
}
|
||||
}
|
||||
|
||||
class ActionCard extends StatelessWidget {
|
||||
class _ActionCard extends StatelessWidget {
|
||||
final Widget icon;
|
||||
final String title;
|
||||
final String caption;
|
||||
final Function onTap;
|
||||
|
||||
const ActionCard({
|
||||
super.key,
|
||||
const _ActionCard({
|
||||
required this.onTap,
|
||||
required this.title,
|
||||
required this.caption,
|
||||
|
@ -133,7 +133,7 @@ class _FriendScreenState extends State<FriendScreen>
|
||||
).paddingAll(14),
|
||||
),
|
||||
SizedBox(
|
||||
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
bottom: TabBar(
|
||||
|
@ -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
@ -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',
|
||||
@ -134,7 +134,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
_syncWidget();
|
||||
context.showSnackbar('accountPersonalizeApplied'.tr);
|
||||
context.showSnackbar('accountProfileApplied'.tr);
|
||||
} else {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
}
|
||||
@ -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(
|
||||
@ -163,7 +163,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
|
||||
);
|
||||
if (resp.statusCode == 200) {
|
||||
_syncWidget();
|
||||
context.showSnackbar('accountPersonalizeApplied'.tr);
|
||||
context.showSnackbar('accountProfileApplied'.tr);
|
||||
} else {
|
||||
context.showErrorDialog(resp.bodyString);
|
||||
}
|
@ -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) {
|
||||
@ -152,7 +165,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
SliverAppBar(
|
||||
centerTitle: false,
|
||||
floating: true,
|
||||
toolbarHeight: SolianTheme.toolbarHeight(context),
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
leadingWidth: 24,
|
||||
automaticallyImplyLeading: false,
|
||||
flexibleSpace: Row(
|
||||
@ -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(
|
||||
@ -207,7 +254,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
||||
onPressed: null,
|
||||
),
|
||||
SizedBox(
|
||||
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -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,49 @@
|
||||
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();
|
||||
|
||||
void requestResetPassword() async {
|
||||
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) {
|
||||
context.showErrorDialog('signinResetPasswordHint'.tr);
|
||||
@ -31,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);
|
||||
@ -52,95 +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(
|
||||
@ -157,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: [
|
||||
@ -200,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> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -205,7 +205,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
||||
: AppBar(
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
centerTitle: true,
|
||||
toolbarHeight: SolianTheme.toolbarHeight(context),
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
title: Obx(
|
||||
() => RichText(
|
||||
textAlign: TextAlign.center,
|
||||
|
@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/controllers/chat_events_controller.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
@ -98,7 +97,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
setState(() => _ongoingCall = Call.fromJson(resp.body));
|
||||
}
|
||||
} catch (e) {
|
||||
print((e as dynamic).stackTrace);
|
||||
context.showErrorDialog(e);
|
||||
}
|
||||
|
||||
@ -156,7 +154,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
|
||||
void _keepUpdateWithServer() {
|
||||
_getOngoingCall();
|
||||
_chatController.getEvents(_channel!, widget.realm);
|
||||
_chatController.getInitialEvents(_channel!, widget.realm);
|
||||
setState(() => _isOutOfSyncSince = null);
|
||||
}
|
||||
|
||||
@ -193,7 +191,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
|
||||
_getOngoingCall();
|
||||
_getChannel().then((_) {
|
||||
_chatController.getEvents(_channel!, widget.realm);
|
||||
_chatController.getInitialEvents(_channel!, widget.realm);
|
||||
_listenMessages();
|
||||
});
|
||||
}
|
||||
@ -217,8 +215,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
title: AppBarTitle(title),
|
||||
centerTitle: false,
|
||||
titleSpacing: SolianTheme.titleSpacing(context),
|
||||
toolbarHeight: SolianTheme.toolbarHeight(context),
|
||||
titleSpacing: AppTheme.titleSpacing(context),
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
Builder(builder: (context) {
|
||||
@ -255,7 +253,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
},
|
||||
),
|
||||
SizedBox(
|
||||
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -276,7 +274,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
channel: _channel!,
|
||||
ongoingCall: _ongoingCall!,
|
||||
onJoin: () {
|
||||
if (!SolianTheme.isLargeScreen(context)) {
|
||||
if (!AppTheme.isLargeScreen(context)) {
|
||||
final ChatCallProvider call = Get.find();
|
||||
call.gotoScreen(context);
|
||||
}
|
||||
@ -295,13 +293,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
},
|
||||
),
|
||||
),
|
||||
Obx(() {
|
||||
if (_chatController.isLoading.isTrue) {
|
||||
return const LinearProgressIndicator().animate().slideY();
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
||||
@ -337,7 +328,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||
),
|
||||
Obx(() {
|
||||
final ChatCallProvider call = Get.find();
|
||||
if (call.isMounted.value && SolianTheme.isLargeScreen(context)) {
|
||||
if (call.isMounted.value && AppTheme.isLargeScreen(context)) {
|
||||
return const Expanded(
|
||||
child: Row(children: [
|
||||
VerticalDivider(width: 0.3, thickness: 0.3),
|
||||
|
@ -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),
|
||||
),
|
||||
];
|
||||
@ -110,10 +120,10 @@ class _ChannelOrganizeScreenState extends State<ChannelOrganizeScreen> {
|
||||
appBar: AppBar(
|
||||
title: AppBarTitle('channelOrganizing'.tr),
|
||||
centerTitle: false,
|
||||
toolbarHeight: SolianTheme.toolbarHeight(context),
|
||||
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,
|
||||
),
|
||||
],
|
||||
|
@ -47,7 +47,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
leading: AppBarLeadingButton.adaptive(context),
|
||||
title: AppBarTitle('chat'.tr),
|
||||
centerTitle: true,
|
||||
toolbarHeight: SolianTheme.toolbarHeight(context),
|
||||
toolbarHeight: AppTheme.toolbarHeight(context),
|
||||
actions: [
|
||||
const BackgroundStateWidget(),
|
||||
const NotificationButton(),
|
||||
@ -95,14 +95,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
|
||||
width: AppTheme.isLargeScreen(context) ? 8 : 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
|