Compare commits

...

126 Commits

Author SHA1 Message Date
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
d8e79fb4f9 🚀 Launch 1.2.1+5 2024-08-04 20:49:11 +08:00
06e0fa465b Article has special badge 2024-08-04 20:48:51 +08:00
895a257f50 Better overflow effect 2024-08-04 20:43:25 +08:00
d9804ba00b 🚸 Enhanced share feature 2024-08-04 18:32:16 +08:00
62ff1c2f1c 🚀 Launch 1.2.1+4 2024-08-04 18:14:28 +08:00
a157596a2e Optimize and fixes 2024-08-04 18:13:59 +08:00
12102bf527 Limit content and read more in posts 2024-08-04 17:39:22 +08:00
c00a018380 🐛 Fix draft box 2024-08-04 17:15:56 +08:00
53b3cac4ca Show hint when dismissible error 2024-08-04 16:26:05 +08:00
185 changed files with 7753 additions and 2647 deletions

View File

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

View File

@ -1,17 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.camera"/> <uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus"/> <uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_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.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/> <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_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/> <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_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
@ -19,31 +19,32 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:label="Solian" android:label="Solian"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:supportsRtl="true"> android:supportsRtl="true"
android:usesCleartextTraffic="true">
<receiver android:exported="false" <receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"/> android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
<receiver android:exported="false" <receiver android:exported="false"
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"> android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON"/> <action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/> <action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:taskAffinity="" android:taskAffinity=""
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
@ -58,29 +59,36 @@
<data android:host="sn.solsynth.dev" /> <data android:host="sn.solsynth.dev" />
<data android:scheme="https" /> <data android:scheme="https" />
<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" /> <data android:scheme="solink" />
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" android:resource="@style/NormalTheme"
/> />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name="com.yalantis.ucrop.UCropActivity" android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/> android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2"/> android:value="2" />
</application> </application>
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and 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. --> In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain" />
</intent> </intent>
</queries> </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"?> <?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"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" /> <item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<!-- You can insert your own image assets here --> </item>
<!-- <item> <item>
<bitmap <bitmap android:gravity="center" android:src="@drawable/splash"/>
android:gravity="center" </item>
android:src="@mipmap/launch_image" />
</item> -->
</layer-list> </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"?> <?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"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" /> <item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<!-- You can insert your own image assets here --> </item>
<!-- <item> <item>
<bitmap <bitmap android:gravity="center" android:src="@drawable/splash"/>
android:gravity="center" </item>
android:src="@mipmap/launch_image" />
</item> -->
</layer-list> </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 <!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame --> the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item> <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> </style>
<!-- Theme applied to the Android Window as soon as the process has started. <!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your 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 <!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame --> the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item> <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> </style>
<!-- Theme applied to the Android Window as soon as the process has started. <!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your This theme determines the color of the Android Window while your

View File

@ -12,6 +12,17 @@ subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}" project.buildDir = "${rootProject.buildDir}/${project.name}"
} }
subprojects { 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") project.evaluationDependsOn(":app")
} }

View File

@ -20,6 +20,7 @@ plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.4.0' apply false id "com.android.application" version '8.4.0' apply false
id "com.google.gms.google-services" version "4.3.15" apply false id "com.google.gms.google-services" version "4.3.15" apply false
id "com.google.firebase.crashlytics" version "2.8.1" apply false
id "org.jetbrains.kotlin.android" version '2.0.0' apply false id "org.jetbrains.kotlin.android" version '2.0.0' apply false
} }

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

View File

@ -254,6 +254,7 @@
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
B1CDA9DD5B638A2BB88053CB /* [CP] Check Pods Manifest.lock */, B1CDA9DD5B638A2BB88053CB /* [CP] Check Pods Manifest.lock */,
7356FAC42C72724B0051A465 /* [Crashlytics] Clear dSYM */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
@ -263,6 +264,7 @@
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
287A33C298CA352A7E7F32A4 /* [CP] Embed Pods Frameworks */, 287A33C298CA352A7E7F32A4 /* [CP] Embed Pods Frameworks */,
0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */, 0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */,
1A9FD6BE5DEE99CDA7399504 /* [Crashlytics] Upload dSYM */,
); );
buildRules = ( buildRules = (
); );
@ -365,6 +367,24 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0; 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 */ = { 259653AE41D478F4C6BAE9B2 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -420,6 +440,24 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 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 */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
@ -433,7 +471,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; 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 */ = { B1CDA9DD5B638A2BB88053CB /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;

View File

@ -1,7 +1,7 @@
import UIKit import UIKit
import Flutter import Flutter
@UIApplicationMain @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
override func application( override func application(
_ application: UIApplication, _ 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" : [ "images" : [
{ {
"idiom" : "universal",
"filename" : "LaunchImage.png", "filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@2x.png", "filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@3x.png", "filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }
], ],
"info" : { "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"> <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
</imageView> <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
</subviews> </subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/> <constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/> <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> </constraints>
</view> </view>
</viewController> </viewController>
@ -32,6 +38,7 @@
</scene> </scene>
</scenes> </scenes>
<resources> <resources>
<image name="LaunchImage" width="168" height="185"/> <image name="LaunchImage" width="1026" height="1024"/>
<image name="LaunchBackground" width="1" height="1"/>
</resources> </resources>
</document> </document>

View File

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

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -112,15 +113,19 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
label: 'bsPreparingData', label: 'bsPreparingData',
action: () async { action: () async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
await Future.wait([ try {
Get.find<StickerProvider>().refreshAvailableStickers(), await Future.wait([
if (auth.isAuthorized.isTrue) Get.find<StickerProvider>().refreshAvailableStickers(),
Get.find<ChannelProvider>().refreshAvailableChannel(), if (auth.isAuthorized.isTrue)
if (auth.isAuthorized.isTrue) Get.find<ChannelProvider>().refreshAvailableChannel(),
Get.find<RelationshipProvider>().refreshRelativeList(), if (auth.isAuthorized.isTrue)
if (auth.isAuthorized.isTrue) Get.find<RelationshipProvider>().refreshRelativeList(),
Get.find<RealmProvider>().refreshAvailableRealms(), if (auth.isAuthorized.isTrue)
]); Get.find<RealmProvider>().refreshAvailableRealms(),
]);
} catch (e) {
context.showErrorDialog(e);
}
}, },
), ),
( (
@ -163,24 +168,25 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isBusy || _isErrored) { if (_isBusy || _isErrored) {
return Material( return GestureDetector(
color: Theme.of(context).colorScheme.surface, child: Material(
child: Column( color: Theme.of(context).colorScheme.surface,
mainAxisSize: MainAxisSize.max, child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisSize: MainAxisSize.max,
children: [ mainAxisAlignment: MainAxisAlignment.spaceAround,
SizedBox( children: [
height: 280, SizedBox(
child: Align( height: 280,
alignment: Alignment.bottomCenter, child: Align(
child: ClipRRect( alignment: Alignment.bottomCenter,
borderRadius: const BorderRadius.all(Radius.circular(16)), child: ClipRRect(
child: Image.asset('assets/logo.png', width: 80, height: 80), borderRadius: const BorderRadius.all(Radius.circular(16)),
child:
Image.asset('assets/logo.png', width: 80, height: 80),
),
), ),
), ),
), Column(
GestureDetector(
child: Column(
children: [ children: [
if (_isErrored && !_isDismissable && !_isBusy) if (_isErrored && !_isDismissable && !_isBusy)
const Icon(Icons.cancel, size: 24), const Icon(Icons.cancel, size: 24),
@ -192,7 +198,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
height: 24, height: 24,
child: CircularProgressIndicator(strokeWidth: 3), child: CircularProgressIndicator(strokeWidth: 3),
), ),
const SizedBox(height: 12), const Gap(12),
CenteredContainer( CenteredContainer(
maxWidth: 280, maxWidth: 280,
child: Column( child: Column(
@ -215,6 +221,15 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
color: _unFocusColor, color: _unFocusColor,
), ),
).paddingOnly(bottom: 4), ).paddingOnly(bottom: 4),
if (!_isBusy && _isErrored && _isDismissable)
Text(
'bsDismissibleErrorHint'.tr,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: _unFocusColor,
),
).paddingOnly(bottom: 5),
Text( Text(
'2024 © Solsynth LLC', '2024 © Solsynth LLC',
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -228,25 +243,25 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
), ),
], ],
), ),
onTap: () { ],
if (_isBusy) return; ),
if (_isDismissable) {
setState(() {
_isBusy = false;
_isErrored = false;
});
} else {
setState(() {
_isBusy = true;
_isErrored = false;
_periodCursor = 0;
});
_runPeriods();
}
},
)
],
), ),
onTap: () {
if (_isBusy) return;
if (_isDismissable) {
setState(() {
_isBusy = false;
_isErrored = false;
});
} else {
setState(() {
_isBusy = true;
_isErrored = false;
_periodCursor = 0;
});
_runPeriods();
}
},
); );
} }

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

View File

@ -1,19 +1,23 @@
import 'dart:math';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/last_read.dart';
class PostListController extends GetxController { class PostListController extends GetxController {
String? author; String? author;
/// The polling source modifier. /// The polling source modifier.
/// - `0`: default recommendations /// - `0`: default recommendations
/// - `1`: shuffle mode /// - `1`: friend mode
/// - `2`: shuffle mode
RxInt mode = 0.obs; RxInt mode = 0.obs;
/// The paging controller for infinite loading. /// 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<int, Post> pagingController =
PagingController(firstPageKey: 0); PagingController(firstPageKey: 0);
@ -95,6 +99,9 @@ class PostListController extends GetxController {
final idx = <dynamic>{}; final idx = <dynamic>{};
postList.retainWhere((x) => idx.add(x.id)); postList.retainWhere((x) => idx.add(x.id));
var lastId = postList.map((x) => x.id).reduce(max);
Get.find<LastReadProvider>().feedLastReadAt = lastId;
return result; return result;
} }
@ -111,10 +118,23 @@ class PostListController extends GetxController {
author: author, author: author,
); );
} else { } else {
resp = await provider.listRecommendations( switch (mode.value) {
pageKey, case 2:
channel: mode.value == 0 ? null : 'shuffle', 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) { } catch (e) {
rethrow; 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:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
extension SolianExtenions on BuildContext { extension SolianExtenions on BuildContext {
void showSnackbar(String content, {SnackBarAction? action}) { void showSnackbar(String content, {SnackBarAction? action}) {
@ -48,15 +50,48 @@ extension SolianExtenions on BuildContext {
} }
Future<void> showErrorDialog(dynamic exception) { Future<void> showErrorDialog(dynamic exception) {
var stack = StackTrace.current; Widget content = Text(exception.toString().capitalize!);
var stackTrace = '$stack'; 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>( return showDialog<void>(
useRootNavigator: true, useRootNavigator: true,
context: this, context: this,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: Text('errorHappened'.tr), title: Text('errorHappened'.tr),
content: Text('${exception.toString().capitalize!}\n\nStack Trace: $stackTrace'), content: content,
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx), onPressed: () => Navigator.pop(ctx),

View File

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

View File

@ -1,16 +1,20 @@
import 'dart:ui';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart'; import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:media_kit/media_kit.dart';
import 'package:protocol_handler/protocol_handler.dart'; import 'package:protocol_handler/protocol_handler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:solian/bootstrapper.dart'; import 'package:solian/bootstrapper.dart';
import 'package:solian/firebase_options.dart'; import 'package:solian/firebase_options.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/providers/attachment_uploader.dart'; import 'package:solian/providers/attachment_uploader.dart';
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/stickers.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
@ -29,32 +33,28 @@ import 'package:solian/translations.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
void main() async { void main() async {
await SentryFlutter.init( WidgetsFlutterBinding.ensureInitialized();
(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();
await Future.wait([ await Future.wait([
_initializeFirebase(), _initializeFirebase(),
_initializePlatformComponents(), _initializePlatformComponents(),
]); ]);
GoRouter.optionURLReflectsImperativeAPIs = true; GoRouter.optionURLReflectsImperativeAPIs = true;
usePathUrlStrategy(); usePathUrlStrategy();
runApp(const SolianApp()); runApp(const SolianApp());
},
);
} }
Future<void> _initializeFirebase() async { Future<void> _initializeFirebase() async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); 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 { Future<void> _initializePlatformComponents() async {
@ -107,7 +107,7 @@ class SolianApp extends StatelessWidget {
return SystemShell( return SystemShell(
child: ScaffoldMessenger( child: ScaffoldMessenger(
child: BootstrapperShell( child: BootstrapperShell(
child: child ?? const SizedBox(), child: child ?? const SizedBox.shrink(),
), ),
), ),
); );
@ -129,5 +129,8 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => RealmProvider()); Get.lazyPut(() => RealmProvider());
Get.lazyPut(() => ChatCallProvider()); Get.lazyPut(() => ChatCallProvider());
Get.lazyPut(() => AttachmentUploaderController()); 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 { class Account {
int id; int id;
DateTime createdAt; DateTime createdAt;
@ -10,6 +15,7 @@ class Account {
dynamic avatar; dynamic avatar;
dynamic banner; dynamic banner;
String description; String description;
AccountProfile? profile;
List<AccountBadge>? badges; List<AccountBadge>? badges;
String? emailAddress; String? emailAddress;
int? externalId; int? externalId;
@ -26,55 +32,19 @@ class Account {
required this.avatar, required this.avatar,
required this.banner, required this.banner,
required this.description, required this.description,
required this.profile,
required this.badges, required this.badges,
required this.emailAddress, required this.emailAddress,
this.externalId, this.externalId,
}); });
factory Account.fromJson(Map<String, dynamic> json) => Account( factory Account.fromJson(Map<String, dynamic> json) =>
id: json['id'], _$AccountFromJson(json);
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'],
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$AccountToJson(this);
'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,
};
} }
@JsonSerializable()
class AccountBadge { class AccountBadge {
int id; int id;
DateTime createdAt; DateTime createdAt;
@ -94,25 +64,40 @@ class AccountBadge {
required this.type, required this.type,
}); });
factory AccountBadge.fromJson(Map<String, dynamic> json) => AccountBadge( factory AccountBadge.fromJson(Map<String, dynamic> json) =>
id: json['id'], _$AccountBadgeFromJson(json);
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'],
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$AccountBadgeToJson(this);
'id': id, }
'account_id': accountId,
'created_at': createdAt.toIso8601String(), @JsonSerializable()
'updated_at': updatedAt.toIso8601String(), class AccountProfile {
'deleted_at': deletedAt?.toIso8601String(), int id;
'metadata': metadata, DateTime createdAt;
'type': type, 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);
} }

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

@ -0,0 +1,110 @@
// 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?,
externalId: (json['external_id'] as num?)?.toInt(),
);
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,
'external_id': instance.externalId,
};
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 { class AccountStatus {
bool isDisturbable; bool isDisturbable;
bool isOnline; bool isOnline;
@ -11,21 +16,13 @@ class AccountStatus {
required this.status, required this.status,
}); });
factory AccountStatus.fromJson(Map<String, dynamic> json) => AccountStatus( factory AccountStatus.fromJson(Map<String, dynamic> json) =>
isDisturbable: json['is_disturbable'], _$AccountStatusFromJson(json);
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,
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$AccountStatusToJson(this);
'is_disturbable': isDisturbable,
'is_online': isOnline,
'last_seen_at': lastSeenAt?.toIso8601String(),
'status': status?.toJson(),
};
} }
@JsonSerializable()
class Status { class Status {
int id; int id;
DateTime createdAt; DateTime createdAt;
@ -53,31 +50,7 @@ class Status {
required this.accountId, required this.accountId,
}); });
factory Status.fromJson(Map<String, dynamic> json) => Status( factory Status.fromJson(Map<String, dynamic> json) => _$StatusFromJson(json);
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'],
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$StatusToJson(this);
'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,
};
} }

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'; 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 { class Attachment {
int id; int id;
DateTime createdAt; DateTime createdAt;
DateTime updatedAt; DateTime updatedAt;
DateTime? deletedAt; DateTime? deletedAt;
String rid;
String uuid; String uuid;
int size; int size;
String name; String name;
String alt; String alt;
String usage;
String mimetype; String mimetype;
String hash; String hash;
int destination; int destination;
bool isAnalyzed; bool isAnalyzed;
bool isUploaded;
Map<String, dynamic>? metadata; Map<String, dynamic>? metadata;
Map<String, dynamic>? fileChunks;
bool isMature; bool isMature;
Account? account; Account? account;
int? accountId; int? accountId;
@ -24,58 +48,25 @@ class Attachment {
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
required this.deletedAt, required this.deletedAt,
required this.rid,
required this.uuid, required this.uuid,
required this.size, required this.size,
required this.name, required this.name,
required this.alt, required this.alt,
required this.usage,
required this.mimetype, required this.mimetype,
required this.hash, required this.hash,
required this.destination, required this.destination,
required this.isAnalyzed, required this.isAnalyzed,
required this.isUploaded,
required this.metadata, required this.metadata,
required this.fileChunks,
required this.isMature, required this.isMature,
required this.account, required this.account,
required this.accountId, required this.accountId,
}); });
factory Attachment.fromJson(Map<String, dynamic> json) => Attachment( factory Attachment.fromJson(Map<String, dynamic> json) =>
id: json['id'], _$AttachmentFromJson(json);
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'],
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$AttachmentToJson(this);
'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,
};
}

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:livekit_client/livekit_client.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
part 'call.g.dart';
@JsonSerializable()
class Call { class Call {
int id; int id;
DateTime createdAt; DateTime createdAt;
@ -10,7 +14,8 @@ class Call {
String externalId; String externalId;
int founderId; int founderId;
int channelId; int channelId;
List<dynamic> participants; @JsonKey(defaultValue: [])
List<dynamic>? participants;
Channel channel; Channel channel;
Call({ Call({
@ -26,32 +31,9 @@ class Call {
required this.channel, required this.channel,
}); });
factory Call.fromJson(Map<String, dynamic> json) => Call( factory Call.fromJson(Map<String, dynamic> json) => _$CallFromJson(json);
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']),
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$CallToJson(this);
'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(),
};
} }
enum ParticipantStatsType { 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/account.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
part 'channel.g.dart';
@JsonSerializable()
class Channel { class Channel {
int id; int id;
DateTime createdAt; DateTime createdAt;
@ -17,6 +21,7 @@ class Channel {
int? realmId; int? realmId;
bool isEncrypted; bool isEncrypted;
@JsonKey(includeFromJson: false, includeToJson: true)
bool isAvailable = false; bool isAvailable = false;
Channel({ Channel({
@ -36,44 +41,22 @@ class Channel {
required this.realmId, required this.realmId,
}); });
factory Channel.fromJson(Map<String, dynamic> json) => Channel( factory Channel.fromJson(Map<String, dynamic> json) =>
id: json['id'], _$ChannelFromJson(json);
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'],
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$ChannelToJson(this);
'id': id,
'created_at': createdAt.toIso8601String(), @override
'updated_at': updatedAt.toIso8601String(), bool operator ==(Object other) {
'deleted_at': deletedAt, if (other is! Channel) return false;
'alias': alias, return id == other.id;
'name': name, }
'description': description,
'type': type, @override
'members': members?.map((e) => e.toJson()).toList(), int get hashCode => id;
'account': account.toJson(),
'account_id': accountId,
'realm': realm?.toJson(),
'realm_id': realmId,
'is_encrypted': isEncrypted,
};
} }
@JsonSerializable()
class ChannelMember { class ChannelMember {
int id; int id;
DateTime createdAt; DateTime createdAt;
@ -95,25 +78,8 @@ class ChannelMember {
required this.notify, required this.notify,
}); });
factory ChannelMember.fromJson(Map<String, dynamic> json) => ChannelMember( factory ChannelMember.fromJson(Map<String, dynamic> json) =>
id: json['id'], _$ChannelMemberFromJson(json);
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'],
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$ChannelMemberToJson(this);
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt,
'channel_id': channelId,
'account_id': accountId,
'account': account.toJson(),
'notify': notify,
};
} }

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'; import 'package:solian/models/channel.dart';
part 'event.g.dart';
@JsonSerializable()
class Event { class Event {
int id; int id;
String uuid; String uuid;
@ -10,10 +13,11 @@ class Event {
Map<String, dynamic> body; Map<String, dynamic> body;
String type; String type;
Channel? channel; Channel? channel;
Sender sender; ChannelMember sender;
int channelId; int channelId;
int senderId; int senderId;
@JsonKey(includeFromJson: false, includeToJson: true)
bool isPending = false; bool isPending = false;
Event({ Event({
@ -30,40 +34,18 @@ class Event {
required this.senderId, required this.senderId,
}); });
factory Event.fromJson(Map<String, dynamic> json) => Event( factory Event.fromJson(Map<String, dynamic> json) => _$EventFromJson(json);
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'],
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$EventToJson(this);
'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,
};
} }
@JsonSerializable()
class EventMessageBody { class EventMessageBody {
@JsonKey(defaultValue: '')
String text; String text;
@JsonKey(defaultValue: 'plain')
String algorithm; String algorithm;
List<int>? attachments; List<String>? attachments;
int? quoteEvent; int? quoteEvent;
int? relatedEvent; int? relatedEvent;
List<int>? relatedUsers; List<int>? relatedUsers;
@ -78,69 +60,7 @@ class EventMessageBody {
}); });
factory EventMessageBody.fromJson(Map<String, dynamic> json) => factory EventMessageBody.fromJson(Map<String, dynamic> json) =>
EventMessageBody( _$EventMessageBodyFromJson(json);
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,
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$EventMessageBodyToJson(this);
'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,
};
} }

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 { class Notification {
int id; int id;
DateTime createdAt; DateTime createdAt;
@ -25,35 +30,8 @@ class Notification {
required this.accountId, required this.accountId,
}); });
factory Notification.fromJson(Map<String, dynamic> json) => Notification( factory Notification.fromJson(Map<String, dynamic> json) =>
id: json['id'] ?? 0, _$NotificationFromJson(json);
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'],
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$NotificationToJson(this);
'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,
};
} }

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 { class NetworkPackage {
@JsonKey(name: 'w')
String method; String method;
@JsonKey(name: 'e')
String? endpoint;
@JsonKey(name: 'm')
String? message; String? message;
@JsonKey(name: 'p')
Map<String, dynamic>? payload; Map<String, dynamic>? payload;
NetworkPackage({ NetworkPackage({
required this.method, required this.method,
this.endpoint,
this.message, this.message,
this.payload, this.payload,
}); });
factory NetworkPackage.fromJson(Map<String, dynamic> json) => NetworkPackage( factory NetworkPackage.fromJson(Map<String, dynamic> json) =>
method: json['w'], _$NetworkPackageFromJson(json);
message: json['m'],
payload: json['p'],
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$NetworkPackageToJson(this);
'w': method,
'm': message,
'p': payload,
};
} }

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 { class PaginationResult {
int count; int count;
List<dynamic>? data; List<dynamic>? data;
@ -8,10 +13,7 @@ class PaginationResult {
}); });
factory PaginationResult.fromJson(Map<String, dynamic> json) => factory PaginationResult.fromJson(Map<String, dynamic> json) =>
PaginationResult(count: json['count'], data: json['data']); _$PaginationResultFromJson(json);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$PaginationResultToJson(this);
'count': count,
'data': data,
};
} }

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/account.dart';
import 'package:solian/models/feed.dart'; import 'package:solian/models/post_categories.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
part 'post.g.dart';
@JsonSerializable()
class Post { class Post {
int id; int id;
DateTime createdAt; DateTime createdAt;
DateTime updatedAt; DateTime updatedAt;
DateTime? editedAt; DateTime? editedAt;
DateTime? deletedAt; DateTime? deletedAt;
String? alias;
String? areaAlias;
dynamic body; dynamic body;
List<Tag>? tags; List<Tag>? tags;
List<Category>? categories; List<Category>? categories;
@ -33,6 +39,8 @@ class Post {
required this.updatedAt, required this.updatedAt,
required this.editedAt, required this.editedAt,
required this.deletedAt, required this.deletedAt,
required this.alias,
required this.areaAlias,
required this.type, required this.type,
required this.body, required this.body,
required this.tags, required this.tags,
@ -53,77 +61,15 @@ class Post {
required this.metric, required this.metric,
}); });
factory Post.fromJson(Map<String, dynamic> json) => Post( factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
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,
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$PostToJson(this);
'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(),
};
} }
@JsonSerializable()
class PostMetric { class PostMetric {
int reactionCount; int reactionCount;
@JsonKey(defaultValue: {})
Map<String, int> reactionList; Map<String, int> reactionList;
int replyCount; int replyCount;
@ -133,22 +79,8 @@ class PostMetric {
required this.replyCount, required this.replyCount,
}); });
factory PostMetric.fromJson(Map<String, dynamic> json) => PostMetric( factory PostMetric.fromJson(Map<String, dynamic> json) =>
reactionCount: json['reaction_count'], _$PostMetricFromJson(json);
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>()
: {},
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$PostMetricToJson(this);
'reaction_count': reactionCount,
'reply_count': replyCount,
'reaction_list': reactionList,
};
} }

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

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'; import 'package:solian/models/account.dart';
part 'relations.g.dart';
@JsonSerializable()
class Relationship { class Relationship {
int id; int id;
DateTime createdAt; DateTime createdAt;
@ -23,27 +27,8 @@ class Relationship {
required this.status, required this.status,
}); });
factory Relationship.fromJson(Map<String, dynamic> json) => Relationship( factory Relationship.fromJson(Map<String, dynamic> json) =>
id: json['id'], _$RelationshipFromJson(json);
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'],
);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => _$RelationshipToJson(this);
'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,
};
} }

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

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

View File

@ -1,24 +1,27 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:collection';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:cross_file/cross_file.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path/path.dart' show basename;
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/providers/content/attachment.dart'; import 'package:solian/providers/content/attachment.dart';
class AttachmentUploadTask { class AttachmentUploadTask {
File file; XFile file;
String usage; String pool;
Map<String, dynamic>? metadata; Map<String, dynamic>? metadata;
Map<String, int>? chunkFiles;
double progress = 0; double? progress;
bool isUploading = false; bool isUploading = false;
bool isCompleted = false; bool isCompleted = false;
dynamic error; dynamic error;
AttachmentUploadTask({ AttachmentUploadTask({
required this.file, required this.file,
required this.usage, required this.pool,
this.metadata, this.metadata,
}); });
} }
@ -73,32 +76,36 @@ class AttachmentUploaderController extends GetxController {
_startProgressSyncTimer(); _startProgressSyncTimer();
queueOfUpload[queueIndex].isUploading = true; queueOfUpload[queueIndex].isUploading = true;
queueOfUpload[queueIndex].progress = 0;
final task = queueOfUpload[queueIndex]; final task = queueOfUpload[queueIndex];
final result = await _rawUploadAttachment( try {
await task.file.readAsBytes(), final result = await _chunkedUploadAttachment(
task.file.path, task.file,
task.usage, task.pool,
null, null,
onProgress: (value) { onData: (_) {},
queueOfUpload[queueIndex].progress = value; onProgress: (progress) {
_progressOfUpload = value; queueOfUpload[queueIndex].progress = progress;
}, _progressOfUpload = progress;
onError: (err) { },
queueOfUpload[queueIndex].error = err; );
queueOfUpload[queueIndex].isUploading = false; 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) { isUploading.value = false;
queueOfUpload.removeAt(queueIndex);
} }
_stopProgressSyncTimer();
_syncProgress();
isUploading.value = false; return null;
return result;
} }
Future<void> performUploadQueue({ Future<void> performUploadQueue({
@ -115,100 +122,126 @@ class AttachmentUploaderController extends GetxController {
} }
queueOfUpload[idx].isUploading = true; queueOfUpload[idx].isUploading = true;
queueOfUpload[idx].progress = 0;
final task = queueOfUpload[idx]; final task = queueOfUpload[idx];
final result = await _rawUploadAttachment( try {
await task.file.readAsBytes(), final result = await _chunkedUploadAttachment(
task.file.path, task.file,
task.usage, task.pool,
null, null,
onProgress: (value) { onData: (_) {},
queueOfUpload[idx].progress = value; onProgress: (progress) {
_progressOfUpload = (idx + value) / queueOfUpload.length; queueOfUpload[idx].progress = progress;
}, },
onError: (err) { );
queueOfUpload[idx].error = err; if (result != null) onData(result);
queueOfUpload[idx].isUploading = false; } catch (err) {
}, queueOfUpload[idx].error = err;
); queueOfUpload[idx].isUploading = false;
_progressOfUpload = (idx + 1) / queueOfUpload.length; } finally {
if (result != null) onData(result); _progressOfUpload = (idx + 1) / queueOfUpload.length;
}
queueOfUpload[idx].isUploading = false; queueOfUpload[idx].isUploading = false;
queueOfUpload[idx].isCompleted = true; queueOfUpload[idx].isCompleted = true;
} }
queueOfUpload.value = queueOfUpload.removeWhere((x) => x.error == null);
queueOfUpload.where((x) => x.error == null).toList(growable: true);
_stopProgressSyncTimer(); _stopProgressSyncTimer();
_syncProgress(); _syncProgress();
isUploading.value = false; isUploading.value = false;
} }
Future<void> uploadAttachmentWithCallback( Future<Attachment?> uploadAttachmentFromData(
Uint8List data, Uint8List data,
String path, String path,
String usage, String pool,
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,
Map<String, dynamic>? metadata, Map<String, dynamic>? metadata,
) async { ) async {
if (isUploading.value) throw Exception('uploading blocked'); if (isUploading.value) throw Exception('uploading blocked');
isUploading.value = true; isUploading.value = true;
final result = await _rawUploadAttachment(
data,
path,
usage,
metadata,
onProgress: (progress) {
progressOfUpload.value = progress;
},
);
isUploading.value = false;
return result;
}
Future<Attachment?> _rawUploadAttachment( final AttachmentProvider attach = Get.find();
Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
{Function(double)? onProgress, Function(dynamic err)? onError}) async {
final AttachmentProvider provider = Get.find();
try { try {
final result = await provider.createAttachment( final result = await attach.createAttachmentDirectly(
data, data,
path, path,
usage, pool,
metadata, metadata,
onProgress: onProgress,
); );
return result; return result;
} catch (err) { } catch (_) {
if (onError != null) {
onError(err);
}
return null; 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.dart';
import 'package:get/get_connect/http/src/request/request.dart'; import 'package:get/get_connect/http/src/request/request.dart';
import 'package:solian/controllers/chat_events_controller.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/providers/websocket.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@ -81,7 +83,7 @@ class AuthProvider extends GetConnect {
'grant_type': 'refresh_token', 'grant_type': 'refresh_token',
}); });
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw RequestException(resp);
} }
credentials = TokenSet( credentials = TokenSet(
accessToken: resp.body['access_token'], accessToken: resp.body['access_token'],
@ -128,7 +130,7 @@ class AuthProvider extends GetConnect {
} }
Future<void> ensureCredentials() async { Future<void> ensureCredentials() async {
if (isAuthorized.isFalse) throw Exception('unauthorized'); if (isAuthorized.isFalse) throw const UnauthorizedException();
if (credentials == null) await loadCredentials(); if (credentials == null) await loadCredentials();
if (credentials!.isExpired) { if (credentials!.isExpired) {
@ -158,7 +160,7 @@ class AuthProvider extends GetConnect {
'password': password, 'password': password,
}); });
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.body); throw RequestException(resp);
} else if (resp.body['is_finished'] == false) { } else if (resp.body['is_finished'] == false) {
throw RiskyAuthenticateException(resp.body['ticket']['id']); throw RiskyAuthenticateException(resp.body['ticket']['id']);
} }
@ -218,7 +220,7 @@ class AuthProvider extends GetConnect {
final client = configureClient('auth'); final client = configureClient('auth');
final resp = await client.get('/users/me'); final resp = await client.get('/users/me');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw RequestException(resp);
} }
userProfile.value = resp.body; userProfile.value = resp.body;

View File

@ -2,6 +2,8 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.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:livekit_client/livekit_client.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:solian/models/call.dart'; import 'package:solian/models/call.dart';
@ -17,6 +19,10 @@ class ChatCallProvider extends GetxController {
RxBool isReady = false.obs; RxBool isReady = false.obs;
RxBool isMounted = false.obs; RxBool isMounted = false.obs;
RxBool isInitialized = false.obs; RxBool isInitialized = false.obs;
RxBool isBusy = false.obs;
RxString lastDuration = '00:00:00'.obs;
Timer? lastDurationUpdateTimer;
String? token; String? token;
String? endpoint; String? endpoint;
@ -38,6 +44,34 @@ class ChatCallProvider extends GetxController {
RxList<ParticipantTrack> participantTracks = RxList.empty(growable: true); RxList<ParticipantTrack> participantTracks = RxList.empty(growable: true);
Rx<ParticipantTrack?> focusTrack = Rx(null); 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 { Future<void> checkPermissions() async {
if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) { if (lkPlatformIs(PlatformType.macOS) || lkPlatformIs(PlatformType.linux)) {
return; return;
@ -56,7 +90,7 @@ class ChatCallProvider extends GetxController {
Future<(String, String)> getRoomToken() async { Future<(String, String)> getRoomToken() async {
final AuthProvider auth = Get.find(); 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 client = auth.configureClient('messaging');
@ -69,7 +103,7 @@ class ChatCallProvider extends GetxController {
endpoint = 'wss://${resp.body['endpoint']}'; endpoint = 'wss://${resp.body['endpoint']}';
return (token!, endpoint!); return (token!, endpoint!);
} else { } else {
throw Exception(resp.bodyString); throw RequestException(resp);
} }
} }
@ -88,7 +122,30 @@ class ChatCallProvider extends GetxController {
void initRoom() { void initRoom() {
initHardware(); 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(); listener = room.createListener();
WakelockPlus.enable(); WakelockPlus.enable();
} }
@ -96,36 +153,12 @@ class ChatCallProvider extends GetxController {
void joinRoom(String url, String token) async { void joinRoom(String url, String token) async {
if (isMounted.value) { if (isMounted.value) {
return; return;
} else {
isMounted.value = true;
} }
try { try {
await room.connect( await room.connect(
url, url,
token, 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( fastConnectOptions: FastConnectOptions(
microphone: TrackOption(track: audioTrack.value), microphone: TrackOption(track: audioTrack.value),
camera: TrackOption(track: videoTrack.value), camera: TrackOption(track: videoTrack.value),
@ -133,6 +166,8 @@ class ChatCallProvider extends GetxController {
); );
} catch (e) { } catch (e) {
rethrow; rethrow;
} finally {
isMounted.value = true;
} }
} }
@ -152,7 +187,7 @@ class ChatCallProvider extends GetxController {
void onRoomDidUpdate() => sortParticipants(); void onRoomDidUpdate() => sortParticipants();
void setupRoom() { void setupRoom() {
if(isInitialized.value) return; if (isInitialized.value) return;
sortParticipants(); sortParticipants();
room.addListener(onRoomDidUpdate); room.addListener(onRoomDidUpdate);
@ -164,6 +199,7 @@ class ChatCallProvider extends GetxController {
Hardware.instance.setSpeakerphoneOn(true); Hardware.instance.setSpeakerphoneOn(true);
} }
isBusy.value = false;
isInitialized.value = true; isInitialized.value = true;
} }
@ -366,6 +402,7 @@ class ChatCallProvider extends GetxController {
} }
void disposeRoom() { void disposeRoom() {
isBusy.value = false;
isMounted.value = false; isMounted.value = false;
isInitialized.value = false; isInitialized.value = false;
current.value = null; current.value = null;

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
import 'package:get/get.dart'; 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/models/realm.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
@ -8,7 +10,7 @@ class RealmProvider extends GetxController {
Future<void> refreshAvailableRealms() async { Future<void> refreshAvailableRealms() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized'); if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
isLoading.value = true; isLoading.value = true;
final resp = await listAvailableRealm(); final resp = await listAvailableRealm();
@ -21,13 +23,13 @@ class RealmProvider extends GetxController {
Future<Response> getRealm(String alias) async { Future<Response> getRealm(String alias) async {
final AuthProvider auth = Get.find(); 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 client = auth.configureClient('auth');
final resp = await client.get('/realms/$alias'); final resp = await client.get('/realms/$alias');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw RequestException(resp);
} }
return resp; return resp;
@ -35,13 +37,13 @@ class RealmProvider extends GetxController {
Future<Response> listAvailableRealm() async { Future<Response> listAvailableRealm() async {
final AuthProvider auth = Get.find(); 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 client = auth.configureClient('auth');
final resp = await client.get('/realms/me/available'); final resp = await client.get('/realms/me/available');
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw RequestException(resp);
} }
return 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:floor/floor.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart'; import 'package:solian/models/event.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
@ -16,6 +17,27 @@ Future<MessageHistoryDb> createHistoryDb() async {
.addMigrations([migration1to2]).build(); .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 { Future<Event?> getRemoteEvent(int id, Channel channel, String scope) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null; if (auth.isAuthorized.isFalse) return null;
@ -29,20 +51,20 @@ Future<Event?> getRemoteEvent(int id, Channel channel, String scope) async {
if (resp.statusCode == 404) { if (resp.statusCode == 404) {
return null; return null;
} else if (resp.statusCode != 200) { } else if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw RequestException(resp);
} }
return Event.fromJson(resp.body); return Event.fromJson(resp.body);
} }
Future<(List<Event>, int)?> getRemoteEvents( Future<(List<Event>, int)?> getRemoteEvents(
Channel channel, Channel channel,
String scope, { String scope, {
required int remainDepth, required int remainDepth,
bool Function(List<Event> items)? onBrake, bool Function(List<Event> items)? onBrake,
take = 10, take = 10,
offset = 0, offset = 0,
}) async { }) async {
if (remainDepth <= 0) { if (remainDepth <= 0) {
return null; return null;
} }
@ -57,7 +79,7 @@ Future<(List<Event>, int)?> getRemoteEvents(
); );
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw RequestException(resp);
} }
final PaginationResult response = PaginationResult.fromJson(resp.body); final PaginationResult response = PaginationResult.fromJson(resp.body);
@ -69,13 +91,13 @@ Future<(List<Event>, int)?> getRemoteEvents(
} }
final expandResult = (await getRemoteEvents( final expandResult = (await getRemoteEvents(
channel, channel,
scope, scope,
remainDepth: remainDepth - 1, remainDepth: remainDepth - 1,
take: take, take: take,
offset: offset + result.length, offset: offset + result.length,
)) ))
?.$1 ?? ?.$1 ??
List.empty(); List.empty();
return ([...result, ...expandResult], response.count); return ([...result, ...expandResult], response.count);

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/notification.dart'; import 'package:solian/models/notification.dart';
import 'package:solian/models/packet.dart'; import 'package:solian/models/packet.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
@ -50,31 +51,31 @@ class WebSocketProvider extends GetxController {
} }
final AuthProvider auth = Get.find(); 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 { 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); websocket = WebSocketChannel.connect(uri);
await websocket?.ready; await websocket?.ready;
} catch (e) { listen();
isConnected.value = true;
} catch (err) {
log('Unable connect dealer via websocket... $err');
if (!noRetry) { if (!noRetry) {
await auth.refreshCredentials(); await auth.refreshCredentials();
return connect(noRetry: true); return connect(noRetry: true);
} }
} finally {
isConnecting.value = false;
} }
listen();
isConnected.value = true;
isConnecting.value = false;
} }
void disconnect() { void disconnect() {
@ -87,6 +88,7 @@ class WebSocketProvider extends GetxController {
websocket?.stream.listen( websocket?.stream.listen(
(event) { (event) {
final packet = NetworkPackage.fromJson(jsonDecode(event)); final packet = NetworkPackage.fromJson(jsonDecode(event));
log('Websocket incoming message: ${packet.method} ${packet.message}');
stream.sink.add(packet); stream.sink.add(packet);
}, },
onDone: () { onDone: () {
@ -147,8 +149,8 @@ class WebSocketProvider extends GetxController {
'device_token': token, 'device_token': token,
'device_id': deviceUuid, 'device_id': deviceUuid,
}); });
if (resp.statusCode != 200) { if (resp.statusCode != 200 && resp.statusCode != 400) {
throw Exception(resp.bodyString); 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_detail.dart';
import 'package:solian/screens/channel/channel_organize.dart'; import 'package:solian/screens/channel/channel_organize.dart';
import 'package:solian/screens/chat.dart'; import 'package:solian/screens/chat.dart';
import 'package:solian/screens/dashboard.dart';
import 'package:solian/screens/feed/search.dart'; import 'package:solian/screens/feed/search.dart';
import 'package:solian/screens/posts/post_detail.dart'; import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/screens/feed/draft_box.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_detail.dart';
import 'package:solian/screens/realms/realm_organize.dart'; import 'package:solian/screens/realms/realm_organize.dart';
import 'package:solian/screens/realms/realm_view.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/posts/post_editor.dart';
import 'package:solian/screens/settings.dart'; import 'package:solian/screens/settings.dart';
import 'package:solian/shells/root_shell.dart'; import 'package:solian/shells/root_shell.dart';
@ -34,6 +35,14 @@ abstract class AppRouter {
child: child, child: child,
), ),
routes: [ routes: [
GoRoute(
path: '/',
name: 'dashboard',
builder: (context, state) => TitleShell(
state: state,
child: const DashboardScreen(),
),
),
_feedRoute, _feedRoute,
_chatRoute, _chatRoute,
_realmRoute, _realmRoute,
@ -63,9 +72,9 @@ abstract class AppRouter {
builder: (context, state, child) => child, builder: (context, state, child) => child,
routes: [ routes: [
GoRoute( GoRoute(
path: '/', path: '/feed',
name: 'home', name: 'feed',
builder: (context, state) => const HomeScreen(), builder: (context, state) => const FeedScreen(),
), ),
GoRoute( GoRoute(
path: '/feed/search', path: '/feed/search',
@ -104,7 +113,6 @@ abstract class AppRouter {
reply: arguments?.reply, reply: arguments?.reply,
repost: arguments?.repost, repost: arguments?.repost,
realm: arguments?.realm, realm: arguments?.realm,
postListController: arguments?.postListController,
mode: int.tryParse(state.uri.queryParameters['mode'] ?? '0') ?? 0, mode: int.tryParse(state.uri.queryParameters['mode'] ?? '0') ?? 0,
), ),
transitionsBuilder: transitionsBuilder:

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -21,7 +22,7 @@ class AboutScreen extends StatelessWidget {
borderRadius: const BorderRadius.all(Radius.circular(16)), borderRadius: const BorderRadius.all(Radius.circular(16)),
child: Image.asset('assets/logo.png', width: 120, height: 120), child: Image.asset('assets/logo.png', width: 120, height: 120),
), ),
const SizedBox(height: 8), const Gap(8),
Text( Text(
'Solian', 'Solian',
style: Theme.of(context).textTheme.headlineMedium, style: Theme.of(context).textTheme.headlineMedium,
@ -30,12 +31,12 @@ class AboutScreen extends StatelessWidget {
'The Solar Network', 'The Solar Network',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
), ),
const SizedBox(height: 8), const Gap(8),
FutureBuilder( FutureBuilder(
future: PackageInfo.fromPlatform(), future: PackageInfo.fromPlatform(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const SizedBox(); return const SizedBox.shrink();
} }
return Text( return Text(
@ -45,7 +46,7 @@ class AboutScreen extends StatelessWidget {
}, },
), ),
Text('Copyright © ${DateTime.now().year} Solsynth LLC'), Text('Copyright © ${DateTime.now().year} Solsynth LLC'),
const SizedBox(height: 16), const Gap(16),
TextButton( TextButton(
style: denseButtonStyle, style: denseButtonStyle,
child: const Text('App Details'), 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.', '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( applicationIcon: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16)), 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'); launchUrlString('https://solsynth.dev/products/solar-network');
}, },
), ),
const SizedBox(height: 16), const Gap(16),
const Text( const Text(
'Open-sourced under AGPLv3', 'Open-sourced under AGPLv3',
style: TextStyle( style: TextStyle(

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/controllers/post_list_controller.dart'; import 'package:solian/controllers/post_list_controller.dart';
@ -7,10 +8,12 @@ import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_heading.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/posts/post_list.dart'; import 'package:solian/widgets/posts/post_list.dart';
@ -83,10 +86,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
} }
int get _userSocialCreditPoints { int get _userSocialCreditPoints {
int birthPart = return _totalUpvote * 2 - _totalDownvote + _postController.postTotal.value;
DateTime.now().difference(_userinfo!.createdAt.toLocal()).inSeconds;
birthPart = birthPart >> 16;
return _totalUpvote * 2 - _totalDownvote + birthPart;
} }
@override @override
@ -96,8 +96,9 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
_postController = PostListController(author: widget.name); _postController = PostListController(author: widget.name);
_albumPagingController.addPageRequestListener((pageKey) async { _albumPagingController.addPageRequestListener((pageKey) async {
final client = ServiceFinder.configureClient('files'); final client = ServiceFinder.configureClient('files');
final resp = await client final resp = await client.get(
.get('/attachments?take=10&offset=$pageKey&author=${widget.name}'); '/attachments?take=10&offset=$pageKey&author=${widget.name}&original=true',
);
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body); final result = PaginationResult.fromJson(resp.body);
final out = result.data final out = result.data
@ -144,7 +145,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
return Material( return Material(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: DefaultTabController( child: DefaultTabController(
length: 2, length: 3,
child: NestedScrollView( child: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [ return [
@ -156,12 +157,11 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
flexibleSpace: Row( flexibleSpace: Row(
children: [ children: [
AppBarLeadingButton.adaptive(context) ?? AppBarLeadingButton.adaptive(context) ?? const Gap(8),
const SizedBox(width: 8), const Gap(8),
const SizedBox(width: 8),
if (_userinfo != null) if (_userinfo != null)
AccountAvatar(content: _userinfo!.avatar, radius: 16), AccountAvatar(content: _userinfo!.avatar, radius: 16),
const SizedBox(width: 12), const Gap(12),
Expanded( Expanded(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -213,6 +213,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
), ),
bottom: TabBar( bottom: TabBar(
tabs: [ tabs: [
Tab(text: 'profilePage'.tr),
Tab(text: 'profilePosts'.tr), Tab(text: 'profilePosts'.tr),
Tab(text: 'profileAlbum'.tr), Tab(text: 'profileAlbum'.tr),
], ],
@ -223,6 +224,24 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
body: TabBarView( body: TabBarView(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
children: [ children: [
Column(
children: [
const Gap(16),
AccountHeadingWidget(
name: _userinfo!.name,
nick: _userinfo!.nick,
desc: _userinfo!.description,
badges: _userinfo!.badges,
banner: _userinfo!.banner,
avatar: _userinfo!.avatar,
status: Get.find<StatusProvider>()
.getSomeoneStatus(_userinfo!.name),
detail: _userinfo,
profile: _userinfo!.profile,
extraWidgets: const [],
),
],
),
RefreshIndicator( RefreshIndicator(
onRefresh: () => Future.wait([ onRefresh: () => Future.wait([
_postController.reloadAllOver(), _postController.reloadAllOver(),
@ -243,7 +262,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
), ),
], ],
), ),
const SizedBox(height: 16), const Gap(16),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
@ -300,6 +319,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
PostWarpedListWidget( PostWarpedListWidget(
isPinned: false, isPinned: false,
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(),
), ),
]), ]),
), ),

View File

@ -1,13 +1,13 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/models/stickers.dart'; import 'package:solian/models/stickers.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/stickers.dart'; import 'package:solian/providers/stickers.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/auto_cache_image.dart';
import 'package:solian/widgets/stickers/sticker_uploader.dart'; import 'package:solian/widgets/stickers/sticker_uploader.dart';
class StickerScreen extends StatefulWidget { class StickerScreen extends StatefulWidget {
@ -66,11 +66,11 @@ class _StickerScreenState extends State<StickerScreen> {
Widget _buildEmoteEntry(Sticker item, String prefix) { Widget _buildEmoteEntry(Sticker item, String prefix) {
final imageUrl = ServiceFinder.buildUrl( final imageUrl = ServiceFinder.buildUrl(
'files', 'files',
'/attachments/${item.attachmentId}', '/attachments/${item.attachment.rid}',
); );
return ListTile( return ListTile(
title: Text(item.name), title: Text(item.name),
subtitle: Text(':${'$prefix${item.alias}'.camelCase}:'), subtitle: Text(item.textWarpedPlaceholder),
contentPadding: const EdgeInsets.only(left: 16, right: 14), contentPadding: const EdgeInsets.only(left: 16, right: 14),
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -93,17 +93,12 @@ class _StickerScreenState extends State<StickerScreen> {
), ),
], ],
), ),
leading: PlatformInfo.canCacheImage leading: AutoCacheImage(
? CachedNetworkImage( imageUrl,
imageUrl: imageUrl, width: 28,
width: 28, height: 28,
height: 28, noErrorWidget: true,
) ),
: Image.network(
imageUrl,
width: 28,
height: 28,
),
); );
} }
@ -159,15 +154,25 @@ class _StickerScreenState extends State<StickerScreen> {
builderDelegate: PagedChildBuilderDelegate( builderDelegate: PagedChildBuilderDelegate(
itemBuilder: (BuildContext context, item, int index) { itemBuilder: (BuildContext context, item, int index) {
return ExpansionTile( return ExpansionTile(
title: Text(item.name), title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(item.name),
const Gap(6),
Badge(
label: Text('#${item.id}'),
)
],
),
subtitle: Text( subtitle: Text(
item.description, item.description,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
children: item.stickers children: item.stickers?.map((x) {
?.map((x) => _buildEmoteEntry(x, item.prefix)) x.pack = item;
.toList() ?? return _buildEmoteEntry(x, item.prefix);
}).toList() ??
List.empty(), List.empty(),
); );
}, },

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