Compare commits

...

121 Commits

Author SHA1 Message Date
19ec0a7ede 🚀 Launch 1.2.1+35 2024-09-12 20:29:08 +08:00
f3b2a2a0ac 👽 The removal of external id 2024-09-11 23:40:23 +08:00
bba38e6845 🌐 Localize daily sign history chart 2024-09-11 22:03:43 +08:00
408a2489e2 ♻️ Move translations out of dart code 2024-09-11 19:56:32 +08:00
133213b430 🚑 Hot fix thumbnail duration issue 2024-09-10 23:52:10 +08:00
2ff142a84f 🚀 Launch 1.2.1+33 2024-09-10 23:46:08 +08:00
e385f79df2 Attachment thumbnail 2024-09-10 22:47:28 +08:00
8d9a8b5435 🧑‍💻 All in one auto cache image widget 2024-09-10 21:53:05 +08:00
c5a975b5ed Setting of attachment thumbnail 2024-09-10 21:36:10 +08:00
1210cda998 Roll back to use media_kit as media player 2024-09-09 23:09:16 +08:00
3b56b94242 🚀 Launch 1.2.1+30 2024-09-09 13:07:21 +08:00
34043e722b Able to play music 2024-09-08 22:43:01 +08:00
e4a5ac9d0a 🚑 Fix cannot load ongoing call 2024-09-08 16:03:50 +08:00
c991590b27 🚀 Launch 1.2.1+28 2024-09-08 12:47:27 +08:00
d8b2c7f81e User experience & level 2024-09-08 12:32:21 +08:00
1fd042bcae 🐛 Bug fixes on profile page 2024-09-08 01:48:01 +08:00
0a04c72468 Daily sign history 2024-09-08 00:23:59 +08:00
4e8f2ddef3 🎨 Use SizedBox.shrink instead of empty SizedBox for placeholder 2024-09-07 17:48:07 +08:00
85f97521e5 🎨 Use Gap instead of empty SizedBox 2024-09-07 17:45:44 +08:00
9c451f485a 🎨 Refactored all models with json annotation to make code cleaner 2024-09-07 15:36:06 +08:00
f836b22c73 💄 Remove message ripple effect 2024-09-06 23:55:44 +08:00
18fba0c9e7 🚀 Launch 1.2.1+27 2024-09-05 21:41:01 +08:00
c68138e516 🐛 Fix missing gap between sections in dashboard 2024-09-05 21:13:50 +08:00
cedd0b083a 🐛 Fix overflow in dashboard 2024-09-05 20:28:22 +08:00
1a0721ba3a Better dashboard design for large screen (and mobile device) 2024-09-05 20:25:17 +08:00
a75f42e440 🚀 Launch 1.2.1+26 2024-09-03 23:22:18 +08:00
e4a6ff2da4 🌐 Localize dashboard 2024-09-03 23:21:46 +08:00
baa6b401d3 Better whats new 2024-09-03 23:07:20 +08:00
bd1369e72d ⬆️ Upgrade pod deps 2024-09-02 23:49:09 +08:00
10520b4448 🚀 Ready to launch 1.2.1+25 2024-09-02 23:16:54 +08:00
cab2217793 💄 Hint on dashboard 2024-09-02 23:15:24 +08:00
4e4e551e2f Daily sign 2024-09-02 23:11:40 +08:00
597a8a802a Dashboard basis 2024-09-01 17:20:26 +08:00
fff756cbe0 🐛 Bug fixes on link expansion doesn't show on merged chat event 2024-08-26 12:13:09 +08:00
e38778dbf9 🍱 Splash screen 2024-08-26 01:23:30 +08:00
cc9081b011 🐛 Fix image loading issue on web 2024-08-24 11:47:40 +08:00
14e8f7b775 🐛 Bug fixes and optimization 2024-08-23 23:16:41 +08:00
a70e6c7118 Typing indicator 2024-08-23 22:43:04 +08:00
48ca885a2c 🚀 Launch 1.2.1+22 2024-08-22 01:06:10 +08:00
09cb340a9d 🐛 Fix personalize page issue 2024-08-22 00:42:17 +08:00
b6ebd6bef6 🐛 Drawer will expand on mobile device 2024-08-21 20:55:22 +08:00
2ec25fd1a2 Drawer tooltip on collapse mode 2024-08-21 19:35:29 +08:00
bc99865ba8 💫 Animated collapsible sidebar 2024-08-21 19:11:27 +08:00
f834351ce2 Basis collapse sidebar 2024-08-21 17:00:59 +08:00
0f1a02f65b 🐛 Try to fix protocol handler issue on android 2024-08-21 16:02:00 +08:00
6ad0a34645 Call on large screen able to full screen 2024-08-21 15:57:45 +08:00
fdc71475fc 💄 Optimize message hint 2024-08-21 15:45:55 +08:00
047defebd1 🥅 Better request failed exceptions 2024-08-21 15:39:29 +08:00
6148e889aa 🥅 Better unauthorized exceptions 2024-08-21 15:25:50 +08:00
1d7affcd84 🐛 Bug fixes 2024-08-21 13:14:40 +08:00
cc1e0599aa 🐛 Fix link expand match markdown link 2024-08-21 10:06:05 +08:00
221b97901f 💄 Optimize uploader 2024-08-21 10:01:09 +08:00
498bb0e5fb Run upload chunks at the same time (max 3) 2024-08-21 09:33:34 +08:00
aa94dfcfe0 Multipart upload 2024-08-21 01:53:16 +08:00
65d9253876 🐛 Fix svg site icon cause invalid image data 2024-08-21 00:48:51 +08:00
3ac510c4b1 🐛 Bug fixes 2024-08-20 01:19:18 +08:00
253cd1ecbd Call in same screen on large screen 2024-08-20 01:10:15 +08:00
c82c48dfec 🐛 Fix attachments padding 2024-08-19 22:45:22 +08:00
433beec2dd 💄 Optimize large screen ux 2024-08-19 22:38:36 +08:00
3a1e7537dd 🐛 Fix alignment issue 2024-08-19 22:25:49 +08:00
9170ae6be7 💄 Line up attachments & expansion of link 2024-08-19 22:25:17 +08:00
a5ee5b7f09 💄 Better attachment layout 2024-08-19 22:13:25 +08:00
32e6658f3d Better link expand layout on large screen 2024-08-19 20:13:08 +08:00
e45d9b39d5 Post link expand
 Cache link expansion image
2024-08-19 19:56:44 +08:00
cf1cfecb08 Link expand 2024-08-19 19:36:01 +08:00
95ea3e558f 🚀 Launch 1.2.1+18 2024-08-19 09:43:25 +08:00
0006a94632 🐛 Fix local db old data cause crash 2024-08-19 09:19:29 +08:00
7ea18dbe12 💄 Update styles 2024-08-19 01:54:32 +08:00
6004b74724 🚀 Launch 1.2.1+17 2024-08-19 01:35:57 +08:00
4d82ae8058 🐛 Bug fixes
⬆️ Add firebase performance
2024-08-19 01:35:38 +08:00
7fe26d0df0 🚀 Launch 1.2.1+16 2024-08-19 00:33:20 +08:00
80bade0e03 View posts posted by friends 2024-08-19 00:33:03 +08:00
b63db7fe76 👽 Support use realm alias instead of id 2024-08-19 00:14:09 +08:00
49f73f5f04 ⬆️ Support new attachments system 2024-08-18 22:51:52 +08:00
98749f42c0 ⬆️ Upgrade deps 2024-08-17 19:18:51 +08:00
f0e6bd64f4 ♻️ Refactor video player 2024-08-17 19:02:57 +08:00
3bea3a114a Post alias 2024-08-17 18:44:20 +08:00
454f711656 ⬆️ Upgrade deps 2024-08-16 23:27:38 +08:00
82e4c923e7 📈 Simple log user share 2024-08-16 23:08:05 +08:00
5b4d8282ae Re-google (firebase) 2024-08-16 22:59:34 +08:00
cf767a1d94 💄 Optimized post editor 2024-08-16 21:06:50 +08:00
af93a8386a ⬆️ Upgrade deps 2024-08-16 01:05:21 +08:00
29ca263130 🚀 Launch 1.2.1+13 2024-08-16 01:03:55 +08:00
7332f68d9c Live preview of post editor 2024-08-16 00:52:36 +08:00
e9e6f3313e 👽 Use capital to deal with mfa 2024-08-13 10:54:42 +08:00
85764c37c2 🚨 Fix livekit android complie issue
Following issue:
https://github.com/livekit/client-sdk-flutter/issues/569
2024-08-12 09:06:30 +08:00
ef1f29f905 🐛 Fix edit post won't rollback thumbnail 2024-08-11 02:07:09 +08:00
22026efa7d Thumbnail 2024-08-11 01:57:58 +08:00
4a3e6a9e15 🚀 Launch 1.2.1+12 2024-08-11 00:50:25 +08:00
00092ba7b6 Some useful options 2024-08-11 00:36:27 +08:00
b5da8ece4a Use capital share link 2024-08-10 18:24:47 +08:00
dfe9165bc9 🐛 Bug fixes on upload attachment 2024-08-10 01:17:31 +08:00
3d45b54236 ⬆️ Upgrade flutter & deps 2024-08-10 01:16:40 +08:00
7f63fe7f0e 💄 Better sidebar navigation 2024-08-10 00:51:54 +08:00
bc5dbab9c5 Dismissible refresh notification 2024-08-10 00:49:21 +08:00
9910fc7a92 Channel content auto refresh after long time background activity 2024-08-10 00:43:55 +08:00
2356eac118 Better side navigation bar 2024-08-09 22:59:24 +08:00
0135b8d838 Better screenshare 2024-08-09 22:40:05 +08:00
8ec33ccbf4 🚨 Fix CarouselController import issue 2024-08-07 19:21:01 +08:00
d267316a35 💄 Better emotes 2024-08-07 19:11:52 +08:00
138da60e55 🚸 Prevent user from sending empty message 2024-08-07 19:02:49 +08:00
4562c2f991 🐛 Fix able send space message 2024-08-07 18:31:26 +08:00
8009f4ca9b 💄 Better sidebar navigation 2024-08-07 18:24:16 +08:00
54dee9702b 🐛 Fix attachments max width 2024-08-07 14:34:41 +08:00
94385564bd 🐛 Fix dupe attachment notification 2024-08-07 14:27:23 +08:00
0b2309816f 🐛 Fix desktop panic when download things 2024-08-07 13:50:50 +08:00
8283272a3b 🗑️ Fix mis-import 2024-08-07 01:49:03 +08:00
eb02a47e9a 💄 Fixes and improvements 2024-08-07 01:47:53 +08:00
7c0c1ec94f 💄 Optimize styles 2024-08-07 01:20:23 +08:00
272044a77e 💄 Optimize logo in signup & signin popup 2024-08-07 01:06:57 +08:00
39c22b1cf6 Sticker has pack id 2024-08-07 00:56:06 +08:00
98c3bb912d Stickers auto resize 2024-08-07 00:52:34 +08:00
035b92d9b8 Rollback sized container 2024-08-07 00:12:44 +08:00
0bfc0bd61b 🌐 Update en translation 2024-08-07 00:08:29 +08:00
de00a20eee 💄 Better call ui 2024-08-06 23:23:02 +08:00
73982f48d6 🐛 Bug fixes 2024-08-06 20:00:13 +08:00
1d36b30361 Video won't load until click 2024-08-06 19:39:07 +08:00
dea743a307 Username hint 2024-08-06 18:34:46 +08:00
c48bd3e758 Stickers hint 2024-08-06 18:18:40 +08:00
56bbf73b5e Better sticker & able embed attachment into markdown 2024-08-06 16:24:47 +08:00
4f6c5aa053 🐛 Bug fixes 2024-08-04 21:12:35 +08:00
187 changed files with 8116 additions and 3245 deletions

View File

@ -1,6 +1,7 @@
plugins {
id "com.android.application"
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}

View File

@ -1,17 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<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.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
<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.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
@ -19,31 +19,32 @@
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="Solian"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:supportsRtl="true">
android:label="Solian"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:usesCleartextTraffic="true">
<receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"/>
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
@ -58,29 +59,36 @@
<data android:host="sn.solsynth.dev" />
<data android:scheme="https" />
<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"
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2"/>
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
@ -89,8 +97,8 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@ -12,6 +12,17 @@ subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
// TO FIX LIVEKIT ISSUE BY THIS
// https://github.com/livekit/client-sdk-flutter/issues/569#issuecomment-2275686786
afterEvaluate { project ->
if (project.plugins.hasPlugin("com.android.application") ||
project.plugins.hasPlugin("com.android.library")) {
project.android {
compileSdkVersion 34
buildToolsVersion "34.0.0"
}
}
}
project.evaluationDependsOn(":app")
}

View File

@ -20,6 +20,7 @@ plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.4.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
}

395
assets/locales/en_us.json Normal file
View File

@ -0,0 +1,395 @@
{
"done": "Done",
"hide": "Hide",
"okay": "Okay",
"next": "Next",
"reset": "Reset",
"page": "Page",
"home": "Home",
"guest": "Guest",
"draft": "Draft",
"dashboard": "Dashboard",
"today": "Today",
"yesterday": "Yesterday",
"draftSave": "Save",
"draftBox": "Draft Box",
"more": "More",
"share": "Share",
"shareNoUri": "Share text content",
"alias": "Alias",
"feed": "Feed",
"explore": "Explore",
"posts": "Posts",
"unlink": "Unlink",
"feedSearch": "Search Feed",
"feedSearchWithTag": "Searching with tag #@key",
"feedSearchWithCategory": "Searching in category @category",
"feedUnreadCount": "@count posts you may missed",
"messages": "Messages",
"messagesUnreadCount": "@count messages unread",
"dailySign": "Daily Sign",
"dailySignAction": "Sign Today",
"dailySignHistoryAction": "View History",
"dailySignNone": "You haven't sign today",
"dailySignTier0": "Everything may not be good",
"dailySignTier1": "Something may be wrong",
"dailySignTier2": "Just so so",
"dailySignTier3": "Something may goes well",
"dailySignTier4": "Everything will be awesome",
"dailySignHistoryTitle": "Fortune History",
"dailySignHistoryRecent": "Recent Fortune",
"dailySignHistoryReward": "Reward Trends",
"dashboardFooter": "Don't be serious, just for fun.",
"visitProfilePage": "Visit Profile Page",
"profilePage": "Page",
"profilePosts": "Posts",
"profileAlbum": "Album",
"chat": "Chat",
"apply": "Apply",
"cancel": "Cancel",
"confirm": "Confirm",
"leave": "Leave",
"loading": "Loading...",
"about": "About",
"edit": "Edit",
"delete": "Delete",
"settings": "Settings",
"search": "Search",
"post": "Post",
"article": "Article",
"reply": "Reply",
"repost": "Repost",
"openInAlbum": "Open in album",
"openInBrowser": "Open in browser",
"notification": "Notification",
"notificationUnreadCount": "@count unread notifications",
"errorHappened": "An error occurred",
"errorHappenedUnauthorized": "Unauthorized request, please sign in or try resign in.",
"forgotPassword": "Forgot password",
"email": "Email",
"username": "Username",
"nickname": "Nickname",
"password": "Password",
"title": "Title",
"description": "Description",
"birthday": "Birthday",
"firstName": "First Name",
"lastName": "Last Name",
"account": "Account",
"accountPersonalize": "Personalize",
"accountPersonalizeApplied": "Account personalize settings has been saved.",
"accountStickers": "Stickers",
"accountFriend": "Friend",
"accountFriendNew": "New friend",
"accountFriendNewHint": "Use someone's username to send a request of making friends with them!",
"accountFriendPending": "Friend requests",
"accountFriendBlocked": "Friend blocklist",
"accountFriendListHint": "Swipe left to decline, right to approve",
"accountFriendRequestSent": "Friend request sent, waiting for processing...",
"accountSuspended": "Account was suspended",
"accountSuspendedAt": "Account was suspended since @date",
"aspectRatio": "Aspect Ratio",
"aspectRatioSquare": "Square",
"aspectRatioPortrait": "Portrait",
"aspectRatioLandscape": "Landscape",
"unsignedIn": "Unsigned in",
"signin": "Sign in",
"signinRequired": "Sign in",
"signinRequiredHint": "Sign in to get full access of Solar Network",
"signinGreeting": "Welcome back\nSolar Network",
"signinCaption": "Sign in to create post, start a realm, message your friend and more!",
"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!",
"signup": "Sign up",
"signupGreeting": "Welcome onboard",
"signupCaption": "Create an account on Solarpass and then get the access of entire Solar Network!",
"signupDone": "Sign up successfully.",
"signupDoneCaption": "You successfully created an account on Solar Network. Now go sign in!",
"signout": "Sign out",
"joinedAt": "Joined at @date",
"riskDetection": "Risk Detected",
"matureContent": "Mature Content",
"matureContentCaption": "The content is rated and may not suitable for everyone to view",
"notifyAllRead": "Mark all as read",
"notifyEmpty": "All notifications read",
"notifyEmptyCaption": "It seems like nothing happened recently",
"totalSocialCreditPoints": "Social Credit Points",
"totalPostCount": "Posts",
"totalUpvote": "Upvote",
"totalDownvote": "Downvote",
"clear": "Clear",
"pinPost": "Pin this post",
"unpinPost": "Unpin this post",
"postRestoreFromLocal": "Restore from local",
"postAutoSaveAt": "Auto saved at @date",
"postCategoriesAndTags": "Categories n' Tags",
"postPublishDate": "Publish Date",
"postPublishAt": "Publish At",
"postPublishedUntil": "Publish Until",
"postPublishZone": "Publish Zone",
"postPublishZoneNone": "None",
"postVisibility": "Visibility",
"postVisibilityAll": "Everyone",
"postVisibilityFriends": "Friends",
"postVisibilitySelected": "Selected visible",
"postVisibilityFiltered": "Selected invisible",
"postVisibilityNone": "Only me",
"postVisibleUsers": "Visible users",
"postInvisibleUsers": "Invisible users",
"postOverview": "Overview",
"postThumbnail": "Thumbnail",
"postThumbnailAttachmentNew": "Upload thumbnail",
"postThumbnailAttachment": "Attachment serial number",
"postPinned": "Pinned",
"postListNews": "News",
"postListFriends": "Friends",
"postListShuffle": "Random",
"postEditorModeStory": "Post a post",
"postEditorModeArticle": "Post an article",
"postEditor": "Post editor",
"articleEditor": "Create new article",
"articleDetail": "Article details",
"draftBoxOpen": "Open draft box",
"postNew": "Create a new post",
"postNewInRealmHint": "Add post in realm @realm",
"postAction": "Post",
"postEdited": "Edited at @date",
"postNewCreated": "Created at @date",
"attachmentHint": "@count attachment(s)",
"postInRealm": "In @realm",
"postDetail": "Post",
"postReplies": "Replies",
"postPublish": "Post a post",
"articlePublish": "Write an article",
"articleTitlePlaceholder": "Title",
"articleDescriptionPlaceholder": "Description",
"articleContentPlaceholder": "Content",
"postIdentityNotify": "You will post this post as",
"postContentPlaceholder": "What's happened?!",
"postTagsPlaceholder": "Tags",
"postReaction": "Reactions of the Post",
"postActionList": "Actions of Post",
"postReplyAction": "Make a reply",
"postRepliedNotify": "Replied a post from @username.",
"postRepostedNotify": "Reposted a post from @username.",
"postInRealmNotify": "You're posting in realm @realm.",
"postEditingNotify": "You're editing as post from you.",
"postReplyingNotify": "You're replying a post from @username.",
"postRepostingNotify": "You're reposting a post from @username.",
"postDeletionConfirm": "Confirm post deletion",
"postDeletionConfirmCaption": "Are your sure to delete post \"@content\"? This action cannot be undone!",
"reactAdd": "React",
"reactCompleted": "Your reaction has been added",
"reactUncompleted": "Your reaction has been removed",
"attachmentUploadBy": "Upload by",
"attachmentAutoUpload": "Auto Upload",
"attachmentUploadQueue": "Upload Queue",
"attachmentUploadQueueStart": "Start All",
"attachmentUploadInProgress": "There are attachments being uploaded. Please wait until all attachments have been uploaded before proceeding...",
"attachmentAttached": "Exists Files",
"attachmentUploadBlocked": "Upload blocked, there is currently a task in progress...",
"attachmentAdd": "Attach file",
"attachmentAddGalleryPhoto": "Gallery photo",
"attachmentAddGalleryVideo": "Gallery video",
"attachmentAddCameraPhoto": "Capture photo",
"attachmentAddCameraVideo": "Capture video",
"attachmentAddClipboard": "Paste file",
"attachmentAddFile": "Attach file",
"attachmentAddLink": "Link attachments",
"attachmentAddLinkHint": "Enter attachment serial number to link that attachment",
"attachmentAddLinkInput": "Serial number",
"attachmentSetting": "Adjust attachment",
"attachmentAlt": "Alternative text",
"attachmentLoadFailed": "Load Attachment Failed",
"attachmentLoadFailedCaption": "Something went wrong during loading the attachment metadata...",
"attachmentUploading": "Uploading @name...",
"attachmentUploadingWebMode": "Uploading @name... Due to browser's limitation, calculate attachment information may cause some lag...",
"realm": "Realm",
"realms": "Realms",
"realmOrganizing": "Organize a realm",
"realmAlias": "Alias (Identifier)",
"realmName": "Name",
"realmDescription": "Description",
"realmPublic": "Public Realm",
"realmCommunity": "Community Realm",
"realmDetail": "Realm detail",
"realmMember": "Realm member",
"realmMembers": "Realm members",
"realmMembersAdd": "Add realm members",
"realmMembersAddHint": "Into @realm",
"realmAdjust": "Realm adjustment",
"realmSettings": "Realm settings",
"realmEditingNotify": "You're editing realm @realm",
"realmLeaveConfirm": "Confirm realm quit",
"realmLeaveConfirmCaption": "Are you sure you want leave realm @realm? Your content published in this realm will not be deleted.",
"realmDeletionConfirm": "Confirm realm deletion",
"realmDeletionConfirmCaption": "Are you sure to delete realm @realm? This action cannot be undone!",
"channels": "Channels",
"channelNew": "Create a new channel",
"channelNewInRealmHint": "Create channel in realm @realm",
"channelOrganizing": "Organize a channel",
"channelOrganizeCommon": "Create regular channel",
"channelOrganizeDirect": "Create DM",
"channelOrganizeDirectHint": "Choose friend to create DM",
"channelInRealmNotify": "You're creating channel in realm @realm",
"channelEditingNotify": "You're editing channel @channel",
"channelAlias": "Alias (Identifier)",
"channelName": "Name",
"channelDescription": "Description",
"channelDirectDescription": "Direct message with @username",
"channelEncrypted": "Encrypted Channel",
"channelMember": "Channel member",
"channelMembers": "Channel members",
"channelMembersAdd": "Add channel members",
"channelMembersAddHint": "Into @channel",
"channelType": "Channel type",
"channelTypeCommon": "Regular",
"channelTypeDirect": "DM",
"channelAdjust": "Channel adjustment",
"channelDetail": "Channel detail",
"channelSettings": "Channel settings",
"channelLeaveConfirm": "Confirm channel quit",
"channelLeaveConfirmCaption": "Are you sure to leave channel @channel? All your messages will be deleted!",
"channelDeletionConfirm": "Confirm channel deletion",
"channelDeletionConfirmCaption": "Are you sure to delete channel @channel? This action cannot be undone!",
"channelCategoryDirect": "DM",
"channelCategoryDirectHint": "Your direct messages",
"channelNotifyLevel": "Notify level",
"channelNotifyLevelAll": "All",
"channelNotifyLevelMentioned": "Only mentioned",
"channelNotifyLevelNone": "Ignore all",
"channelNotifyLevelApplied": "Your notification settings has been applied.",
"messageUnSync": "Messages Un-synced",
"messageUnSyncCaption": "@count message(s) still in un-synced.",
"messageSending": "Sending...",
"messageEditDesc": "Edited message @id",
"messageDeleteDesc": "Deleted message @id",
"messageCallStartDesc": "@user starts a call",
"messageCallEndDesc": "Call last for @duration",
"messageTypeUnsupported": "Unsupported Message: @type",
"messageInputPlaceholder": "Message @channel",
"messageActionList": "Actions of Message",
"messageDeletionConfirm": "Confirm message deletion",
"messageDeletionConfirmCaption": "Are your sure to delete message @id? This action cannot be undone!",
"call": "Call",
"callOngoing": "A call is ongoing...",
"callOngoingEmpty": "A call is on hold...",
"callOngoingParticipants": "@count people are calling...",
"callOngoingJoined": "Call last @duration",
"callJoin": "Join",
"callResume": "Resume",
"callMicrophone": "Microphone",
"callMicrophoneDisabled": "Microphone Disabled",
"callMicrophoneSelect": "Select Microphone",
"callCamera": "Camera",
"callCameraDisabled": "Camera Disabled",
"callCameraSelect": "Select Camera",
"callSpeakerSelect": "Select Speaker",
"callDisconnected": "Call Disconnected... @reason",
"callMicrophoneOn": "Turn Microphone On",
"callMicrophoneOff": "Turn Microphone Off",
"callCameraOn": "Turn Camera On",
"callCameraOff": "Turn Camera Off",
"callVideoFlip": "Flip Video Input",
"callSpeakerphoneToggle": "Toggle Speakerphone Mode",
"callScreenOn": "Start Screen Sharing",
"callScreenOff": "Stop Screen Sharing",
"callDisconnect": "Disconnect",
"callDisconnectCaption": "Are you sure you want to disconnect from this call? You can also just return to the page, and the call will continue in the background.",
"callParticipantAction": "Participant Actions",
"callParticipantMicrophoneOff": "Mute Participant",
"callParticipantMicrophoneOn": "Unmute Participant",
"callParticipantVideoOff": "Turn Off Participant Video",
"callParticipantVideoOn": "Turn On Participant Video",
"callAlreadyOngoing": "A call is already ongoing",
"badge": "Badge",
"badges": "Badges",
"badgeGrantAt": "Badge awarded on @date",
"badgeSolsynthStaff": "Solsynth Staff",
"badgeSolarOriginalCitizen": "Solar Network Natives",
"badgeGreatCommunityContributor": "Great Community Contributor",
"pushNotifyRegister": "Register Push Notification Device",
"pushNotifyRegisterCaption": "Activating push notifications allows you to get our latest notifications even when the app is completely closed. We use Apple's official push service on iOS/macOS devices; other devices provide push notifications through Google Firebase. To register a device for push notifications, you may need to connect to Google's servers and install the Google Framework on your device. Although you dismiss this dialog, this registration will be auto performed when you next time launch the app.",
"pushNotifyRegisterDone": "Push notifications has been activated.",
"pushNotifyRegisterFailed": "Unable to active push notification... @reason",
"accountChangeStatus": "Change Status",
"accountCustomStatus": "Set Custom Status",
"accountClearStatus": "Clear Status",
"accountStatusOnline": "Online",
"accountStatusSilent": "Do not Disturb",
"accountStatusSilentDesc": "The notification will stop popping up",
"accountStatusInvisible": "Invisible",
"accountStatusInvisibleDesc": "Will show as offline, but all features still remain normal",
"accountStatusOffline": "Offline",
"accountLastSeenAt": "@date ago online",
"accountStatusLabel": "Status Text",
"accountStatusClearAt": "Clear At",
"accountStatusNegative": "Negative",
"accountStatusNeutral": "Neutral",
"accountStatusPositive": "Positive",
"bsLoadingTheme": "Loading Theme",
"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.",
"bsCheckingServer": "Checking Server Status",
"bsCheckingServerFail": "Unable connect to server, check your network connection",
"bsCheckingServerDown": "Server currently unavailable, please retry later",
"bsAuthorizing": "Authorizing",
"bsEstablishingConn": "Establishing Connection",
"bsPreparingData": "Preparing User Data",
"bsRegisteringPushNotify": "Enabling Push Notifications",
"bsDismissibleErrorHint": "Click anywhere to ignore this error",
"postShareContent": "@content\n\n@username on the Solar Network\nCheck it out: @link",
"postShareSubject": "@username posted a post on the Solar Network",
"themeColor": "Global Theme Color",
"themeColorRed": "Modern Red",
"themeColorBlue": "Classic Blue",
"themeColorMiku": "Miku Blue",
"themeColorKagamine": "Kagamine Yellow",
"themeColorLuka": "Luka Pink",
"stickerDeletionConfirm": "Confirm sticker delete",
"stickerDeletionConfirmCaption": "Are you sure to delete sticker @name? This action cannot be undo.",
"themeColorApplied": "Global theme color has been applied.",
"attachmentSaved": "Attachment saved to your system album.",
"cropImage": "Crop Image",
"stickerUploader": "Upload sticker",
"stickerUploaderAttachmentNew": "Upload sticker",
"stickerUploaderAttachment": "Attachment serial number",
"stickerUploaderPack": "Sticker pack serial number",
"stickerUploaderPackHint": "Don't have pack id? Head to creator platform and create one!",
"stickerUploaderAlias": "Alias",
"stickerUploaderAliasHint": "Will be used as a placeholder with the sticker pack prefix when entered.",
"stickerUploaderName": "Name",
"stickerUploaderNameHint": "A human-friendly name given to the user in the sticker selection interface.",
"readMore": "Read more",
"attachmentUnload": "Not Loaded",
"attachmentUnloadCaption": "In order to save traffic, this attachment is not loaded automatically. Click it to start loading.",
"callStatusConnected": "Connected",
"callStatusDisconnected": "Disconnected",
"callStatusConnecting": "Connecting",
"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",
"unknown": "Unknown",
"collapse": "Collapse",
"expand": "Expand",
"typingMessage": "@user are typing...",
"userLevel0": "Newbie",
"userLevel1": "Novice",
"userLevel2": "Apprentice",
"userLevel3": "Explorer",
"userLevel4": "Adventurer",
"userLevel5": "Warrior",
"userLevel6": "Knight",
"userLevel7": "Champion",
"userLevel8": "Hero",
"userLevel9": "Master",
"userLevel10": "Grandmaster",
"userLevel11": "Legend",
"userLevel12": "Mythic",
"userLevel13": "Immortal"
}

396
assets/locales/zh_cn.json Normal file
View File

@ -0,0 +1,396 @@
{
"done": "完成",
"hide": "隐藏",
"okay": "确认",
"home": "首页",
"next": "下一步",
"reset": "重置",
"cancel": "取消",
"confirm": "确认",
"leave": "离开",
"loading": "载入中…",
"guest": "游客",
"about": "关于",
"edit": "编辑",
"delete": "删除",
"settings": "设置",
"page": "页面",
"draft": "草稿",
"draftSave": "存为草稿",
"draftBox": "草稿箱",
"more": "更多",
"share": "分享",
"shareNoUri": "分享文字内容",
"alias": "别名",
"feed": "资讯",
"explore": "探索",
"posts": "帖子",
"unlink": "移除链接",
"dashboard": "仪表盘",
"today": "今日",
"yesterday": "昨日",
"feedSearch": "搜索资讯",
"feedSearchWithTag": "检索带有 #@key 标签的资讯",
"feedSearchWithCategory": "检索位于分类 @category 的资讯",
"feedUnreadCount": "@count 条你可能错过的帖子",
"messages": "消息",
"messagesUnreadCount": "@count 条未读的消息",
"dailySign": "签到",
"dailySignAction": "烧香拜佛",
"dailySignHistoryAction": "查看运势历史",
"dailySignNone": "今日未拜访佛祖",
"dailySignTier0": "诸事不宜",
"dailySignTier1": "有些不宜",
"dailySignTier2": "平平淡淡",
"dailySignTier3": "有些事宜",
"dailySignTier4": "诸事皆宜",
"dailySignHistoryTitle": "运势历史",
"dailySignHistoryRecent": "近期运势",
"dailySignHistoryReward": "成果趋势",
"dashboardFooter": "占卜多少沾点玩,人生还得靠实力",
"visitProfilePage": "造访个人主页",
"profilePage": "主页",
"profilePosts": "帖子",
"profileAlbum": "相簿",
"chat": "聊天",
"apply": "应用",
"search": "搜索",
"post": "帖子",
"article": "文章",
"reply": "回复",
"repost": "转帖",
"openInAlbum": "在相簿中打开",
"openInBrowser": "在浏览器中打开",
"notification": "通知",
"notificationUnreadCount": "@count 条未读通知",
"errorHappened": "发生错误了",
"errorHappenedUnauthorized": "未经授权的请求,请登录或尝试重新登录。",
"errorHappenedRequestBad": "请求错误,服务器拒绝处理该请求,请检查您的请求数据。",
"errorHappenedRequestForbidden": "请求错误,权限不足。",
"errorHappenedRequestNotFound": "请求错误,请求的数据不存在。",
"errorHappenedRequestConnection": "网络请求失败,请检查连接状态与服务状态后再试。",
"errorHappenedRequestUnknown": "请求错误,类型未知,请将本提示完整截图提交反馈。",
"forgotPassword": "忘记密码",
"email": "邮件地址",
"username": "用户名",
"nickname": "显示名",
"password": "密码",
"title": "标题",
"description": "简介",
"birthday": "生日",
"firstName": "名称",
"lastName": "姓氏",
"account": "账号",
"accountPersonalize": "个性化",
"accountPersonalizeApplied": "账户的个性化设置已保存。",
"accountStickers": "贴图",
"accountFriend": "好友",
"accountFriendNew": "添加好友",
"accountFriendNewHint": "使用他人的用户名来发送一个好友请求吧!",
"accountFriendPending": "好友请求",
"accountFriendBlocked": "好友黑名单",
"accountFriendListHint": "左滑来拒绝,右滑来接受",
"accountFriendRequestSent": "好友请求已发送,等待处理对方中……",
"accountSuspended": "帐号被停用",
"accountSuspendedAt": "该帐号自 @date 起被停用",
"aspectRatio": "纵横比",
"aspectRatioSquare": "方型",
"aspectRatioPortrait": "竖型",
"aspectRatioLandscape": "横型",
"unsignedIn": "未登录",
"signin": "登录",
"signinRequired": "需要登录",
"signinRequiredHint": "登陆以获得 Solar Network 的全部功能使用权。",
"signinGreeting": "欢迎回来\nSolar Network",
"signinCaption": "登录以发表帖子、文章、创建领域、和你的朋友聊天,以及获取更多功能!",
"signinRiskDetected": "检测到风险,点击下一步按钮来打开一个网页,并通过在其上面登录来通过安全检查。",
"signinResetPasswordHint": "请先填写用户名以发送重置密码请求。",
"signinResetPasswordSent": "重置密码请求已发送,在绑定邮件收件箱可收取一份包含重置密码链接的邮件。",
"signup": "注册",
"signupGreeting": "欢迎加入\nSolar Network",
"signupCaption": "在 Solarpass 注册一个账号以获得整个 Solar Network 的存取权!",
"signupDone": "注册成功",
"signupDoneCaption": "你成功地注册了一个帐户,现在去尝试登陆吧!",
"signout": "登出",
"joinedAt": "加入于 @date",
"riskDetection": "检测到风险",
"matureContent": "评级内容",
"matureContentCaption": "该内容已被评级为家长指导级或以上,这可能说明内容包含一系列不友好的成分",
"notifyAllRead": "已读所有通知",
"notifyEmpty": "通知箱为空",
"notifyEmptyCaption": "看起来最近没发生什么呢",
"totalSocialCreditPoints": "社会信用点",
"totalPostCount": "总帖数",
"totalUpvote": "获顶数",
"totalDownvote": "获踩数",
"clear": "清除",
"pinPost": "置顶本帖",
"unpinPost": "取消置顶本帖",
"postRestoreFromLocal": "内容从本地暂存回复",
"postAutoSaveAt": "已自动保存于 @date",
"postCategoriesAndTags": "分类与标签",
"postPublishDate": "发布时间",
"postPublishAt": "发布帖子于",
"postPublishedUntil": "取消发布于",
"postPublishZone": "帖子发布区",
"postPublishZoneNone": "无所属领域",
"postVisibility": "帖子可见性",
"postVisibilityAll": "所有人可见",
"postVisibilityFriends": "仅好友可见",
"postVisibilitySelected": "选中者可见",
"postVisibilityFiltered": "选中者不可见",
"postVisibilityNone": "仅自己可见",
"postVisibleUsers": "可见帖子者",
"postInvisibleUsers": "隐藏帖子者",
"postOverview": "帖子概览",
"postThumbnail": "帖子缩略图",
"postThumbnailAttachmentNew": "上传附件作为缩略图",
"postThumbnailAttachment": "附件序列号",
"postPinned": "已置顶",
"postEditorModeStory": "发个帖子",
"postEditorModeArticle": "撰写文章",
"postEditor": "帖子编辑器",
"articleEditor": "撰写文章",
"articleDetail": "文章详情",
"draftBoxOpen": "打开草稿箱",
"postListNews": "新鲜事",
"postListFriends": "好友圈",
"postListShuffle": "打乱看",
"postNew": "创建新帖子",
"postNewInRealmHint": "在领域 @realm 里发表新帖子",
"postAction": "发表",
"postEdited": "编辑于 @date",
"postNewCreated": "创建于 @date",
"postInRealm": "发表于 @realm",
"attachmentHint": "@count 个附件",
"postDetail": "帖子详情",
"postReplies": "帖子回复",
"postPublish": "编辑帖子",
"postIdentityNotify": "你将会以本身份发表帖子",
"postContentPlaceholder": "发生什么事了?!",
"postTagsPlaceholder": "标签",
"postReaction": "帖子的反应",
"postActionList": "帖子的操作",
"postReplyAction": "发表一则回复",
"postRepliedNotify": "回了一个 @username 的帖子",
"postRepostedNotify": "转了一个 @username 的帖子",
"postInRealmNotify": "你正在领域 @realm 中发表帖子",
"postEditingNotify": "你正在编辑一个你发布的帖子",
"postReplyingNotify": "你正在回一个来自 @username 的帖子",
"postRepostingNotify": "你正在转一个来自 @username 的帖子",
"postDeletionConfirm": "确认删除帖子",
"postDeletionConfirmCaption": "你确定要删除帖子 “@content” 吗?该操作不可撤销。",
"reactAdd": "作出反应",
"reactCompleted": "你的反应已被添加",
"reactUncompleted": "你的反应已被移除",
"attachmentUploadBy": "由上传",
"attachmentAutoUpload": "自动上传",
"attachmentUploadQueue": "上传队列",
"attachmentUploadQueueStart": "整队上传",
"attachmentUploadInProgress": "有附件正在上传,请等待所有附件上传完毕后再进行操作……",
"attachmentAttached": "已附附件",
"attachmentUploadBlocked": "上传受阻,当前已有任务进行中……",
"attachmentAdd": "附加附件",
"attachmentAddGalleryPhoto": "相册照片",
"attachmentAddGalleryVideo": "相册视频",
"attachmentAddCameraPhoto": "拍摄图片",
"attachmentAddCameraVideo": "拍摄视频",
"attachmentAddClipboard": "粘贴文件",
"attachmentAddFile": "附加文件",
"attachmentAddLink": "链接附件",
"attachmentAddLinkHint": "输入附件的神秘代号来链接对应附件",
"attachmentAddLinkInput": "神秘代号",
"attachmentSetting": "调整附件",
"attachmentAlt": "替代文字",
"attachmentLoadFailed": "加载失败",
"attachmentLoadFailedCaption": "有错误发生于加载附件元数据的过程中了…",
"attachmentUploading": "上传附件 @name 中…",
"attachmentUploadingWebMode": "上传附件 @name 中… 由于浏览器单线程限制,计算所需资源可能会导致界面卡顿…",
"realm": "领域",
"realms": "领域",
"realmOrganizing": "组织领域",
"realmAlias": "别称(标识符)",
"realmName": "显示名称",
"realmDescription": "领域简介",
"realmPublic": "公开领域",
"realmCommunity": "社区领域",
"realmDetail": "领域详情",
"realmMember": "领域成员",
"realmMembers": "领域成员",
"realmMembersAdd": "添加领域成员",
"realmMembersAddHint": "到 @realm",
"realmAdjust": "调整领域",
"realmSettings": "领域设置",
"realmEditingNotify": "你正在编辑领域 @realm",
"realmLeaveConfirm": "确认离开领域",
"realmLeaveConfirmCaption": "你确认要离开领域 @realm 吗?你在该领域发表的内容不会被删除。",
"realmDeletionConfirm": "确认删除领域",
"realmDeletionConfirmCaption": "你确定要删除领域 @realm 嘛?该操作不可撤销。",
"channels": "频道",
"channelNew": "创建新频道",
"channelNewInRealmHint": "在领域 @realm 里创建新频道",
"channelOrganizing": "组织频道",
"channelOrganizeCommon": "创建普通频道",
"channelOrganizeDirect": "创建私信频道",
"channelOrganizeDirectHint": "选择好友来创建私信",
"channelInRealmNotify": "你正在领域 @realm 中创建频道",
"channelEditingNotify": "你正在编辑频道 @channel",
"channelAlias": "别称(标识符)",
"channelName": "显示名称",
"channelDescription": "频道简介",
"channelDirectDescription": "与 @username 的私聊",
"channelEncrypted": "加密频道",
"channelMember": "频道成员",
"channelMembers": "频道成员",
"channelMembersAdd": "添加频道成员",
"channelMembersAddHint": "到 @channel",
"channelType": "频道类型",
"channelTypeCommon": "普通频道",
"channelTypeDirect": "私信聊天",
"channelAdjust": "调整频道",
"channelDetail": "频道详情",
"channelSettings": "频道设置",
"channelLeaveConfirm": "确认离开频道",
"channelLeaveConfirmCaption": "你确认要离开频道 @channel 吗?你在这个频道的消息将被删除。",
"channelDeletionConfirm": "确认删除频道",
"channelDeletionConfirmCaption": "你确认要删除频道 @channel 吗?该操作不可撤销。",
"channelCategoryDirect": "私聊频道",
"channelCategoryDirectHint": "你的所有私聊频道",
"channelNotifyLevel": "通知等级",
"channelNotifyLevelAll": "全部通知",
"channelNotifyLevelMentioned": "仅提及",
"channelNotifyLevelNone": "忽略一切",
"channelNotifyLevelApplied": "你的通知设置已经应用。",
"messageUnSync": "消息未同步",
"messageUnSyncCaption": "还有 @count 条消息未同步",
"messageSending": "消息发送中…",
"messageEditDesc": "修改了消息 @id",
"messageDeleteDesc": "删除了消息 @id",
"messageCallStartDesc": "@user 发起了一次通话",
"messageCallEndDesc": "通话持续了 @duration",
"messageTypeUnsupported": "不支持的消息类型 @type",
"messageInputPlaceholder": "发消息于 @channel",
"messageActionList": "消息的操作",
"messageDeletionConfirm": "确认删除消息",
"messageDeletionConfirmCaption": "你确定要删除消息 @id 吗?该操作不可撤销。",
"call": "通话",
"callOngoing": "一则通话正在进行中…",
"callOngoingEmpty": "一则通话待机中…",
"callOngoingParticipants": "@count 人正在进行通话…",
"callOngoingJoined": "通话进行 @duration",
"callJoin": "加入",
"callResume": "恢复",
"callMicrophone": "麦克风",
"callMicrophoneDisabled": "麦克风禁用",
"callMicrophoneSelect": "选择麦克风",
"callCamera": "摄像头",
"callCameraDisabled": "摄像头禁用",
"callCameraSelect": "选择摄像头",
"callSpeakerSelect": "选择扬声器",
"callDisconnected": "通话已断开… @reason",
"callMicrophoneOn": "开启麦克风",
"callMicrophoneOff": "关闭麦克风",
"callCameraOn": "开启摄像头",
"callCameraOff": "关闭摄像头",
"callVideoFlip": "翻转视频输入",
"callSpeakerphoneToggle": "切换扬声器模式",
"callScreenOn": "启动屏幕分享",
"callScreenOff": "关闭屏幕分享",
"callDisconnect": "断开连接",
"callDisconnectCaption": "你确定要断开与该则通话的连接吗?你也可以直接返回页面,通话将在后台继续。",
"callParticipantAction": "通话参与者的操作",
"callParticipantMicrophoneOff": "静音参与者",
"callParticipantMicrophoneOn": "解除静音参与者",
"callParticipantVideoOff": "静音参与者",
"callParticipantVideoOn": "解除静音参与者",
"callAlreadyOngoing": "当前正在进行一则通话",
"badge": "徽章",
"badges": "徽章",
"badgeGrantAt": "徽章颁发于 @date",
"badgeSolsynthStaff": "Solsynth 工作人员",
"badgeSolarOriginalCitizen": "Solar Network 原住民",
"badgeGreatCommunityContributor": "优秀社区贡献者",
"pushNotifyRegister": "注册推送通知设备",
"pushNotifyRegisterCaption": "激活推送通知便可以让你在应用程序完全关闭的时候仍然获取到我们最新的通知。在 iOS/macOS 设备上我们使用 Apple 官方的推送服务;其他设备则通过 Google Firebase 提供推送通知。要注册推送通知设备,您可能需要连接到 Google 的服务器(在中国大陆不可用)并在您的设备上安装 Google Framework。即使您关闭此对话框下次启动应用程序时仍会自动执行此注册。",
"pushNotifyRegisterDone": "推送通知已成功激活",
"pushNotifyRegisterFailed": "推送通知激活失败…… @reason",
"accountChangeStatus": "变更状态",
"accountCustomStatus": "自定义状态",
"accountClearStatus": "清除状态",
"accountStatusOnline": "在线",
"accountStatusSilent": "请勿打扰",
"accountStatusSilentDesc": "将会暂停所有通知推送",
"accountStatusInvisible": "隐身",
"accountStatusInvisibleDesc": "将会在他人界面显示离线,但不影响功能使用",
"accountStatusOffline": "离线",
"accountLastSeenAt": "最后上线于 @date 前",
"accountStatusLabel": "状态文字",
"accountStatusClearAt": "清除状态于",
"accountStatusNegative": "负面",
"accountStatusNeutral": "中性",
"accountStatusPositive": "积极",
"bsLoadingTheme": "正在装载主题",
"bsCheckForUpdate": "正在检查更新",
"bsCheckForUpdateFailed": "无法检查更新",
"bsCheckForUpdateNew": "发现新版本",
"bsCheckForUpdateDescApple": "请前往 TestFlight 并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。",
"bsCheckForUpdateDescCommon": "请前往我们的网站下载并安装最新版本的应用程序,以防止出现错误并获取最新功能。",
"bsCheckingServer": "检查服务器状态中",
"bsCheckingServerFail": "无法连接至服务器,请检查你的网络连接状态",
"bsCheckingServerDown": "当前服务器不可用,请稍后重试",
"bsAuthorizing": "正在授权中",
"bsEstablishingConn": "部署连接中",
"bsPreparingData": "正在准备用户资料",
"bsRegisteringPushNotify": "正在启用推送通知",
"bsDismissibleErrorHint": "点击任意地方忽略此错误",
"postShareContent": "@content\n\n@username 在 Solar Network\n原帖地址@link",
"postShareSubject": "@username 在 Solar Network 上发布了一篇帖子",
"themeColor": "全局主题色",
"themeColorRed": "现代红",
"themeColorBlue": "经典蓝",
"themeColorMiku": "未来蓝",
"themeColorKagamine": "镜音黄",
"themeColorLuka": "流音粉",
"themeColorApplied": "全局主题颜色已应用",
"stickerDeletionConfirm": "确认删除贴图",
"stickerDeletionConfirmCaption": "你确认要删除贴图 @name 吗?该操作不可撤销。",
"attachmentSaved": "附件已保存到系统相册",
"cropImage": "裁剪图片",
"stickerUploader": "上传贴图",
"stickerUploaderAttachmentNew": "上传附件作为贴图",
"stickerUploaderAttachment": "附件序列号",
"stickerUploaderPack": "贴图包序号",
"stickerUploaderPackHint": "没有该序号?请转到我们的创作者平台创建一个贴图包。",
"stickerUploaderAlias": "贴图别名",
"stickerUploaderAliasHint": "将会在输入时使用和贴图包前缀组成占位符。",
"stickerUploaderName": "贴图名称",
"stickerUploaderNameHint": "在贴图选择界面提供给用户的人类友好名称。",
"readMore": "阅读更多",
"attachmentUnload": "附件未加载",
"attachmentUnloadCaption": "为了节省流量,本附件未自动加载,点一下来开始加载。",
"callStatusConnected": "已连接",
"callStatusDisconnected": "已断开",
"callStatusConnecting": "连接中",
"callStatusReconnected": "重连中",
"messageOutOfSync": "消息可能与服务器脱节",
"messageOutOfSyncCaption": "由于 App 进入后台,消息列表可能与服务器存在时差,点击刷新。",
"messageHistoryWipe": "清除消息记录",
"unknown": "未知",
"collapse": "折叠",
"expand": "展开",
"typingMessage": "@user 正在输入中…",
"userLevel0": "不慕名利",
"userLevel1": "初出茅庐",
"userLevel2": "小试牛刀",
"userLevel3": "磨杵成针",
"userLevel4": "披荆斩棘",
"userLevel5": "力挽狂澜",
"userLevel6": "一骑当千",
"userLevel7": "所向披靡",
"userLevel8": "气吞山河",
"userLevel9": "登峰造极",
"userLevel10": "出神入化",
"userLevel11": "名垂千古",
"userLevel12": "独占鳌头",
"userLevel13": "万古流芳"
}

7
build.yaml Normal file
View File

@ -0,0 +1,7 @@
targets:
$default:
builders:
json_serializable:
options:
explicit_to_json: true
field_rename: snake

View File

@ -38,39 +38,126 @@ PODS:
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/CoreOnly (10.29.0):
- FirebaseCore (= 10.29.0)
- Firebase/Messaging (10.29.0):
- Firebase/Analytics (11.0.0):
- Firebase/Core
- Firebase/Core (11.0.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 10.29.0)
- firebase_core (3.3.0):
- Firebase/CoreOnly (= 10.29.0)
- Flutter
- firebase_messaging (15.0.4):
- Firebase/Messaging (= 10.29.0)
- FirebaseAnalytics (~> 11.0.0)
- Firebase/CoreOnly (11.0.0):
- FirebaseCore (= 11.0.0)
- Firebase/Crashlytics (11.0.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 11.0.0)
- Firebase/Messaging (11.0.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.0.0)
- Firebase/Performance (11.0.0):
- Firebase/CoreOnly
- FirebasePerformance (~> 11.0.0)
- firebase_analytics (11.3.0):
- Firebase/Analytics (= 11.0.0)
- firebase_core
- Flutter
- FirebaseCore (10.29.0):
- FirebaseCoreInternal (~> 10.0)
- GoogleUtilities/Environment (~> 7.12)
- GoogleUtilities/Logger (~> 7.12)
- FirebaseCoreInternal (10.29.0):
- "GoogleUtilities/NSData+zlib (~> 7.8)"
- FirebaseInstallations (10.29.0):
- FirebaseCore (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8)
- PromisesObjC (~> 2.1)
- FirebaseMessaging (10.29.0):
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleDataTransport (~> 9.3)
- GoogleUtilities/AppDelegateSwizzler (~> 7.8)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/Reachability (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8)
- nanopb (< 2.30911.0, >= 2.30908.0)
- firebase_core (3.4.0):
- Firebase/CoreOnly (= 11.0.0)
- Flutter
- firebase_crashlytics (4.1.0):
- Firebase/Crashlytics (= 11.0.0)
- firebase_core
- Flutter
- firebase_messaging (15.1.0):
- Firebase/Messaging (= 11.0.0)
- firebase_core
- Flutter
- firebase_performance (0.10.0-5):
- Firebase/Performance (= 11.0.0)
- firebase_core
- Flutter
- FirebaseABTesting (11.1.0):
- FirebaseCore (~> 11.0)
- FirebaseAnalytics (11.0.0):
- FirebaseAnalytics/AdIdSupport (= 11.0.0)
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/AdIdSupport (11.0.0):
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- GoogleAppMeasurement (= 11.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- FirebaseCore (11.0.0):
- FirebaseCoreInternal (~> 11.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.1.0):
- FirebaseCore (~> 11.0)
- FirebaseCoreInternal (11.1.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseCrashlytics (11.0.0):
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- FirebaseRemoteConfigInterop (~> 11.0)
- FirebaseSessions (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/Environment (~> 8.0)
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- FirebaseInstallations (11.1.0):
- FirebaseCore (~> 11.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.0.0):
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Reachability (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0)
- FirebasePerformance (11.0.0):
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- FirebaseRemoteConfig (~> 11.0)
- FirebaseSessions (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0)
- FirebaseRemoteConfig (11.1.0):
- FirebaseABTesting (~> 11.0)
- FirebaseCore (~> 11.0)
- FirebaseInstallations (~> 11.0)
- FirebaseRemoteConfigInterop (~> 11.0)
- FirebaseSharedSwift (~> 11.0)
- GoogleUtilities/Environment (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- FirebaseRemoteConfigInterop (11.1.0)
- FirebaseSessions (11.1.0):
- FirebaseCore (~> 11.0)
- FirebaseCoreExtension (~> 11.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- nanopb (~> 3.30910.0)
- PromisesSwift (~> 2.1)
- FirebaseSharedSwift (11.1.0)
- Flutter (1.0.0)
- flutter_keyboard_visibility (0.0.1):
- Flutter
- flutter_native_splash (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- flutter_webrtc (0.11.3):
@ -79,33 +166,54 @@ PODS:
- gal (1.0.0):
- Flutter
- FlutterMacOS
- GoogleDataTransport (9.4.1):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/AppDelegateSwizzler (7.13.3):
- GoogleAppMeasurement (11.0.0):
- GoogleAppMeasurement/AdIdSupport (= 11.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/AdIdSupport (11.0.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 11.0.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/WithoutAdIdSupport (11.0.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/MethodSwizzler (~> 8.0)
- GoogleUtilities/Network (~> 8.0)
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- nanopb (~> 3.30910.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleUtilities/AppDelegateSwizzler (8.0.2):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (7.13.3):
- GoogleUtilities/Environment (8.0.2):
- GoogleUtilities/Privacy
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Logger (7.13.3):
- GoogleUtilities/Logger (8.0.2):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (7.13.3):
- GoogleUtilities/MethodSwizzler (8.0.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.0.2):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (7.13.3)":
- "GoogleUtilities/NSData+zlib (8.0.2)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (7.13.3)
- GoogleUtilities/Reachability (7.13.3):
- GoogleUtilities/Privacy (8.0.2)
- GoogleUtilities/Reachability (8.0.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (7.13.3):
- GoogleUtilities/UserDefaults (8.0.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- image_cropper (0.0.4):
@ -113,7 +221,7 @@ PODS:
- TOCropViewController (~> 2.7.4)
- image_picker_ios (0.0.1):
- Flutter
- livekit_client (2.2.3):
- livekit_client (2.2.4):
- Flutter
- WebRTC-SDK (= 125.6422.04)
- media_kit_libs_ios_video (1.0.4):
@ -122,11 +230,11 @@ PODS:
- Flutter
- media_kit_video (0.0.1):
- Flutter
- nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0)
- nanopb/decode (2.30910.0)
- nanopb/encode (2.30910.0)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- package_info_plus (0.4.5):
- Flutter
- pasteboard (0.0.1):
@ -136,19 +244,18 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- pointer_interceptor_ios (0.0.1):
- Flutter
- PromisesObjC (2.4.0)
- PromisesSwift (2.4.0):
- PromisesObjC (= 2.4.0)
- protocol_handler_ios (0.0.1):
- Flutter
- screen_brightness_ios (0.1.0):
- Flutter
- SDWebImage (5.19.4):
- SDWebImage/Core (= 5.19.4)
- SDWebImage/Core (5.19.4)
- Sentry/HybridSDK (8.32.0)
- sentry_flutter (8.6.0):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.32.0)
- SDWebImage (5.19.7):
- SDWebImage/Core (= 5.19.7)
- SDWebImage/Core (5.19.7)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@ -171,9 +278,14 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- firebase_performance (from `.symlinks/plugins/firebase_performance/ios`)
- Flutter (from `Flutter`)
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/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`)
- gal (from `.symlinks/plugins/gal/darwin`)
@ -187,9 +299,9 @@ DEPENDENCIES:
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
@ -202,16 +314,26 @@ SPEC REPOS:
- DKImagePickerController
- DKPhotoGallery
- Firebase
- FirebaseABTesting
- FirebaseAnalytics
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseCrashlytics
- FirebaseInstallations
- FirebaseMessaging
- FirebasePerformance
- FirebaseRemoteConfig
- FirebaseRemoteConfigInterop
- FirebaseSessions
- FirebaseSharedSwift
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleUtilities
- nanopb
- PromisesObjC
- PromisesSwift
- SDWebImage
- Sentry
- SwiftyGif
- TOCropViewController
- WebRTC-SDK
@ -223,12 +345,22 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
firebase_analytics:
:path: ".symlinks/plugins/firebase_analytics/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_crashlytics:
:path: ".symlinks/plugins/firebase_crashlytics/ios"
firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios"
firebase_performance:
:path: ".symlinks/plugins/firebase_performance/ios"
Flutter:
:path: Flutter
flutter_keyboard_visibility:
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_webrtc:
@ -255,12 +387,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
pointer_interceptor_ios:
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
protocol_handler_ios:
:path: ".symlinks/plugins/protocol_handler_ios/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
sentry_flutter:
:path: ".symlinks/plugins/sentry_flutter/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@ -280,36 +412,51 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d
firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb
firebase_messaging: c862b3d2b973ecc769194dc8de09bd22c77ae757
FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16
FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366
Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9
firebase_analytics: 1a66fe8d4375eccff44671ea37897683a78b2675
firebase_core: ceec591a66629daaee82d3321551692c4a871493
firebase_crashlytics: e4f04180f443d5a8b56fbc0685bdbd7d90dd26f0
firebase_messaging: 15d8b557010f3bb7b98d0302e1c7c8fbcd244425
firebase_performance: d373c742649e2d85d92cc223b4511c3d132887ef
FirebaseABTesting: c2e22c3aab99afa81d0561708b2c1c356c556976
FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a
FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383
FirebaseCoreExtension: aa5c9779c2d0d39d83f1ceb3fdbafe80c4feecfa
FirebaseCoreInternal: adefedc9a88dbe393c4884640a73ec9e8e790f8c
FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b
FirebaseInstallations: d0a8fea5a6fa91abc661591cf57c0f0d70863e57
FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742
FirebasePerformance: efdc02bacb1b4710588c9f867011605c081cdf79
FirebaseRemoteConfig: 05521e937b72e01847a7128da5a492327364c705
FirebaseRemoteConfigInterop: abf8b1bbc0bf1b84abd22b66746926410bf91a87
FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a
FirebaseSharedSwift: 260a35e08943ec810d820a70bc0359136351d0c5
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
GoogleAppMeasurement: 6e49ffac7d3f2c3ded9cc663f912a13b67bbd0de
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
livekit_client: bad83a7776a41abc42e1f26d903eeac9164c8a9f
livekit_client: d079c5f040d4bf2b80440ff0ae997725a183e4bc
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
nanopb: 438bc412db1928dac798aa6fd75726007be04262
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
pointer_interceptor_ios: 508241697ff0947f853c061945a8b822463947c1
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb
sentry_flutter: 090351ce1ff5f96a4b33ef9455b7e3b28185387d
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec

View File

@ -254,6 +254,7 @@
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
B1CDA9DD5B638A2BB88053CB /* [CP] Check Pods Manifest.lock */,
7356FAC42C72724B0051A465 /* [Crashlytics] Clear dSYM */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
@ -263,6 +264,7 @@
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
287A33C298CA352A7E7F32A4 /* [CP] Embed Pods Frameworks */,
0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */,
1A9FD6BE5DEE99CDA7399504 /* [Crashlytics] Upload dSYM */,
);
buildRules = (
);
@ -365,6 +367,24 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
1A9FD6BE5DEE99CDA7399504 /* [Crashlytics] Upload dSYM */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[Crashlytics] Upload dSYM";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nsleep 1 # Without this, there seems a chance that the script runs before dSYM generation is finished \n$PODS_ROOT/FirebaseCrashlytics/upload-symbols -gsp $PROJECT_DIR/Runner/GoogleService-Info.plist -p ios $DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME\n";
};
259653AE41D478F4C6BAE9B2 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -420,6 +440,24 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
7356FAC42C72724B0051A465 /* [Crashlytics] Clear dSYM */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[Crashlytics] Clear dSYM";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\n#!/bin/bash\nrm -rf $DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME\n";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@ -433,7 +471,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
B1CDA9DD5B638A2BB88053CB /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;

View File

@ -1,7 +1,7 @@
import UIKit
import Flutter
@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"filename" : "background.png",
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "darkbackground.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,23 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 233 KiB

View File

@ -16,13 +16,19 @@
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
</constraints>
</view>
</viewController>
@ -32,6 +38,7 @@
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
<image name="LaunchImage" width="1026" height="1024"/>
<image name="LaunchBackground" width="1" height="1"/>
</resources>
</document>

View File

@ -1,85 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>solink</string>
</array>
</dict>
</array>
<key>FirebaseMessagingAutoInitEnabled</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Solian</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>solian</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Allow you take photo/video for your message or post</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow you record audio for your message or post</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow you add photo to your message or post</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>remote-notification</string>
<string>voip</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>solink</string>
</array>
</dict>
</array>
<key>FirebaseMessagingAutoInitEnabled</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Solian</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>solian</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Allow you take photo/video for your message or post</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow you record audio for your message or post</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow you add photo to your message or post</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>fetch</string>
<string>remote-notification</string>
<string>voip</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIStatusBarHidden</key>
<false/>
</dict>
</plist>

View File

@ -1,4 +1,5 @@
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';
@ -112,15 +113,19 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
label: 'bsPreparingData',
action: () async {
final AuthProvider auth = Get.find();
await Future.wait([
Get.find<StickerProvider>().refreshAvailableStickers(),
if (auth.isAuthorized.isTrue)
Get.find<ChannelProvider>().refreshAvailableChannel(),
if (auth.isAuthorized.isTrue)
Get.find<RelationshipProvider>().refreshRelativeList(),
if (auth.isAuthorized.isTrue)
Get.find<RealmProvider>().refreshAvailableRealms(),
]);
try {
await Future.wait([
Get.find<StickerProvider>().refreshAvailableStickers(),
if (auth.isAuthorized.isTrue)
Get.find<ChannelProvider>().refreshAvailableChannel(),
if (auth.isAuthorized.isTrue)
Get.find<RelationshipProvider>().refreshRelativeList(),
if (auth.isAuthorized.isTrue)
Get.find<RealmProvider>().refreshAvailableRealms(),
]);
} catch (e) {
context.showErrorDialog(e);
}
},
),
(
@ -193,7 +198,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
height: 24,
child: CircularProgressIndicator(strokeWidth: 3),
),
const SizedBox(height: 12),
const Gap(12),
CenteredContainer(
maxWidth: 280,
child: Column(

View File

@ -10,12 +10,14 @@ import 'package:solian/widgets/posts/editor/post_editor_categories_tags.dart';
import 'package:solian/widgets/posts/editor/post_editor_date.dart';
import 'package:solian/widgets/posts/editor/post_editor_overview.dart';
import 'package:solian/widgets/posts/editor/post_editor_publish_zone.dart';
import 'package:solian/widgets/posts/editor/post_editor_thumbnail.dart';
import 'package:solian/widgets/posts/editor/post_editor_visibility.dart';
import 'package:shared_preferences/shared_preferences.dart';
class PostEditorController extends GetxController {
late final SharedPreferences _prefs;
final aliasController = TextEditingController();
final titleController = TextEditingController();
final descriptionController = TextEditingController();
final contentController = TextEditingController();
@ -29,8 +31,9 @@ class PostEditorController extends GetxController {
Rx<Realm?> realmZone = Rx(null);
Rx<DateTime?> publishedAt = Rx(null);
Rx<DateTime?> publishedUntil = Rx(null);
RxList<int> attachments = RxList<int>.empty(growable: true);
RxList<String> attachments = RxList<String>.empty(growable: true);
RxList<String> tags = RxList<String>.empty(growable: true);
Rx<String?> thumbnail = Rx(null);
RxList<int> visibleUsers = RxList.empty(growable: true);
RxList<int> invisibleUsers = RxList.empty(growable: true);
@ -113,18 +116,27 @@ class PostEditorController extends GetxController {
return showModalBottomSheet(
context: context,
builder: (context) => AttachmentEditorPopup(
usage: 'i.attachment',
pool: 'interactive',
initialAttachments: attachments,
onAdd: (int value) {
onAdd: (String value) {
attachments.add(value);
},
onRemove: (int value) {
onRemove: (String value) {
attachments.remove(value);
},
),
);
}
Future<void> editThumbnail(BuildContext context) {
return showDialog(
context: context,
builder: (context) => PostEditorThumbnailDialog(
controller: this,
),
);
}
void toggleDraftMode() {
isDraft.value = !isDraft.value;
}
@ -157,6 +169,7 @@ class PostEditorController extends GetxController {
}
void currentClear() {
aliasController.clear();
titleController.clear();
descriptionController.clear();
contentController.clear();
@ -165,6 +178,7 @@ class PostEditorController extends GetxController {
visibleUsers.clear();
invisibleUsers.clear();
visibility.value = 0;
thumbnail.value = null;
publishedAt.value = null;
publishedUntil.value = null;
isDraft.value = false;
@ -185,17 +199,25 @@ class PostEditorController extends GetxController {
type = value.type;
editTo.value = value;
realmZone.value = value.realm;
isDraft.value = value.isDraft ?? false;
aliasController.text = value.alias ?? '';
titleController.text = value.body['title'] ?? '';
descriptionController.text = value.body['description'] ?? '';
contentController.text = value.body['content'] ?? '';
publishedAt.value = value.publishedAt;
publishedUntil.value = value.publishedUntil;
tags.value =
value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty();
tags.value = List.from(
value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty(),
growable: true,
);
tags.refresh();
attachments.value = value.body['attachments']?.cast<int>() ?? List.empty();
attachments.value = List.from(
value.body['attachments'] ?? List.empty(),
growable: true,
);
attachments.refresh();
thumbnail.value = value.body['thumbnail'];
contentLength.value = contentController.text.length;
}
@ -243,9 +265,11 @@ class PostEditorController extends GetxController {
Map<String, dynamic> get payload {
return {
'alias': aliasController.text,
'title': title,
'description': description,
'content': contentController.text,
'thumbnail': thumbnail.value,
'tags': tags.map((x) => {'alias': x}).toList(),
'attachments': attachments,
'visible_users': visibleUsers,
@ -263,19 +287,33 @@ class PostEditorController extends GetxController {
set payload(Map<String, dynamic> value) {
type = value['type'];
tags.value = value['tags'].map((x) => x['alias']).toList().cast<String>();
tags.value = List.from(
value['tags'].map((x) => x['alias']).toList(),
growable: true,
);
aliasController.text = value['alias'] ?? '';
titleController.text = value['title'] ?? '';
descriptionController.text = value['description'] ?? '';
contentController.text = value['content'] ?? '';
attachments.value = value['attachments'].cast<int>() ?? List.empty();
attachments.value = List.from(
value['attachments'] ?? List.empty(),
growable: true,
);
attachments.refresh();
thumbnail.value = value['thumbnail'];
visibility.value = value['visibility'];
isDraft.value = value['is_draft'];
if (value['visible_users'] != null) {
visibleUsers.value = value['visible_users'].cast<int>();
visibleUsers.value = List.from(
value['visible_users'],
growable: true,
);
}
if (value['invisible_users'] != null) {
invisibleUsers.value = value['invisible_users'].cast<int>();
invisibleUsers.value = List.from(
value['invisible_users'],
growable: true,
);
}
if (value['published_at'] != null) {
publishedAt.value = DateTime.parse(value['published_at']).toLocal();
@ -304,11 +342,13 @@ class PostEditorController extends GetxController {
bool get isNotEmpty {
return [
aliasController.text.isNotEmpty,
titleController.text.isNotEmpty,
descriptionController.text.isNotEmpty,
contentController.text.isNotEmpty,
attachments.isNotEmpty,
tags.isNotEmpty
tags.isNotEmpty,
thumbnail.value != null,
].any((x) => x);
}

View File

@ -1,19 +1,23 @@
import 'dart:math';
import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/last_read.dart';
class PostListController extends GetxController {
String? author;
/// The polling source modifier.
/// - `0`: default recommendations
/// - `1`: shuffle mode
/// - `1`: friend mode
/// - `2`: shuffle mode
RxInt mode = 0.obs;
/// The paging controller for infinite loading.
/// Only available when mode is `0`.
/// Only available when mode is `0`, `1` or `2`.
PagingController<int, Post> pagingController =
PagingController(firstPageKey: 0);
@ -95,6 +99,9 @@ class PostListController extends GetxController {
final idx = <dynamic>{};
postList.retainWhere((x) => idx.add(x.id));
var lastId = postList.map((x) => x.id).reduce(max);
Get.find<LastReadProvider>().feedLastReadAt = lastId;
return result;
}
@ -111,10 +118,23 @@ class PostListController extends GetxController {
author: author,
);
} else {
resp = await provider.listRecommendations(
pageKey,
channel: mode.value == 0 ? null : 'shuffle',
);
switch (mode.value) {
case 2:
resp = await provider.listRecommendations(
pageKey,
channel: 'shuffle',
);
break;
case 1:
resp = await provider.listRecommendations(
pageKey,
channel: 'friends',
);
break;
default:
resp = await provider.listRecommendations(pageKey);
break;
}
}
} catch (e) {
rethrow;

View File

@ -0,0 +1,10 @@
import 'package:get/get.dart';
class RequestException implements Exception {
final Response data;
const RequestException(this.data);
@override
String toString() => 'Request failed ${data.statusCode}: ${data.bodyString}';
}

View File

@ -0,0 +1,6 @@
class UnauthorizedException implements Exception {
const UnauthorizedException();
@override
String toString() => 'Unauthorized';
}

View File

@ -1,5 +1,7 @@
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 {
void showSnackbar(String content, {SnackBarAction? action}) {
@ -48,15 +50,48 @@ extension SolianExtenions on BuildContext {
}
Future<void> showErrorDialog(dynamic exception) {
var stack = StackTrace.current;
var stackTrace = '$stack';
Widget content = Text(exception.toString().capitalize!);
if (exception is UnauthorizedException) {
content = Text('errorHappenedUnauthorized'.tr);
}
if (exception is RequestException) {
String overall;
switch (exception.data.statusCode) {
case 400:
overall = 'errorHappenedRequestBad'.tr;
break;
case 401:
overall = 'errorHappenedUnauthorized'.tr;
break;
case 403:
overall = 'errorHappenedRequestForbidden'.tr;
break;
case 404:
overall = 'errorHappenedRequestNotFound'.tr;
break;
case null:
overall = 'errorHappenedRequestConnection'.tr;
break;
default:
overall = 'errorHappenedRequestUnknown'.tr;
break;
}
if (exception.data.statusCode != null) {
content = Text(
'$overall\n\n(${exception.data.statusCode}) ${exception.data.bodyString}',
);
} else {
content = Text(overall);
}
}
return showDialog<void>(
useRootNavigator: true,
context: this,
builder: (ctx) => AlertDialog(
title: Text('errorHappened'.tr),
content: Text('${exception.toString().capitalize!}\n\nStack Trace: $stackTrace'),
content: content,
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),

View File

@ -85,4 +85,5 @@ class DefaultFirebaseOptions {
storageBucket: 'solian-0x001.appspot.com',
measurementId: 'G-EF9BZMKBC3',
);
}
}

View File

@ -1,16 +1,20 @@
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_acrylic/flutter_acrylic.dart';
import 'package:get/get.dart';
import 'package:go_router/go_router.dart';
import 'package:media_kit/media_kit.dart';
import 'package:protocol_handler/protocol_handler.dart';
import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:solian/bootstrapper.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/last_read.dart';
import 'package:solian/providers/link_expander.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart';
@ -29,32 +33,31 @@ import 'package:solian/translations.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
void main() async {
await SentryFlutter.init(
(options) {
options.dsn =
'https://55438cdff9048aa2225df72fdc629c42@o4506965897117696.ingest.us.sentry.io/4507357676437504';
options.tracesSampleRate = 1.0;
options.profilesSampleRate = 1.0;
},
appRunner: () async {
WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
WidgetsFlutterBinding.ensureInitialized();
await Future.wait([
_initializeFirebase(),
_initializePlatformComponents(),
]);
await Future.wait([
_initializeFirebase(),
_initializePlatformComponents(),
]);
GoRouter.optionURLReflectsImperativeAPIs = true;
GoRouter.optionURLReflectsImperativeAPIs = true;
usePathUrlStrategy();
runApp(const SolianApp());
},
);
Get.put(AppTranslations());
await AppTranslations.init();
usePathUrlStrategy();
runApp(const SolianApp());
}
Future<void> _initializeFirebase() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FlutterError.onError = (errorDetails) {
FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails);
};
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
}
Future<void> _initializePlatformComponents() async {
@ -99,15 +102,17 @@ class SolianApp extends StatelessWidget {
routeInformationParser: AppRouter.instance.routeInformationParser,
routeInformationProvider: AppRouter.instance.routeInformationProvider,
backButtonDispatcher: AppRouter.instance.backButtonDispatcher,
translations: SolianMessages(),
translations: Get.find<AppTranslations>(),
locale: Get.deviceLocale,
fallbackLocale: const Locale('en', 'US'),
onInit: () => _initializeProviders(context),
onInit: () {
_initializeProviders(context);
},
builder: (context, child) {
return SystemShell(
child: ScaffoldMessenger(
child: BootstrapperShell(
child: child ?? const SizedBox(),
child: child ?? const SizedBox.shrink(),
),
),
);
@ -129,5 +134,8 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => RealmProvider());
Get.lazyPut(() => ChatCallProvider());
Get.lazyPut(() => AttachmentUploaderController());
Get.lazyPut(() => LinkExpandProvider());
Get.lazyPut(() => DailySignProvider());
Get.lazyPut(() => LastReadProvider());
}
}

View File

@ -1,3 +1,8 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'account.g.dart';
@JsonSerializable()
class Account {
int id;
DateTime createdAt;
@ -10,9 +15,9 @@ class Account {
dynamic avatar;
dynamic banner;
String description;
AccountProfile? profile;
List<AccountBadge>? badges;
String? emailAddress;
int? externalId;
Account({
required this.id,
@ -26,55 +31,18 @@ class Account {
required this.avatar,
required this.banner,
required this.description,
required this.profile,
required this.badges,
required this.emailAddress,
this.externalId,
});
factory Account.fromJson(Map<String, dynamic> json) => Account(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
confirmedAt: json['confirmed_at'] != null
? DateTime.parse(json['confirmed_at'])
: null,
suspendedAt: json['suspended_at'] != null
? DateTime.parse(json['suspended_at'])
: null,
name: json['name'],
nick: json['nick'],
avatar: json['avatar'],
banner: json['banner'],
description: json['description'],
emailAddress: json['email_address'],
badges: json['badges']
?.map((e) => AccountBadge.fromJson(e))
.toList()
.cast<AccountBadge>(),
externalId: json['external_id'],
);
factory Account.fromJson(Map<String, dynamic> json) =>
_$AccountFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'confirmed_at': confirmedAt?.toIso8601String(),
'suspended_at': suspendedAt?.toIso8601String(),
'name': name,
'nick': nick,
'avatar': avatar,
'banner': banner,
'description': description,
'email_address': emailAddress,
'badges': badges?.map((e) => e.toJson()).toList(),
'external_id': externalId,
};
Map<String, dynamic> toJson() => _$AccountToJson(this);
}
@JsonSerializable()
class AccountBadge {
int id;
DateTime createdAt;
@ -94,25 +62,40 @@ class AccountBadge {
required this.type,
});
factory AccountBadge.fromJson(Map<String, dynamic> json) => AccountBadge(
id: json['id'],
accountId: json['account_id'],
updatedAt: DateTime.parse(json['updated_at']),
createdAt: DateTime.parse(json['created_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
metadata: json['metadata'],
type: json['type'],
);
factory AccountBadge.fromJson(Map<String, dynamic> json) =>
_$AccountBadgeFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'account_id': accountId,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'metadata': metadata,
'type': type,
};
Map<String, dynamic> toJson() => _$AccountBadgeToJson(this);
}
@JsonSerializable()
class AccountProfile {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String? firstName;
String? lastName;
int? experience;
DateTime? lastSeenAt;
DateTime? birthday;
int accountId;
AccountProfile({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.firstName,
required this.lastName,
required this.experience,
required this.lastSeenAt,
required this.birthday,
required this.accountId,
});
factory AccountProfile.fromJson(Map<String, dynamic> json) =>
_$AccountProfileFromJson(json);
Map<String, dynamic> toJson() => _$AccountProfileToJson(this);
}

108
lib/models/account.g.dart Normal file
View File

@ -0,0 +1,108 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'account.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Account _$AccountFromJson(Map<String, dynamic> json) => Account(
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),
confirmedAt: json['confirmed_at'] == null
? null
: DateTime.parse(json['confirmed_at'] as String),
suspendedAt: json['suspended_at'] == null
? null
: DateTime.parse(json['suspended_at'] as String),
name: json['name'] as String,
nick: json['nick'] as String,
avatar: json['avatar'],
banner: json['banner'],
description: json['description'] as String,
profile: json['profile'] == null
? null
: AccountProfile.fromJson(json['profile'] as Map<String, dynamic>),
badges: (json['badges'] as List<dynamic>?)
?.map((e) => AccountBadge.fromJson(e as Map<String, dynamic>))
.toList(),
emailAddress: json['email_address'] as String?,
);
Map<String, dynamic> _$AccountToJson(Account instance) => <String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'confirmed_at': instance.confirmedAt?.toIso8601String(),
'suspended_at': instance.suspendedAt?.toIso8601String(),
'name': instance.name,
'nick': instance.nick,
'avatar': instance.avatar,
'banner': instance.banner,
'description': instance.description,
'profile': instance.profile?.toJson(),
'badges': instance.badges?.map((e) => e.toJson()).toList(),
'email_address': instance.emailAddress,
};
AccountBadge _$AccountBadgeFromJson(Map<String, dynamic> json) => AccountBadge(
id: (json['id'] as num).toInt(),
accountId: (json['account_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),
metadata: json['metadata'] as Map<String, dynamic>?,
type: json['type'] as String,
);
Map<String, dynamic> _$AccountBadgeToJson(AccountBadge instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'metadata': instance.metadata,
'type': instance.type,
'account_id': instance.accountId,
};
AccountProfile _$AccountProfileFromJson(Map<String, dynamic> json) =>
AccountProfile(
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),
firstName: json['first_name'] as String?,
lastName: json['last_name'] as String?,
experience: (json['experience'] as num?)?.toInt(),
lastSeenAt: json['last_seen_at'] == null
? null
: DateTime.parse(json['last_seen_at'] as String),
birthday: json['birthday'] == null
? null
: DateTime.parse(json['birthday'] as String),
accountId: (json['account_id'] as num).toInt(),
);
Map<String, dynamic> _$AccountProfileToJson(AccountProfile instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'first_name': instance.firstName,
'last_name': instance.lastName,
'experience': instance.experience,
'last_seen_at': instance.lastSeenAt?.toIso8601String(),
'birthday': instance.birthday?.toIso8601String(),
'account_id': instance.accountId,
};

View File

@ -1,3 +1,8 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'account_status.g.dart';
@JsonSerializable()
class AccountStatus {
bool isDisturbable;
bool isOnline;
@ -11,21 +16,13 @@ class AccountStatus {
required this.status,
});
factory AccountStatus.fromJson(Map<String, dynamic> json) => AccountStatus(
isDisturbable: json['is_disturbable'],
isOnline: json['is_online'],
lastSeenAt: json['last_seen_at'] != null ? DateTime.parse(json['last_seen_at']) : null,
status: json['status'] != null ? Status.fromJson(json['status']) : null,
);
factory AccountStatus.fromJson(Map<String, dynamic> json) =>
_$AccountStatusFromJson(json);
Map<String, dynamic> toJson() => {
'is_disturbable': isDisturbable,
'is_online': isOnline,
'last_seen_at': lastSeenAt?.toIso8601String(),
'status': status?.toJson(),
};
Map<String, dynamic> toJson() => _$AccountStatusToJson(this);
}
@JsonSerializable()
class Status {
int id;
DateTime createdAt;
@ -53,31 +50,7 @@ class Status {
required this.accountId,
});
factory Status.fromJson(Map<String, dynamic> json) => Status(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null,
type: json['type'],
label: json['label'],
attitude: json['attitude'],
isNoDisturb: json['is_no_disturb'],
isInvisible: json['is_invisible'],
clearAt: json['clear_at'] != null ? DateTime.parse(json['clear_at']) : null,
accountId: json['account_id'],
);
factory Status.fromJson(Map<String, dynamic> json) => _$StatusFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'type': type,
'label': label,
'attitude': attitude,
'is_no_disturb': isNoDisturb,
'is_invisible': isInvisible,
'clear_at': clearAt?.toIso8601String(),
'account_id': accountId,
};
Map<String, dynamic> toJson() => _$StatusToJson(this);
}

View File

@ -0,0 +1,59 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'account_status.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AccountStatus _$AccountStatusFromJson(Map<String, dynamic> json) =>
AccountStatus(
isDisturbable: json['is_disturbable'] as bool,
isOnline: json['is_online'] as bool,
lastSeenAt: json['last_seen_at'] == null
? null
: DateTime.parse(json['last_seen_at'] as String),
status: json['status'] == null
? null
: Status.fromJson(json['status'] as Map<String, dynamic>),
);
Map<String, dynamic> _$AccountStatusToJson(AccountStatus instance) =>
<String, dynamic>{
'is_disturbable': instance.isDisturbable,
'is_online': instance.isOnline,
'last_seen_at': instance.lastSeenAt?.toIso8601String(),
'status': instance.status?.toJson(),
};
Status _$StatusFromJson(Map<String, dynamic> json) => Status(
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 String,
label: json['label'] as String,
attitude: (json['attitude'] as num).toInt(),
isNoDisturb: json['is_no_disturb'] as bool,
isInvisible: json['is_invisible'] as bool,
clearAt: json['clear_at'] == null
? null
: DateTime.parse(json['clear_at'] as String),
accountId: (json['account_id'] as num).toInt(),
);
Map<String, dynamic> _$StatusToJson(Status instance) => <String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'type': instance.type,
'label': instance.label,
'attitude': instance.attitude,
'is_no_disturb': instance.isNoDisturb,
'is_invisible': instance.isInvisible,
'clear_at': instance.clearAt?.toIso8601String(),
'account_id': instance.accountId,
};

View File

@ -1,20 +1,44 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:solian/models/account.dart';
part 'attachment.g.dart';
@JsonSerializable()
class AttachmentPlaceholder {
int chunkCount;
int chunkSize;
Attachment meta;
AttachmentPlaceholder({
required this.chunkCount,
required this.chunkSize,
required this.meta,
});
factory AttachmentPlaceholder.fromJson(Map<String, dynamic> json) =>
_$AttachmentPlaceholderFromJson(json);
Map<String, dynamic> toJson() => _$AttachmentPlaceholderToJson(this);
}
@JsonSerializable()
class Attachment {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String rid;
String uuid;
int size;
String name;
String alt;
String usage;
String mimetype;
String hash;
int destination;
bool isAnalyzed;
bool isUploaded;
Map<String, dynamic>? metadata;
Map<String, dynamic>? fileChunks;
bool isMature;
Account? account;
int? accountId;
@ -24,58 +48,25 @@ class Attachment {
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.rid,
required this.uuid,
required this.size,
required this.name,
required this.alt,
required this.usage,
required this.mimetype,
required this.hash,
required this.destination,
required this.isAnalyzed,
required this.isUploaded,
required this.metadata,
required this.fileChunks,
required this.isMature,
required this.account,
required this.accountId,
});
factory Attachment.fromJson(Map<String, dynamic> json) => Attachment(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null,
uuid: json['uuid'],
size: json['size'],
name: json['name'],
alt: json['alt'],
usage: json['usage'],
mimetype: json['mimetype'],
hash: json['hash'],
destination: json['destination'],
isAnalyzed: json['is_analyzed'],
metadata: json['metadata'],
isMature: json['is_mature'],
account: json['account'] != null ? Account.fromJson(json['account']) : null,
accountId: json['account_id'],
);
factory Attachment.fromJson(Map<String, dynamic> json) =>
_$AttachmentFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'uuid': uuid,
'size': size,
'name': name,
'alt': alt,
'usage': usage,
'mimetype': mimetype,
'hash': hash,
'destination': destination,
'is_analyzed': isAnalyzed,
'metadata': metadata,
'is_mature': isMature,
'account': account?.toJson(),
'account_id': accountId,
};
}
Map<String, dynamic> toJson() => _$AttachmentToJson(this);
}

View File

@ -0,0 +1,72 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'attachment.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AttachmentPlaceholder _$AttachmentPlaceholderFromJson(
Map<String, dynamic> json) =>
AttachmentPlaceholder(
chunkCount: (json['chunk_count'] as num).toInt(),
chunkSize: (json['chunk_size'] as num).toInt(),
meta: Attachment.fromJson(json['meta'] as Map<String, dynamic>),
);
Map<String, dynamic> _$AttachmentPlaceholderToJson(
AttachmentPlaceholder instance) =>
<String, dynamic>{
'chunk_count': instance.chunkCount,
'chunk_size': instance.chunkSize,
'meta': instance.meta.toJson(),
};
Attachment _$AttachmentFromJson(Map<String, dynamic> json) => Attachment(
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),
rid: json['rid'] as String,
uuid: json['uuid'] as String,
size: (json['size'] as num).toInt(),
name: json['name'] as String,
alt: json['alt'] as String,
mimetype: json['mimetype'] as String,
hash: json['hash'] as String,
destination: (json['destination'] as num).toInt(),
isAnalyzed: json['is_analyzed'] as bool,
isUploaded: json['is_uploaded'] as bool,
metadata: json['metadata'] as Map<String, dynamic>?,
fileChunks: json['file_chunks'] as Map<String, dynamic>?,
isMature: json['is_mature'] as bool,
account: json['account'] == null
? null
: Account.fromJson(json['account'] as Map<String, dynamic>),
accountId: (json['account_id'] as num?)?.toInt(),
);
Map<String, dynamic> _$AttachmentToJson(Attachment instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'rid': instance.rid,
'uuid': instance.uuid,
'size': instance.size,
'name': instance.name,
'alt': instance.alt,
'mimetype': instance.mimetype,
'hash': instance.hash,
'destination': instance.destination,
'is_analyzed': instance.isAnalyzed,
'is_uploaded': instance.isUploaded,
'metadata': instance.metadata,
'file_chunks': instance.fileChunks,
'is_mature': instance.isMature,
'account': instance.account?.toJson(),
'account_id': instance.accountId,
};

View File

@ -1,6 +1,10 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:solian/models/channel.dart';
part 'call.g.dart';
@JsonSerializable()
class Call {
int id;
DateTime createdAt;
@ -10,7 +14,8 @@ class Call {
String externalId;
int founderId;
int channelId;
List<dynamic> participants;
@JsonKey(defaultValue: [])
List<dynamic>? participants;
Channel channel;
Call({
@ -26,32 +31,9 @@ class Call {
required this.channel,
});
factory Call.fromJson(Map<String, dynamic> json) => Call(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'],
endedAt:
json['ended_at'] != null ? DateTime.parse(json['ended_at']) : null,
externalId: json['external_id'],
founderId: json['founder_id'],
channelId: json['channel_id'],
participants: json['participants'] ?? List.empty(),
channel: Channel.fromJson(json['channel']),
);
factory Call.fromJson(Map<String, dynamic> json) => _$CallFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'ended_at': endedAt?.toIso8601String(),
'external_id': externalId,
'founder_id': founderId,
'channel_id': channelId,
'participants': participants,
'channel': channel.toJson(),
};
Map<String, dynamic> toJson() => _$CallToJson(this);
}
enum ParticipantStatsType {

37
lib/models/call.g.dart Normal file
View File

@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'call.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Call _$CallFromJson(Map<String, dynamic> json) => Call(
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),
endedAt: json['ended_at'] == null
? null
: DateTime.parse(json['ended_at'] as String),
externalId: json['external_id'] as String,
founderId: (json['founder_id'] as num).toInt(),
channelId: (json['channel_id'] as num).toInt(),
participants: json['participants'] as List<dynamic>? ?? [],
channel: Channel.fromJson(json['channel'] as Map<String, dynamic>),
);
Map<String, dynamic> _$CallToJson(Call instance) => <String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'ended_at': instance.endedAt?.toIso8601String(),
'external_id': instance.externalId,
'founder_id': instance.founderId,
'channel_id': instance.channelId,
'participants': instance.participants,
'channel': instance.channel.toJson(),
};

View File

@ -1,6 +1,10 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/realm.dart';
part 'channel.g.dart';
@JsonSerializable()
class Channel {
int id;
DateTime createdAt;
@ -17,6 +21,7 @@ class Channel {
int? realmId;
bool isEncrypted;
@JsonKey(includeFromJson: false, includeToJson: true)
bool isAvailable = false;
Channel({
@ -36,44 +41,22 @@ class Channel {
required this.realmId,
});
factory Channel.fromJson(Map<String, dynamic> json) => Channel(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'],
alias: json['alias'],
name: json['name'],
description: json['description'],
type: json['type'],
members: json['members']
?.map((e) => ChannelMember.fromJson(e))
.toList()
.cast<ChannelMember>(),
account: Account.fromJson(json['account']),
accountId: json['account_id'],
realm: json['realm'] != null ? Realm.fromJson(json['realm']) : null,
realmId: json['realm_id'],
isEncrypted: json['is_encrypted'],
);
factory Channel.fromJson(Map<String, dynamic> json) =>
_$ChannelFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'alias': alias,
'name': name,
'description': description,
'type': type,
'members': members?.map((e) => e.toJson()).toList(),
'account': account.toJson(),
'account_id': accountId,
'realm': realm?.toJson(),
'realm_id': realmId,
'is_encrypted': isEncrypted,
};
Map<String, dynamic> toJson() => _$ChannelToJson(this);
@override
bool operator ==(Object other) {
if (other is! Channel) return false;
return id == other.id;
}
@override
int get hashCode => id;
}
@JsonSerializable()
class ChannelMember {
int id;
DateTime createdAt;
@ -95,25 +78,8 @@ class ChannelMember {
required this.notify,
});
factory ChannelMember.fromJson(Map<String, dynamic> json) => ChannelMember(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'],
channelId: json['channel_id'],
accountId: json['account_id'],
account: Account.fromJson(json['account']),
notify: json['notify'],
);
factory ChannelMember.fromJson(Map<String, dynamic> json) =>
_$ChannelMemberFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'channel_id': channelId,
'account_id': accountId,
'account': account.toJson(),
'notify': notify,
};
Map<String, dynamic> toJson() => _$ChannelMemberToJson(this);
}

74
lib/models/channel.g.dart Normal file
View File

@ -0,0 +1,74 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'channel.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Channel _$ChannelFromJson(Map<String, dynamic> json) => Channel(
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),
alias: json['alias'] as String,
name: json['name'] as String,
description: json['description'] as String,
type: (json['type'] as num).toInt(),
members: (json['members'] as List<dynamic>?)
?.map((e) => ChannelMember.fromJson(e as Map<String, dynamic>))
.toList(),
account: Account.fromJson(json['account'] as Map<String, dynamic>),
accountId: (json['account_id'] as num).toInt(),
isEncrypted: json['is_encrypted'] as bool,
realm: json['realm'] == null
? null
: Realm.fromJson(json['realm'] as Map<String, dynamic>),
realmId: (json['realm_id'] as num?)?.toInt(),
);
Map<String, dynamic> _$ChannelToJson(Channel instance) => <String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'alias': instance.alias,
'name': instance.name,
'description': instance.description,
'type': instance.type,
'members': instance.members?.map((e) => e.toJson()).toList(),
'account': instance.account.toJson(),
'account_id': instance.accountId,
'realm': instance.realm?.toJson(),
'realm_id': instance.realmId,
'is_encrypted': instance.isEncrypted,
'is_available': instance.isAvailable,
};
ChannelMember _$ChannelMemberFromJson(Map<String, dynamic> json) =>
ChannelMember(
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),
channelId: (json['channel_id'] as num).toInt(),
accountId: (json['account_id'] as num).toInt(),
account: Account.fromJson(json['account'] as Map<String, dynamic>),
notify: (json['notify'] as num).toInt(),
);
Map<String, dynamic> _$ChannelMemberToJson(ChannelMember instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'channel_id': instance.channelId,
'account_id': instance.accountId,
'account': instance.account.toJson(),
'notify': instance.notify,
};

View File

@ -0,0 +1,49 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:get/get.dart';
import 'package:solian/models/account.dart';
part 'daily_sign.g.dart';
@JsonSerializable()
class DailySignRecord {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
Account account;
int resultTier;
int resultExperience;
int accountId;
DailySignRecord({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.resultTier,
required this.resultExperience,
required this.account,
required this.accountId,
});
factory DailySignRecord.fromJson(Map<String, dynamic> json) =>
_$DailySignRecordFromJson(json);
Map<String, dynamic> toJson() => _$DailySignRecordToJson(this);
String get symbol => switch (resultTier) {
0 => '\n',
1 => '',
2 => '\n',
3 => '',
_ => '\n',
};
String get overviewSuggestion => switch (resultTier) {
0 => 'dailySignTier0'.tr,
1 => 'dailySignTier1'.tr,
2 => 'dailySignTier2'.tr,
3 => 'dailySignTier3'.tr,
_ => 'dailySignTier4'.tr,
};
}

View File

@ -0,0 +1,33 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'daily_sign.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
DailySignRecord _$DailySignRecordFromJson(Map<String, dynamic> json) =>
DailySignRecord(
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),
resultTier: (json['result_tier'] as num).toInt(),
resultExperience: (json['result_experience'] as num).toInt(),
account: Account.fromJson(json['account'] as Map<String, dynamic>),
accountId: (json['account_id'] as num).toInt(),
);
Map<String, dynamic> _$DailySignRecordToJson(DailySignRecord instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'account': instance.account.toJson(),
'result_tier': instance.resultTier,
'result_experience': instance.resultExperience,
'account_id': instance.accountId,
};

View File

@ -1,6 +1,9 @@
import 'package:solian/models/account.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:solian/models/channel.dart';
part 'event.g.dart';
@JsonSerializable()
class Event {
int id;
String uuid;
@ -10,10 +13,11 @@ class Event {
Map<String, dynamic> body;
String type;
Channel? channel;
Sender sender;
ChannelMember sender;
int channelId;
int senderId;
@JsonKey(includeFromJson: false, includeToJson: true)
bool isPending = false;
Event({
@ -30,40 +34,18 @@ class Event {
required this.senderId,
});
factory Event.fromJson(Map<String, dynamic> json) => Event(
id: json['id'],
uuid: json['uuid'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'],
body: json['body'],
type: json['type'],
channel:
json['channel'] != null ? Channel.fromJson(json['channel']) : null,
sender: Sender.fromJson(json['sender']),
channelId: json['channel_id'],
senderId: json['sender_id'],
);
factory Event.fromJson(Map<String, dynamic> json) => _$EventFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'uuid': uuid,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'body': body,
'type': type,
'channel': channel?.toJson(),
'sender': sender.toJson(),
'channel_id': channelId,
'sender_id': senderId,
};
Map<String, dynamic> toJson() => _$EventToJson(this);
}
@JsonSerializable()
class EventMessageBody {
@JsonKey(defaultValue: '')
String text;
@JsonKey(defaultValue: 'plain')
String algorithm;
List<int>? attachments;
List<String>? attachments;
int? quoteEvent;
int? relatedEvent;
List<int>? relatedUsers;
@ -78,69 +60,7 @@ class EventMessageBody {
});
factory EventMessageBody.fromJson(Map<String, dynamic> json) =>
EventMessageBody(
text: json['text'] ?? '',
algorithm: json['algorithm'] ?? 'plain',
attachments: json['attachments'] != null
? List<int>.from(json['attachments'].map((x) => x))
: null,
quoteEvent: json['quote_event'],
relatedEvent: json['related_event'],
relatedUsers: json['related_users'] != null
? List<int>.from(json['related_users'].map((x) => x))
: null,
);
_$EventMessageBodyFromJson(json);
Map<String, dynamic> toJson() => {
'text': text,
'algorithm': algorithm,
'attachments': attachments?.cast<dynamic>(),
'quote_event': quoteEvent,
'related_event': relatedEvent,
'related_users': relatedUsers?.cast<dynamic>(),
};
}
class Sender {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
Account account;
int channelId;
int accountId;
int notify;
Sender({
required this.id,
required this.createdAt,
required this.updatedAt,
this.deletedAt,
required this.account,
required this.channelId,
required this.accountId,
required this.notify,
});
factory Sender.fromJson(Map<String, dynamic> json) => Sender(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'],
account: Account.fromJson(json['account']),
channelId: json['channel_id'],
accountId: json['account_id'],
notify: json['notify'],
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'account': account.toJson(),
'channel_id': channelId,
'account_id': accountId,
'notify': notify,
};
Map<String, dynamic> toJson() => _$EventMessageBodyToJson(this);
}

64
lib/models/event.g.dart Normal file
View File

@ -0,0 +1,64 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'event.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Event _$EventFromJson(Map<String, dynamic> json) => Event(
id: (json['id'] as num).toInt(),
uuid: json['uuid'] as String,
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),
body: json['body'] as Map<String, dynamic>,
type: json['type'] as String,
channel: json['channel'] == null
? null
: Channel.fromJson(json['channel'] as Map<String, dynamic>),
sender: ChannelMember.fromJson(json['sender'] as Map<String, dynamic>),
channelId: (json['channel_id'] as num).toInt(),
senderId: (json['sender_id'] as num).toInt(),
);
Map<String, dynamic> _$EventToJson(Event instance) => <String, dynamic>{
'id': instance.id,
'uuid': instance.uuid,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'body': instance.body,
'type': instance.type,
'channel': instance.channel?.toJson(),
'sender': instance.sender.toJson(),
'channel_id': instance.channelId,
'sender_id': instance.senderId,
'is_pending': instance.isPending,
};
EventMessageBody _$EventMessageBodyFromJson(Map<String, dynamic> json) =>
EventMessageBody(
text: json['text'] as String? ?? '',
algorithm: json['algorithm'] as String? ?? 'plain',
attachments: (json['attachments'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
quoteEvent: (json['quote_event'] as num?)?.toInt(),
relatedEvent: (json['related_event'] as num?)?.toInt(),
relatedUsers: (json['related_users'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
);
Map<String, dynamic> _$EventMessageBodyToJson(EventMessageBody instance) =>
<String, dynamic>{
'text': instance.text,
'algorithm': instance.algorithm,
'attachments': instance.attachments,
'quote_event': instance.quoteEvent,
'related_event': instance.relatedEvent,
'related_users': instance.relatedUsers,
};

View File

@ -1,83 +0,0 @@
class Tag {
int id;
String alias;
String name;
String description;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
Tag({
required this.id,
required this.alias,
required this.name,
required this.description,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
});
factory Tag.fromJson(Map<String, dynamic> json) => Tag(
id: json['id'],
alias: json['alias'],
name: json['name'],
description: json['description'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
);
Map<String, dynamic> toJson() => {
'id': id,
'alias': alias,
'description': description,
'name': name,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
};
}
class Category {
int id;
String alias;
String name;
String description;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
Category({
required this.id,
required this.alias,
required this.name,
required this.description,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
});
factory Category.fromJson(Map<String, dynamic> json) => Category(
id: json['id'],
alias: json['alias'],
name: json['name'],
description: json['description'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
);
Map<String, dynamic> toJson() => {
'id': id,
'alias': alias,
'description': description,
'name': name,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
};
}

View File

@ -1,32 +0,0 @@
class Keypair {
final String id;
final String algorithm;
final String publicKey;
final String? privateKey;
final bool isOwned;
Keypair({
required this.id,
required this.algorithm,
required this.publicKey,
required this.privateKey,
this.isOwned = false,
});
factory Keypair.fromJson(Map<String, dynamic> json) => Keypair(
id: json['id'],
algorithm: json['algorithm'],
publicKey: json['public_key'],
privateKey: json['private_key'],
isOwned: json['is_owned'],
);
Map<String, dynamic> toJson() => {
'id': id,
'algorithm': algorithm,
'public_key': publicKey,
'private_key': privateKey,
'is_owned': isOwned,
};
}

41
lib/models/link.dart Normal file
View File

@ -0,0 +1,41 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'link.g.dart';
@JsonSerializable()
class LinkMeta {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String entryId;
String? icon;
String url;
String? title;
String? image;
String? video;
String? audio;
String? description;
String? siteName;
LinkMeta({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.entryId,
required this.icon,
required this.url,
required this.title,
required this.image,
required this.video,
required this.audio,
required this.description,
required this.siteName,
});
factory LinkMeta.fromJson(Map<String, dynamic> json) =>
_$LinkMetaFromJson(json);
Map<String, dynamic> toJson() => _$LinkMetaToJson(this);
}

41
lib/models/link.g.dart Normal file
View File

@ -0,0 +1,41 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'link.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
LinkMeta _$LinkMetaFromJson(Map<String, dynamic> json) => LinkMeta(
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),
entryId: json['entry_id'] as String,
icon: json['icon'] as String?,
url: json['url'] as String,
title: json['title'] as String?,
image: json['image'] as String?,
video: json['video'] as String?,
audio: json['audio'] as String?,
description: json['description'] as String?,
siteName: json['site_name'] as String?,
);
Map<String, dynamic> _$LinkMetaToJson(LinkMeta instance) => <String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'entry_id': instance.entryId,
'icon': instance.icon,
'url': instance.url,
'title': instance.title,
'image': instance.image,
'video': instance.video,
'audio': instance.audio,
'description': instance.description,
'site_name': instance.siteName,
};

View File

@ -1,3 +1,8 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'notification.g.dart';
@JsonSerializable()
class Notification {
int id;
DateTime createdAt;
@ -25,35 +30,8 @@ class Notification {
required this.accountId,
});
factory Notification.fromJson(Map<String, dynamic> json) => Notification(
id: json['id'] ?? 0,
createdAt: json['created_at'] == null
? DateTime.now()
: DateTime.parse(json['created_at']),
updatedAt: json['updated_at'] == null
? DateTime.now()
: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'],
title: json['title'],
subtitle: json['subtitle'],
body: json['body'],
avatar: json['avatar'],
picture: json['picture'],
senderId: json['sender_id'],
accountId: json['account_id'],
);
factory Notification.fromJson(Map<String, dynamic> json) =>
_$NotificationFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'title': title,
'subtitle': subtitle,
'body': body,
'avatar': avatar,
'picture': picture,
'sender_id': senderId,
'account_id': accountId,
};
Map<String, dynamic> toJson() => _$NotificationToJson(this);
}

View File

@ -0,0 +1,38 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'notification.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Notification _$NotificationFromJson(Map<String, dynamic> json) => Notification(
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),
title: json['title'] as String,
subtitle: json['subtitle'] as String?,
body: json['body'] as String,
avatar: json['avatar'] as String?,
picture: json['picture'] as String?,
senderId: (json['sender_id'] as num?)?.toInt(),
accountId: (json['account_id'] as num).toInt(),
);
Map<String, dynamic> _$NotificationToJson(Notification instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'title': instance.title,
'subtitle': instance.subtitle,
'body': instance.body,
'avatar': instance.avatar,
'picture': instance.picture,
'sender_id': instance.senderId,
'account_id': instance.accountId,
};

View File

@ -1,23 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'packet.g.dart';
@JsonSerializable()
class NetworkPackage {
@JsonKey(name: 'w')
String method;
@JsonKey(name: 'e')
String? endpoint;
@JsonKey(name: 'm')
String? message;
@JsonKey(name: 'p')
Map<String, dynamic>? payload;
NetworkPackage({
required this.method,
this.endpoint,
this.message,
this.payload,
});
factory NetworkPackage.fromJson(Map<String, dynamic> json) => NetworkPackage(
method: json['w'],
message: json['m'],
payload: json['p'],
);
factory NetworkPackage.fromJson(Map<String, dynamic> json) =>
_$NetworkPackageFromJson(json);
Map<String, dynamic> toJson() => {
'w': method,
'm': message,
'p': payload,
};
Map<String, dynamic> toJson() => _$NetworkPackageToJson(this);
}

23
lib/models/packet.g.dart Normal file
View File

@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'packet.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
NetworkPackage _$NetworkPackageFromJson(Map<String, dynamic> json) =>
NetworkPackage(
method: json['w'] as String,
endpoint: json['e'] as String?,
message: json['m'] as String?,
payload: json['p'] as Map<String, dynamic>?,
);
Map<String, dynamic> _$NetworkPackageToJson(NetworkPackage instance) =>
<String, dynamic>{
'w': instance.method,
'e': instance.endpoint,
'm': instance.message,
'p': instance.payload,
};

View File

@ -1,3 +1,8 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'pagination.g.dart';
@JsonSerializable()
class PaginationResult {
int count;
List<dynamic>? data;
@ -8,10 +13,7 @@ class PaginationResult {
});
factory PaginationResult.fromJson(Map<String, dynamic> json) =>
PaginationResult(count: json['count'], data: json['data']);
_$PaginationResultFromJson(json);
Map<String, dynamic> toJson() => {
'count': count,
'data': data,
};
Map<String, dynamic> toJson() => _$PaginationResultToJson(this);
}

View File

@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'pagination.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PaginationResult _$PaginationResultFromJson(Map<String, dynamic> json) =>
PaginationResult(
count: (json['count'] as num).toInt(),
data: json['data'] as List<dynamic>?,
);
Map<String, dynamic> _$PaginationResultToJson(PaginationResult instance) =>
<String, dynamic>{
'count': instance.count,
'data': instance.data,
};

View File

@ -1,13 +1,19 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/feed.dart';
import 'package:solian/models/post_categories.dart';
import 'package:solian/models/realm.dart';
part 'post.g.dart';
@JsonSerializable()
class Post {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? editedAt;
DateTime? deletedAt;
String? alias;
String? areaAlias;
dynamic body;
List<Tag>? tags;
List<Category>? categories;
@ -33,6 +39,8 @@ class Post {
required this.updatedAt,
required this.editedAt,
required this.deletedAt,
required this.alias,
required this.areaAlias,
required this.type,
required this.body,
required this.tags,
@ -53,77 +61,15 @@ class Post {
required this.metric,
});
factory Post.fromJson(Map<String, dynamic> json) => Post(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
type: json['type'],
body: json['body'],
tags: json['tags']?.map((x) => Tag.fromJson(x)).toList().cast<Tag>(),
categories: json['categories']
?.map((x) => Category.fromJson(x))
.toList()
.cast<Category>(),
replies: json['replies'],
replyId: json['reply_id'],
repostId: json['repost_id'],
realmId: json['realm_id'],
replyTo:
json['reply_to'] != null ? Post.fromJson(json['reply_to']) : null,
repostTo:
json['repost_to'] != null ? Post.fromJson(json['repost_to']) : null,
realm: json['realm'] != null ? Realm.fromJson(json['realm']) : null,
editedAt: json['edited_at'] != null
? DateTime.parse(json['edited_at'])
: null,
publishedAt: json['published_at'] != null
? DateTime.parse(json['published_at'])
: null,
publishedUntil: json['published_until'] != null
? DateTime.parse(json['published_until'])
: null,
pinnedAt: json['pinned_at'] != null
? DateTime.parse(json['pinned_at'])
: null,
isDraft: json['is_draft'],
authorId: json['author_id'],
author: Account.fromJson(json['author']),
metric:
json['metric'] != null ? PostMetric.fromJson(json['metric']) : null,
);
factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'edited_at': editedAt?.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'type': type,
'body': body,
'tags': tags,
'categories': categories,
'replies': replies,
'reply_id': replyId,
'repost_id': repostId,
'realm_id': realmId,
'reply_to': replyTo?.toJson(),
'repost_to': repostTo?.toJson(),
'realm': realm?.toJson(),
'published_at': publishedAt?.toIso8601String(),
'published_until': publishedUntil?.toIso8601String(),
'pinned_at': pinnedAt?.toIso8601String(),
'is_draft': isDraft,
'author_id': authorId,
'author': author.toJson(),
'metric': metric?.toJson(),
};
Map<String, dynamic> toJson() => _$PostToJson(this);
}
@JsonSerializable()
class PostMetric {
int reactionCount;
@JsonKey(defaultValue: {})
Map<String, int> reactionList;
int replyCount;
@ -133,22 +79,8 @@ class PostMetric {
required this.replyCount,
});
factory PostMetric.fromJson(Map<String, dynamic> json) => PostMetric(
reactionCount: json['reaction_count'],
replyCount: json['reply_count'],
reactionList: json['reaction_list'] != null
? json['reaction_list']
.map((key, value) => MapEntry(
key,
int.tryParse(value.toString()) ??
(value is double ? value.toInt() : null)))
.cast<String, int>()
: {},
);
factory PostMetric.fromJson(Map<String, dynamic> json) =>
_$PostMetricFromJson(json);
Map<String, dynamic> toJson() => {
'reaction_count': reactionCount,
'reply_count': replyCount,
'reaction_list': reactionList,
};
Map<String, dynamic> toJson() => _$PostMetricToJson(this);
}

103
lib/models/post.g.dart Normal file
View File

@ -0,0 +1,103 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'post.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Post _$PostFromJson(Map<String, dynamic> json) => Post(
id: (json['id'] as num).toInt(),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
editedAt: json['edited_at'] == null
? null
: DateTime.parse(json['edited_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
alias: json['alias'] as String?,
areaAlias: json['area_alias'] as String?,
type: json['type'] as String,
body: json['body'],
tags: (json['tags'] as List<dynamic>?)
?.map((e) => Tag.fromJson(e as Map<String, dynamic>))
.toList(),
categories: (json['categories'] as List<dynamic>?)
?.map((e) => Category.fromJson(e as Map<String, dynamic>))
.toList(),
replies: (json['replies'] as List<dynamic>?)
?.map((e) => Post.fromJson(e as Map<String, dynamic>))
.toList(),
replyId: (json['reply_id'] as num?)?.toInt(),
repostId: (json['repost_id'] as num?)?.toInt(),
realmId: (json['realm_id'] as num?)?.toInt(),
replyTo: json['reply_to'] == null
? null
: Post.fromJson(json['reply_to'] as Map<String, dynamic>),
repostTo: json['repost_to'] == null
? null
: Post.fromJson(json['repost_to'] as Map<String, dynamic>),
realm: json['realm'] == null
? null
: Realm.fromJson(json['realm'] as Map<String, dynamic>),
publishedAt: json['published_at'] == null
? null
: DateTime.parse(json['published_at'] as String),
publishedUntil: json['published_until'] == null
? null
: DateTime.parse(json['published_until'] as String),
pinnedAt: json['pinned_at'] == null
? null
: DateTime.parse(json['pinned_at'] as String),
isDraft: json['is_draft'] as bool?,
authorId: (json['author_id'] as num).toInt(),
author: Account.fromJson(json['author'] as Map<String, dynamic>),
metric: json['metric'] == null
? null
: PostMetric.fromJson(json['metric'] as Map<String, dynamic>),
);
Map<String, dynamic> _$PostToJson(Post instance) => <String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'edited_at': instance.editedAt?.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'alias': instance.alias,
'area_alias': instance.areaAlias,
'body': instance.body,
'tags': instance.tags?.map((e) => e.toJson()).toList(),
'categories': instance.categories?.map((e) => e.toJson()).toList(),
'replies': instance.replies?.map((e) => e.toJson()).toList(),
'type': instance.type,
'reply_id': instance.replyId,
'repost_id': instance.repostId,
'realm_id': instance.realmId,
'reply_to': instance.replyTo?.toJson(),
'repost_to': instance.repostTo?.toJson(),
'realm': instance.realm?.toJson(),
'published_at': instance.publishedAt?.toIso8601String(),
'published_until': instance.publishedUntil?.toIso8601String(),
'pinned_at': instance.pinnedAt?.toIso8601String(),
'is_draft': instance.isDraft,
'author_id': instance.authorId,
'author': instance.author.toJson(),
'metric': instance.metric?.toJson(),
};
PostMetric _$PostMetricFromJson(Map<String, dynamic> json) => PostMetric(
reactionCount: (json['reaction_count'] as num).toInt(),
reactionList: (json['reaction_list'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, (e as num).toInt()),
) ??
{},
replyCount: (json['reply_count'] as num).toInt(),
);
Map<String, dynamic> _$PostMetricToJson(PostMetric instance) =>
<String, dynamic>{
'reaction_count': instance.reactionCount,
'reaction_list': instance.reactionList,
'reply_count': instance.replyCount,
};

View File

@ -0,0 +1,54 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'post_categories.g.dart';
@JsonSerializable()
class Tag {
int id;
String alias;
String name;
String description;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
Tag({
required this.id,
required this.alias,
required this.name,
required this.description,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
});
factory Tag.fromJson(Map<String, dynamic> json) => _$TagFromJson(json);
Map<String, dynamic> toJson() => _$TagToJson(this);
}
@JsonSerializable()
class Category {
int id;
String alias;
String name;
String description;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
Category({
required this.id,
required this.alias,
required this.name,
required this.description,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
});
factory Category.fromJson(Map<String, dynamic> json) =>
_$CategoryFromJson(json);
Map<String, dynamic> toJson() => _$CategoryToJson(this);
}

View File

@ -0,0 +1,51 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'post_categories.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Tag _$TagFromJson(Map<String, dynamic> json) => Tag(
id: (json['id'] as num).toInt(),
alias: json['alias'] as String,
name: json['name'] as String,
description: json['description'] as String,
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),
);
Map<String, dynamic> _$TagToJson(Tag instance) => <String, dynamic>{
'id': instance.id,
'alias': instance.alias,
'name': instance.name,
'description': instance.description,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
Category _$CategoryFromJson(Map<String, dynamic> json) => Category(
id: (json['id'] as num).toInt(),
alias: json['alias'] as String,
name: json['name'] as String,
description: json['description'] as String,
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),
);
Map<String, dynamic> _$CategoryToJson(Category instance) => <String, dynamic>{
'id': instance.id,
'alias': instance.alias,
'name': instance.name,
'description': instance.description,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@ -1,5 +1,9 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:solian/models/account.dart';
part 'realm.g.dart';
@JsonSerializable()
class Realm {
int id;
DateTime createdAt;
@ -25,35 +29,23 @@ class Realm {
this.accountId,
});
factory Realm.fromJson(Map<String, dynamic> json) => Realm(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
alias: json['alias'],
name: json['name'],
description: json['description'],
isPublic: json['is_public'],
isCommunity: json['is_community'],
accountId: json['account_id'],
);
factory Realm.fromJson(Map<String, dynamic> json) => _$RealmFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'alias': alias,
'name': name,
'description': description,
'is_public': isPublic,
'is_community': isCommunity,
'account_id': accountId,
};
Map<String, dynamic> toJson() => _$RealmToJson(this);
@override
bool operator ==(Object other) {
if (other is Realm) {
return other.id == id;
}
return false;
}
@override
int get hashCode => id;
}
@JsonSerializable()
class RealmMember {
int id;
DateTime createdAt;
@ -75,27 +67,8 @@ class RealmMember {
required this.powerLevel,
});
factory RealmMember.fromJson(Map<String, dynamic> json) => RealmMember(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
realmId: json['realm_id'],
accountId: json['account_id'],
account: Account.fromJson(json['account']),
powerLevel: json['power_level'],
);
factory RealmMember.fromJson(Map<String, dynamic> json) =>
_$RealmMemberFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'realm_id': realmId,
'account_id': accountId,
'account': account.toJson(),
'power_level': powerLevel,
};
Map<String, dynamic> toJson() => _$RealmMemberToJson(this);
}

60
lib/models/realm.g.dart Normal file
View File

@ -0,0 +1,60 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'realm.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Realm _$RealmFromJson(Map<String, dynamic> json) => Realm(
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),
alias: json['alias'] as String,
name: json['name'] as String,
description: json['description'] as String,
isPublic: json['is_public'] as bool,
isCommunity: json['is_community'] as bool,
accountId: (json['account_id'] as num?)?.toInt(),
);
Map<String, dynamic> _$RealmToJson(Realm instance) => <String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'alias': instance.alias,
'name': instance.name,
'description': instance.description,
'is_public': instance.isPublic,
'is_community': instance.isCommunity,
'account_id': instance.accountId,
};
RealmMember _$RealmMemberFromJson(Map<String, dynamic> json) => RealmMember(
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),
realmId: (json['realm_id'] as num).toInt(),
accountId: (json['account_id'] as num).toInt(),
account: Account.fromJson(json['account'] as Map<String, dynamic>),
powerLevel: (json['power_level'] as num).toInt(),
);
Map<String, dynamic> _$RealmMemberToJson(RealmMember instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'realm_id': instance.realmId,
'account_id': instance.accountId,
'account': instance.account.toJson(),
'power_level': instance.powerLevel,
};

View File

@ -1,5 +1,9 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:solian/models/account.dart';
part 'relations.g.dart';
@JsonSerializable()
class Relationship {
int id;
DateTime createdAt;
@ -23,27 +27,8 @@ class Relationship {
required this.status,
});
factory Relationship.fromJson(Map<String, dynamic> json) => Relationship(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'],
accountId: json['account_id'],
relatedId: json['related_id'],
account: Account.fromJson(json['account']),
related: Account.fromJson(json['related']),
status: json['status'],
);
factory Relationship.fromJson(Map<String, dynamic> json) =>
_$RelationshipFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'account_id': accountId,
'related_id': relatedId,
'account': account.toJson(),
'related': related.toJson(),
'status': status,
};
Map<String, dynamic> toJson() => _$RelationshipToJson(this);
}

View File

@ -0,0 +1,34 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'relations.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Relationship _$RelationshipFromJson(Map<String, dynamic> json) => Relationship(
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),
accountId: (json['account_id'] as num).toInt(),
relatedId: (json['related_id'] as num).toInt(),
account: Account.fromJson(json['account'] as Map<String, dynamic>),
related: Account.fromJson(json['related'] as Map<String, dynamic>),
status: (json['status'] as num).toInt(),
);
Map<String, dynamic> _$RelationshipToJson(Relationship instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'account_id': instance.accountId,
'related_id': instance.relatedId,
'account': instance.account.toJson(),
'related': instance.related.toJson(),
'status': instance.status,
};

View File

@ -1,6 +1,11 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/services.dart';
part 'stickers.g.dart';
@JsonSerializable()
class Sticker {
int id;
DateTime createdAt;
@ -30,38 +35,21 @@ class Sticker {
required this.account,
});
factory Sticker.fromJson(Map<String, dynamic> json) => Sticker(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: json['deleted_at'],
alias: json['alias'],
name: json['name'],
attachmentId: json['attachment_id'],
attachment: Attachment.fromJson(json['attachment']),
packId: json['pack_id'],
pack: json['pack'] != null ? StickerPack.fromJson(json['pack']) : null,
accountId: json['account_id'],
account: Account.fromJson(json['account']),
String get textPlaceholder => '${pack?.prefix}$alias';
String get textWarpedPlaceholder => ':$textPlaceholder:';
String get imageUrl => ServiceFinder.buildUrl(
'files',
'/attachments/${attachment.rid}',
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'alias': alias,
'name': name,
'attachment_id': attachmentId,
'attachment': attachment.toJson(),
'pack_id': packId,
'account_id': accountId,
'account': account.toJson(),
};
factory Sticker.fromJson(Map<String, dynamic> json) =>
_$StickerFromJson(json);
Map<String, dynamic> toJson() => _$StickerToJson(this);
}
@JsonSerializable()
class StickerPack {
int id;
DateTime createdAt;
@ -87,36 +75,8 @@ class StickerPack {
required this.account,
});
factory StickerPack.fromJson(Map<String, dynamic> json) => StickerPack(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: json['deleted_at'],
prefix: json['prefix'],
name: json['name'],
description: json['description'],
stickers: json['stickers'] == null
? []
: List<Sticker>.from(
json['stickers']!.map((x) => Sticker.fromJson(x))),
accountId: json['account_id'],
account: Account.fromJson(json['account']),
);
factory StickerPack.fromJson(Map<String, dynamic> json) =>
_$StickerPackFromJson(json);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'prefix': prefix,
'name': name,
'description': description,
'stickers': stickers == null
? []
: List<dynamic>.from(stickers!.map((x) => x.toJson())),
'account_id': accountId,
'account': account.toJson(),
};
Map<String, dynamic> toJson() => _$StickerPackToJson(this);
}

View File

@ -0,0 +1,73 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'stickers.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Sticker _$StickerFromJson(Map<String, dynamic> json) => Sticker(
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),
alias: json['alias'] as String,
name: json['name'] as String,
attachmentId: (json['attachment_id'] as num).toInt(),
attachment:
Attachment.fromJson(json['attachment'] as Map<String, dynamic>),
packId: (json['pack_id'] as num).toInt(),
pack: json['pack'] == null
? null
: StickerPack.fromJson(json['pack'] as Map<String, dynamic>),
accountId: (json['account_id'] as num).toInt(),
account: Account.fromJson(json['account'] as Map<String, dynamic>),
);
Map<String, dynamic> _$StickerToJson(Sticker instance) => <String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'alias': instance.alias,
'name': instance.name,
'attachment_id': instance.attachmentId,
'attachment': instance.attachment.toJson(),
'pack_id': instance.packId,
'pack': instance.pack?.toJson(),
'account_id': instance.accountId,
'account': instance.account.toJson(),
};
StickerPack _$StickerPackFromJson(Map<String, dynamic> json) => StickerPack(
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),
prefix: json['prefix'] as String,
name: json['name'] as String,
description: json['description'] as String,
stickers: (json['stickers'] as List<dynamic>?)
?.map((e) => Sticker.fromJson(e as Map<String, dynamic>))
.toList(),
accountId: (json['account_id'] as num).toInt(),
account: Account.fromJson(json['account'] as Map<String, dynamic>),
);
Map<String, dynamic> _$StickerPackToJson(StickerPack instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'prefix': instance.prefix,
'name': instance.name,
'description': instance.description,
'stickers': instance.stickers?.map((e) => e.toJson()).toList(),
'account_id': instance.accountId,
'account': instance.account.toJson(),
};

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/models/account_status.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
@ -33,15 +35,14 @@ class StatusProvider extends GetConnect {
Future<Response> getCurrentStatus() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('auth');
return await client.get('/users/me/status');
}
Future<Response> getSomeoneStatus(String name) =>
get('/users/$name/status');
Future<Response> getSomeoneStatus(String name) => get('/users/$name/status');
Future<Response> setStatus(
String type,
@ -53,7 +54,7 @@ class StatusProvider extends GetConnect {
DateTime? clearAt,
}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('auth');
@ -74,7 +75,7 @@ class StatusProvider extends GetConnect {
}
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -82,13 +83,13 @@ class StatusProvider extends GetConnect {
Future<Response> clearStatus() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('auth');
final resp = await client.delete('/users/me/status');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;

View File

@ -1,24 +1,27 @@
import 'dart:async';
import 'dart:io';
import 'dart:collection';
import 'dart:typed_data';
import 'package:cross_file/cross_file.dart';
import 'package:get/get.dart';
import 'package:path/path.dart' show basename;
import 'package:solian/models/attachment.dart';
import 'package:solian/providers/content/attachment.dart';
class AttachmentUploadTask {
File file;
String usage;
XFile file;
String pool;
Map<String, dynamic>? metadata;
Map<String, int>? chunkFiles;
double progress = 0;
double? progress;
bool isUploading = false;
bool isCompleted = false;
dynamic error;
AttachmentUploadTask({
required this.file,
required this.usage,
required this.pool,
this.metadata,
});
}
@ -73,32 +76,36 @@ class AttachmentUploaderController extends GetxController {
_startProgressSyncTimer();
queueOfUpload[queueIndex].isUploading = true;
queueOfUpload[queueIndex].progress = 0;
final task = queueOfUpload[queueIndex];
final result = await _rawUploadAttachment(
await task.file.readAsBytes(),
task.file.path,
task.usage,
null,
onProgress: (value) {
queueOfUpload[queueIndex].progress = value;
_progressOfUpload = value;
},
onError: (err) {
queueOfUpload[queueIndex].error = err;
queueOfUpload[queueIndex].isUploading = false;
},
);
try {
final result = await _chunkedUploadAttachment(
task.file,
task.pool,
null,
onData: (_) {},
onProgress: (progress) {
queueOfUpload[queueIndex].progress = progress;
_progressOfUpload = progress;
},
);
return result;
} catch (err) {
queueOfUpload[queueIndex].error = err;
queueOfUpload[queueIndex].isUploading = false;
} finally {
_progressOfUpload = 1;
if (queueOfUpload[queueIndex].error == null) {
queueOfUpload.removeAt(queueIndex);
}
_stopProgressSyncTimer();
_syncProgress();
if (queueOfUpload[queueIndex].error == null) {
queueOfUpload.removeAt(queueIndex);
isUploading.value = false;
}
_stopProgressSyncTimer();
_syncProgress();
isUploading.value = false;
return result;
return null;
}
Future<void> performUploadQueue({
@ -115,100 +122,126 @@ class AttachmentUploaderController extends GetxController {
}
queueOfUpload[idx].isUploading = true;
queueOfUpload[idx].progress = 0;
final task = queueOfUpload[idx];
final result = await _rawUploadAttachment(
await task.file.readAsBytes(),
task.file.path,
task.usage,
null,
onProgress: (value) {
queueOfUpload[idx].progress = value;
_progressOfUpload = (idx + value) / queueOfUpload.length;
},
onError: (err) {
queueOfUpload[idx].error = err;
queueOfUpload[idx].isUploading = false;
},
);
_progressOfUpload = (idx + 1) / queueOfUpload.length;
if (result != null) onData(result);
try {
final result = await _chunkedUploadAttachment(
task.file,
task.pool,
null,
onData: (_) {},
onProgress: (progress) {
queueOfUpload[idx].progress = progress;
},
);
if (result != null) onData(result);
} catch (err) {
queueOfUpload[idx].error = err;
queueOfUpload[idx].isUploading = false;
} finally {
_progressOfUpload = (idx + 1) / queueOfUpload.length;
}
queueOfUpload[idx].isUploading = false;
queueOfUpload[idx].isCompleted = true;
}
queueOfUpload.value =
queueOfUpload.where((x) => x.error == null).toList(growable: true);
queueOfUpload.removeWhere((x) => x.error == null);
_stopProgressSyncTimer();
_syncProgress();
isUploading.value = false;
}
Future<void> uploadAttachmentWithCallback(
Future<Attachment?> uploadAttachmentFromData(
Uint8List data,
String path,
String usage,
Map<String, dynamic>? metadata,
Function(Attachment?) callback,
) async {
if (isUploading.value) throw Exception('uploading blocked');
isUploading.value = true;
final result = await _rawUploadAttachment(
data,
path,
usage,
metadata,
onProgress: (progress) {
progressOfUpload.value = progress;
},
);
isUploading.value = false;
callback(result);
}
Future<Attachment?> uploadAttachment(
Uint8List data,
String path,
String usage,
String pool,
Map<String, dynamic>? metadata,
) async {
if (isUploading.value) throw Exception('uploading blocked');
isUploading.value = true;
final result = await _rawUploadAttachment(
data,
path,
usage,
metadata,
onProgress: (progress) {
progressOfUpload.value = progress;
},
);
isUploading.value = false;
return result;
}
Future<Attachment?> _rawUploadAttachment(
Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
{Function(double)? onProgress, Function(dynamic err)? onError}) async {
final AttachmentProvider provider = Get.find();
final AttachmentProvider attach = Get.find();
try {
final result = await provider.createAttachment(
final result = await attach.createAttachmentDirectly(
data,
path,
usage,
pool,
metadata,
onProgress: onProgress,
);
return result;
} catch (err) {
if (onError != null) {
onError(err);
}
} catch (_) {
return null;
} finally {
isUploading.value = false;
}
}
Future<Attachment?> _chunkedUploadAttachment(
XFile file,
String pool,
Map<String, dynamic>? metadata, {
required Function(AttachmentPlaceholder) onData,
required Function(double) onProgress,
}) async {
final AttachmentProvider attach = Get.find();
final holder = await attach.createAttachmentMultipartPlaceholder(
await file.length(),
file.path,
pool,
metadata,
);
onData(holder);
onProgress(0);
final filename = basename(file.path);
final chunks = holder.meta.fileChunks ?? {};
var currentTask = 0;
final queue = Queue<Future<void>>();
final activeTasks = <Future<void>>[];
for (final entry in chunks.entries) {
queue.add(() async {
final beginCursor = entry.value * holder.chunkSize;
final endCursor = (entry.value + 1) * holder.chunkSize;
final data = Uint8List.fromList(await file
.openRead(beginCursor, endCursor)
.expand((chunk) => chunk)
.toList());
final out = await attach.uploadAttachmentMultipartChunk(
data,
filename,
holder.meta.rid,
entry.key,
);
holder.meta = out;
currentTask++;
onProgress(currentTask / chunks.length);
onData(holder);
}());
}
while (queue.isNotEmpty || activeTasks.isNotEmpty) {
while (activeTasks.length < 3 && queue.isNotEmpty) {
final task = queue.removeFirst();
activeTasks.add(task);
task.then((_) => activeTasks.remove(task));
}
if (activeTasks.isNotEmpty) {
await Future.any(activeTasks);
}
}
return holder.meta;
}
}

View File

@ -7,6 +7,8 @@ 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/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart';
@ -81,7 +83,7 @@ class AuthProvider extends GetConnect {
'grant_type': 'refresh_token',
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
credentials = TokenSet(
accessToken: resp.body['access_token'],
@ -128,7 +130,7 @@ class AuthProvider extends GetConnect {
}
Future<void> ensureCredentials() async {
if (isAuthorized.isFalse) throw Exception('unauthorized');
if (isAuthorized.isFalse) throw const UnauthorizedException();
if (credentials == null) await loadCredentials();
if (credentials!.isExpired) {
@ -158,7 +160,7 @@ class AuthProvider extends GetConnect {
'password': password,
});
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
} else if (resp.body['is_finished'] == false) {
throw RiskyAuthenticateException(resp.body['ticket']['id']);
}
@ -218,7 +220,7 @@ class AuthProvider extends GetConnect {
final client = configureClient('auth');
final resp = await client.get('/users/me');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
userProfile.value = resp.body;

View File

@ -2,6 +2,8 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:solian/models/call.dart';
@ -17,6 +19,10 @@ class ChatCallProvider extends GetxController {
RxBool isReady = false.obs;
RxBool isMounted = false.obs;
RxBool isInitialized = false.obs;
RxBool isBusy = false.obs;
RxString lastDuration = '00:00:00'.obs;
Timer? lastDurationUpdateTimer;
String? token;
String? endpoint;
@ -38,6 +44,34 @@ class ChatCallProvider extends GetxController {
RxList<ParticipantTrack> participantTracks = RxList.empty(growable: true);
Rx<ParticipantTrack?> focusTrack = Rx(null);
void _updateDuration() {
if (current.value == null) {
lastDuration.value = '00:00:00';
return;
}
Duration duration = DateTime.now().difference(current.value!.createdAt);
String twoDigits(int n) => n.toString().padLeft(2, '0');
String formattedTime = '${twoDigits(duration.inHours)}:'
'${twoDigits(duration.inMinutes.remainder(60))}:'
'${twoDigits(duration.inSeconds.remainder(60))}';
lastDuration.value = formattedTime;
}
void enableDurationUpdater() {
_updateDuration();
lastDurationUpdateTimer = Timer.periodic(
const Duration(seconds: 1),
(_) => _updateDuration(),
);
}
void disableDurationUpdater() {
lastDurationUpdateTimer?.cancel();
lastDurationUpdateTimer = null;
}
Future<void> checkPermissions() async {
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
return;
@ -56,7 +90,7 @@ class ChatCallProvider extends GetxController {
Future<(String, String)> getRoomToken() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
@ -69,7 +103,7 @@ class ChatCallProvider extends GetxController {
endpoint = 'wss://${resp.body['endpoint']}';
return (token!, endpoint!);
} else {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
}
@ -88,7 +122,30 @@ class ChatCallProvider extends GetxController {
void initRoom() {
initHardware();
room = Room();
room = Room(
roomOptions: const RoomOptions(
dynacast: true,
adaptiveStream: true,
defaultAudioPublishOptions: AudioPublishOptions(
name: 'call_voice',
stream: 'call_stream',
),
defaultVideoPublishOptions: VideoPublishOptions(
name: 'call_video',
stream: 'call_stream',
simulcast: true,
backupVideoCodec: BackupVideoCodec(enabled: true),
),
defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(
useiOSBroadcastExtension: true,
params: VideoParametersPresets.screenShareH1080FPS30,
),
defaultCameraCaptureOptions: CameraCaptureOptions(
maxFrameRate: 30,
params: VideoParametersPresets.h1080_169,
),
),
);
listener = room.createListener();
WakelockPlus.enable();
}
@ -96,36 +153,12 @@ class ChatCallProvider extends GetxController {
void joinRoom(String url, String token) async {
if (isMounted.value) {
return;
} else {
isMounted.value = true;
}
try {
await room.connect(
url,
token,
roomOptions: const RoomOptions(
dynacast: true,
adaptiveStream: true,
defaultAudioPublishOptions: AudioPublishOptions(
name: 'call_voice',
stream: 'call_stream',
),
defaultVideoPublishOptions: VideoPublishOptions(
name: 'call_video',
stream: 'call_stream',
simulcast: true,
backupVideoCodec: BackupVideoCodec(enabled: true),
),
defaultScreenShareCaptureOptions: ScreenShareCaptureOptions(
useiOSBroadcastExtension: true,
params: VideoParametersPresets.screenShareH1080FPS30,
),
defaultCameraCaptureOptions: CameraCaptureOptions(
maxFrameRate: 30,
params: VideoParametersPresets.h1080_169,
),
),
fastConnectOptions: FastConnectOptions(
microphone: TrackOption(track: audioTrack.value),
camera: TrackOption(track: videoTrack.value),
@ -133,6 +166,8 @@ class ChatCallProvider extends GetxController {
);
} catch (e) {
rethrow;
} finally {
isMounted.value = true;
}
}
@ -152,7 +187,7 @@ class ChatCallProvider extends GetxController {
void onRoomDidUpdate() => sortParticipants();
void setupRoom() {
if(isInitialized.value) return;
if (isInitialized.value) return;
sortParticipants();
room.addListener(onRoomDidUpdate);
@ -164,6 +199,7 @@ class ChatCallProvider extends GetxController {
Hardware.instance.setSpeakerphoneOn(true);
}
isBusy.value = false;
isInitialized.value = true;
}
@ -366,6 +402,7 @@ class ChatCallProvider extends GetxController {
}
void disposeRoom() {
isBusy.value = false;
isMounted.value = false;
isInitialized.value = false;
current.value = null;

View File

@ -2,12 +2,13 @@ import 'dart:convert';
import 'dart:typed_data';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:path/path.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
import 'package:dio/dio.dart' as dio;
class AttachmentProvider extends GetConnect {
static Map<String, String> mimetypeOverrides = {
@ -20,22 +21,22 @@ class AttachmentProvider extends GetConnect {
httpClient.baseUrl = ServiceFinder.buildUrl('files', null);
}
final Map<int, Attachment> _cachedResponses = {};
final Map<String, Attachment> _cachedResponses = {};
Future<List<Attachment?>> listMetadata(
List<int> id, {
List<String> rid, {
noCache = false,
}) async {
if (id.isEmpty) return List.empty();
if (rid.isEmpty) return List.empty();
List<Attachment?> result = List.filled(id.length, null);
List<int> pendingQuery = List.empty(growable: true);
List<Attachment?> result = List.filled(rid.length, null);
List<String> pendingQuery = List.empty(growable: true);
if (!noCache) {
for (var idx = 0; idx < id.length; idx++) {
if (_cachedResponses.containsKey(id[idx])) {
result[idx] = _cachedResponses[id[idx]];
for (var idx = 0; idx < rid.length; idx++) {
if (_cachedResponses.containsKey(rid[idx])) {
result[idx] = _cachedResponses[rid[idx]];
} else {
pendingQuery.add(id[idx]);
pendingQuery.add(rid[idx]);
}
}
}
@ -52,12 +53,12 @@ class AttachmentProvider extends GetConnect {
rawOut.data!.map((x) => Attachment.fromJson(x)).toList();
for (final item in out) {
if (item.destination != 0 && item.isAnalyzed) {
_cachedResponses[item.id] = item;
_cachedResponses[item.rid] = item;
}
}
for (var i = 0; i < out.length; i++) {
for (var j = 0; j < id.length; j++) {
if (out[i].id == id[j]) {
for (var j = 0; j < rid.length; j++) {
if (out[i].rid == rid[j]) {
result[j] = out[i];
}
}
@ -66,16 +67,16 @@ class AttachmentProvider extends GetConnect {
return result;
}
Future<Attachment?> getMetadata(int id, {noCache = false}) async {
if (!noCache && _cachedResponses.containsKey(id)) {
return _cachedResponses[id]!;
Future<Attachment?> getMetadata(String rid, {noCache = false}) async {
if (!noCache && _cachedResponses.containsKey(rid)) {
return _cachedResponses[rid]!;
}
final resp = await get('/attachments/$id/meta');
final resp = await get('/attachments/$rid/meta');
if (resp.statusCode == 200) {
final result = Attachment.fromJson(resp.body);
if (result.destination != 0 && result.isAnalyzed) {
_cachedResponses[id] = result;
_cachedResponses[rid] = result;
}
return result;
}
@ -83,14 +84,21 @@ class AttachmentProvider extends GetConnect {
return null;
}
Future<Attachment> createAttachment(
Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
{Function(double)? onProgress}) async {
Future<Attachment> createAttachmentDirectly(
Uint8List data,
String path,
String pool,
Map<String, dynamic>? metadata,
) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final filePayload =
dio.MultipartFile.fromBytes(data, filename: basename(path));
final client = auth.configureClient(
'uc',
timeout: const Duration(minutes: 3),
);
final filePayload = MultipartFile(data, filename: basename(path));
final fileAlt = basename(path).contains('.')
? basename(path).substring(0, basename(path).lastIndexOf('.'))
: basename(path);
@ -103,51 +111,103 @@ class AttachmentProvider extends GetConnect {
if (mimetypeOverrides.keys.contains(fileExt)) {
mimetypeOverride = mimetypeOverrides[fileExt];
}
final payload = dio.FormData.fromMap({
final payload = FormData({
'alt': fileAlt,
'file': filePayload,
'usage': usage,
'pool': pool,
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
'metadata': jsonEncode(metadata),
});
final resp = await dio.Dio(
dio.BaseOptions(
baseUrl: ServiceFinder.buildUrl('files', null),
headers: {'Authorization': 'Bearer ${auth.credentials!.accessToken}'},
),
).post(
'/attachments',
data: payload,
onSendProgress: (count, total) {
if (onProgress != null) onProgress(count / total);
},
);
final resp = await client.post('/attachments', payload);
if (resp.statusCode != 200) {
throw Exception(resp.data);
throw RequestException(resp);
}
return Attachment.fromJson(resp.data);
return Attachment.fromJson(resp.body);
}
Future<AttachmentPlaceholder> createAttachmentMultipartPlaceholder(
int size,
String path,
String pool,
Map<String, dynamic>? metadata,
) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('uc');
final fileAlt = basename(path).contains('.')
? basename(path).substring(0, basename(path).lastIndexOf('.'))
: basename(path);
final fileExt = basename(path)
.substring(basename(path).lastIndexOf('.') + 1)
.toLowerCase();
// Override for some files cannot be detected mimetype by server-side
String? mimetypeOverride;
if (mimetypeOverrides.keys.contains(fileExt)) {
mimetypeOverride = mimetypeOverrides[fileExt];
}
final resp = await client.post('/attachments/multipart', {
'alt': fileAlt,
'name': basename(path),
'size': size,
'pool': pool,
if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
'metadata': metadata,
});
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return AttachmentPlaceholder.fromJson(resp.body);
}
Future<Attachment> uploadAttachmentMultipartChunk(
Uint8List data,
String name,
String rid,
String cid,
) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient(
'uc',
timeout: const Duration(minutes: 3),
);
final payload = FormData({
'file': MultipartFile(data, filename: name),
});
final resp = await client.post('/attachments/multipart/$rid/$cid', payload);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Attachment.fromJson(resp.body);
}
Future<Response> updateAttachment(
int id,
String alt,
String usage, {
String alt, {
required Map<String, dynamic> metadata,
bool isMature = false,
}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('files');
var resp = await client.put('/attachments/$id', {
'alt': alt,
'usage': usage,
'metadata': metadata,
'is_mature': isMature,
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -155,19 +215,19 @@ class AttachmentProvider extends GetConnect {
Future<Response> deleteAttachment(int id) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('files');
var resp = await client.delete('/attachments/$id');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
}
void clearCache({int? id}) {
void clearCache({String? id}) {
if (id != null) {
_cachedResponses.remove(id);
} else {

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/widgets/account/relative_select.dart';
@ -16,7 +18,7 @@ class ChannelProvider extends GetxController {
Future<void> refreshAvailableChannel() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
isLoading.value = true;
final resp = await listAvailableChannel();
@ -29,13 +31,13 @@ class ChannelProvider extends GetxController {
Future<Response> getChannel(String alias, {String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
final resp = await client.get('/channels/$realm/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -44,13 +46,13 @@ class ChannelProvider extends GetxController {
Future<Response> getMyChannelProfile(String alias,
{String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
final resp = await client.get('/channels/$realm/$alias/me');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -59,7 +61,7 @@ class ChannelProvider extends GetxController {
Future<Response?> getChannelOngoingCall(String alias,
{String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
@ -67,7 +69,7 @@ class ChannelProvider extends GetxController {
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -75,13 +77,13 @@ class ChannelProvider extends GetxController {
Future<Response> listChannel({String scope = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
final resp = await client.get('/channels/$scope');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -89,13 +91,13 @@ class ChannelProvider extends GetxController {
Future<Response> listAvailableChannel({String realm = 'global'}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
final resp = await client.get('/channels/$realm/me/available');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -103,13 +105,13 @@ class ChannelProvider extends GetxController {
Future<Response> createChannel(String scope, dynamic payload) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
final resp = await client.post('/channels/$scope', payload);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -118,7 +120,7 @@ class ChannelProvider extends GetxController {
Future<Response?> createDirectChannel(
BuildContext context, String scope) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final related = await showModalBottomSheet(
useRootNavigator: true,
@ -141,7 +143,7 @@ class ChannelProvider extends GetxController {
'is_encrypted': false,
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -149,13 +151,13 @@ class ChannelProvider extends GetxController {
Future<Response> updateChannel(String scope, int id, dynamic payload) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('messaging');
final resp = await client.put('/channels/$scope/$id', payload);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;

View File

@ -1,4 +1,6 @@
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart';
@ -8,20 +10,43 @@ class PostProvider extends GetConnect {
httpClient.baseUrl = ServiceFinder.buildUrl('interactive', null);
}
Future<Response> seeWhatsNew(int pivot) async {
GetConnect client;
final AuthProvider auth = Get.find();
if (auth.isAuthorized.value) {
client = auth.configureClient('co');
} else {
client = ServiceFinder.configureClient('co');
}
final resp = await client.get('/whats-new?pivot=$pivot');
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return resp;
}
Future<Response> listRecommendations(int page,
{int? realm, String? channel}) async {
{String? realm, String? channel}) async {
GetConnect client;
final AuthProvider auth = Get.find();
final queries = [
'take=${10}',
'offset=$page',
if (realm != null) 'realmId=$realm',
if (realm != null) 'realm=$realm',
];
final resp = await get(
if (auth.isAuthorized.value) {
client = auth.configureClient('co');
} else {
client = ServiceFinder.configureClient('co');
}
final resp = await client.get(
channel == null
? '/recommendations?${queries.join('&')}'
: '/recommendations/$channel?${queries.join('&')}',
);
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
}
return resp;
@ -29,7 +54,7 @@ class PostProvider extends GetConnect {
Future<Response> listDraft(int page) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final queries = [
'take=${10}',
@ -38,25 +63,25 @@ class PostProvider extends GetConnect {
final client = auth.configureClient('interactive');
final resp = await client.get('/posts/drafts?${queries.join('&')}');
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
}
return resp;
}
Future<Response> listPost(int page,
{int? realm, String? author, tag, category}) async {
{String? realm, String? author, tag, category}) async {
final queries = [
'take=${10}',
'offset=$page',
if (tag != null) 'tag=$tag',
if (category != null) 'category=$category',
if (author != null) 'author=$author',
if (realm != null) 'realmId=$realm',
if (realm != null) 'realm=$realm',
];
final resp = await get('/posts?${queries.join('&')}');
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
}
return resp;
@ -65,7 +90,7 @@ class PostProvider extends GetConnect {
Future<Response> listPostReplies(String alias, int page) async {
final resp = await get('/posts/$alias/replies?take=${10}&offset=$page');
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
}
return resp;
@ -74,7 +99,7 @@ class PostProvider extends GetConnect {
Future<Response> getPost(String alias) async {
final resp = await get('/posts/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
}
return resp;
@ -83,7 +108,7 @@ class PostProvider extends GetConnect {
Future<Response> getArticle(String alias) async {
final resp = await get('/articles/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.body);
throw RequestException(resp);
}
return resp;

View File

@ -1,4 +1,6 @@
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/providers/auth.dart';
@ -8,7 +10,7 @@ class RealmProvider extends GetxController {
Future<void> refreshAvailableRealms() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
isLoading.value = true;
final resp = await listAvailableRealm();
@ -21,13 +23,13 @@ class RealmProvider extends GetxController {
Future<Response> getRealm(String alias) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('auth');
final resp = await client.get('/realms/$alias');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -35,13 +37,13 @@ class RealmProvider extends GetxController {
Future<Response> listAvailableRealm() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('auth');
final resp = await client.get('/realms/me/available');
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;

View File

@ -0,0 +1,58 @@
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/models/daily_sign.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
class DailySignProvider extends GetxController {
Future<List<DailySignRecord>> listLastRecord(int take) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('id');
final resp = await client.get('/daily?take=$take');
if (resp.statusCode != 200 && resp.statusCode != 404) {
throw RequestException(resp);
} else if (resp.statusCode == 404) {
return List.empty();
}
final result = PaginationResult.fromJson(resp.body);
return List.from(
result.data?.map((x) => DailySignRecord.fromJson(x)) ?? [],
);
}
Future<DailySignRecord?> getToday() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('id');
final resp = await client.get('/daily/today');
if (resp.statusCode != 200 && resp.statusCode != 404) {
throw RequestException(resp);
} else if (resp.statusCode == 404) {
return null;
}
return DailySignRecord.fromJson(resp.body);
}
Future<DailySignRecord> signToday() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
final client = auth.configureClient('id');
final resp = await client.post('/daily', {});
if (resp.statusCode != 200) {
throw RequestException(resp);
}
return DailySignRecord.fromJson(resp.body);
}
}

View File

@ -0,0 +1,26 @@
extension DurationToHumanReadableString on Duration {
String toHumanReadableString({padZero = true}) {
final mm = inMinutes
.remainder(60)
.toString()
.padLeft(2, !padZero && inHours == 0 ? '' : '0');
final ss = inSeconds.remainder(60).toString().padLeft(2, '0');
if (inHours > 0) {
final hh = inHours.toString().padLeft(2, !padZero ? '' : '0');
return '$hh:$mm:$ss';
}
return '$mm:$ss';
}
}
extension ParseDuration on Duration {
static Duration fromString(String duration) {
final parts = duration.split(':').reversed.toList();
final seconds = int.parse(parts[0]);
final minutes = parts.length > 1 ? int.parse(parts[1]) : 0;
final hours = parts.length > 2 ? int.parse(parts[2]) : 0;
return Duration(hours: hours, minutes: minutes, seconds: seconds);
}
}

View File

@ -0,0 +1,51 @@
import 'package:get/get.dart';
import 'package:intl/intl.dart';
class ExperienceProvider extends GetxController {
static List<int> experienceToLevelRequirements = [
0, // Level 0
100, // Level 1
400, // Level 2
900, // Level 3
1600, // Level 4
2500, // Level 5
3600, // Level 6
4900, // Level 7
6400, // Level 8
8100, // Level 9
10000, // Level 10
12100, // Level 11
14400, // Level 12
36800 // Level 13
];
static List<String> levelLabelMapping =
List.generate(experienceToLevelRequirements.length, (x) => 'userLevel$x');
static (int level, String label) getLevelFromExp(int experience) {
final exp = experienceToLevelRequirements.reversed
.firstWhere((x) => x <= experience);
final idx = experienceToLevelRequirements.indexOf(exp);
return (idx, levelLabelMapping[idx]);
}
static double calcLevelUpProgress(int experience) {
final exp = experienceToLevelRequirements.reversed
.firstWhere((x) => x <= experience);
final idx = experienceToLevelRequirements.indexOf(exp);
if (idx + 1 >= experienceToLevelRequirements.length) return 1;
final nextExp = experienceToLevelRequirements[idx + 1];
return exp / nextExp;
}
static String calcLevelUpProgressLevel(int experience) {
final exp = experienceToLevelRequirements.reversed
.firstWhere((x) => x <= experience);
final idx = experienceToLevelRequirements.indexOf(exp);
if (idx + 1 >= experienceToLevelRequirements.length) return 'Infinity';
final nextExp = experienceToLevelRequirements[idx + 1];
final formatter =
NumberFormat.compactCurrency(symbol: '', decimalDigits: 1);
return '${formatter.format(exp)}/${formatter.format(nextExp)}';
}
}

View File

@ -0,0 +1,54 @@
import 'dart:math';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
class LastReadProvider extends GetxController {
int? _feedLastReadAt;
int? _messagesLastReadAt;
int? get feedLastReadAt => _feedLastReadAt;
int? get messagesLastReadAt => _messagesLastReadAt;
set feedLastReadAt(int? value) {
if (value == _feedLastReadAt) return;
final newValue = max(_feedLastReadAt ?? 0, value ?? 0);
if (newValue != _feedLastReadAt) {
_feedLastReadAt = newValue;
_saveToStorage();
}
}
set messagesLastReadAt(int? value) {
if (value == _messagesLastReadAt) return;
final newValue = max(_messagesLastReadAt ?? 0, value ?? 0);
if (newValue != _messagesLastReadAt) {
_messagesLastReadAt = newValue;
_saveToStorage();
}
}
LastReadProvider() {
_revertFromStorage();
}
Future<void> _revertFromStorage() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('feed_last_read_at')) {
_feedLastReadAt = prefs.getInt('feed_last_read_at')!;
}
if (prefs.containsKey('messages_last_read_at')) {
_messagesLastReadAt = prefs.getInt('messages_last_read_at');
}
}
Future<void> _saveToStorage() async {
final prefs = await SharedPreferences.getInstance();
if (_feedLastReadAt != null) {
prefs.setInt('feed_last_read_at', _feedLastReadAt!);
}
if (_messagesLastReadAt != null) {
prefs.setInt('messages_last_read_at', _messagesLastReadAt!);
}
}
}

View File

@ -0,0 +1,26 @@
import 'dart:convert';
import 'dart:developer';
import 'package:get/get.dart';
import 'package:solian/models/link.dart';
import 'package:solian/services.dart';
class LinkExpandProvider extends GetxController {
final Map<String, LinkMeta?> _cachedResponse = {};
Future<LinkMeta?> expandLink(String url) async {
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 resp = await client.get('/api/links/$target');
if (resp.statusCode != 200) {
log('Unable to expand link ($url), status: ${resp.statusCode}, response: ${resp.body}');
_cachedResponse[target] = null;
return null;
}
final result = LinkMeta.fromJson(resp.body);
_cachedResponse[target] = result;
return result;
}
}

View File

@ -1,5 +1,6 @@
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';
@ -16,6 +17,27 @@ Future<MessageHistoryDb> createHistoryDb() async {
.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;
@ -29,20 +51,20 @@ Future<Event?> getRemoteEvent(int id, Channel channel, String scope) async {
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
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 {
Channel channel,
String scope, {
required int remainDepth,
bool Function(List<Event> items)? onBrake,
take = 10,
offset = 0,
}) async {
if (remainDepth <= 0) {
return null;
}
@ -57,7 +79,7 @@ Future<(List<Event>, int)?> getRemoteEvents(
);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);
@ -69,13 +91,13 @@ Future<(List<Event>, int)?> getRemoteEvents(
}
final expandResult = (await getRemoteEvents(
channel,
scope,
remainDepth: remainDepth - 1,
take: take,
offset: offset + result.length,
))
?.$1 ??
channel,
scope,
remainDepth: remainDepth - 1,
take: take,
offset: offset + result.length,
))
?.$1 ??
List.empty();
return ([...result, ...expandResult], response.count);

View File

@ -1,4 +1,5 @@
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/relations.dart';
import 'package:solian/providers/auth.dart';
@ -42,7 +43,7 @@ class RelationshipProvider extends GetxController {
final client = auth.configureClient('auth');
final resp = await client.post('/users/me/relations?related=$username', {});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -57,7 +58,7 @@ class RelationshipProvider extends GetxController {
{},
);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;
@ -71,7 +72,7 @@ class RelationshipProvider extends GetxController {
{'status': status},
);
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
throw RequestException(resp);
}
return resp;

View File

@ -5,9 +5,12 @@ import 'package:solian/services.dart';
class StickerProvider extends GetxController {
final RxMap<String, String> aliasImageMapping = RxMap();
final RxMap<String, List<Sticker>> availableStickers = RxMap();
final RxList<Sticker> availableStickers = RxList.empty(growable: true);
Future<void> refreshAvailableStickers() async {
availableStickers.clear();
aliasImageMapping.clear();
final client = ServiceFinder.configureClient('files');
final resp = await client.get(
'/stickers/manifest?take=100',
@ -20,16 +23,9 @@ class StickerProvider extends GetxController {
for (final pack in out) {
for (final sticker in (pack.stickers ?? List<Sticker>.empty())) {
sticker.pack = pack;
final imageUrl = ServiceFinder.buildUrl(
'files',
'/attachments/${sticker.attachmentId}',
);
aliasImageMapping['${pack.prefix}${sticker.alias}'.camelCase!] =
imageUrl;
if (availableStickers[pack.prefix] == null) {
availableStickers[pack.prefix] = List.empty(growable: true);
}
availableStickers[pack.prefix]!.add(sticker);
aliasImageMapping[sticker.textPlaceholder.toUpperCase()] =
sticker.imageUrl;
availableStickers.add(sticker);
}
}
}

View File

@ -6,6 +6,7 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/notification.dart';
import 'package:solian/models/packet.dart';
import 'package:solian/models/pagination.dart';
@ -50,31 +51,31 @@ class WebSocketProvider extends GetxController {
}
final AuthProvider auth = Get.find();
await auth.ensureCredentials();
if (auth.credentials == null) await auth.loadCredentials();
final uri = Uri.parse(ServiceFinder.buildUrl(
'dealer',
'/api/ws?tk=${auth.credentials!.accessToken}',
).replaceFirst('http', 'ws'));
isConnecting.value = true;
try {
await auth.ensureCredentials();
final uri = Uri.parse(ServiceFinder.buildUrl(
'dealer',
'/api/ws?tk=${auth.credentials!.accessToken}',
).replaceFirst('http', 'ws'));
isConnecting.value = true;
websocket = WebSocketChannel.connect(uri);
await websocket?.ready;
} catch (e) {
listen();
isConnected.value = true;
} catch (err) {
log('Unable connect dealer via websocket... $err');
if (!noRetry) {
await auth.refreshCredentials();
return connect(noRetry: true);
}
} finally {
isConnecting.value = false;
}
listen();
isConnected.value = true;
isConnecting.value = false;
}
void disconnect() {
@ -87,6 +88,7 @@ class WebSocketProvider extends GetxController {
websocket?.stream.listen(
(event) {
final packet = NetworkPackage.fromJson(jsonDecode(event));
log('Websocket incoming message: ${packet.method} ${packet.message}');
stream.sink.add(packet);
},
onDone: () {
@ -147,8 +149,8 @@ class WebSocketProvider extends GetxController {
'device_token': token,
'device_id': deviceUuid,
});
if (resp.statusCode != 200) {
throw Exception(resp.bodyString);
if (resp.statusCode != 200 && resp.statusCode != 400) {
throw RequestException(resp);
}
}

View File

@ -12,6 +12,7 @@ import 'package:solian/screens/channel/channel_chat.dart';
import 'package:solian/screens/channel/channel_detail.dart';
import 'package:solian/screens/channel/channel_organize.dart';
import 'package:solian/screens/chat.dart';
import 'package:solian/screens/dashboard.dart';
import 'package:solian/screens/feed/search.dart';
import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/screens/feed/draft_box.dart';
@ -19,7 +20,7 @@ import 'package:solian/screens/realms.dart';
import 'package:solian/screens/realms/realm_detail.dart';
import 'package:solian/screens/realms/realm_organize.dart';
import 'package:solian/screens/realms/realm_view.dart';
import 'package:solian/screens/home.dart';
import 'package:solian/screens/feed.dart';
import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/screens/settings.dart';
import 'package:solian/shells/root_shell.dart';
@ -34,6 +35,14 @@ abstract class AppRouter {
child: child,
),
routes: [
GoRoute(
path: '/',
name: 'dashboard',
builder: (context, state) => TitleShell(
state: state,
child: const DashboardScreen(),
),
),
_feedRoute,
_chatRoute,
_realmRoute,
@ -63,9 +72,9 @@ abstract class AppRouter {
builder: (context, state, child) => child,
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => const HomeScreen(),
path: '/feed',
name: 'feed',
builder: (context, state) => const FeedScreen(),
),
GoRoute(
path: '/feed/search',

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher_string.dart';
@ -21,7 +22,7 @@ class AboutScreen extends StatelessWidget {
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Image.asset('assets/logo.png', width: 120, height: 120),
),
const SizedBox(height: 8),
const Gap(8),
Text(
'Solian',
style: Theme.of(context).textTheme.headlineMedium,
@ -30,12 +31,12 @@ class AboutScreen extends StatelessWidget {
'The Solar Network',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
const Gap(8),
FutureBuilder(
future: PackageInfo.fromPlatform(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox();
return const SizedBox.shrink();
}
return Text(
@ -45,7 +46,7 @@ class AboutScreen extends StatelessWidget {
},
),
Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
const SizedBox(height: 16),
const Gap(16),
TextButton(
style: denseButtonStyle,
child: const Text('App Details'),
@ -59,7 +60,8 @@ class AboutScreen extends StatelessWidget {
'The Solar Network App is an intuitive and self-hostable social network and computing platform. Experience the freedom of a user-friendly design that empowers you to create and connect with communities on your own terms. Embrace the future of social networking with a platform that prioritizes your independence and privacy.',
applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Image.asset('assets/logo.png', width: 60, height: 60),
child:
Image.asset('assets/logo.png', width: 60, height: 60),
),
);
},
@ -71,7 +73,7 @@ class AboutScreen extends StatelessWidget {
launchUrlString('https://solsynth.dev/products/solar-network');
},
),
const SizedBox(height: 16),
const Gap(16),
const Text(
'Open-sourced under AGPLv3',
style: TextStyle(

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/relations.dart';
@ -56,7 +57,7 @@ class _FriendScreenState extends State<FriendScreen>
mainAxisSize: MainAxisSize.min,
children: [
Text('accountFriendNewHint'.tr, textAlign: TextAlign.left),
const SizedBox(height: 18),
const Gap(18),
TextField(
controller: controller,
decoration: InputDecoration(

View File

@ -16,7 +16,7 @@ class NotificationScreen extends StatefulWidget {
class _NotificationScreenState extends State<NotificationScreen> {
bool _isBusy = false;
Future<void> markAllRead() async {
Future<void> _markAllRead() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
@ -40,7 +40,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
setState(() => _isBusy = false);
}
Future<void> markOneRead(notify.Notification element, int index) async {
Future<void> _markOneRead(notify.Notification element, int index) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return;
@ -64,7 +64,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
@override
Widget build(BuildContext context) {
final WebSocketProvider provider = Get.find();
final WebSocketProvider ws = Get.find();
return SizedBox(
height: MediaQuery.of(context).size.height * 0.85,
@ -83,7 +83,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
if (provider.notifications.isEmpty)
if (ws.notifications.isEmpty)
SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
@ -96,7 +96,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
),
),
),
if (provider.notifications.isNotEmpty)
if (ws.notifications.isNotEmpty)
SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
@ -104,14 +104,14 @@ class _NotificationScreenState extends State<NotificationScreen> {
child: ListTile(
leading: const Icon(Icons.checklist),
title: Text('notifyAllRead'.tr),
onTap: _isBusy ? null : () => markAllRead(),
onTap: _isBusy ? null : () => _markAllRead(),
),
),
),
SliverList.separated(
itemCount: provider.notifications.length,
itemCount: ws.notifications.length,
itemBuilder: (BuildContext context, int index) {
var element = provider.notifications[index];
var element = ws.notifications[index];
return Dismissible(
key: Key(const Uuid().v4()),
background: Container(
@ -135,7 +135,7 @@ class _NotificationScreenState extends State<NotificationScreen> {
],
),
),
onDismissed: (_) => markOneRead(element, index),
onDismissed: (_) => _markOneRead(element, index),
);
},
separatorBuilder: (_, __) =>

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';
@ -30,8 +31,8 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
final _descriptionController = TextEditingController();
final _birthdayController = TextEditingController();
int? _avatar;
int? _banner;
String? _avatar;
String? _banner;
DateTime? _birthday;
bool _isBusy = false;
@ -109,14 +110,14 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
setState(() => _isBusy = true);
final AttachmentProvider provider = Get.find();
final AttachmentProvider attach = Get.find();
Attachment? attachResult;
try {
attachResult = await provider.createAttachment(
attachResult = await attach.createAttachmentDirectly(
await file.readAsBytes(),
file.path,
'p.$position',
'avatar',
null,
);
} catch (e) {
@ -129,7 +130,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
final resp = await client.put(
'/users/me/$position',
{'attachment': attachResult.id},
{'attachment': attachResult.rid},
);
if (resp.statusCode == 200) {
_syncWidget();
@ -185,7 +186,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
child: ListView(
children: [
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
const SizedBox(height: 24),
const Gap(24),
Stack(
children: [
AccountAvatar(content: _avatar, radius: 40),
@ -202,7 +203,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
),
],
).paddingSymmetric(horizontal: padding),
const SizedBox(height: 16),
const Gap(16),
Stack(
children: [
ClipRRect(
@ -247,7 +248,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
),
],
).paddingSymmetric(horizontal: padding),
const SizedBox(height: 24),
const Gap(24),
Row(
children: [
Flexible(
@ -262,7 +263,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
),
),
),
const SizedBox(width: 16),
const Gap(16),
Flexible(
flex: 1,
child: TextField(
@ -275,7 +276,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
),
],
).paddingSymmetric(horizontal: padding),
const SizedBox(height: 16),
const Gap(16),
Row(
children: [
Flexible(
@ -288,7 +289,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
),
),
),
const SizedBox(width: 16),
const Gap(16),
Flexible(
flex: 1,
child: TextField(
@ -301,7 +302,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
),
],
).paddingSymmetric(horizontal: padding),
const SizedBox(height: 16),
const Gap(16),
TextField(
controller: _descriptionController,
keyboardType: TextInputType.multiline,
@ -312,7 +313,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
labelText: 'description'.tr,
),
).paddingSymmetric(horizontal: padding),
const SizedBox(height: 16),
const Gap(16),
TextField(
controller: _birthdayController,
readOnly: true,
@ -322,7 +323,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
),
onTap: () => _selectBirthday(),
).paddingSymmetric(horizontal: padding),
const SizedBox(height: 16),
const Gap(16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [

Some files were not shown because too many files have changed in this diff Show More