Compare commits

...

78 Commits

Author SHA1 Message Date
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
19eabfaba1 🚀 Launch 1.2.1+2 2024-08-04 13:27:14 +08:00
ec2eadad6d 🐛 Fix bootstrapper icon issue 2024-08-04 12:59:13 +08:00
54e176e75d 🐛 Fix post editor cannot reply either repost 2024-08-04 12:55:05 +08:00
0a7ccaeefa 🐛 Fix attachment editor title overflow 2024-08-04 12:23:39 +08:00
a5f093e185 🐛 Fix unauthorized wont load stickers 2024-08-04 11:10:25 +08:00
a4f68dd175 🚀 Launch 1.2.1+1 2024-08-04 01:54:35 +08:00
8067c35c70 Follow the manifest to load emotes 2024-08-04 01:53:52 +08:00
ebe381053e Load emojis 2024-08-04 01:37:54 +08:00
03f2470dae Basic sticker management 2024-08-04 01:03:09 +08:00
ea434815cf Create sticker
 Single file mode attachment editor and more options
2024-08-03 21:29:48 +08:00
bbea4b4359 🍱 Update app icons 2024-08-03 17:44:36 +08:00
e0b485cc81 🐛 Fix mis-style 2024-08-03 14:00:52 +08:00
87bb37ac01 ⚗️ Markdown embed content 2024-08-03 12:29:13 +08:00
126 changed files with 3599 additions and 1327 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"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 24 KiB

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
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 360 KiB

View File

@ -38,24 +38,78 @@ PODS:
- file_picker (0.0.1): - file_picker (0.0.1):
- DKImagePickerController/PhotoGallery - DKImagePickerController/PhotoGallery
- Flutter - Flutter
- Firebase/Analytics (10.29.0):
- Firebase/Core
- Firebase/Core (10.29.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 10.29.0)
- Firebase/CoreOnly (10.29.0): - Firebase/CoreOnly (10.29.0):
- FirebaseCore (= 10.29.0) - FirebaseCore (= 10.29.0)
- Firebase/Crashlytics (10.29.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 10.29.0)
- Firebase/Messaging (10.29.0): - Firebase/Messaging (10.29.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 10.29.0) - FirebaseMessaging (~> 10.29.0)
- Firebase/Performance (10.29.0):
- Firebase/CoreOnly
- FirebasePerformance (~> 10.29.0)
- firebase_analytics (11.2.1):
- Firebase/Analytics (= 10.29.0)
- firebase_core
- Flutter
- firebase_core (3.3.0): - firebase_core (3.3.0):
- Firebase/CoreOnly (= 10.29.0) - Firebase/CoreOnly (= 10.29.0)
- Flutter - Flutter
- firebase_crashlytics (4.0.4):
- Firebase/Crashlytics (= 10.29.0)
- firebase_core
- Flutter
- firebase_messaging (15.0.4): - firebase_messaging (15.0.4):
- Firebase/Messaging (= 10.29.0) - Firebase/Messaging (= 10.29.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_performance (0.10.0-4):
- Firebase/Performance (= 10.29.0)
- firebase_core
- Flutter
- FirebaseABTesting (10.29.0):
- FirebaseCore (~> 10.0)
- FirebaseAnalytics (10.29.0):
- FirebaseAnalytics/AdIdSupport (= 10.29.0)
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 7.11)
- GoogleUtilities/MethodSwizzler (~> 7.11)
- GoogleUtilities/Network (~> 7.11)
- "GoogleUtilities/NSData+zlib (~> 7.11)"
- nanopb (< 2.30911.0, >= 2.30908.0)
- FirebaseAnalytics/AdIdSupport (10.29.0):
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleAppMeasurement (= 10.29.0)
- GoogleUtilities/AppDelegateSwizzler (~> 7.11)
- GoogleUtilities/MethodSwizzler (~> 7.11)
- GoogleUtilities/Network (~> 7.11)
- "GoogleUtilities/NSData+zlib (~> 7.11)"
- nanopb (< 2.30911.0, >= 2.30908.0)
- FirebaseCore (10.29.0): - FirebaseCore (10.29.0):
- FirebaseCoreInternal (~> 10.0) - FirebaseCoreInternal (~> 10.0)
- GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Environment (~> 7.12)
- GoogleUtilities/Logger (~> 7.12) - GoogleUtilities/Logger (~> 7.12)
- FirebaseCoreExtension (10.29.0):
- FirebaseCore (~> 10.0)
- FirebaseCoreInternal (10.29.0): - FirebaseCoreInternal (10.29.0):
- "GoogleUtilities/NSData+zlib (~> 7.8)" - "GoogleUtilities/NSData+zlib (~> 7.8)"
- FirebaseCrashlytics (10.29.0):
- FirebaseCore (~> 10.5)
- FirebaseInstallations (~> 10.0)
- FirebaseRemoteConfigInterop (~> 10.23)
- FirebaseSessions (~> 10.5)
- GoogleDataTransport (~> 9.2)
- GoogleUtilities/Environment (~> 7.8)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesObjC (~> 2.1)
- FirebaseInstallations (10.29.0): - FirebaseInstallations (10.29.0):
- FirebaseCore (~> 10.0) - FirebaseCore (~> 10.0)
- GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Environment (~> 7.8)
@ -70,7 +124,39 @@ PODS:
- GoogleUtilities/Reachability (~> 7.8) - GoogleUtilities/Reachability (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8)
- nanopb (< 2.30911.0, >= 2.30908.0) - nanopb (< 2.30911.0, >= 2.30908.0)
- FirebasePerformance (10.29.0):
- FirebaseCore (~> 10.5)
- FirebaseInstallations (~> 10.0)
- FirebaseRemoteConfig (~> 10.0)
- FirebaseSessions (~> 10.5)
- GoogleDataTransport (~> 9.2)
- GoogleUtilities/Environment (~> 7.13)
- GoogleUtilities/ISASwizzler (~> 7.13)
- GoogleUtilities/MethodSwizzler (~> 7.13)
- GoogleUtilities/UserDefaults (~> 7.13)
- nanopb (< 2.30911.0, >= 2.30908.0)
- FirebaseRemoteConfig (10.29.0):
- FirebaseABTesting (~> 10.0)
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
- FirebaseRemoteConfigInterop (~> 10.23)
- FirebaseSharedSwift (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- "GoogleUtilities/NSData+zlib (~> 7.8)"
- FirebaseRemoteConfigInterop (10.29.0)
- FirebaseSessions (10.29.0):
- FirebaseCore (~> 10.5)
- FirebaseCoreExtension (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleDataTransport (~> 9.2)
- GoogleUtilities/Environment (~> 7.13)
- GoogleUtilities/UserDefaults (~> 7.13)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesSwift (~> 2.1)
- FirebaseSharedSwift (10.29.0)
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_keyboard_visibility (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,6 +165,26 @@ PODS:
- gal (1.0.0): - gal (1.0.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- GoogleAppMeasurement (10.29.0):
- GoogleAppMeasurement/AdIdSupport (= 10.29.0)
- GoogleUtilities/AppDelegateSwizzler (~> 7.11)
- GoogleUtilities/MethodSwizzler (~> 7.11)
- GoogleUtilities/Network (~> 7.11)
- "GoogleUtilities/NSData+zlib (~> 7.11)"
- nanopb (< 2.30911.0, >= 2.30908.0)
- GoogleAppMeasurement/AdIdSupport (10.29.0):
- GoogleAppMeasurement/WithoutAdIdSupport (= 10.29.0)
- GoogleUtilities/AppDelegateSwizzler (~> 7.11)
- GoogleUtilities/MethodSwizzler (~> 7.11)
- GoogleUtilities/Network (~> 7.11)
- "GoogleUtilities/NSData+zlib (~> 7.11)"
- nanopb (< 2.30911.0, >= 2.30908.0)
- GoogleAppMeasurement/WithoutAdIdSupport (10.29.0):
- GoogleUtilities/AppDelegateSwizzler (~> 7.11)
- GoogleUtilities/MethodSwizzler (~> 7.11)
- GoogleUtilities/Network (~> 7.11)
- "GoogleUtilities/NSData+zlib (~> 7.11)"
- nanopb (< 2.30911.0, >= 2.30908.0)
- GoogleDataTransport (9.4.1): - GoogleDataTransport (9.4.1):
- GoogleUtilities/Environment (~> 7.7) - GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30911.0, >= 2.30908.0) - nanopb (< 2.30911.0, >= 2.30908.0)
@ -91,9 +197,14 @@ PODS:
- GoogleUtilities/Environment (7.13.3): - GoogleUtilities/Environment (7.13.3):
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- PromisesObjC (< 3.0, >= 1.2) - PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/ISASwizzler (7.13.3):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Logger (7.13.3):
- GoogleUtilities/Environment - GoogleUtilities/Environment
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- GoogleUtilities/MethodSwizzler (7.13.3):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/Network (7.13.3): - GoogleUtilities/Network (7.13.3):
- GoogleUtilities/Logger - GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib" - "GoogleUtilities/NSData+zlib"
@ -113,15 +224,9 @@ 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):
- Flutter
- media_kit_native_event_loop (1.0.0):
- Flutter
- media_kit_video (0.0.1):
- Flutter
- nanopb (2.30910.0): - nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0) - nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0) - nanopb/encode (= 2.30910.0)
@ -136,19 +241,16 @@ 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): - SDWebImage (5.19.6):
- Flutter - SDWebImage/Core (= 5.19.6)
- SDWebImage (5.19.4): - SDWebImage/Core (5.19.6)
- SDWebImage/Core (= 5.19.4)
- SDWebImage/Core (5.19.4)
- Sentry/HybridSDK (8.32.0)
- sentry_flutter (8.6.0):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.32.0)
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
@ -161,8 +263,9 @@ PODS:
- TOCropViewController (2.7.4) - TOCropViewController (2.7.4)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
- volume_controller (0.0.1): - video_player_avfoundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS
- wakelock_plus (0.0.1): - wakelock_plus (0.0.1):
- Flutter - Flutter
- WebRTC-SDK (125.6422.04) - WebRTC-SDK (125.6422.04)
@ -171,30 +274,30 @@ 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_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`)
- image_cropper (from `.symlinks/plugins/image_cropper/ios`) - image_cropper (from `.symlinks/plugins/image_cropper/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- livekit_client (from `.symlinks/plugins/livekit_client/ios`) - livekit_client (from `.symlinks/plugins/livekit_client/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- 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`)
- 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`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS: SPEC REPOS:
@ -202,16 +305,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 +336,20 @@ 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_secure_storage: flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios" :path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_webrtc: flutter_webrtc:
@ -241,12 +362,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/image_picker_ios/ios" :path: ".symlinks/plugins/image_picker_ios/ios"
livekit_client: livekit_client:
:path: ".symlinks/plugins/livekit_client/ios" :path: ".symlinks/plugins/livekit_client/ios"
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop:
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
pasteboard: pasteboard:
@ -255,12 +370,10 @@ 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:
: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:
@ -269,8 +382,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/sqflite/darwin" :path: ".symlinks/plugins/sqflite/darwin"
url_launcher_ios: url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios" :path: ".symlinks/plugins/url_launcher_ios/ios"
volume_controller: video_player_avfoundation:
:path: ".symlinks/plugins/volume_controller/ios" :path: ".symlinks/plugins/video_player_avfoundation/darwin"
wakelock_plus: wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios" :path: ".symlinks/plugins/wakelock_plus/ios"
@ -281,42 +394,52 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d
firebase_analytics: 04491d1ee74c8e7c2330c96afc54188a969b06ee
firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb
firebase_crashlytics: e3d3e0c99bad5aaab5908385133dea8ec344693f
firebase_messaging: c862b3d2b973ecc769194dc8de09bd22c77ae757 firebase_messaging: c862b3d2b973ecc769194dc8de09bd22c77ae757
firebase_performance: 8643e815a354ee94da1192cd69335a48a7b625a4
FirebaseABTesting: d87f56707159bae64e269757a6e963d490f2eebe
FirebaseAnalytics: 23717de130b779aa506e757edb9713d24b6ffeda
FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16 FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16
FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f
FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934
FirebaseCrashlytics: 34647b41e18de773717fdd348a22206f2f9bc774
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366 FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366
FirebasePerformance: d0ac4aa90f8c1aedeb8d0329a56e2d77d8d9e004
FirebaseRemoteConfig: 48ef3f243742a8d72422ccfc9f986e19d7de53fd
FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d
FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc
FirebaseSharedSwift: 20530f495084b8d840f78a100d8c5ee613375f6e
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1 gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GoogleAppMeasurement: f9de05ee17401e3355f68e8fc8b5064d429f5918
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
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_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
nanopb: 438bc412db1928dac798aa6fd75726007be04262 nanopb: 438bc412db1928dac798aa6fd75726007be04262
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 SDWebImage: a79252b60f4678812d94316c91da69ec83089c9f
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb
sentry_flutter: 090351ce1ff5f96a4b33ef9455b7e3b28185387d
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3 WebRTC-SDK: c3d69a87e7185fad3568f6f3cff7c9ac5890acf3

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,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 B

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 815 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.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';
@ -9,6 +8,7 @@ import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@ -112,13 +112,15 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
label: 'bsPreparingData', label: 'bsPreparingData',
action: () async { action: () async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isTrue) {
await Future.wait([ await Future.wait([
Get.find<RealmProvider>().refreshAvailableRealms(), Get.find<StickerProvider>().refreshAvailableStickers(),
if (auth.isAuthorized.isTrue)
Get.find<ChannelProvider>().refreshAvailableChannel(), Get.find<ChannelProvider>().refreshAvailableChannel(),
if (auth.isAuthorized.isTrue)
Get.find<RelationshipProvider>().refreshRelativeList(), Get.find<RelationshipProvider>().refreshRelativeList(),
if (auth.isAuthorized.isTrue)
Get.find<RealmProvider>().refreshAvailableRealms(),
]); ]);
}
}, },
), ),
( (
@ -142,7 +144,7 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
try { try {
for (var idx = 0; idx < _periods.length; idx++) { for (var idx = 0; idx < _periods.length; idx++) {
await _periods[idx].action(); await _periods[idx].action();
if (_isErrored) break; if (_isErrored && !_isDismissable) break;
if (_periodCursor < _periods.length - 1) { if (_periodCursor < _periods.length - 1) {
setState(() => _periodCursor++); setState(() => _periodCursor++);
} }
@ -161,7 +163,8 @@ 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(
child: Material(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: Column( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
@ -171,19 +174,20 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
height: 280, height: 280,
child: Align( child: Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Image.asset('assets/logo.png', width: 80, height: 80) child: ClipRRect(
.animate(onPlay: (c) => c.repeat()) borderRadius: const BorderRadius.all(Radius.circular(16)),
.rotate(duration: 850.ms, curve: Curves.easeInOut), child:
Image.asset('assets/logo.png', width: 80, height: 80),
), ),
), ),
GestureDetector( ),
child: Column( Column(
children: [ children: [
if (_isErrored && !_isDismissable) if (_isErrored && !_isDismissable && !_isBusy)
const Icon(Icons.cancel, size: 24), const Icon(Icons.cancel, size: 24),
if (_isErrored && _isDismissable) if (_isErrored && _isDismissable && !_isBusy)
const Icon(Icons.warning, size: 24), const Icon(Icons.warning, size: 24),
if (!_isErrored && _isBusy) if ((_isErrored && _isDismissable && _isBusy) || _isBusy)
const SizedBox( const SizedBox(
width: 24, width: 24,
height: 24, height: 24,
@ -212,6 +216,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,
@ -225,6 +238,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
), ),
], ],
), ),
],
),
),
onTap: () { onTap: () {
if (_isBusy) return; if (_isBusy) return;
if (_isDismissable) { if (_isDismissable) {
@ -241,9 +257,6 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
_runPeriods(); _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

@ -9,11 +9,12 @@ class PostListController extends GetxController {
/// 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` or `1`.
PagingController<int, Post> pagingController = PagingController<int, Post> pagingController =
PagingController(firstPageKey: 0); PagingController(firstPageKey: 0);
@ -111,10 +112,23 @@ class PostListController extends GetxController {
author: author, author: author,
); );
} else { } else {
switch (mode.value) {
case 2:
resp = await provider.listRecommendations( resp = await provider.listRecommendations(
pageKey, pageKey,
channel: mode.value == 0 ? null : 'shuffle', 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

@ -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,18 @@
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/stickers.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
@ -28,16 +30,7 @@ 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(
(options) {
options.dsn =
'https://55438cdff9048aa2225df72fdc629c42@o4506965897117696.ingest.us.sentry.io/4507357676437504';
options.tracesSampleRate = 1.0;
options.profilesSampleRate = 1.0;
},
appRunner: () async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
MediaKit.ensureInitialized();
await Future.wait([ await Future.wait([
_initializeFirebase(), _initializeFirebase(),
@ -48,12 +41,17 @@ void main() async {
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 {
@ -120,6 +118,7 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => AuthProvider()); Get.lazyPut(() => AuthProvider());
Get.lazyPut(() => RelationshipProvider()); Get.lazyPut(() => RelationshipProvider());
Get.lazyPut(() => PostProvider()); Get.lazyPut(() => PostProvider());
Get.lazyPut(() => StickerProvider());
Get.lazyPut(() => AttachmentProvider()); Get.lazyPut(() => AttachmentProvider());
Get.lazyPut(() => WebSocketProvider()); Get.lazyPut(() => WebSocketProvider());
Get.lazyPut(() => StatusProvider()); Get.lazyPut(() => StatusProvider());

View File

@ -5,11 +5,11 @@ class Attachment {
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;
@ -24,11 +24,11 @@ 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,
@ -43,19 +43,22 @@ class Attachment {
id: json['id'], id: json['id'],
createdAt: DateTime.parse(json['created_at']), createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']), updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null ? DateTime.parse(json['deleted_at']) : null, deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: null,
rid: json['rid'],
uuid: json['uuid'], uuid: json['uuid'],
size: json['size'], size: json['size'],
name: json['name'], name: json['name'],
alt: json['alt'], alt: json['alt'],
usage: json['usage'],
mimetype: json['mimetype'], mimetype: json['mimetype'],
hash: json['hash'], hash: json['hash'],
destination: json['destination'], destination: json['destination'],
isAnalyzed: json['is_analyzed'], isAnalyzed: json['is_analyzed'],
metadata: json['metadata'], metadata: json['metadata'],
isMature: json['is_mature'], isMature: json['is_mature'],
account: json['account'] != null ? Account.fromJson(json['account']) : null, account:
json['account'] != null ? Account.fromJson(json['account']) : null,
accountId: json['account_id'], accountId: json['account_id'],
); );
@ -64,11 +67,11 @@ class Attachment {
'created_at': createdAt.toIso8601String(), 'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(), 'deleted_at': deletedAt?.toIso8601String(),
'rid': rid,
'uuid': uuid, 'uuid': uuid,
'size': size, 'size': size,
'name': name, 'name': name,
'alt': alt, 'alt': alt,
'usage': usage,
'mimetype': mimetype, 'mimetype': mimetype,
'hash': hash, 'hash': hash,
'destination': destination, 'destination': destination,

View File

@ -63,7 +63,7 @@ class Event {
class EventMessageBody { class EventMessageBody {
String text; String text;
String algorithm; String algorithm;
List<int>? attachments; List<String>? attachments;
int? quoteEvent; int? quoteEvent;
int? relatedEvent; int? relatedEvent;
List<int>? relatedUsers; List<int>? relatedUsers;
@ -82,7 +82,7 @@ class EventMessageBody {
text: json['text'] ?? '', text: json['text'] ?? '',
algorithm: json['algorithm'] ?? 'plain', algorithm: json['algorithm'] ?? 'plain',
attachments: json['attachments'] != null attachments: json['attachments'] != null
? List<int>.from(json['attachments'].map((x) => x)) ? List<String>.from(json['attachments']?.whereType<String>())
: null, : null,
quoteEvent: json['quote_event'], quoteEvent: json['quote_event'],
relatedEvent: json['related_event'], relatedEvent: json['related_event'],

View File

@ -8,6 +8,8 @@ class Post {
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 +35,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,
@ -60,6 +64,8 @@ class Post {
deletedAt: json['deleted_at'] != null deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at']) ? DateTime.parse(json['deleted_at'])
: null, : null,
alias: json['alias'],
areaAlias: json['area_alias'],
type: json['type'], type: json['type'],
body: json['body'], body: json['body'],
tags: json['tags']?.map((x) => Tag.fromJson(x)).toList().cast<Tag>(), tags: json['tags']?.map((x) => Tag.fromJson(x)).toList().cast<Tag>(),
@ -101,6 +107,8 @@ class Post {
'updated_at': updatedAt.toIso8601String(), 'updated_at': updatedAt.toIso8601String(),
'edited_at': editedAt?.toIso8601String(), 'edited_at': editedAt?.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(), 'deleted_at': deletedAt?.toIso8601String(),
'alias': alias,
'area_alias': areaAlias,
'type': type, 'type': type,
'body': body, 'body': body,
'tags': tags, 'tags': tags,

View File

@ -52,6 +52,17 @@ class Realm {
'is_community': isCommunity, 'is_community': isCommunity,
'account_id': accountId, 'account_id': accountId,
}; };
@override
bool operator ==(Object other) {
if (other is Realm) {
return other.id == id;
}
return false;
}
@override
int get hashCode => id;
} }
class RealmMember { class RealmMember {

131
lib/models/stickers.dart Normal file
View File

@ -0,0 +1,131 @@
import 'package:solian/models/account.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/services.dart';
class Sticker {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String alias;
String name;
int attachmentId;
Attachment attachment;
int packId;
StickerPack? pack;
int accountId;
Account account;
Sticker({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.alias,
required this.name,
required this.attachmentId,
required this.attachment,
required this.packId,
required this.pack,
required this.accountId,
required this.account,
});
String get textPlaceholder => '${pack?.prefix}$alias';
String get textWarpedPlaceholder => ':$textPlaceholder:';
String get imageUrl => ServiceFinder.buildUrl(
'files',
'/attachments/${attachment.rid}',
);
factory Sticker.fromJson(Map<String, dynamic> json) => Sticker(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: json['deleted_at'],
alias: json['alias'],
name: json['name'],
attachmentId: json['attachment_id'],
attachment: Attachment.fromJson(json['attachment']),
packId: json['pack_id'],
pack: json['pack'] != null ? StickerPack.fromJson(json['pack']) : null,
accountId: json['account_id'],
account: Account.fromJson(json['account']),
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'alias': alias,
'name': name,
'attachment_id': attachmentId,
'attachment': attachment.toJson(),
'pack_id': packId,
'account_id': accountId,
'account': account.toJson(),
};
}
class StickerPack {
int id;
DateTime createdAt;
DateTime updatedAt;
DateTime? deletedAt;
String prefix;
String name;
String description;
List<Sticker>? stickers;
int accountId;
Account account;
StickerPack({
required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.prefix,
required this.name,
required this.description,
required this.stickers,
required this.accountId,
required this.account,
});
factory StickerPack.fromJson(Map<String, dynamic> json) => StickerPack(
id: json['id'],
createdAt: DateTime.parse(json['created_at']),
updatedAt: DateTime.parse(json['updated_at']),
deletedAt: json['deleted_at'] != null
? DateTime.parse(json['deleted_at'])
: json['deleted_at'],
prefix: json['prefix'],
name: json['name'],
description: json['description'],
stickers: json['stickers'] == null
? []
: List<Sticker>.from(
json['stickers']!.map((x) => Sticker.fromJson(x))),
accountId: json['account_id'],
account: Account.fromJson(json['account']),
);
Map<String, dynamic> toJson() => {
'id': id,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt.toIso8601String(),
'deleted_at': deletedAt?.toIso8601String(),
'prefix': prefix,
'name': name,
'description': description,
'stickers': stickers == null
? []
: List<dynamic>.from(stickers!.map((x) => x.toJson())),
'account_id': accountId,
'account': account.toJson(),
};
}

View File

@ -14,6 +14,7 @@ class AttachmentUploadTask {
double progress = 0; double progress = 0;
bool isUploading = false; bool isUploading = false;
bool isCompleted = false; bool isCompleted = false;
dynamic error;
AttachmentUploadTask({ AttachmentUploadTask({
required this.file, required this.file,
@ -66,7 +67,7 @@ class AttachmentUploaderController extends GetxController {
queueOfUpload.remove(task); queueOfUpload.remove(task);
} }
Future<Attachment> performSingleTask(int queueIndex) async { Future<Attachment?> performSingleTask(int queueIndex) async {
isUploading.value = true; isUploading.value = true;
progressOfUpload.value = 0; progressOfUpload.value = 0;
@ -83,9 +84,15 @@ class AttachmentUploaderController extends GetxController {
queueOfUpload[queueIndex].progress = value; queueOfUpload[queueIndex].progress = value;
_progressOfUpload = value; _progressOfUpload = value;
}, },
onError: (err) {
queueOfUpload[queueIndex].error = err;
queueOfUpload[queueIndex].isUploading = false;
},
); );
if (queueOfUpload[queueIndex].error == null) {
queueOfUpload.removeAt(queueIndex); queueOfUpload.removeAt(queueIndex);
}
_stopProgressSyncTimer(); _stopProgressSyncTimer();
_syncProgress(); _syncProgress();
@ -103,6 +110,10 @@ class AttachmentUploaderController extends GetxController {
_startProgressSyncTimer(); _startProgressSyncTimer();
for (var idx = 0; idx < queueOfUpload.length; idx++) { for (var idx = 0; idx < queueOfUpload.length; idx++) {
if (queueOfUpload[idx].isUploading || queueOfUpload[idx].error != null) {
continue;
}
queueOfUpload[idx].isUploading = true; queueOfUpload[idx].isUploading = true;
final task = queueOfUpload[idx]; final task = queueOfUpload[idx];
@ -115,15 +126,19 @@ class AttachmentUploaderController extends GetxController {
queueOfUpload[idx].progress = value; queueOfUpload[idx].progress = value;
_progressOfUpload = (idx + value) / queueOfUpload.length; _progressOfUpload = (idx + value) / queueOfUpload.length;
}, },
onError: (err) {
queueOfUpload[idx].error = err;
queueOfUpload[idx].isUploading = false;
},
); );
_progressOfUpload = (idx + 1) / queueOfUpload.length; _progressOfUpload = (idx + 1) / queueOfUpload.length;
onData(result); if (result != null) onData(result);
queueOfUpload[idx].isUploading = false; queueOfUpload[idx].isUploading = false;
queueOfUpload[idx].isCompleted = false; queueOfUpload[idx].isCompleted = true;
} }
queueOfUpload.clear(); queueOfUpload.removeWhere((x) => x.error == null);
_stopProgressSyncTimer(); _stopProgressSyncTimer();
_syncProgress(); _syncProgress();
@ -133,9 +148,9 @@ class AttachmentUploaderController extends GetxController {
Future<void> uploadAttachmentWithCallback( Future<void> uploadAttachmentWithCallback(
Uint8List data, Uint8List data,
String path, String path,
String usage, String pool,
Map<String, dynamic>? metadata, Map<String, dynamic>? metadata,
Function(Attachment) callback, Function(Attachment?) callback,
) async { ) async {
if (isUploading.value) throw Exception('uploading blocked'); if (isUploading.value) throw Exception('uploading blocked');
@ -143,7 +158,7 @@ class AttachmentUploaderController extends GetxController {
final result = await _rawUploadAttachment( final result = await _rawUploadAttachment(
data, data,
path, path,
usage, pool,
metadata, metadata,
onProgress: (progress) { onProgress: (progress) {
progressOfUpload.value = progress; progressOfUpload.value = progress;
@ -153,10 +168,10 @@ class AttachmentUploaderController extends GetxController {
callback(result); callback(result);
} }
Future<Attachment> uploadAttachment( Future<Attachment?> uploadAttachment(
Uint8List data, Uint8List data,
String path, String path,
String usage, String pool,
Map<String, dynamic>? metadata, Map<String, dynamic>? metadata,
) async { ) async {
if (isUploading.value) throw Exception('uploading blocked'); if (isUploading.value) throw Exception('uploading blocked');
@ -165,7 +180,7 @@ class AttachmentUploaderController extends GetxController {
final result = await _rawUploadAttachment( final result = await _rawUploadAttachment(
data, data,
path, path,
usage, pool,
metadata, metadata,
onProgress: (progress) { onProgress: (progress) {
progressOfUpload.value = progress; progressOfUpload.value = progress;
@ -175,21 +190,24 @@ class AttachmentUploaderController extends GetxController {
return result; return result;
} }
Future<Attachment> _rawUploadAttachment( Future<Attachment?> _rawUploadAttachment(
Uint8List data, String path, String usage, Map<String, dynamic>? metadata, Uint8List data, String path, String pool, Map<String, dynamic>? metadata,
{Function(double)? onProgress}) async { {Function(double)? onProgress, Function(dynamic err)? onError}) async {
final AttachmentProvider provider = Get.find(); final AttachmentProvider provider = Get.find();
try { try {
final result = await provider.createAttachment( final result = await provider.createAttachment(
data, data,
path, path,
usage, pool,
metadata, metadata,
onProgress: onProgress, onProgress: onProgress,
); );
return result; return result;
} catch (err) { } catch (err) {
rethrow; if (onError != null) {
onError(err);
}
return null;
} }
} }
} }

View File

@ -88,22 +88,7 @@ class ChatCallProvider extends GetxController {
void initRoom() { void initRoom() {
initHardware(); initHardware();
room = Room(); room = Room(
listener = room.createListener();
WakelockPlus.enable();
}
void joinRoom(String url, String token) async {
if (isMounted.value) {
return;
} else {
isMounted.value = true;
}
try {
await room.connect(
url,
token,
roomOptions: const RoomOptions( roomOptions: const RoomOptions(
dynacast: true, dynacast: true,
adaptiveStream: true, adaptiveStream: true,
@ -126,6 +111,22 @@ class ChatCallProvider extends GetxController {
params: VideoParametersPresets.h1080_169, params: VideoParametersPresets.h1080_169,
), ),
), ),
);
listener = room.createListener();
WakelockPlus.enable();
}
void joinRoom(String url, String token) async {
if (isMounted.value) {
return;
} else {
isMounted.value = true;
}
try {
await room.connect(
url,
token,
fastConnectOptions: FastConnectOptions( fastConnectOptions: FastConnectOptions(
microphone: TrackOption(track: audioTrack.value), microphone: TrackOption(track: audioTrack.value),
camera: TrackOption(track: videoTrack.value), camera: TrackOption(track: videoTrack.value),
@ -152,7 +153,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);

View File

@ -20,20 +20,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 {
List<Attachment?> result = List.filled(id.length, null); if (rid.isEmpty) return List.empty();
List<int> pendingQuery = List.empty(growable: true);
List<Attachment?> result = List.filled(rid.length, null);
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]);
} }
} }
} }
@ -50,12 +52,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];
} }
} }
@ -64,16 +66,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;
} }
@ -82,11 +84,13 @@ class AttachmentProvider extends GetConnect {
} }
Future<Attachment> createAttachment( Future<Attachment> createAttachment(
Uint8List data, String path, String usage, Map<String, dynamic>? metadata, Uint8List data, String path, String pool, Map<String, dynamic>? metadata,
{Function(double)? onProgress}) async { {Function(double)? onProgress}) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized'); if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
await auth.ensureCredentials();
final filePayload = final filePayload =
dio.MultipartFile.fromBytes(data, filename: basename(path)); dio.MultipartFile.fromBytes(data, filename: basename(path));
final fileAlt = basename(path).contains('.') final fileAlt = basename(path).contains('.')
@ -104,7 +108,7 @@ class AttachmentProvider extends GetConnect {
final payload = dio.FormData.fromMap({ final payload = dio.FormData.fromMap({
'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),
}); });
@ -129,8 +133,7 @@ class AttachmentProvider extends GetConnect {
Future<Response> updateAttachment( Future<Response> updateAttachment(
int id, int id,
String alt, String alt, {
String usage, {
bool isMature = false, bool isMature = false,
}) async { }) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
@ -140,7 +143,6 @@ class AttachmentProvider extends GetConnect {
var resp = await client.put('/attachments/$id', { var resp = await client.put('/attachments/$id', {
'alt': alt, 'alt': alt,
'usage': usage,
'is_mature': isMature, 'is_mature': isMature,
}); });
@ -165,7 +167,7 @@ class AttachmentProvider extends GetConnect {
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

@ -9,13 +9,20 @@ class PostProvider extends GetConnect {
} }
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('&')}',
@ -45,14 +52,14 @@ class PostProvider extends GetConnect {
} }
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) {

View File

@ -0,0 +1,34 @@
import 'package:get/get.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/models/stickers.dart';
import 'package:solian/services.dart';
class StickerProvider extends GetxController {
final RxMap<String, String> aliasImageMapping = RxMap();
final RxList<Sticker> availableStickers = RxList.empty(growable: true);
Future<void> refreshAvailableStickers() async {
availableStickers.clear();
aliasImageMapping.clear();
final client = ServiceFinder.configureClient('files');
final resp = await client.get(
'/stickers/manifest?take=100',
);
if (resp.statusCode == 200) {
final result = PaginationResult.fromJson(resp.body);
final out = result.data?.map((e) => StickerPack.fromJson(e)).toList();
if (out == null) return;
for (final pack in out) {
for (final sticker in (pack.stickers ?? List<Sticker>.empty())) {
sticker.pack = pack;
aliasImageMapping[sticker.textPlaceholder.toUpperCase()] =
sticker.imageUrl;
availableStickers.add(sticker);
}
}
}
availableStickers.refresh();
}
}

View File

@ -7,6 +7,7 @@ import 'package:solian/screens/account.dart';
import 'package:solian/screens/account/friend.dart'; import 'package:solian/screens/account/friend.dart';
import 'package:solian/screens/account/personalize.dart'; import 'package:solian/screens/account/personalize.dart';
import 'package:solian/screens/account/profile_page.dart'; import 'package:solian/screens/account/profile_page.dart';
import 'package:solian/screens/account/stickers.dart';
import 'package:solian/screens/channel/channel_chat.dart'; 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';
@ -103,7 +104,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:
@ -226,6 +226,14 @@ abstract class AppRouter {
name: 'accountFriend', name: 'accountFriend',
builder: (context, state) => const FriendScreen(), builder: (context, state) => const FriendScreen(),
), ),
GoRoute(
path: '/account/stickers',
name: 'accountStickers',
builder: (context, state) => TitleShell(
state: state,
child: const StickerScreen(),
),
),
GoRoute( GoRoute(
path: '/account/personalize', path: '/account/personalize',
name: 'accountPersonalize', name: 'accountPersonalize',

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.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';
@ -18,9 +17,11 @@ class AboutScreen extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Image.asset('assets/logo.png', width: 64, height: 64) ClipRRect(
.animate(onPlay: (c) => c.repeat()) borderRadius: const BorderRadius.all(Radius.circular(16)),
.rotate(duration: 1000.ms), child: Image.asset('assets/logo.png', width: 120, height: 120),
),
const SizedBox(height: 8),
Text( Text(
'Solian', 'Solian',
style: Theme.of(context).textTheme.headlineMedium, style: Theme.of(context).textTheme.headlineMedium,
@ -56,10 +57,9 @@ class AboutScreen extends StatelessWidget {
applicationVersion: '${info.version} (${info.buildNumber})', applicationVersion: '${info.version} (${info.buildNumber})',
applicationLegalese: applicationLegalese:
'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: Image.asset( applicationIcon: ClipRRect(
'assets/logo.png', borderRadius: const BorderRadius.all(Radius.circular(16)),
width: 56, child: Image.asset('assets/logo.png', width: 60, height: 60),
height: 56,
), ),
); );
}, },

View File

@ -46,6 +46,11 @@ class _AccountScreenState extends State<AccountScreen> {
'accountFriend'.tr, 'accountFriend'.tr,
'accountFriend', 'accountFriend',
), ),
(
const Icon(Icons.emoji_symbols),
'accountStickers'.tr,
'accountStickers',
),
]; ];
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();

View File

@ -43,7 +43,7 @@ class _FriendScreenState extends State<FriendScreen>
_relations.where((x) => x.status == 0).length; _relations.where((x) => x.status == 0).length;
} }
void promptAddFriend() async { void _promptAddFriend() async {
final RelationshipProvider provider = Get.find(); final RelationshipProvider provider = Get.find();
final controller = TextEditingController(); final controller = TextEditingController();
@ -146,7 +146,7 @@ class _FriendScreenState extends State<FriendScreen>
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add), child: const Icon(Icons.add),
onPressed: () => promptAddFriend(), onPressed: () => _promptAddFriend(),
), ),
body: TabBarView( body: TabBarView(
controller: _tabController, controller: _tabController,

View File

@ -86,11 +86,17 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
toolbarTitle: 'cropImage'.tr, toolbarTitle: 'cropImage'.tr,
toolbarColor: Theme.of(context).colorScheme.primary, toolbarColor: Theme.of(context).colorScheme.primary,
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary, toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
aspectRatioPresets: [CropAspectRatioPreset.square], aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
), ),
IOSUiSettings( IOSUiSettings(
title: 'cropImage'.tr, title: 'cropImage'.tr,
aspectRatioPresets: [CropAspectRatioPreset.square], aspectRatioPresets: [
if (position == 'avatar') CropAspectRatioPreset.square,
if (position == 'banner') _BannerCropAspectRatioPreset(),
],
), ),
WebUiSettings( WebUiSettings(
context: context, context: context,
@ -110,7 +116,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
attachResult = await provider.createAttachment( attachResult = await provider.createAttachment(
await file.readAsBytes(), await file.readAsBytes(),
file.path, file.path,
'p.$position', 'avatar',
null, null,
); );
} catch (e) { } catch (e) {
@ -346,3 +352,11 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
super.dispose(); super.dispose();
} }
} }
class _BannerCropAspectRatioPreset extends CropAspectRatioPresetData {
@override
(int, int)? get data => (16, 7);
@override
String get name => '16x7';
}

View File

@ -40,7 +40,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
List<Post> _pinnedPosts = List.empty(); List<Post> _pinnedPosts = List.empty();
int _totalUpvote = 0, _totalDownvote = 0; int _totalUpvote = 0, _totalDownvote = 0;
Future<void> getUserinfo() async { Future<void> _getUserinfo() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
var client = ServiceFinder.configureClient('auth'); var client = ServiceFinder.configureClient('auth');
@ -114,7 +114,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
} }
}); });
getUserinfo(); _getUserinfo();
getPinnedPosts(); getPinnedPosts();
} }
@ -189,8 +189,11 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
: () async { : () async {
setState(() => _isMakingFriend = true); setState(() => _isMakingFriend = true);
try { try {
await _relationshipProvider.makeFriend(widget.name); await _relationshipProvider
context.showSnackbar('accountFriendRequestSent'.tr); .makeFriend(widget.name);
context.showSnackbar(
'accountFriendRequestSent'.tr,
);
} catch (e) { } catch (e) {
context.showErrorDialog(e); context.showErrorDialog(e);
} finally { } finally {
@ -274,6 +277,8 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
color: color:
Theme.of(context).colorScheme.surfaceContainerLow, Theme.of(context).colorScheme.surfaceContainerLow,
child: PostListEntryWidget( child: PostListEntryWidget(
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
item: element, item: element,
isClickable: true, isClickable: true,
isNestedClickable: true, isNestedClickable: true,
@ -295,6 +300,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
PostWarpedListWidget( PostWarpedListWidget(
isPinned: false, isPinned: false,
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(),
), ),
]), ]),
), ),

View File

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

View File

@ -79,8 +79,8 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
onPressed: () { onPressed: () {
const redirect = 'solink://auth?status=done'; const redirect = 'solink://auth?status=done';
launchUrlString( launchUrlString(
ServiceFinder.buildUrl('passport', ServiceFinder.buildUrl('capital',
'/mfa?redirect_uri=$redirect&ticketId=${e.ticketId}'), '/auth/mfa?redirect_uri=$redirect&ticketId=${e.ticketId}'),
mode: LaunchMode.inAppWebView, mode: LaunchMode.inAppWebView,
); );
Navigator.pop(context); Navigator.pop(context);
@ -136,8 +136,10 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Image.asset('assets/logo.png', width: 64, height: 64) ClipRRect(
.paddingOnly(bottom: 4), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 4),
Text( Text(
'signinGreeting'.tr, 'signinGreeting'.tr,
style: const TextStyle( style: const TextStyle(

View File

@ -70,8 +70,10 @@ class _SignUpPopupState extends State<SignUpPopup> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Image.asset('assets/logo.png', width: 64, height: 64) ClipRRect(
.paddingOnly(bottom: 4), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Image.asset('assets/logo.png', width: 64, height: 64),
).paddingOnly(bottom: 4),
Text( Text(
'signupGreeting'.tr, 'signupGreeting'.tr,
style: const TextStyle( style: const TextStyle(

View File

@ -9,6 +9,7 @@ import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/chat/call/call_controls.dart'; import 'package:solian/widgets/chat/call/call_controls.dart';
import 'package:solian/widgets/chat/call/call_participant.dart'; import 'package:solian/widgets/chat/call/call_participant.dart';
import 'package:livekit_client/livekit_client.dart' as livekit;
class CallScreen extends StatefulWidget { class CallScreen extends StatefulWidget {
const CallScreen({super.key}); const CallScreen({super.key});
@ -161,13 +162,13 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns, crossAxisCount: columns,
childAspectRatio: tileWidth / tileHeight, childAspectRatio: tileWidth / tileHeight,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
), ),
itemCount: math.max(0, call.participantTracks.length), itemCount: math.max(0, call.participantTracks.length),
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final track = call.participantTracks[index]; final track = call.participantTracks[index];
return Padding( return Card(
padding: const EdgeInsets.all(16),
child: Card(
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget( child: InteractiveParticipantWidget(
@ -181,11 +182,10 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
}, },
), ),
), ),
),
); );
}, },
), ),
); ).paddingAll(8);
}); });
} }
@ -246,8 +246,77 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
height: 64, height: 64,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
const Expanded(child: SizedBox()), Builder(builder: (context) {
final call = Get.find<ChatCallProvider>();
final connectionQuality =
call.room.localParticipant?.connectionQuality ??
livekit.ConnectionQuality.unknown;
return Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(call.room.serverRegion ?? 'unknown'),
const SizedBox(width: 6),
Text(call.room.serverVersion ?? 'unknown')
],
),
Row(
children: [
Text(
{
livekit.ConnectionState.disconnected:
'callStatusDisconnected'.tr,
livekit.ConnectionState.connected:
'callStatusConnected'.tr,
livekit.ConnectionState.connecting:
'callStatusConnecting'.tr,
livekit.ConnectionState.reconnecting:
'callStatusReconnecting'.tr,
}[call.room.connectionState]!,
),
const SizedBox(width: 6),
if (connectionQuality !=
livekit.ConnectionQuality.unknown)
Icon(
{
livekit.ConnectionQuality.excellent:
Icons.signal_cellular_alt,
livekit.ConnectionQuality.good:
Icons.signal_cellular_alt_2_bar,
livekit.ConnectionQuality.poor:
Icons.signal_cellular_alt_1_bar,
}[connectionQuality],
color: {
livekit.ConnectionQuality.excellent:
Colors.green,
livekit.ConnectionQuality.good:
Colors.orange,
livekit.ConnectionQuality.poor:
Colors.red,
}[connectionQuality],
size: 16,
)
else
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
).paddingAll(3),
],
),
],
),
);
}),
IconButton( IconButton(
icon: _layoutMode == 0 icon: _layoutMode == 0
? const Icon(Icons.view_list) ? const Icon(Icons.view_list)
@ -257,7 +326,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
}, },
), ),
], ],
).paddingSymmetric(horizontal: 10), ).paddingOnly(left: 20, right: 16),
), ),
), ),
Expanded( Expanded(

View File

@ -20,7 +20,6 @@ import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/channel/channel_call_indicator.dart'; import 'package:solian/widgets/channel/channel_call_indicator.dart';
import 'package:solian/widgets/chat/call/chat_call_action.dart'; import 'package:solian/widgets/chat/call/chat_call_action.dart';
import 'package:solian/widgets/chat/chat_event.dart';
import 'package:solian/widgets/chat/chat_event_list.dart'; import 'package:solian/widgets/chat/chat_event_list.dart';
import 'package:solian/widgets/chat/chat_message_input.dart'; import 'package:solian/widgets/chat/chat_message_input.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
@ -39,7 +38,10 @@ class ChannelChatScreen extends StatefulWidget {
State<ChannelChatScreen> createState() => _ChannelChatScreenState(); State<ChannelChatScreen> createState() => _ChannelChatScreenState();
} }
class _ChannelChatScreenState extends State<ChannelChatScreen> { class _ChannelChatScreenState extends State<ChannelChatScreen>
with WidgetsBindingObserver {
DateTime? _isOutOfSyncSince;
bool _isBusy = false; bool _isBusy = false;
int? _accountId; int? _accountId;
@ -123,20 +125,38 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}); });
} }
void _keepUpdateWithServer() {
_getOngoingCall();
_chatController.getEvents(_channel!, widget.realm);
setState(() => _isOutOfSyncSince = null);
}
Event? _messageToReplying; Event? _messageToReplying;
Event? _messageToEditing; Event? _messageToEditing;
Widget buildHistoryBody(Event item, {bool isMerged = false}) { @override
return ChatEvent( void didChangeAppLifecycleState(AppLifecycleState state) {
key: Key('m${item.uuid}'), switch (state) {
item: item, case AppLifecycleState.resumed:
isMerged: isMerged, if (_isOutOfSyncSince == null) break;
chatController: _chatController, if (DateTime.now().difference(_isOutOfSyncSince!).inSeconds < 60) break;
); _keepUpdateWithServer();
break;
case AppLifecycleState.paused:
if (mounted) {
setState(() => _isOutOfSyncSince = DateTime.now());
}
break;
default:
break;
}
} }
@override @override
void initState() { void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_accountId = Get.find<AuthProvider>().userProfile.value!['id']; _accountId = Get.find<AuthProvider>().userProfile.value!['id'];
_chatController = ChatEventController(); _chatController = ChatEventController();
@ -147,21 +167,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_chatController.getEvents(_channel!, widget.realm); _chatController.getEvents(_channel!, widget.realm);
_listenMessages(); _listenMessages();
}); });
super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isBusy || _channel == null) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: const Center(
child: CircularProgressIndicator(),
),
);
}
String title = _channel?.name ?? 'loading'.tr; String title = _channel?.name ?? 'loading'.tr;
String? placeholder; String? placeholder;
@ -185,7 +194,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
actions: [ actions: [
const BackgroundStateWidget(), const BackgroundStateWidget(),
Builder(builder: (context) { Builder(builder: (context) {
if (_isBusy) return const SizedBox(); if (_isBusy || _channel == null) return const SizedBox();
return ChatCallButton( return ChatCallButton(
realm: _channel!.realm, realm: _channel!.realm,
channel: _channel!, channel: _channel!,
@ -195,6 +205,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
IconButton( IconButton(
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
onPressed: () { onPressed: () {
if (_channel == null) return;
AppRouter.instance AppRouter.instance
.pushNamed( .pushNamed(
'channelDetail', 'channelDetail',
@ -219,7 +231,14 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
), ),
], ],
), ),
body: Column( body: Builder(builder: (context) {
if (_isBusy || _channel == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
return Column(
children: [ children: [
if (_ongoingCall != null) if (_ongoingCall != null)
ChannelCallIndicator( ChannelCallIndicator(
@ -239,6 +258,25 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}, },
), ),
), ),
if (_isOutOfSyncSince != null)
ListTile(
contentPadding: const EdgeInsets.only(left: 16, right: 8),
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
leading: const Icon(Icons.history_toggle_off),
title: Text('messageOutOfSync'.tr),
subtitle: Text('messageOutOfSyncCaption'.tr),
trailing: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() => _isOutOfSyncSince = null);
},
),
onTap: _isBusy
? null
: () {
_keepUpdateWithServer();
},
),
Obx(() { Obx(() {
if (_chatController.isLoading.isTrue) { if (_chatController.isLoading.isTrue) {
return const LinearProgressIndicator().animate().slideY(); return const LinearProgressIndicator().animate().slideY();
@ -272,13 +310,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
), ),
), ),
], ],
), );
}),
); );
} }
@override @override
void dispose() { void dispose() {
_subscription?.cancel(); _subscription?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
} }
} }

View File

@ -21,7 +21,7 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
final PagingController<int, Post> _pagingController = final PagingController<int, Post> _pagingController =
PagingController(firstPageKey: 0); PagingController(firstPageKey: 0);
getPosts(int pageKey) async { _getPosts(int pageKey) async {
final PostProvider provider = Get.find(); final PostProvider provider = Get.find();
Response resp; Response resp;
@ -49,7 +49,7 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_pagingController.addPageRequestListener(getPosts); _pagingController.addPageRequestListener(_getPosts);
} }
@override @override
@ -76,6 +76,9 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
itemBuilder: (context, item, index) { itemBuilder: (context, item, index) {
return PostOwnedListEntry( return PostOwnedListEntry(
item: item, item: item,
isFullContent: true,
backgroundColor:
Theme.of(context).colorScheme.surfaceContainerLow,
onTap: () async { onTap: () async {
showModalBottomSheet( showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
@ -85,7 +88,13 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
noReact: true, noReact: true,
), ),
).then((value) { ).then((value) {
if (value != null) _pagingController.refresh(); if (value is Future) {
value.then((_) {
_pagingController.refresh();
});
} else if (value != null) {
_pagingController.refresh();
}
}); });
}, },
).paddingOnly(left: 12, right: 12, bottom: 4); ).paddingOnly(left: 12, right: 12, bottom: 4);

View File

@ -77,7 +77,10 @@ class _FeedSearchScreenState extends State<FeedSearchScreen> {
onRefresh: () => Future.sync(() => _pagingController.refresh()), onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
PostWarpedListWidget(controller: _pagingController), PostWarpedListWidget(
controller: _pagingController,
onUpdate: () => _pagingController.refresh(),
),
], ],
), ),
), ),

View File

@ -4,8 +4,8 @@ import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/account/notification.dart'; import 'package:solian/screens/account/notification.dart';
import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/signin_required_overlay.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
@ -28,34 +28,37 @@ class _HomeScreenState extends State<HomeScreen>
void initState() { void initState() {
super.initState(); super.initState();
_postController = PostListController(); _postController = PostListController();
_tabController = TabController(length: 2, vsync: this); _tabController = TabController(length: 3, vsync: this);
_tabController.addListener(() { _tabController.addListener(() {
switch (_tabController.index) {
case 0:
case 1:
if (_postController.mode.value == _tabController.index) return; if (_postController.mode.value == _tabController.index) return;
_postController.mode.value = _tabController.index; _postController.mode.value = _tabController.index;
_postController.reloadAllOver(); _postController.reloadAllOver();
}
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AuthProvider auth = Get.find();
return Material( return Material(
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: Scaffold( child: Scaffold(
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add), child: const Icon(Icons.add),
onPressed: () { onPressed: () async {
showModalBottomSheet( final value = await showModalBottomSheet(
useRootNavigator: true, useRootNavigator: true,
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: (context) => PostCreatePopup( builder: (context) => const PostCreatePopup(),
controller: _postController,
),
); );
if (value is Future) {
value.then((_) {
_postController.reloadAllOver();
});
} else if (value != null) {
_postController.reloadAllOver();
}
}, },
), ),
body: NestedScrollView( body: NestedScrollView(
@ -78,6 +81,7 @@ class _HomeScreenState extends State<HomeScreen>
controller: _tabController, controller: _tabController,
tabs: [ tabs: [
Tab(text: 'postListNews'.tr), Tab(text: 'postListNews'.tr),
Tab(text: 'postListFriends'.tr),
Tab(text: 'postListShuffle'.tr), Tab(text: 'postListShuffle'.tr),
], ],
), ),
@ -100,9 +104,27 @@ class _HomeScreenState extends State<HomeScreen>
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
PostWarpedListWidget( PostWarpedListWidget(
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(),
), ),
]), ]),
), ),
Obx(() {
if (auth.isAuthorized.value) {
return RefreshIndicator(
onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [
PostWarpedListWidget(
controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(),
),
]),
);
} else {
return SigninRequiredOverlay(
onSignedIn: () => _postController.reloadAllOver(),
);
}
}),
PostShuffleSwiper(controller: _postController), PostShuffleSwiper(controller: _postController),
], ],
); );
@ -121,12 +143,10 @@ class _HomeScreenState extends State<HomeScreen>
class PostCreatePopup extends StatelessWidget { class PostCreatePopup extends StatelessWidget {
final bool hideDraftBox; final bool hideDraftBox;
final PostListController controller;
const PostCreatePopup({ const PostCreatePopup({
super.key, super.key,
this.hideDraftBox = false, this.hideDraftBox = false,
required this.controller,
}); });
@override @override
@ -142,13 +162,14 @@ class PostCreatePopup extends StatelessWidget {
icon: const Icon(Icons.post_add), icon: const Icon(Icons.post_add),
label: 'postEditorModeStory'.tr, label: 'postEditorModeStory'.tr,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(
context,
AppRouter.instance.pushNamed( AppRouter.instance.pushNamed(
'postEditor', 'postEditor',
extra: PostPublishArguments(postListController: controller),
queryParameters: { queryParameters: {
'mode': 0.toString(), 'mode': 0.toString(),
}, },
),
); );
}, },
), ),
@ -156,13 +177,14 @@ class PostCreatePopup extends StatelessWidget {
icon: const Icon(Icons.description), icon: const Icon(Icons.description),
label: 'postEditorModeArticle'.tr, label: 'postEditorModeArticle'.tr,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(
context,
AppRouter.instance.pushNamed( AppRouter.instance.pushNamed(
'postEditor', 'postEditor',
extra: PostPublishArguments(postListController: controller),
queryParameters: { queryParameters: {
'mode': 1.toString(), 'mode': 1.toString(),
}, },
),
); );
}, },
), ),
@ -170,8 +192,10 @@ class PostCreatePopup extends StatelessWidget {
icon: const Icon(Icons.drafts), icon: const Icon(Icons.drafts),
label: 'draftBoxOpen'.tr, label: 'draftBoxOpen'.tr,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(
AppRouter.instance.pushNamed('draftBox'); context,
AppRouter.instance.pushNamed('draftBox'),
);
}, },
), ),
]; ];
@ -201,7 +225,12 @@ class PostCreatePopup extends StatelessWidget {
children: [ children: [
x.icon, x.icon,
const SizedBox(height: 8), const SizedBox(height: 8),
Text(x.label), Expanded(
child: Text(
x.label,
overflow: TextOverflow.fade,
),
),
], ],
).paddingAll(18), ).paddingAll(18),
), ),

View File

@ -59,8 +59,10 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
SliverToBoxAdapter( SliverToBoxAdapter(
child: PostItem( child: PostItem(
item: item!, item: item!,
isClickable: true, isClickable: false,
isOverrideEmbedClickable: true,
isFullDate: true, isFullDate: true,
isFullContent: true,
isShowReply: false, isShowReply: false,
isContentSelectable: true, isContentSelectable: true,
), ),

View File

@ -5,7 +5,6 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:markdown_toolbar/markdown_toolbar.dart'; import 'package:markdown_toolbar/markdown_toolbar.dart';
import 'package:solian/controllers/post_editor_controller.dart'; import 'package:solian/controllers/post_editor_controller.dart';
import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
@ -15,6 +14,7 @@ import 'package:solian/router.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/markdown_text_content.dart';
import 'package:solian/widgets/posts/post_item.dart'; import 'package:solian/widgets/posts/post_item.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
@ -23,14 +23,12 @@ class PostPublishArguments {
final Post? reply; final Post? reply;
final Post? repost; final Post? repost;
final Realm? realm; final Realm? realm;
final PostListController? postListController;
PostPublishArguments({ PostPublishArguments({
this.edit, this.edit,
this.reply, this.reply,
this.repost, this.repost,
this.realm, this.realm,
this.postListController,
}); });
} }
@ -39,7 +37,6 @@ class PostPublishScreen extends StatefulWidget {
final Post? reply; final Post? reply;
final Post? repost; final Post? repost;
final Realm? realm; final Realm? realm;
final PostListController? postListController;
final int mode; final int mode;
const PostPublishScreen({ const PostPublishScreen({
@ -48,7 +45,6 @@ class PostPublishScreen extends StatefulWidget {
this.reply, this.reply,
this.repost, this.repost,
this.realm, this.realm,
this.postListController,
required this.mode, required this.mode,
}); });
@ -69,7 +65,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
final AttachmentUploaderController uploader = Get.find(); final AttachmentUploaderController uploader = Get.find();
if (uploader.queueOfUpload.any( if (uploader.queueOfUpload.any(
((x) => x.usage == 'i.attachment' && x.isUploading), ((x) => x.isUploading),
)) { )) {
context.showErrorDialog('attachmentUploadInProgress'.tr); context.showErrorDialog('attachmentUploadInProgress'.tr);
return; return;
@ -94,10 +90,8 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);
} else { } else {
_editorController.currentClear();
_editorController.localClear(); _editorController.localClear();
if (widget.postListController != null) {
widget.postListController!.reloadAllOver();
}
AppRouter.instance.pop(resp.body); AppRouter.instance.pop(resp.body);
} }
@ -128,7 +122,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (widget.edit == null) _editorController.localRead(); if (widget.edit == null && widget.reply == null && widget.repost == null) {
_editorController.localRead();
}
if (widget.reply != null) {
_editorController.replyTo.value = widget.reply;
}
if (widget.repost != null) {
_editorController.repostTo.value = widget.repost;
}
_editorController.contentController.addListener(() => setState(() {})); _editorController.contentController.addListener(() => setState(() {}));
_syncWidget(); _syncWidget();
} }
@ -174,11 +176,20 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
children: [ children: [
ListTile( ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow, tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
title: Text( title: Row(
children: [
Text(
_editorController.title ?? 'title'.tr, _editorController.title ?? 'title'.tr,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(width: 6),
if (_editorController.aliasController.text.isNotEmpty)
Badge(
label: Text('#${_editorController.aliasController.text}'),
),
],
),
subtitle: Text( subtitle: Text(
_editorController.description ?? 'description'.tr, _editorController.description ?? 'description'.tr,
maxLines: 2, maxLines: 2,
@ -219,10 +230,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
collapsedBackgroundColor: collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer, Theme.of(context).colorScheme.surfaceContainer,
children: [ children: [
PostItem( Container(
constraints: const BoxConstraints(maxHeight: 280),
child: SingleChildScrollView(
child: PostItem(
item: _replyTo!, item: _replyTo!,
isReactable: false, isReactable: false,
).paddingOnly(bottom: 8), ).paddingOnly(bottom: 8),
),
),
], ],
), ),
if (_repostTo != null) if (_repostTo != null)
@ -237,17 +253,28 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
collapsedBackgroundColor: collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer, Theme.of(context).colorScheme.surfaceContainer,
children: [ children: [
PostItem( Container(
constraints: const BoxConstraints(maxHeight: 280),
child: SingleChildScrollView(
child: PostItem(
item: _repostTo!, item: _repostTo!,
isReactable: false, isReactable: false,
).paddingOnly(bottom: 8), ).paddingOnly(bottom: 8),
),
),
], ],
), ),
if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
children: [
Expanded( Expanded(
child: ListView( child: ListView(
children: [ children: [
if (_isBusy)
const LinearProgressIndicator().animate().scaleX(),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
@ -258,24 +285,21 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
autofocus: true, autofocus: true,
autocorrect: true, autocorrect: true,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
controller: _editorController.contentController, controller:
_editorController.contentController,
focusNode: _contentFocusNode, focusNode: _contentFocusNode,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
hintText: 'postContentPlaceholder'.tr, hintText: 'postContentPlaceholder'.tr,
), ),
onTapOutside: (_) => onTapOutside: (_) => FocusManager
FocusManager.instance.primaryFocus?.unfocus(), .instance.primaryFocus
?.unfocus(),
), ),
), ),
const SizedBox(height: 120) const SizedBox(height: 120)
], ],
), ),
), ),
Material(
color: Theme.of(context).colorScheme.surface,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Obx(() { Obx(() {
final textStyle = TextStyle( final textStyle = TextStyle(
fontSize: 12, fontSize: 12,
@ -297,7 +321,8 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
child: Row( child: Row(
children: [ children: [
if (showFactors[0]) if (showFactors[0])
Text('postRestoreFromLocal'.tr, style: textStyle) Text('postRestoreFromLocal'.tr,
style: textStyle)
.paddingOnly(right: 4), .paddingOnly(right: 4),
if (showFactors[0]) if (showFactors[0])
InkWell( InkWell(
@ -332,29 +357,64 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
) )
.fade(curve: Curves.easeInOut, duration: 300.ms); .fade(curve: Curves.easeInOut, duration: 300.ms);
}), }),
if (_editorController.mode.value == 0) ],
Obx(
() => TweenAnimationBuilder<double>(
tween: Tween(
begin: 0,
end: _editorController.contentLength.value / 4096,
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
builder: (context, value, _) => LinearProgressIndicator(
minHeight: 2,
color: _editorController.contentLength.value > 4096
? Colors.red[900]
: Theme.of(context).colorScheme.primary,
value: value,
), ),
), ),
if (SolianTheme.isLargeScreen(context))
const VerticalDivider(width: 0.3, thickness: 0.3)
.paddingSymmetric(
horizontal: 16,
), ),
if (SolianTheme.isLargeScreen(context))
Expanded(
child: SingleChildScrollView(
child: MarkdownTextContent(
content: _editorController.contentController.text,
parentId: 'post-editor-preview',
).paddingOnly(top: 12, right: 16),
),
),
],
),
),
Material(
color: Theme.of(context).colorScheme.surface,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(thickness: 0.3, height: 0.3),
SizedBox( SizedBox(
height: 56, height: 56,
child: ListView( child: ListView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
children: [ children: [
if (_editorController.mode.value == 0)
Obx(
() => TweenAnimationBuilder<double>(
tween: Tween(
begin: 0,
end: _editorController.contentLength.value /
4096,
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
builder: (context, value, _) => SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 3,
backgroundColor: Theme.of(context)
.colorScheme
.secondaryContainer,
color: _editorController.contentLength.value >
4096
? Colors.red[900]
: Theme.of(context).colorScheme.primary,
value: value,
),
).paddingAll(10),
),
).paddingSymmetric(horizontal: 4),
Obx(() { Obx(() {
final isDraft = _editorController.isDraft.value; final isDraft = _editorController.isDraft.value;
return IconButton( return IconButton(
@ -443,6 +503,23 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
_editorController.editPublishZone(context); _editorController.editPublishZone(context);
}, },
), ),
IconButton(
icon: Obx(() {
return badges.Badge(
showBadge:
_editorController.thumbnail.value != null,
position: badges.BadgePosition.topEnd(
top: -4,
end: -6,
),
child: const Icon(Icons.preview),
);
}),
color: Theme.of(context).colorScheme.primary,
onPressed: () {
_editorController.editThumbnail(context);
},
),
IconButton( IconButton(
icon: Obx(() { icon: Obx(() {
return badges.Badge( return badges.Badge(
@ -465,14 +542,15 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
MarkdownToolbar( MarkdownToolbar(
hideImage: true, hideImage: true,
useIncludedTextField: false, useIncludedTextField: false,
backgroundColor: Colors.transparent, backgroundColor:
Theme.of(context).colorScheme.surface,
iconColor: Theme.of(context).colorScheme.onSurface, iconColor: Theme.of(context).colorScheme.onSurface,
controller: _editorController.contentController, controller: _editorController.contentController,
focusNode: _contentFocusNode, focusNode: _contentFocusNode,
borderRadius: borderRadius:
const BorderRadius.all(Radius.circular(20)), const BorderRadius.all(Radius.circular(20)),
width: 40, width: 40,
).paddingSymmetric(horizontal: 4), ).paddingSymmetric(horizontal: 2),
], ],
).paddingSymmetric(horizontal: 6, vertical: 8), ).paddingSymmetric(horizontal: 6, vertical: 8),
), ),

View File

@ -171,7 +171,7 @@ class _RealmPostListWidgetState extends State<RealmPostListWidget> {
Response resp; Response resp;
try { try {
resp = await provider.listPost(pageKey, realm: widget.realm.id); resp = await provider.listPost(pageKey, realm: widget.realm.alias);
} catch (e) { } catch (e) {
_pagingController.error = e; _pagingController.error = e;
return; return;

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
@ -93,6 +94,18 @@ class _SettingScreenState extends State<SettingScreen> {
AppRouter.instance.pushNamed('about'); AppRouter.instance.pushNamed('about');
}, },
), ),
TextButton(
style: const ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
),
child: Text('messageHistoryWipe'.tr),
onPressed: () {
final chatHistory = ChatEventController();
chatHistory.initialize().then((_) async {
await chatHistory.database.localEvents.wipeLocalEvents();
});
},
),
], ],
).paddingSymmetric(horizontal: 12, vertical: 8), ).paddingSymmetric(horizontal: 12, vertical: 8),
], ],

View File

@ -5,15 +5,15 @@ abstract class ServiceFinder {
static const String dealerUrl = static const String dealerUrl =
devFlag ? 'http://localhost:8442' : 'https://api.sn.solsynth.dev'; devFlag ? 'http://localhost:8442' : 'https://api.sn.solsynth.dev';
static const String passportUrl = static const String capitalUrl =
devFlag ? 'http://localhost:8444' : 'https://id.solsynth.dev'; devFlag ? 'http://localhost:8444' : 'https://solsynth.dev';
static String buildUrl(String serviceName, String? append) { static String buildUrl(String serviceName, String? append) {
append ??= ''; append ??= '';
if (serviceName == 'dealer') { if (serviceName == 'dealer') {
return '$dealerUrl$append'; return '$dealerUrl$append';
} else if (serviceName == 'passport') { } else if (serviceName == 'capital') {
return '$passportUrl$append'; return '$capitalUrl$append';
} }
return '$dealerUrl/cgi/$serviceName$append'; return '$dealerUrl/cgi/$serviceName$append';
} }

View File

@ -1,3 +1,4 @@
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
@ -29,6 +30,15 @@ class RootShell extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final routeName = state.topRoute?.name; final routeName = state.topRoute?.name;
if (routeName != null) {
FirebaseAnalytics.instance.logEvent(
name: 'screen_view',
parameters: {
'firebase_screen': routeName,
},
);
}
return Scaffold( return Scaffold(
key: rootScaffoldKey, key: rootScaffoldKey,
drawer: SolianTheme.isLargeScreen(context) drawer: SolianTheme.isLargeScreen(context)

View File

@ -12,6 +12,8 @@ const i18nEnglish = {
'draftBox': 'Draft Box', 'draftBox': 'Draft Box',
'more': 'More', 'more': 'More',
'share': 'Share', 'share': 'Share',
'shareNoUri': 'Share text content',
'alias': 'Alias',
'feed': 'Feed', 'feed': 'Feed',
'unlink': 'Unlink', 'unlink': 'Unlink',
'feedSearch': 'Search Feed', 'feedSearch': 'Search Feed',
@ -52,6 +54,7 @@ const i18nEnglish = {
'account': 'Account', 'account': 'Account',
'accountPersonalize': 'Personalize', 'accountPersonalize': 'Personalize',
'accountPersonalizeApplied': 'Account personalize settings has been saved.', 'accountPersonalizeApplied': 'Account personalize settings has been saved.',
'accountStickers': 'Stickers',
'accountFriend': 'Friend', 'accountFriend': 'Friend',
'accountFriendNew': 'New friend', 'accountFriendNew': 'New friend',
'accountFriendNewHint': 'accountFriendNewHint':
@ -117,8 +120,12 @@ const i18nEnglish = {
'postVisibleUsers': 'Visible users', 'postVisibleUsers': 'Visible users',
'postInvisibleUsers': 'Invisible users', 'postInvisibleUsers': 'Invisible users',
'postOverview': 'Overview', 'postOverview': 'Overview',
'postThumbnail': 'Thumbnail',
'postThumbnailAttachmentNew': 'Upload thumbnail',
'postThumbnailAttachment': 'Attachment serial number',
'postPinned': 'Pinned', 'postPinned': 'Pinned',
'postListNews': 'News', 'postListNews': 'News',
'postListFriends': 'Friends',
'postListShuffle': 'Random', 'postListShuffle': 'Random',
'postEditorModeStory': 'Post a post', 'postEditorModeStory': 'Post a post',
'postEditorModeArticle': 'Post an article', 'postEditorModeArticle': 'Post an article',
@ -162,9 +169,11 @@ const i18nEnglish = {
'attachmentAutoUpload': 'Auto Upload', 'attachmentAutoUpload': 'Auto Upload',
'attachmentUploadQueue': 'Upload Queue', 'attachmentUploadQueue': 'Upload Queue',
'attachmentUploadQueueStart': 'Start All', 'attachmentUploadQueueStart': 'Start All',
'attachmentUploadInProgress': 'There are attachments being uploaded. Please wait until all attachments have been uploaded before proceeding...', 'attachmentUploadInProgress':
'There are attachments being uploaded. Please wait until all attachments have been uploaded before proceeding...',
'attachmentAttached': 'Exists Files', 'attachmentAttached': 'Exists Files',
'attachmentUploadBlocked': 'Upload blocked, there is currently a task in progress...', 'attachmentUploadBlocked':
'Upload blocked, there is currently a task in progress...',
'attachmentAdd': 'Attach attachments', 'attachmentAdd': 'Attach attachments',
'attachmentAddGalleryPhoto': 'Gallery photo', 'attachmentAddGalleryPhoto': 'Gallery photo',
'attachmentAddGalleryVideo': 'Gallery video', 'attachmentAddGalleryVideo': 'Gallery video',
@ -173,7 +182,8 @@ const i18nEnglish = {
'attachmentAddClipboard': 'Paste file', 'attachmentAddClipboard': 'Paste file',
'attachmentAddFile': 'Attach file', 'attachmentAddFile': 'Attach file',
'attachmentAddLink': 'Link attachments', 'attachmentAddLink': 'Link attachments',
'attachmentAddLinkHint': 'Enter attachment serial number to link that attachment', 'attachmentAddLinkHint':
'Enter attachment serial number to link that attachment',
'attachmentAddLinkInput': 'Serial number', 'attachmentAddLinkInput': 'Serial number',
'attachmentSetting': 'Adjust attachment', 'attachmentSetting': 'Adjust attachment',
'attachmentAlt': 'Alternative text', 'attachmentAlt': 'Alternative text',
@ -317,8 +327,10 @@ const i18nEnglish = {
'bsCheckForUpdate': 'Checking For Updates', 'bsCheckForUpdate': 'Checking For Updates',
'bsCheckForUpdateFailed': 'Unable to Check Updates', 'bsCheckForUpdateFailed': 'Unable to Check Updates',
'bsCheckForUpdateNew': 'Found New Version', 'bsCheckForUpdateNew': 'Found New Version',
'bsCheckForUpdateDescApple': 'Please head to TestFlight and update your app to latest version to prevent error happens and get latest functions.', 'bsCheckForUpdateDescApple':
'bsCheckForUpdateDescCommon': 'Please head to our website download and install latest version of application to prevent error happens and get latest functions.', 'Please head to TestFlight and update your app to latest version to prevent error happens and get latest functions.',
'bsCheckForUpdateDescCommon':
'Please head to our website download and install latest version of application to prevent error happens and get latest functions.',
'bsCheckingServer': 'Checking Server Status', 'bsCheckingServer': 'Checking Server Status',
'bsCheckingServerFail': 'bsCheckingServerFail':
'Unable connect to server, check your network connection', 'Unable connect to server, check your network connection',
@ -327,6 +339,7 @@ const i18nEnglish = {
'bsEstablishingConn': 'Establishing Connection', 'bsEstablishingConn': 'Establishing Connection',
'bsPreparingData': 'Preparing User Data', 'bsPreparingData': 'Preparing User Data',
'bsRegisteringPushNotify': 'Enabling Push Notifications', 'bsRegisteringPushNotify': 'Enabling Push Notifications',
'bsDismissibleErrorHint': 'Click anywhere to ignore this error',
'postShareContent': 'postShareContent':
'@content\n\n@username on the Solar Network\nCheck it out: @link', '@content\n\n@username on the Solar Network\nCheck it out: @link',
'postShareSubject': '@username posted a post on the Solar Network', 'postShareSubject': '@username posted a post on the Solar Network',
@ -336,7 +349,34 @@ const i18nEnglish = {
'themeColorMiku': 'Miku Blue', 'themeColorMiku': 'Miku Blue',
'themeColorKagamine': 'Kagamine Yellow', 'themeColorKagamine': 'Kagamine Yellow',
'themeColorLuka': 'Luka Pink', 'themeColorLuka': 'Luka Pink',
'stickerDeletionConfirm': 'Confirm sticker delete',
'stickerDeletionConfirmCaption':
'Are you sure to delete sticker @name? This action cannot be undo.',
'themeColorApplied': 'Global theme color has been applied.', 'themeColorApplied': 'Global theme color has been applied.',
'attachmentSaved': 'Attachment saved to your system album.', 'attachmentSaved': 'Attachment saved to your system album.',
'cropImage': 'Crop Image', 'cropImage': 'Crop Image',
'stickerUploader': 'Upload sticker',
'stickerUploaderAttachmentNew': 'Upload sticker',
'stickerUploaderAttachment': 'Attachment serial number',
'stickerUploaderPack': 'Sticker pack serial number',
'stickerUploaderPackHint':
'Don\'t have pack id? Head to creator platform and create one!',
'stickerUploaderAlias': 'Alias',
'stickerUploaderAliasHint':
'Will be used as a placeholder with the sticker pack prefix when entered.',
'stickerUploaderName': 'Name',
'stickerUploaderNameHint':
'A human-friendly name given to the user in the sticker selection interface.',
'readMore': 'Read more',
'attachmentUnload': 'Not Loaded',
'attachmentUnloadCaption':
'In order to save traffic, this attachment is not loaded automatically. Click it to start loading.',
'callStatusConnected': 'Connected',
'callStatusDisconnected': 'Disconnected',
'callStatusConnecting': 'Connecting',
'callStatusReconnected': 'Reconnecting',
'messageOutOfSync': 'May Out of Sync with Server',
'messageOutOfSyncCaption':
'Since the App has entered the background, there may be a time difference between the message list and the server. Click to Refresh.',
'messageHistoryWipe': 'Wipe local message history',
}; };

View File

@ -20,6 +20,8 @@ const i18nSimplifiedChinese = {
'draftBox': '草稿箱', 'draftBox': '草稿箱',
'more': '更多', 'more': '更多',
'share': '分享', 'share': '分享',
'shareNoUri': '分享文字内容',
'alias': '别名',
'feed': '资讯', 'feed': '资讯',
'unlink': '移除链接', 'unlink': '移除链接',
'feedSearch': '搜索资讯', 'feedSearch': '搜索资讯',
@ -52,6 +54,7 @@ const i18nSimplifiedChinese = {
'account': '账号', 'account': '账号',
'accountPersonalize': '个性化', 'accountPersonalize': '个性化',
'accountPersonalizeApplied': '账户的个性化设置已保存。', 'accountPersonalizeApplied': '账户的个性化设置已保存。',
'accountStickers': '贴图',
'accountFriend': '好友', 'accountFriend': '好友',
'accountFriendNew': '添加好友', 'accountFriendNew': '添加好友',
'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!', 'accountFriendNewHint': '使用他人的用户名来发送一个好友请求吧!',
@ -111,6 +114,9 @@ const i18nSimplifiedChinese = {
'postVisibleUsers': '可见帖子者', 'postVisibleUsers': '可见帖子者',
'postInvisibleUsers': '隐藏帖子者', 'postInvisibleUsers': '隐藏帖子者',
'postOverview': '帖子概览', 'postOverview': '帖子概览',
'postThumbnail': '帖子缩略图',
'postThumbnailAttachmentNew': '上传附件作为缩略图',
'postThumbnailAttachment': '附件序列号',
'postPinned': '已置顶', 'postPinned': '已置顶',
'postEditorModeStory': '发个帖子', 'postEditorModeStory': '发个帖子',
'postEditorModeArticle': '撰写文章', 'postEditorModeArticle': '撰写文章',
@ -119,6 +125,7 @@ const i18nSimplifiedChinese = {
'articleDetail': '文章详情', 'articleDetail': '文章详情',
'draftBoxOpen': '打开草稿箱', 'draftBoxOpen': '打开草稿箱',
'postListNews': '新鲜事', 'postListNews': '新鲜事',
'postListFriends': '好友圈',
'postListShuffle': '打乱看', 'postListShuffle': '打乱看',
'postNew': '创建新帖子', 'postNew': '创建新帖子',
'postNewInRealmHint': '在领域 @realm 里发表新帖子', 'postNewInRealmHint': '在领域 @realm 里发表新帖子',
@ -305,6 +312,7 @@ const i18nSimplifiedChinese = {
'bsEstablishingConn': '部署连接中', 'bsEstablishingConn': '部署连接中',
'bsPreparingData': '正在准备用户资料', 'bsPreparingData': '正在准备用户资料',
'bsRegisteringPushNotify': '正在启用推送通知', 'bsRegisteringPushNotify': '正在启用推送通知',
'bsDismissibleErrorHint': '点击任意地方忽略此错误',
'postShareContent': '@content\n\n@username 在 Solar Network\n原帖地址:@link', 'postShareContent': '@content\n\n@username 在 Solar Network\n原帖地址:@link',
'postShareSubject': '@username 在 Solar Network 上发布了一篇帖子', 'postShareSubject': '@username 在 Solar Network 上发布了一篇帖子',
'themeColor': '全局主题色', 'themeColor': '全局主题色',
@ -314,6 +322,27 @@ const i18nSimplifiedChinese = {
'themeColorKagamine': '镜音黄', 'themeColorKagamine': '镜音黄',
'themeColorLuka': '流音粉', 'themeColorLuka': '流音粉',
'themeColorApplied': '全局主题颜色已应用', 'themeColorApplied': '全局主题颜色已应用',
'stickerDeletionConfirm': '确认删除贴图',
'stickerDeletionConfirmCaption': '你确认要删除贴图 @name 吗?该操作不可撤销。',
'attachmentSaved': '附件已保存到系统相册', 'attachmentSaved': '附件已保存到系统相册',
'cropImage': '裁剪图片', 'cropImage': '裁剪图片',
'stickerUploader': '上传贴图',
'stickerUploaderAttachmentNew': '上传附件作为贴图',
'stickerUploaderAttachment': '附件序列号',
'stickerUploaderPack': '贴图包序号',
'stickerUploaderPackHint': '没有该序号?请转到我们的创作者平台创建一个贴图包。',
'stickerUploaderAlias': '贴图别名',
'stickerUploaderAliasHint': '将会在输入时使用和贴图包前缀组成占位符。',
'stickerUploaderName': '贴图名称',
'stickerUploaderNameHint': '在贴图选择界面提供给用户的人类友好名称。',
'readMore': '阅读更多',
'attachmentUnload': '附件未加载',
'attachmentUnloadCaption': '为了节省流量,本附件未自动加载,点一下来开始加载。',
'callStatusConnected': '已连接',
'callStatusDisconnected': '已断开',
'callStatusConnecting': '连接中',
'callStatusReconnected': '重连中',
'messageOutOfSync': '消息可能与服务器脱节',
'messageOutOfSyncCaption': '由于 App 进入后台,消息列表可能与服务器存在时差,点击刷新。',
'messageHistoryWipe': '清除消息记录',
}; };

View File

@ -24,7 +24,6 @@ class AccountAvatar extends StatelessWidget {
if (content is String) { if (content is String) {
direct = content.startsWith('http'); direct = content.startsWith('http');
if (!isEmpty) isEmpty = content.isEmpty; if (!isEmpty) isEmpty = content.isEmpty;
if (!isEmpty) isEmpty = content.endsWith('/attachments/0');
} }
final url = direct final url = direct

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/account_status.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
@ -8,9 +7,9 @@ import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_heading.dart'; import 'package:solian/widgets/account/account_heading.dart';
class AccountProfilePopup extends StatefulWidget { class AccountProfilePopup extends StatefulWidget {
final Account account; final String name;
const AccountProfilePopup({super.key, required this.account}); const AccountProfilePopup({super.key, required this.name});
@override @override
State<AccountProfilePopup> createState() => _AccountProfilePopupState(); State<AccountProfilePopup> createState() => _AccountProfilePopupState();
@ -18,38 +17,75 @@ class AccountProfilePopup extends StatefulWidget {
class _AccountProfilePopupState extends State<AccountProfilePopup> { class _AccountProfilePopupState extends State<AccountProfilePopup> {
bool _isBusy = true; bool _isBusy = true;
dynamic _hasError;
Account? _userinfo; Account? _userinfo;
void getUserinfo() async { void _getUserinfo() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
try {
final client = ServiceFinder.configureClient('auth'); final client = ServiceFinder.configureClient('auth');
final resp = await client.get('/users/${widget.account.name}'); final resp = await client.get('/users/${widget.name}');
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
setState(() {
_userinfo = Account.fromJson(resp.body); _userinfo = Account.fromJson(resp.body);
setState(() => _isBusy = false); _isBusy = false;
});
} else { } else {
context.showErrorDialog(resp.bodyString); setState(() {
Navigator.pop(context); _hasError = resp.bodyString;
_isBusy = false;
});
}
} catch (e) {
setState(() {
_hasError = e;
_isBusy = false;
});
} }
} }
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
getUserinfo(); _getUserinfo();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isBusy || _userinfo == null) { if (_isBusy) {
return SizedBox( return SizedBox(
height: MediaQuery.of(context).size.height * 0.75, height: MediaQuery.of(context).size.height * 0.75,
child: const Center(child: CircularProgressIndicator()), child: const Center(child: CircularProgressIndicator()),
); );
} }
if (_hasError != null) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.75,
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.cancel, size: 24),
const SizedBox(height: 12),
Text(
_hasError.toString(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: _unFocusColor,
),
),
],
),
);
}
return SizedBox( return SizedBox(
height: MediaQuery.of(context).size.height * 0.75, height: MediaQuery.of(context).size.height * 0.75,
child: Column( child: Column(

View File

@ -226,7 +226,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
onTapOutside: (_) => onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
const SizedBox(height: 5), const SizedBox(height: 8),
TextField( TextField(
controller: _clearAtController, controller: _clearAtController,
readOnly: true, readOnly: true,
@ -238,7 +238,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
), ),
onTap: () => selectClearAt(), onTap: () => selectClearAt(),
), ),
const SizedBox(height: 5), const SizedBox(height: 8),
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Wrap( child: Wrap(
@ -281,7 +281,7 @@ class _AccountStatusEditorDialogState extends State<AccountStatusEditorDialog> {
], ],
), ),
), ),
const SizedBox(height: 5), const SizedBox(height: 8),
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Wrap( child: Wrap(

View File

@ -35,7 +35,7 @@ class SilverRelativeList extends StatelessWidget {
context: context, context: context,
builder: (context) => builder: (context) =>
AccountProfilePopup( AccountProfilePopup(
account: element.related, name: element.related.name,
), ),
); );
}, },

View File

@ -16,10 +16,12 @@ class AttachmentAttrEditorDialog extends StatefulWidget {
}); });
@override @override
State<AttachmentAttrEditorDialog> createState() => _AttachmentAttrEditorDialogState(); State<AttachmentAttrEditorDialog> createState() =>
_AttachmentAttrEditorDialogState();
} }
class _AttachmentAttrEditorDialogState extends State<AttachmentAttrEditorDialog> { class _AttachmentAttrEditorDialogState
extends State<AttachmentAttrEditorDialog> {
final _altController = TextEditingController(); final _altController = TextEditingController();
bool _isBusy = false; bool _isBusy = false;
@ -33,11 +35,10 @@ class _AttachmentAttrEditorDialogState extends State<AttachmentAttrEditorDialog>
final resp = await provider.updateAttachment( final resp = await provider.updateAttachment(
widget.item.id, widget.item.id,
_altController.value.text, _altController.value.text,
widget.item.usage,
isMature: _isMature, isMature: _isMature,
); );
Get.find<AttachmentProvider>().clearCache(id: widget.item.id); Get.find<AttachmentProvider>().clearCache(id: widget.item.rid);
setState(() => _isBusy = false); setState(() => _isBusy = false);
return Attachment.fromJson(resp.body); return Attachment.fromJson(resp.body);

View File

@ -22,17 +22,27 @@ import 'package:solian/widgets/attachments/attachment_attr_editor.dart';
import 'package:solian/widgets/attachments/attachment_fullscreen.dart'; import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
class AttachmentEditorPopup extends StatefulWidget { class AttachmentEditorPopup extends StatefulWidget {
final String usage; final String pool;
final List<int> initialAttachments; final bool singleMode;
final void Function(int) onAdd; final bool imageOnly;
final void Function(int) onRemove; final bool autoUpload;
final double? imageMaxWidth;
final double? imageMaxHeight;
final List<String>? initialAttachments;
final void Function(String) onAdd;
final void Function(String) onRemove;
const AttachmentEditorPopup({ const AttachmentEditorPopup({
super.key, super.key,
required this.usage, required this.pool,
required this.initialAttachments,
required this.onAdd, required this.onAdd,
required this.onRemove, required this.onRemove,
this.singleMode = false,
this.imageOnly = false,
this.autoUpload = false,
this.imageMaxWidth,
this.imageMaxHeight,
this.initialAttachments,
}); });
@override @override
@ -43,7 +53,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final _imagePicker = ImagePicker(); final _imagePicker = ImagePicker();
final AttachmentUploaderController _uploadController = Get.find(); final AttachmentUploaderController _uploadController = Get.find();
bool _isAutoUpload = false; late bool _isAutoUpload = widget.autoUpload;
bool _isBusy = false; bool _isBusy = false;
bool _isFirstTimeBusy = true; bool _isFirstTimeBusy = true;
@ -54,13 +64,28 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
final medias = await _imagePicker.pickMultiImage(); if (widget.singleMode) {
final medias = await _imagePicker.pickMultiImage(
maxWidth: widget.imageMaxWidth,
maxHeight: widget.imageMaxHeight,
);
if (medias.isEmpty) return; if (medias.isEmpty) return;
_enqueueTaskBatch(medias.map((x) { _enqueueTaskBatch(medias.map((x) {
final file = File(x.path); final file = File(x.path);
return AttachmentUploadTask(file: file, usage: widget.usage); return AttachmentUploadTask(file: file, usage: widget.pool);
})); }));
} else {
final media = await _imagePicker.pickMedia(
maxWidth: widget.imageMaxWidth,
maxHeight: widget.imageMaxHeight,
);
if (media == null) return;
_enqueueTask(
AttachmentUploadTask(file: File(media.path), usage: widget.pool),
);
}
} }
Future<void> _pickVideoToUpload() async { Future<void> _pickVideoToUpload() async {
@ -72,7 +97,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final file = File(media.path); final file = File(media.path);
_enqueueTask( _enqueueTask(
AttachmentUploadTask(file: file, usage: widget.usage), AttachmentUploadTask(file: file, usage: widget.pool),
); );
} }
@ -88,7 +113,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
List<File> files = result.paths.map((path) => File(path!)).toList(); List<File> files = result.paths.map((path) => File(path!)).toList();
_enqueueTaskBatch(files.map((x) { _enqueueTaskBatch(files.map((x) {
return AttachmentUploadTask(file: x, usage: widget.usage); return AttachmentUploadTask(file: x, usage: widget.pool);
})); }));
} }
@ -106,7 +131,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
final file = File(media.path); final file = File(media.path);
_enqueueTask( _enqueueTask(
AttachmentUploadTask(file: file, usage: widget.usage), AttachmentUploadTask(file: file, usage: widget.pool),
); );
} }
@ -156,14 +181,13 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
WidgetsBinding.instance.addPostFrameCallback((_) => controller.dispose()); WidgetsBinding.instance.addPostFrameCallback((_) => controller.dispose());
if (input == null || input.isEmpty) return; if (input == null || input.isEmpty) return;
final value = int.tryParse(input);
if (value == null) return;
final AttachmentProvider attach = Get.find(); final AttachmentProvider attach = Get.find();
final result = await attach.getMetadata(value); final result = await attach.getMetadata(input);
if (result != null) { if (result != null) {
widget.onAdd(result.id); widget.onAdd(result.rid);
setState(() => _attachments.add(result)); setState(() => _attachments.add(result));
if (widget.singleMode) Navigator.pop(context);
} }
} }
@ -176,12 +200,14 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
_uploadController.uploadAttachmentWithCallback( _uploadController.uploadAttachmentWithCallback(
data, data,
'Pasted Image', 'Pasted Image',
widget.usage, widget.pool,
null, null,
(item) { (item) {
widget.onAdd(item.id); if (item == null) return;
widget.onAdd(item.rid);
if (mounted) { if (mounted) {
setState(() => _attachments.add(item)); setState(() => _attachments.add(item));
if (widget.singleMode) Navigator.pop(context);
} }
}, },
); );
@ -209,12 +235,12 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
void _revertMetadataList() { void _revertMetadataList() {
final AttachmentProvider attach = Get.find(); final AttachmentProvider attach = Get.find();
if (widget.initialAttachments.isEmpty) { if (widget.initialAttachments?.isEmpty ?? true) {
_isFirstTimeBusy = false; _isFirstTimeBusy = false;
return; return;
} else { } else {
_attachments = List.filled( _attachments = List.filled(
widget.initialAttachments.length, widget.initialAttachments!.length,
null, null,
growable: true, growable: true,
); );
@ -222,7 +248,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
setState(() => _isBusy = true); setState(() => _isBusy = true);
attach.listMetadata(widget.initialAttachments).then((result) { attach
.listMetadata(widget.initialAttachments ?? List.empty())
.then((result) {
setState(() { setState(() {
_attachments = result; _attachments = result;
_isBusy = false; _isBusy = false;
@ -349,7 +377,15 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
child: Icon(Icons.check), child: Icon(Icons.check),
), ),
), ),
if (!element.isCompleted && canBeCrop) if (element.error != null)
IconButton(
tooltip: element.error!.toString(),
icon: const Icon(Icons.warning),
onPressed: () {},
),
if (!element.isCompleted &&
element.error == null &&
canBeCrop)
Obx( Obx(
() => IconButton( () => IconButton(
color: Colors.teal, color: Colors.teal,
@ -362,7 +398,9 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
}, },
), ),
), ),
if (!element.isCompleted && !element.isUploading) if (!element.isCompleted &&
!element.isUploading &&
element.error == null)
Obx( Obx(
() => IconButton( () => IconButton(
color: Colors.green, color: Colors.green,
@ -373,10 +411,14 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
: () { : () {
_uploadController _uploadController
.performSingleTask(index) .performSingleTask(index)
.then((r) { .then((out) {
widget.onAdd(r.id); if (out == null) return;
widget.onAdd(out.rid);
if (mounted) { if (mounted) {
setState(() => _attachments.add(r)); setState(() => _attachments.add(out));
if (widget.singleMode) {
Navigator.pop(context);
}
} }
}); });
}, },
@ -471,7 +513,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
), ),
onTap: () { onTap: () {
_deleteAttachment(element).then((_) { _deleteAttachment(element).then((_) {
widget.onRemove(element.id); widget.onRemove(element.rid);
setState(() => _attachments.removeAt(index)); setState(() => _attachments.removeAt(index));
}); });
}, },
@ -485,7 +527,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
), ),
), ),
onTap: () { onTap: () {
widget.onRemove(element.id); widget.onRemove(element.rid);
setState(() => _attachments.removeAt(index)); setState(() => _attachments.removeAt(index));
}, },
), ),
@ -516,9 +558,10 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
void _startUploading() { void _startUploading() {
_uploadController.performUploadQueue(onData: (r) { _uploadController.performUploadQueue(onData: (r) {
widget.onAdd(r.id); widget.onAdd(r.rid);
if (mounted) { if (mounted) {
setState(() => _attachments.add(r)); setState(() => _attachments.add(r));
if (widget.singleMode) Navigator.pop(context);
} }
}); });
} }
@ -539,7 +582,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
if (_uploadController.isUploading.value) return; if (_uploadController.isUploading.value) return;
_enqueueTaskBatch(detail.files.map((x) { _enqueueTaskBatch(detail.files.map((x) {
final file = File(x.path); final file = File(x.path);
return AttachmentUploadTask(file: file, usage: widget.usage); return AttachmentUploadTask(file: file, usage: widget.pool);
})); }));
}, },
child: Column( child: Column(
@ -551,9 +594,17 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Expanded(
child: Row(
children: [
Expanded(
child: Text(
'attachmentAdd'.tr, 'attachmentAdd'.tr,
style: Theme.of(context).textTheme.headlineSmall, style:
Theme.of(context).textTheme.headlineSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Obx(() { Obx(() {
@ -563,7 +614,8 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
height: 18, height: 18,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2.5, strokeWidth: 2.5,
value: _uploadController.progressOfUpload.value, value: _uploadController
.progressOfUpload.value,
), ),
); );
} }
@ -572,6 +624,10 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
], ],
), ),
), ),
],
),
),
const SizedBox(width: 20),
Text('attachmentAutoUpload'.tr), Text('attachmentAutoUpload'.tr),
const SizedBox(width: 8), const SizedBox(width: 8),
Switch( Switch(
@ -670,6 +726,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
ignoring: _uploadController.isUploading.value, ignoring: _uploadController.isUploading.value,
child: Container( child: Container(
height: 64, height: 64,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(
top: BorderSide( top: BorderSide(
@ -686,9 +743,10 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
runAlignment: WrapAlignment.center, runAlignment: WrapAlignment.center,
children: [ children: [
if (PlatformInfo.isDesktop || if ((PlatformInfo.isDesktop ||
PlatformInfo.isIOS || PlatformInfo.isIOS ||
PlatformInfo.isWeb) PlatformInfo.isWeb) &&
!widget.imageOnly)
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.paste), icon: const Icon(Icons.paste),
label: Text('attachmentAddClipboard'.tr), label: Text('attachmentAddClipboard'.tr),
@ -701,6 +759,7 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _pickPhotoToUpload(), onPressed: () => _pickPhotoToUpload(),
), ),
if (!widget.imageOnly)
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.add_road), icon: const Icon(Icons.add_road),
label: Text('attachmentAddGalleryVideo'.tr), label: Text('attachmentAddGalleryVideo'.tr),
@ -713,18 +772,21 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _takeMediaToUpload(false), onPressed: () => _takeMediaToUpload(false),
), ),
if (!widget.imageOnly)
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.video_camera_back_outlined), icon: const Icon(Icons.video_camera_back_outlined),
label: Text('attachmentAddCameraVideo'.tr), label: Text('attachmentAddCameraVideo'.tr),
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _takeMediaToUpload(true), onPressed: () => _takeMediaToUpload(true),
), ),
if (!widget.imageOnly)
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.file_present_rounded), icon: const Icon(Icons.file_present_rounded),
label: Text('attachmentAddFile'.tr), label: Text('attachmentAddFile'.tr),
style: const ButtonStyle(visualDensity: density), style: const ButtonStyle(visualDensity: density),
onPressed: () => _pickFileToUpload(), onPressed: () => _pickFileToUpload(),
), ),
if (!widget.imageOnly)
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.link), icon: const Icon(Icons.link),
label: Text('attachmentAddFile'.tr), label: Text('attachmentAddFile'.tr),

View File

@ -67,10 +67,10 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
Future<void> _saveToAlbum() async { Future<void> _saveToAlbum() async {
final url = ServiceFinder.buildUrl( final url = ServiceFinder.buildUrl(
'files', 'files',
'/attachments/${widget.item.id}', '/attachments/${widget.item.rid}',
); );
if (PlatformInfo.isWeb) { if (PlatformInfo.isWeb || PlatformInfo.isDesktop) {
await launchUrlString(url); await launchUrlString(url);
return; return;
} }
@ -257,6 +257,10 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
child: Wrap( child: Wrap(
spacing: 6, spacing: 6,
children: [ children: [
Text(
'#${widget.item.rid}',
style: metaTextStyle,
),
if (widget.item.metadata?['width'] != null && if (widget.item.metadata?['width'] != null &&
widget.item.metadata?['height'] != null) widget.item.metadata?['height'] != null)
Text( Text(

View File

@ -1,19 +1,21 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/platform.dart'; import 'package:solian/platform.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:video_player/video_player.dart';
class AttachmentItem extends StatefulWidget { class AttachmentItem extends StatefulWidget {
final String parentId; final String parentId;
final Attachment item; final Attachment item;
final bool showBadge; final bool showBadge;
final bool showHideButton; final bool showHideButton;
final bool autoload;
final BoxFit fit; final BoxFit fit;
final String? badge; final String? badge;
final Function? onHide; final Function? onHide;
@ -26,6 +28,7 @@ class AttachmentItem extends StatefulWidget {
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.showBadge = true, this.showBadge = true,
this.showHideButton = true, this.showHideButton = true,
this.autoload = false,
this.onHide, this.onHide,
}); });
@ -48,7 +51,10 @@ class _AttachmentItemState extends State<AttachmentItem> {
onHide: widget.onHide, onHide: widget.onHide,
); );
case 'video': case 'video':
return _AttachmentItemVideo(item: widget.item); return _AttachmentItemVideo(
item: widget.item,
autoload: widget.autoload,
);
default: default:
return Center( return Center(
child: Container( child: Container(
@ -85,7 +91,7 @@ class _AttachmentItemState extends State<AttachmentItem> {
launchUrlString( launchUrlString(
ServiceFinder.buildUrl( ServiceFinder.buildUrl(
'files', 'files',
'/attachments/${widget.item.id}', '/attachments/${widget.item.rid}',
), ),
); );
}, },
@ -129,7 +135,7 @@ class _AttachmentItemImage extends StatelessWidget {
fit: fit, fit: fit,
imageUrl: ServiceFinder.buildUrl( imageUrl: ServiceFinder.buildUrl(
'files', 'files',
'/attachments/${item.id}', '/attachments/${item.rid}',
), ),
progressIndicatorBuilder: (context, url, downloadProgress) { progressIndicatorBuilder: (context, url, downloadProgress) {
return Center( return Center(
@ -213,41 +219,96 @@ class _AttachmentItemImage extends StatelessWidget {
class _AttachmentItemVideo extends StatefulWidget { class _AttachmentItemVideo extends StatefulWidget {
final Attachment item; final Attachment item;
final bool autoload;
const _AttachmentItemVideo({required this.item}); const _AttachmentItemVideo({
required this.item,
this.autoload = false,
});
@override @override
State<_AttachmentItemVideo> createState() => _AttachmentItemVideoState(); State<_AttachmentItemVideo> createState() => _AttachmentItemVideoState();
} }
class _AttachmentItemVideoState extends State<_AttachmentItemVideo> { class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
late final _player = Player( VideoPlayerController? _playerController;
configuration: const PlayerConfiguration(logLevel: MPVLogLevel.error), ChewieController? _chewieController;
bool _showContent = false;
Future<void> _startLoad() async {
final ratio = widget.item.metadata?['ratio'] ?? 16 / 9;
_playerController = VideoPlayerController.networkUrl(
Uri.parse(
ServiceFinder.buildUrl('files', '/attachments/${widget.item.rid}'),
),
); );
late final _controller = VideoController(_player); _playerController!.initialize();
_chewieController = ChewieController(
aspectRatio: ratio,
videoPlayerController: _playerController!,
);
setState(() => _showContent = true);
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_player.open( if (widget.autoload) {
Media( _startLoad();
ServiceFinder.buildUrl('files', '/attachments/${widget.item.id}'),
),
play: false,
);
} }
@override
Widget build(BuildContext context) {
return Video(
aspectRatio: widget.item.metadata?['ratio'] ?? 16 / 9,
controller: _controller,
);
} }
@override @override
void dispose() { void dispose() {
_player.dispose(); _playerController?.dispose();
_chewieController?.dispose();
super.dispose(); super.dispose();
} }
@override
Widget build(BuildContext context) {
final ratio = widget.item.metadata?['ratio'] ?? 16 / 9;
if (!_showContent || _chewieController == null) {
return GestureDetector(
child: AspectRatio(
aspectRatio: ratio,
child: CenteredContainer(
maxWidth: 280,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.not_started,
color: Colors.white,
size: 32,
),
const SizedBox(height: 8),
Text(
'attachmentUnload'.tr,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'attachmentUnloadCaption'.tr,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
],
),
),
),
onTap: () {
_startLoad();
},
);
}
return Chewie(
controller: _chewieController!,
);
}
} }

View File

@ -3,19 +3,21 @@ import 'dart:ui';
import 'package:carousel_slider/carousel_slider.dart'; import 'package:carousel_slider/carousel_slider.dart';
import 'package:dismissible_page/dismissible_page.dart'; import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart' hide CarouselController;
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/widgets/attachments/attachment_item.dart'; import 'package:solian/widgets/attachments/attachment_item.dart';
import 'package:solian/providers/content/attachment.dart'; import 'package:solian/providers/content/attachment.dart';
import 'package:solian/widgets/attachments/attachment_fullscreen.dart'; import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
import 'package:solian/widgets/sized_container.dart';
class AttachmentList extends StatefulWidget { class AttachmentList extends StatefulWidget {
final String parentId; final String parentId;
final List<int> attachmentsId; final List<String> attachmentsId;
final bool isGrid; final bool isGrid;
final bool isForceGrid; final bool isForceGrid;
final bool autoload;
final double flatMaxHeight; final double flatMaxHeight;
final double? width; final double? width;
@ -27,6 +29,7 @@ class AttachmentList extends StatefulWidget {
required this.attachmentsId, required this.attachmentsId,
this.isGrid = false, this.isGrid = false,
this.isForceGrid = false, this.isForceGrid = false,
this.autoload = false,
this.flatMaxHeight = 720, this.flatMaxHeight = 720,
this.width, this.width,
this.viewport, this.viewport,
@ -54,10 +57,12 @@ class _AttachmentListState extends State<AttachmentList> {
} }
attach.listMetadata(widget.attachmentsId).then((result) { attach.listMetadata(widget.attachmentsId).then((result) {
if (mounted) {
setState(() { setState(() {
_attachmentsMeta = result; _attachmentsMeta = result;
_isLoading = false; _isLoading = false;
}); });
}
_calculateAspectRatio(); _calculateAspectRatio();
}); });
} }
@ -67,6 +72,7 @@ class _AttachmentListState extends State<AttachmentList> {
double? consistentValue; double? consistentValue;
int portrait = 0, square = 0, landscape = 0; int portrait = 0, square = 0, landscape = 0;
for (var entry in _attachmentsMeta) { for (var entry in _attachmentsMeta) {
if (entry == null) continue;
if (entry!.metadata?['ratio'] != null) { if (entry!.metadata?['ratio'] != null) {
if (entry.metadata?['ratio'] is int) { if (entry.metadata?['ratio'] is int) {
consistentValue ??= entry.metadata?['ratio'].toDouble(); consistentValue ??= entry.metadata?['ratio'].toDouble();
@ -108,6 +114,7 @@ class _AttachmentListState extends State<AttachmentList> {
showBadge: _attachmentsMeta.length > 1 && !widget.isGrid, showBadge: _attachmentsMeta.length > 1 && !widget.isGrid,
showBorder: widget.attachmentsId.length > 1, showBorder: widget.attachmentsId.length > 1,
showMature: _showMature, showMature: _showMature,
autoload: widget.autoload,
onReveal: (value) { onReveal: (value) {
setState(() => _showMature = value); setState(() => _showMature = value);
}, },
@ -135,8 +142,9 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
final isNotPureImage = _attachmentsMeta final isNotPureImage = _attachmentsMeta.any(
.any((x) => x?.mimetype.split('/').firstOrNull != 'image'); (x) => x?.mimetype.split('/').firstOrNull != 'image',
);
if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) { if (widget.isGrid && (widget.isForceGrid || !isNotPureImage)) {
const radius = BorderRadius.all(Radius.circular(8)); const radius = BorderRadius.all(Radius.circular(8));
return GridView.builder( return GridView.builder(
@ -154,8 +162,10 @@ class _AttachmentListState extends State<AttachmentList> {
final element = _attachmentsMeta[idx]; final element = _attachmentsMeta[idx];
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: border: Border.all(
Border.all(color: Theme.of(context).dividerColor, width: 1), color: Theme.of(context).dividerColor,
width: 1,
),
borderRadius: radius, borderRadius: radius,
), ),
child: ClipRRect( child: ClipRRect(
@ -207,6 +217,7 @@ class AttachmentListEntry extends StatelessWidget {
final bool showBadge; final bool showBadge;
final bool showMature; final bool showMature;
final bool isDense; final bool isDense;
final bool autoload;
final Function(bool) onReveal; final Function(bool) onReveal;
const AttachmentListEntry({ const AttachmentListEntry({
@ -220,6 +231,7 @@ class AttachmentListEntry extends StatelessWidget {
this.showBadge = false, this.showBadge = false,
this.showMature = false, this.showMature = false,
this.isDense = false, this.isDense = false,
this.autoload = false,
}); });
@override @override
@ -258,6 +270,7 @@ class AttachmentListEntry extends StatelessWidget {
item: item!, item: item!,
badge: showBadge ? badgeContent : null, badge: showBadge ? badgeContent : null,
showHideButton: !item!.isMature || showMature, showHideButton: !item!.isMature || showMature,
autoload: autoload,
onHide: () { onHide: () {
onReveal(false); onReveal(false);
}, },
@ -272,9 +285,8 @@ class AttachmentListEntry extends StatelessWidget {
), ),
), ),
if (item!.isMature && !showMature) if (item!.isMature && !showMature)
Center( CenteredContainer(
child: Container( maxWidth: 280,
constraints: const BoxConstraints(maxWidth: 280),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@ -302,7 +314,6 @@ class AttachmentListEntry extends StatelessWidget {
], ],
), ),
), ),
),
], ],
), ),
), ),
@ -322,3 +333,51 @@ class AttachmentListEntry extends StatelessWidget {
); );
} }
} }
class AttachmentSelfContainedEntry extends StatefulWidget {
final String rid;
final String parentId;
final bool isDense;
const AttachmentSelfContainedEntry({
super.key,
required this.rid,
required this.parentId,
this.isDense = false,
});
@override
State<AttachmentSelfContainedEntry> createState() =>
_AttachmentSelfContainedEntryState();
}
class _AttachmentSelfContainedEntryState
extends State<AttachmentSelfContainedEntry> {
bool _showMature = false;
@override
Widget build(BuildContext context) {
final AttachmentProvider attachments = Get.find();
return FutureBuilder(
future: attachments.getMetadata(widget.rid),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
return AttachmentListEntry(
item: snapshot.data,
isDense: widget.isDense,
parentId: widget.parentId,
showMature: _showMature,
onReveal: (value) {
setState(() => _showMature = value);
},
);
},
);
}
}

View File

@ -57,7 +57,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void promptAddMember() async { void _promptAddMember() async {
final input = await showModalBottomSheet( final input = await showModalBottomSheet(
context: context, context: context,
builder: (context) { builder: (context) {
@ -141,7 +141,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
'channelMembersAddHint' 'channelMembersAddHint'
.trParams({'channel': '#${widget.channel.alias}'}), .trParams({'channel': '#${widget.channel.alias}'}),
), ),
onTap: () => promptAddMember(), onTap: () => _promptAddMember(),
), ),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
@ -160,7 +160,7 @@ class _ChannelMemberListPopupState extends State<ChannelMemberListPopup> {
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
context: context, context: context,
builder: (context) => AccountProfilePopup( builder: (context) => AccountProfilePopup(
account: element.account, name: element.account.name,
), ),
); );
}, },

View File

@ -159,8 +159,9 @@ class _ControlsWidgetState extends State<ControlsWidget> {
} }
var track = await LocalVideoTrack.createScreenShareTrack( var track = await LocalVideoTrack.createScreenShareTrack(
ScreenShareCaptureOptions( ScreenShareCaptureOptions(
captureScreenAudio: true,
sourceId: source.id, sourceId: source.id,
maxFrameRate: 15.0, maxFrameRate: 30.0,
), ),
); );
await _participant.publishVideoTrack(track); await _participant.publishVideoTrack(track);
@ -174,6 +175,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
var track = await LocalVideoTrack.createScreenShareTrack( var track = await LocalVideoTrack.createScreenShareTrack(
const ScreenShareCaptureOptions( const ScreenShareCaptureOptions(
useiOSBroadcastExtension: true, useiOSBroadcastExtension: true,
captureScreenAudio: true,
maxFrameRate: 30.0, maxFrameRate: 30.0,
), ),
); );

View File

@ -39,8 +39,8 @@ class ChatEvent extends StatelessWidget {
Widget _buildAttachment(BuildContext context, {bool isMinimal = false}) { Widget _buildAttachment(BuildContext context, {bool isMinimal = false}) {
final attachments = item.body['attachments'] != null final attachments = item.body['attachments'] != null
? List<int>.from(item.body['attachments'].map((x) => x)) ? List<String>.from(item.body['attachments']?.whereType<String>())
: List<int>.empty(); : List<String>.empty();
if (attachments.isEmpty) return const SizedBox(); if (attachments.isEmpty) return const SizedBox();
@ -50,10 +50,10 @@ class ChatEvent extends StatelessWidget {
return Row( return Row(
children: [ children: [
Icon( Icon(
Icons.attachment, Icons.file_copy,
size: 18, size: 15,
color: unFocusColor, color: unFocusColor,
).paddingOnly(right: 6), ).paddingOnly(right: 5),
Text( Text(
'attachmentHint'.trParams( 'attachmentHint'.trParams(
{'count': attachments.length.toString()}, {'count': attachments.length.toString()},
@ -221,6 +221,8 @@ class ChatEvent extends StatelessWidget {
], ],
), ),
_buildContent().paddingOnly(left: 0.5), _buildContent().paddingOnly(left: 0.5),
_buildAttachment(context, isMinimal: true)
.paddingOnly(left: 0),
], ],
), ),
), ),
@ -243,7 +245,7 @@ class ChatEvent extends StatelessWidget {
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
context: context, context: context,
builder: (context) => AccountProfilePopup( builder: (context) => AccountProfilePopup(
account: item.sender.account, name: item.sender.account.name,
), ),
); );
}, },

View File

@ -31,7 +31,11 @@ class ChatEventMessageActionLog extends StatelessWidget {
).paddingOnly( ).paddingOnly(
left: isQuote ? 0 : (isMerged ? 64 : 12), left: isQuote ? 0 : (isMerged ? 64 : 12),
top: 2, top: 2,
bottom: isHasMerged ? 2 : 0, bottom: isQuote
? 0
: isHasMerged
? 2
: 0,
), ),
); );
} }

View File

@ -23,16 +23,19 @@ class ChatEventMessage extends StatelessWidget {
final body = EventMessageBody.fromJson(item.body); final body = EventMessageBody.fromJson(item.body);
final hasAttachment = body.attachments?.isNotEmpty ?? false; final hasAttachment = body.attachments?.isNotEmpty ?? false;
if (body.text.isEmpty && hasAttachment) { if (body.text.isEmpty &&
hasAttachment &&
!isContentPreviewing &&
!isQuote) {
final unFocusColor = final unFocusColor =
Theme.of(context).colorScheme.onSurface.withOpacity(0.75); Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
return Row( return Row(
children: [ children: [
Icon( Icon(
Icons.attachment, Icons.file_copy,
size: 18, size: 15,
color: unFocusColor, color: unFocusColor,
).paddingOnly(right: 6), ).paddingOnly(right: 5),
Text( Text(
'attachmentHint'.trParams( 'attachmentHint'.trParams(
{'count': body.attachments?.length.toString() ?? 0.toString()}, {'count': body.attachments?.length.toString() ?? 0.toString()},
@ -43,7 +46,11 @@ class ChatEventMessage extends StatelessWidget {
); );
} }
return MarkdownTextContent(content: body.text); return MarkdownTextContent(
parentId: 'm${item.id}',
isSelectable: true,
content: body.text,
);
} }
Widget _buildBody(BuildContext context) { Widget _buildBody(BuildContext context) {
@ -63,7 +70,9 @@ class ChatEventMessage extends StatelessWidget {
left: isQuote ? 0 : 12, left: isQuote ? 0 : 12,
right: isQuote ? 0 : 12, right: isQuote ? 0 : 12,
top: body.quoteEvent == null ? 2 : 0, top: body.quoteEvent == null ? 2 : 0,
bottom: hasAttachment && !isContentPreviewing ? 4 : (isHasMerged ? 2 : 0), bottom: hasAttachment && !isContentPreviewing && !isQuote
? 4
: (isHasMerged ? 2 : 0),
); );
} }
} }

View File

@ -1,17 +1,36 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.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/account.dart'; import 'package:solian/models/account.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/platform.dart';
import 'package:solian/providers/attachment_uploader.dart'; import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart'; import 'package:solian/widgets/attachments/attachment_editor.dart';
import 'package:solian/widgets/chat/chat_event.dart'; import 'package:solian/widgets/chat/chat_event.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ChatMessageSuggestion {
final String type;
final Widget leading;
final String display;
final String content;
ChatMessageSuggestion({
required this.type,
required this.leading,
required this.display,
required this.content,
});
}
class ChatMessageInput extends StatefulWidget { class ChatMessageInput extends StatefulWidget {
final Event? edit; final Event? edit;
final Event? reply; final Event? reply;
@ -40,7 +59,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
final TextEditingController _textController = TextEditingController(); final TextEditingController _textController = TextEditingController();
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
final List<int> _attachments = List.empty(growable: true); final List<String> _attachments = List.empty(growable: true);
Event? _editTo; Event? _editTo;
Event? _replyTo; Event? _replyTo;
@ -49,7 +68,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => AttachmentEditorPopup( builder: (context) => AttachmentEditorPopup(
usage: 'm.attachment', pool: 'messaging',
initialAttachments: _attachments, initialAttachments: _attachments,
onAdd: (value) { onAdd: (value) {
setState(() { setState(() {
@ -84,7 +103,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
final AttachmentUploaderController uploader = Get.find(); final AttachmentUploaderController uploader = Get.find();
if (uploader.queueOfUpload.any( if (uploader.queueOfUpload.any(
((x) => x.usage == 'm.attachment' && x.isUploading), ((x) => x.isUploading),
)) { )) {
context.showErrorDialog('attachmentUploadInProgress'.tr); context.showErrorDialog('attachmentUploadInProgress'.tr);
return; return;
@ -110,12 +129,14 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
client = auth.configureClient('messaging'); client = auth.configureClient('messaging');
if (_textController.text.trim().isEmpty && _attachments.isEmpty) return;
const uuid = Uuid(); const uuid = Uuid();
final payload = { final payload = {
'uuid': uuid.v4(), 'uuid': uuid.v4(),
'type': _editTo == null ? 'messages.new' : 'messages.edit', 'type': _editTo == null ? 'messages.new' : 'messages.edit',
'body': { 'body': {
'text': _textController.text, 'text': _textController.text.trim(),
'algorithm': 'plain', 'algorithm': 'plain',
'attachments': List.from(_attachments), 'attachments': List.from(_attachments),
'related_users': [ 'related_users': [
@ -156,7 +177,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
widget.onSent(message); widget.onSent(message);
} }
resetInput(); _resetInput();
if (_editTo != null) { if (_editTo != null) {
resp = await client.put( resp = await client.put(
@ -175,7 +196,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
} }
} }
void resetInput() { void _resetInput() {
if (widget.onReset != null) widget.onReset!(); if (widget.onReset != null) widget.onReset!();
_editTo = null; _editTo = null;
_replyTo = null; _replyTo = null;
@ -184,11 +205,13 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
setState(() {}); setState(() {});
} }
void syncWidget() { void _syncWidget() {
if (widget.edit != null && widget.edit!.type.startsWith('messages')) { if (widget.edit != null && widget.edit!.type.startsWith('messages')) {
final body = EventMessageBody.fromJson(widget.edit!.body); final body = EventMessageBody.fromJson(widget.edit!.body);
_editTo = widget.edit!; _editTo = widget.edit!;
_textController.text = body.text; _textController.text = body.text;
_attachments.addAll(
widget.edit!.body['attachments']?.cast<int>() ?? List.empty());
} }
if (widget.reply != null) { if (widget.reply != null) {
_replyTo = widget.reply!; _replyTo = widget.reply!;
@ -197,9 +220,52 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
setState(() {}); setState(() {});
} }
Widget _buildSuggestion(ChatMessageSuggestion suggestion) {
return ListTile(
leading: suggestion.leading,
title: Text(suggestion.display),
subtitle: Text(suggestion.content),
);
}
void _insertSuggestion(ChatMessageSuggestion suggestion) {
final replaceText =
_textController.text.substring(0, _textController.selection.baseOffset);
var startText = '';
final afterText = replaceText == _textController.text
? ''
: _textController.text
.substring(_textController.selection.baseOffset + 1);
var insertText = '';
if (suggestion.type == 'emotes') {
insertText = suggestion.content;
startText = replaceText.replaceFirstMapped(
RegExp(r':(?:([-\w]+)~)?([-\w]+)$'),
(Match m) => insertText,
);
}
if (suggestion.type == 'users') {
insertText = suggestion.content;
startText = replaceText.replaceFirstMapped(
RegExp(r'(?:\s|^)@([-\w]+)$'),
(Match m) => insertText,
);
}
if (insertText.isNotEmpty && startText.isNotEmpty) {
_textController.text = startText + afterText;
_textController.selection = TextSelection(
baseOffset: startText.length,
extentOffset: startText.length,
);
}
}
@override @override
void didUpdateWidget(covariant ChatMessageInput oldWidget) { void didUpdateWidget(covariant ChatMessageInput oldWidget) {
syncWidget(); _syncWidget();
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
} }
@ -207,7 +273,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final notifyBannerActions = [ final notifyBannerActions = [
TextButton( TextButton(
onPressed: resetInput, onPressed: _resetInput,
child: Text('cancel'.tr), child: Text('cancel'.tr),
) )
]; ];
@ -251,7 +317,20 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: TextField( child: TypeAheadField<ChatMessageSuggestion>(
direction: VerticalDirection.up,
hideOnEmpty: true,
hideOnLoading: true,
controller: _textController,
focusNode: _focusNode,
hideOnSelect: false,
debounceDuration: const Duration(milliseconds: 500),
onSelected: (value) {
_insertSuggestion(value);
},
itemBuilder: (context, item) => _buildSuggestion(item),
builder: (context, controller, focusNode) {
return TextField(
controller: _textController, controller: _textController,
focusNode: _focusNode, focusNode: _focusNode,
maxLines: null, maxLines: null,
@ -259,13 +338,80 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
decoration: InputDecoration.collapsed( decoration: InputDecoration.collapsed(
hintText: widget.placeholder ?? hintText: widget.placeholder ??
'messageInputPlaceholder'.trParams( 'messageInputPlaceholder'.trParams({
{'channel': '#${widget.channel.alias}'}, 'channel': '#${widget.channel.alias}',
), }),
), ),
onSubmitted: (_) => _sendMessage(), onSubmitted: (_) => _sendMessage(),
onTapOutside: (_) => onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
);
},
suggestionsCallback: (search) async {
final searchText = _textController.text
.substring(0, _textController.selection.baseOffset);
final emojiMatch = RegExp(r':(?:([-\w]+)~)?([-\w]+)$')
.firstMatch(searchText);
if (emojiMatch != null) {
final StickerProvider stickers = Get.find();
final emoteSearch = emojiMatch[2]!;
return stickers.availableStickers
.where(
(x) => x.textWarpedPlaceholder
.toUpperCase()
.contains(emoteSearch.toUpperCase()),
)
.map(
(x) => ChatMessageSuggestion(
type: 'emotes',
leading: PlatformInfo.canCacheImage
? CachedNetworkImage(
imageUrl: x.imageUrl,
width: 28,
height: 28,
)
: Image.network(
x.imageUrl,
width: 28,
height: 28,
),
display: x.name,
content: x.textWarpedPlaceholder,
),
)
.toList();
}
final userMatch =
RegExp(r'(?:\s|^)@([-\w]+)$').firstMatch(searchText);
if (userMatch != null) {
final userSearch = userMatch[1]!.toLowerCase();
final AuthProvider auth = Get.find();
final client = auth.configureClient('auth');
final resp = await client.get(
'/users/search?probe=$userSearch',
);
final List<Account> result = resp.body
.map((x) => Account.fromJson(x))
.toList()
.cast<Account>();
return result
.map(
(x) => ChatMessageSuggestion(
type: 'users',
leading: AccountAvatar(content: x.avatar),
display: x.nick,
content: '@${x.name}',
),
)
.toList();
}
return null;
},
), ),
), ),
IconButton( IconButton(

View File

@ -1,19 +1,33 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown_selectionarea/flutter_markdown.dart'; import 'package:flutter_markdown_selectionarea/flutter_markdown.dart';
import 'package:get/get.dart';
import 'package:markdown/markdown.dart' as markdown; import 'package:markdown/markdown.dart' as markdown;
import 'package:markdown/markdown.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'account/account_profile_popup.dart';
class MarkdownTextContent extends StatelessWidget { class MarkdownTextContent extends StatelessWidget {
final String content; final String content;
final String parentId;
final bool isSelectable; final bool isSelectable;
const MarkdownTextContent({ const MarkdownTextContent({
super.key, super.key,
required this.content, required this.content,
required this.parentId,
this.isSelectable = false, this.isSelectable = false,
}); });
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
final emojiRegex = RegExp(r':([-\w]+):');
final emojiMatch = emojiRegex.allMatches(content);
final isOnlyEmoji = content.replaceAll(emojiRegex, '').trim().isEmpty;
return Markdown( return Markdown(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@ -34,18 +48,110 @@ class MarkdownTextContent extends StatelessWidget {
extensionSet: markdown.ExtensionSet( extensionSet: markdown.ExtensionSet(
markdown.ExtensionSet.gitHubFlavored.blockSyntaxes, markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
<markdown.InlineSyntax>[ <markdown.InlineSyntax>[
_UserNameCardInlineSyntax(),
_CustomEmoteInlineSyntax(),
markdown.EmojiSyntax(), markdown.EmojiSyntax(),
markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(), markdown.AutolinkExtensionSyntax(),
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes ...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
], ],
), ),
onTapLink: (text, href, title) async { onTapLink: (text, href, title) async {
if (href == null) return; if (href == null) return;
if (href.startsWith('solink://')) {
final segments = href.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'users':
showModalBottomSheet(
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: Theme.of(context).colorScheme.surface,
context: context,
builder: (context) => AccountProfilePopup(
name: segments[1],
),
);
}
return;
}
await launchUrlString( await launchUrlString(
href, href,
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
); );
}, },
imageBuilder: (uri, title, alt) {
var url = uri.toString();
double? width, height;
BoxFit? fit;
if (url.startsWith('solink://')) {
final segments = url.replaceFirst('solink://', '').split('/');
switch (segments[0]) {
case 'stickers':
double radius = 8;
final StickerProvider sticker = Get.find();
url = sticker.aliasImageMapping[segments[1].toUpperCase()]!;
if (emojiMatch.length <= 1 && isOnlyEmoji) {
width = 128;
height = 128;
} else if (emojiMatch.length <= 3 && isOnlyEmoji) {
width = 32;
height = 32;
} else {
radius = 4;
width = 16;
height = 16;
}
fit = BoxFit.contain;
return ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(radius)),
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: PlatformInfo.canCacheImage
? CachedNetworkImage(
imageUrl: url,
width: width,
height: height,
fit: fit,
)
: Image.network(
url,
width: width,
height: height,
fit: fit,
),
),
).paddingSymmetric(vertical: 4);
case 'attachments':
const radius = BorderRadius.all(Radius.circular(8));
return LimitedBox(
maxHeight: MediaQuery.of(context).size.width,
child: ClipRRect(
borderRadius: radius,
child: AttachmentSelfContainedEntry(
isDense: true,
parentId: parentId,
rid: segments[1],
),
),
).paddingSymmetric(vertical: 4);
}
}
return PlatformInfo.canCacheImage
? CachedNetworkImage(
imageUrl: url,
width: width,
height: height,
fit: fit,
)
: Image.network(
url,
width: width,
height: height,
fit: fit,
);
},
); );
} }
@ -57,3 +163,39 @@ class MarkdownTextContent extends StatelessWidget {
return _buildContent(context); return _buildContent(context);
} }
} }
class _UserNameCardInlineSyntax extends InlineSyntax {
_UserNameCardInlineSyntax() : super(r'@[a-zA-Z0-9_]+');
@override
bool onMatch(markdown.InlineParser parser, Match match) {
final alias = match[0]!;
final anchor = markdown.Element.text('a', alias)
..attributes['href'] = Uri.encodeFull(
'solink://users/${alias.substring(1)}',
);
parser.addNode(anchor);
return true;
}
}
class _CustomEmoteInlineSyntax extends InlineSyntax {
_CustomEmoteInlineSyntax() : super(r':([-\w]+):');
@override
bool onMatch(markdown.InlineParser parser, Match match) {
final StickerProvider sticker = Get.find();
final alias = match[1]!.toUpperCase();
if (sticker.aliasImageMapping[alias] == null) {
parser.advanceBy(1);
return false;
}
final element = markdown.Element.empty('img');
element.attributes['src'] = 'solink://stickers/$alias';
parser.addNode(element);
return true;
}
}

View File

@ -4,17 +4,17 @@ import 'package:get/utils.dart';
abstract class AppNavigation { abstract class AppNavigation {
static List<AppNavigationDestination> destinations = [ static List<AppNavigationDestination> destinations = [
AppNavigationDestination( AppNavigationDestination(
icon: const Icon(Icons.home), icon: Icons.home,
label: 'home'.tr, label: 'home'.tr,
page: 'home', page: 'home',
), ),
AppNavigationDestination( AppNavigationDestination(
icon: const Icon(Icons.workspaces), icon: Icons.workspaces,
label: 'realms'.tr, label: 'realms'.tr,
page: 'realms', page: 'realms',
), ),
AppNavigationDestination( AppNavigationDestination(
icon: const Icon(Icons.forum), icon: Icons.forum,
label: 'channelTypeDirect'.tr, label: 'channelTypeDirect'.tr,
page: 'chat', page: 'chat',
), ),
@ -25,7 +25,7 @@ abstract class AppNavigation {
} }
class AppNavigationDestination { class AppNavigationDestination {
final Widget icon; final IconData icon;
final String label; final String label;
final String page; final String page;

View File

@ -1,18 +1,19 @@
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/account_status.dart'; import 'package:solian/models/account_status.dart';
import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/shells/root_shell.dart'; import 'package:solian/shells/root_shell.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_status_action.dart'; import 'package:solian/widgets/account/account_status_action.dart';
import 'package:solian/widgets/channel/channel_list.dart';
import 'package:solian/widgets/navigation/app_navigation.dart'; import 'package:solian/widgets/navigation/app_navigation.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
import 'package:solian/widgets/navigation/app_navigation_regions.dart';
class AppNavigationDrawer extends StatefulWidget { class AppNavigationDrawer extends StatefulWidget {
final String? routeName; final String? routeName;
@ -24,12 +25,8 @@ class AppNavigationDrawer extends StatefulWidget {
} }
class _AppNavigationDrawerState extends State<AppNavigationDrawer> { class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
int? _selectedIndex = 0;
AccountStatus? _accountStatus; AccountStatus? _accountStatus;
late final ChannelProvider _channels;
Future<void> _getStatus() async { Future<void> _getStatus() async {
final StatusProvider provider = Get.find(); final StatusProvider provider = Get.find();
@ -41,56 +38,26 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
}); });
} }
void _detectSelectedIndex() {
if (widget.routeName == null) return;
final nameList = AppNavigation.destinations.map((x) => x.page).toList();
final idx = nameList.indexOf(widget.routeName!);
_selectedIndex = idx != -1 ? idx : null;
}
void _closeDrawer() { void _closeDrawer() {
rootScaffoldKey.currentState!.closeDrawer(); rootScaffoldKey.currentState!.closeDrawer();
} }
Widget _buildSettingButton() {
return IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
AppRouter.instance.pushNamed('settings');
setState(() => _selectedIndex = null);
_closeDrawer();
});
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_channels = Get.find();
_detectSelectedIndex();
_getStatus(); _getStatus();
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
_detectSelectedIndex();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
return NavigationDrawer( return Drawer(
backgroundColor: backgroundColor:
SolianTheme.isLargeScreen(context) ? Colors.transparent : null, SolianTheme.isLargeScreen(context) ? Colors.transparent : null,
selectedIndex: _selectedIndex, child: SafeArea(
onDestinationSelected: (idx) { bottom: false,
setState(() => _selectedIndex = idx); child: Column(
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page);
_closeDrawer();
},
children: [ children: [
Obx(() { Obx(() {
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) { if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
@ -99,17 +66,15 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
leading: const Icon(Icons.account_circle), leading: const Icon(Icons.account_circle),
title: Text('guest'.tr), title: Text('guest'.tr),
subtitle: Text('unsignedIn'.tr), subtitle: Text('unsignedIn'.tr),
trailing: _buildSettingButton(),
onTap: () { onTap: () {
AppRouter.instance.goNamed('account'); AppRouter.instance.goNamed('account');
setState(() => _selectedIndex = null);
_closeDrawer(); _closeDrawer();
}, },
); );
} }
return ListTile( return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.only(left: 20, right: 20),
title: Text( title: Text(
auth.userProfile.value!['nick'], auth.userProfile.value!['nick'],
maxLines: 1, maxLines: 1,
@ -138,7 +103,8 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
: Colors.grey; : Colors.grey;
final RelationshipProvider relations = Get.find(); final RelationshipProvider relations = Get.find();
final accountNotifications = relations.friendRequestCount.value; final accountNotifications =
relations.friendRequestCount.value;
return badges.Badge( return badges.Badge(
badgeContent: Text( badgeContent: Text(
@ -152,7 +118,8 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
), ),
child: badges.Badge( child: badges.Badge(
showBadge: _accountStatus != null, showBadge: _accountStatus != null,
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor), badgeStyle:
badges.BadgeStyle(badgeColor: statusBadgeColor),
position: badges.BadgePosition.bottomEnd( position: badges.BadgePosition.bottomEnd(
bottom: 0, bottom: 0,
end: -2, end: -2,
@ -163,10 +130,8 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
), ),
); );
}), }),
trailing: _buildSettingButton(),
onTap: () { onTap: () {
AppRouter.instance.goNamed('account'); AppRouter.instance.goNamed('account');
setState(() => _selectedIndex = null);
_closeDrawer(); _closeDrawer();
}, },
onLongPress: () { onLongPress: () {
@ -181,61 +146,57 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
}); });
}, },
); );
}).paddingOnly(top: 8), }).paddingSymmetric(vertical: 8),
const Divider(thickness: 0.3, height: 1).paddingOnly( const Divider(thickness: 0.3, height: 1),
bottom: 12, Column(
top: 8, children: AppNavigation.destinations
.map(
(e) => ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
), ),
...AppNavigation.destinations.map( leading: Icon(e.icon, size: 20).paddingAll(2),
(e) => NavigationDrawerDestination( title: Text(e.label),
icon: e.icon, enabled: true,
label: Text(e.label), onTap: () {
AppRouter.instance.goNamed(e.page);
_closeDrawer();
},
), ),
), )
const Divider(thickness: 0.3, height: 1).paddingOnly( .toList(),
top: 12, ).paddingSymmetric(vertical: 8),
), const Divider(thickness: 0.3, height: 1),
Obx(() { Expanded(
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) { child: AppNavigationRegions(
return const SizedBox(); onSelected: (item) {
}
final selfId = auth.userProfile.value!['id'];
return Column(
children: [
Theme(
data: Theme.of(context)
.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
title: Text('channels'.tr),
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
children: [
Obx(
() => SizedBox(
height: 360,
child: RefreshIndicator(
onRefresh: () => _channels.refreshAvailableChannel(),
child: ChannelListWidget(
channels: _channels.groupChannels,
selfId: selfId,
isDense: true,
useReplace: true,
onSelected: (_) {
setState(() => _selectedIndex = null);
_closeDrawer(); _closeDrawer();
}, },
), ),
), ),
const Divider(thickness: 0.3, height: 1),
Column(
children: [
ListTile(
minTileHeight: 0,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
), ),
leading: const Icon(Icons.settings, size: 20).paddingAll(2),
title: Text('settings'.tr),
onTap: () {
AppRouter.instance.pushNamed('settings');
_closeDrawer();
},
),
],
).paddingOnly(
top: 8,
bottom: math.max(8, MediaQuery.of(context).padding.bottom),
), ),
], ],
), ),
), ),
],
);
}),
],
); );
} }
} }

View File

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/router.dart';
import 'package:collection/collection.dart';
class AppNavigationRegions extends StatelessWidget {
final Function(Channel item) onSelected;
const AppNavigationRegions({super.key, required this.onSelected});
void _gotoChannel(Channel item) {
AppRouter.instance.pushReplacementNamed(
'channelChat',
pathParameters: {'alias': item.alias},
queryParameters: {
if (item.realmId != null) 'realm': item.realm!.alias,
},
);
onSelected(item);
}
Widget _buildEntry(BuildContext context, Channel item) {
const padding = EdgeInsets.symmetric(horizontal: 20);
return ListTile(
minTileHeight: 0,
leading: const Icon(Icons.tag_outlined),
contentPadding: padding,
title: Text(item.name),
subtitle: Text(
item.description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: () => _gotoChannel(item),
);
}
@override
Widget build(BuildContext context) {
final ChannelProvider channels = Get.find();
return Obx(() {
final List<Channel> noRealmGroupChannels = channels.availableChannels
.where((x) => x.type == 0 && x.realmId == null)
.toList();
final List<Channel> hasRealmGroupChannels = channels.availableChannels
.where((x) => x.type == 0 && x.realmId != null)
.toList();
return CustomScrollView(
slivers: [
const SliverPadding(padding: EdgeInsets.only(top: 8)),
SliverList.builder(
itemCount: noRealmGroupChannels.length,
itemBuilder: (context, index) {
final element = noRealmGroupChannels[index];
return _buildEntry(context, element);
},
),
SliverList.list(
children: hasRealmGroupChannels
.groupListsBy((x) => x.realm)
.entries
.map((element) {
return ExpansionTile(
minTileHeight: 0,
initiallyExpanded: true,
tilePadding: const EdgeInsets.only(left: 20, right: 24),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer,
title: Text(element.value.first.realm!.name),
leading: const Icon(Icons.workspaces, size: 16)
.paddingSymmetric(horizontal: 4),
children:
element.value.map((x) => _buildEntry(context, x)).toList(),
);
}).toList(),
),
const SliverPadding(padding: EdgeInsets.only(bottom: 8)),
],
);
});
}
}

View File

@ -19,7 +19,7 @@ class PostEditorCategoriesDialog extends StatelessWidget {
initialTags: controller.tags, initialTags: controller.tags,
hintText: 'postTagsPlaceholder'.tr, hintText: 'postTagsPlaceholder'.tr,
onUpdate: (value) { onUpdate: (value) {
controller.tags.value = value; controller.tags.value = List.from(value, growable: true);
controller.tags.refresh(); controller.tags.refresh();
}, },
), ),

View File

@ -14,12 +14,25 @@ class PostEditorOverviewDialog extends StatelessWidget {
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextField(
autofocus: true,
autocorrect: true,
controller: controller.aliasController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
hintText: 'alias'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextField( TextField(
autofocus: true, autofocus: true,
autocorrect: true, autocorrect: true,
controller: controller.titleController, controller: controller.titleController,
decoration: InputDecoration( decoration: InputDecoration(
border: const UnderlineInputBorder(), isDense: true,
border: const OutlineInputBorder(),
hintText: 'title'.tr, hintText: 'title'.tr,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
@ -33,7 +46,8 @@ class PostEditorOverviewDialog extends StatelessWidget {
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
controller: controller.descriptionController, controller: controller.descriptionController,
decoration: InputDecoration( decoration: InputDecoration(
border: const UnderlineInputBorder(), isDense: true,
border: const OutlineInputBorder(),
hintText: 'description'.tr, hintText: 'description'.tr,
), ),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),

View File

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/controllers/post_editor_controller.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart';
class PostEditorThumbnailDialog extends StatefulWidget {
final PostEditorController controller;
const PostEditorThumbnailDialog({super.key, required this.controller});
@override
State<PostEditorThumbnailDialog> createState() =>
_PostEditorThumbnailDialogState();
}
class _PostEditorThumbnailDialogState extends State<PostEditorThumbnailDialog> {
final TextEditingController _attachmentController = TextEditingController();
void _promptUploadNewAttachment() {
showModalBottomSheet(
context: context,
builder: (context) => AttachmentEditorPopup(
pool: 'interactive',
singleMode: true,
imageOnly: true,
autoUpload: true,
onAdd: (value) {
setState(() {
_attachmentController.text = value.toString();
});
widget.controller.thumbnail.value = value;
},
initialAttachments: const [],
onRemove: (_) {},
),
);
}
@override
void initState() {
_attachmentController.text =
widget.controller.thumbnail.value?.toString() ?? '';
super.initState();
}
@override
void dispose() {
_attachmentController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('postThumbnail'.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text('postThumbnailAttachmentNew'.tr),
contentPadding: const EdgeInsets.only(left: 16, right: 13),
trailing: const Icon(Icons.chevron_right),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
onTap: () {
_promptUploadNewAttachment();
},
),
const SizedBox(height: 8),
TextField(
controller: _attachmentController,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
prefixText: '#',
labelText: 'postThumbnailAttachment'.tr,
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
],
),
actions: [
TextButton(
onPressed: () {
widget.controller.thumbnail.value = _attachmentController.text;
Navigator.pop(context);
},
child: Text('confirm'.tr),
),
],
);
}
}

View File

@ -1,5 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:firebase_analytics/firebase_analytics.dart';
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:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -7,6 +8,7 @@ import 'package:get/get.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/posts/post_editor.dart'; import 'package:solian/screens/posts/post_editor.dart';
@ -38,13 +40,32 @@ class _PostActionState extends State<PostAction> {
}); });
} }
Future<void> _doShare() async { Future<void> _doShare({bool noUri = false}) async {
ShareResult result;
String id;
final box = context.findRenderObject() as RenderBox?; final box = context.findRenderObject() as RenderBox?;
await Share.share( if (widget.item.alias?.isNotEmpty ?? false) {
id = '${widget.item.areaAlias}/${widget.item.alias}';
} else {
id = '${widget.item.id}';
}
if ((PlatformInfo.isAndroid || PlatformInfo.isIOS) && !noUri) {
result = await Share.shareUri(
Uri.parse('https://solsynth.dev/posts/$id'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
} else {
final extraContent = <String?>[
widget.item.body['title'],
widget.item.body['description'],
].where((x) => x != null && x.isNotEmpty).toList();
final isExtraNotEmpty = extraContent.any((x) => x != null);
result = await Share.share(
'postShareContent'.trParams({ 'postShareContent'.trParams({
'username': widget.item.author.nick, 'username': widget.item.author.nick,
'content': widget.item.body['content'] ?? 'no content', 'content':
'link': 'https://sn.solsynth.dev/posts/view/${widget.item.id}', '${extraContent.join('\n')}${isExtraNotEmpty ? '\n\n' : ''}${widget.item.body['content'] ?? 'no content'}',
'link': 'https://solsynth.dev/posts/$id',
}), }),
subject: 'postShareSubject'.trParams({ subject: 'postShareSubject'.trParams({
'username': widget.item.author.nick, 'username': widget.item.author.nick,
@ -53,6 +74,15 @@ class _PostActionState extends State<PostAction> {
); );
} }
if (result.status != ShareResultStatus.dismissed) {
await FirebaseAnalytics.instance.logShare(
contentType: 'Post',
itemId: widget.item.id.toString(),
method: result.raw,
);
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -72,10 +102,28 @@ class _PostActionState extends State<PostAction> {
'postActionList'.tr, 'postActionList'.tr,
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
), ),
Row(
children: [
Text( Text(
'#${widget.item.id.toString().padLeft(8, '0')}', '#${widget.item.id.toString().padLeft(8, '0')}',
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
if (widget.item.alias?.isNotEmpty ?? false)
Text(
'·',
style: Theme.of(context).textTheme.bodySmall,
).paddingSymmetric(horizontal: 6),
if (widget.item.alias?.isNotEmpty ?? false)
Expanded(
child: Text(
'${widget.item.areaAlias}:${widget.item.alias}',
style: Theme.of(context).textTheme.bodySmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
], ],
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
if (_isBusy) const LinearProgressIndicator().animate().scaleX(), if (_isBusy) const LinearProgressIndicator().animate().scaleX(),
@ -86,6 +134,16 @@ class _PostActionState extends State<PostAction> {
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.share), leading: const Icon(Icons.share),
title: Text('share'.tr), title: Text('share'.tr),
trailing: PlatformInfo.isIOS || PlatformInfo.isAndroid
? IconButton(
icon: const Icon(Icons.link_off),
tooltip: 'shareNoUri'.tr,
onPressed: () async {
await _doShare(noUri: true);
Navigator.pop(context);
},
)
: null,
onTap: () async { onTap: () async {
await _doShare(); await _doShare();
Navigator.pop(context); Navigator.pop(context);
@ -97,13 +155,13 @@ class _PostActionState extends State<PostAction> {
leading: const FaIcon(FontAwesomeIcons.reply, size: 20), leading: const FaIcon(FontAwesomeIcons.reply, size: 20),
title: Text('reply'.tr), title: Text('reply'.tr),
onTap: () async { onTap: () async {
final value = await AppRouter.instance.pushNamed( Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor', 'postEditor',
extra: PostPublishArguments(reply: widget.item), extra: PostPublishArguments(reply: widget.item),
),
); );
if (value != null) {
Navigator.pop(context, true);
}
}, },
), ),
if (!widget.noReact) if (!widget.noReact)
@ -112,13 +170,13 @@ class _PostActionState extends State<PostAction> {
leading: const FaIcon(FontAwesomeIcons.retweet, size: 20), leading: const FaIcon(FontAwesomeIcons.retweet, size: 20),
title: Text('repost'.tr), title: Text('repost'.tr),
onTap: () async { onTap: () async {
final value = await AppRouter.instance.pushNamed( Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor', 'postEditor',
extra: PostPublishArguments(repost: widget.item), extra: PostPublishArguments(repost: widget.item),
),
); );
if (value != null) {
Navigator.pop(context, true);
}
}, },
), ),
if (_canModifyContent && !widget.noReact) if (_canModifyContent && !widget.noReact)
@ -146,13 +204,13 @@ class _PostActionState extends State<PostAction> {
leading: const Icon(Icons.edit), leading: const Icon(Icons.edit),
title: Text('edit'.tr), title: Text('edit'.tr),
onTap: () async { onTap: () async {
final value = await AppRouter.instance.pushNamed( Navigator.pop(
context,
AppRouter.instance.pushNamed(
'postEditor', 'postEditor',
extra: PostPublishArguments(edit: widget.item), extra: PostPublishArguments(edit: widget.item),
),
); );
if (value != null) {
Navigator.pop(context, true);
}
}, },
), ),
if (_canModifyContent) if (_canModifyContent)

View File

@ -1,5 +1,6 @@
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get_utils/get_utils.dart'; import 'package:get/get_utils/get_utils.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -22,7 +23,9 @@ class PostItem extends StatefulWidget {
final bool isReactable; final bool isReactable;
final bool isShowReply; final bool isShowReply;
final bool isShowEmbed; final bool isShowEmbed;
final bool isOverrideEmbedClickable;
final bool isFullDate; final bool isFullDate;
final bool isFullContent;
final bool isContentSelectable; final bool isContentSelectable;
final String? attachmentParent; final String? attachmentParent;
final Color? backgroundColor; final Color? backgroundColor;
@ -35,7 +38,9 @@ class PostItem extends StatefulWidget {
this.isReactable = true, this.isReactable = true,
this.isShowReply = true, this.isShowReply = true,
this.isShowEmbed = true, this.isShowEmbed = true,
this.isOverrideEmbedClickable = false,
this.isFullDate = false, this.isFullDate = false,
this.isFullContent = false,
this.isContentSelectable = false, this.isContentSelectable = false,
this.attachmentParent, this.attachmentParent,
this.backgroundColor, this.backgroundColor,
@ -71,14 +76,33 @@ class _PostItemState extends State<PostItem> {
} }
} }
Widget _buildThumbnail() {
if (widget.item.body['thumbnail'] == null) return const SizedBox();
final border = BorderSide(
color: Theme.of(context).dividerColor,
width: 0.3,
);
return Container(
decoration: BoxDecoration(border: Border(top: border, bottom: border)),
child: AspectRatio(
aspectRatio: 16 / 9,
child: AttachmentSelfContainedEntry(
rid: widget.item.body['thumbnail'],
parentId: 'p${item.id}-thumbnail',
),
),
);
}
Widget _buildHeader() { Widget _buildHeader() {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (widget.isCompact) if (widget.isCompact)
AccountAvatar( AccountAvatar(
content: item.author.avatar.toString(), content: item.author.avatar.toString(),
radius: 10, radius: 10,
).paddingOnly(left: 2), ).paddingOnly(left: 2, top: 1),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -105,18 +129,26 @@ class _PostItemState extends State<PostItem> {
item.body['description'], item.body['description'],
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
if (item.body['description'] != null ||
item.body['title'] != null)
const Divider(thickness: 0.3, height: 1).paddingSymmetric(
vertical: 8,
),
], ],
).paddingOnly(left: widget.isCompact ? 6 : 12), ).paddingOnly(left: widget.isCompact ? 6 : 12),
), ),
if (widget.item.type == 'article')
Badge(
label: Text('article'.tr),
).paddingOnly(top: 3),
], ],
); );
} }
Widget _buildHeaderDivider() {
if (item.body['description'] != null || item.body['title'] != null) {
return const Divider(thickness: 0.3, height: 1).paddingSymmetric(
vertical: 8,
);
}
return const SizedBox();
}
Widget _buildFooter() { Widget _buildFooter() {
List<String> labels = List.empty(growable: true); List<String> labels = List.empty(growable: true);
if (widget.item.editedAt != null) { if (widget.item.editedAt != null) {
@ -164,6 +196,7 @@ class _PostItemState extends State<PostItem> {
Widget _buildReply(BuildContext context) { Widget _buildReply(BuildContext context) {
return OpenContainer( return OpenContainer(
tappable: widget.isClickable || widget.isOverrideEmbedClickable,
closedBuilder: (_, openContainer) => Column( closedBuilder: (_, openContainer) => Column(
children: [ children: [
Row( Row(
@ -202,13 +235,15 @@ class _PostItemState extends State<PostItem> {
), ),
closedElevation: 0, closedElevation: 0,
openElevation: 0, openElevation: 0,
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface, closedColor:
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface, openColor: Theme.of(context).colorScheme.surface,
); );
} }
Widget _buildRepost(BuildContext context) { Widget _buildRepost(BuildContext context) {
return OpenContainer( return OpenContainer(
tappable: widget.isClickable || widget.isOverrideEmbedClickable,
closedBuilder: (_, openContainer) => Column( closedBuilder: (_, openContainer) => Column(
children: [ children: [
Row( Row(
@ -247,15 +282,18 @@ class _PostItemState extends State<PostItem> {
), ),
closedElevation: 0, closedElevation: 0,
openElevation: 0, openElevation: 0,
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface, closedColor:
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface, openColor: Theme.of(context).colorScheme.surface,
); );
} }
double _contentHeight = 0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final List<int> attachments = item.body['attachments'] is List final List<String> attachments = item.body['attachments'] is List
? item.body['attachments']?.cast<int>() ? List.from(item.body['attachments']?.whereType<String>())
: List.empty(); : List.empty();
final hasAttachment = attachments.isNotEmpty; final hasAttachment = attachments.isNotEmpty;
@ -263,8 +301,20 @@ class _PostItemState extends State<PostItem> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildThumbnail().paddingOnly(bottom: 8),
_buildHeader().paddingSymmetric(horizontal: 12), _buildHeader().paddingSymmetric(horizontal: 12),
MarkdownTextContent( _buildHeaderDivider().paddingSymmetric(horizontal: 12),
Stack(
children: [
SizedContainer(
maxWidth: 640,
maxHeight: widget.isFullContent ? double.infinity : 80,
child: _MeasureSize(
onChange: (size) {
setState(() => _contentHeight = size.height);
},
child: MarkdownTextContent(
parentId: 'p${item.id}',
content: item.body['content'], content: item.body['content'],
isSelectable: widget.isContentSelectable, isSelectable: widget.isContentSelectable,
).paddingOnly( ).paddingOnly(
@ -273,15 +323,41 @@ class _PostItemState extends State<PostItem> {
top: 2, top: 2,
bottom: hasAttachment ? 4 : 0, bottom: hasAttachment ? 4 : 0,
), ),
),
),
if (_contentHeight >= 80 && !widget.isFullContent)
Align(
alignment: Alignment.bottomCenter,
child: IgnorePointer(
child: Container(
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surfaceContainerLow,
Theme.of(context)
.colorScheme
.surface
.withOpacity(0),
],
),
),
),
),
),
],
),
_buildFooter().paddingOnly(left: 16), _buildFooter().paddingOnly(left: 16),
if (attachments.isNotEmpty) if (attachments.isNotEmpty)
Row( Row(
children: [ children: [
Icon( Icon(
Icons.attachment, Icons.file_copy,
size: 18, size: 15,
color: _unFocusColor, color: _unFocusColor,
).paddingOnly(right: 6), ).paddingOnly(right: 5),
Text( Text(
'attachmentHint'.trParams( 'attachmentHint'.trParams(
{'count': attachments.length.toString()}, {'count': attachments.length.toString()},
@ -295,9 +371,11 @@ class _PostItemState extends State<PostItem> {
} }
return OpenContainer( return OpenContainer(
tappable: widget.isClickable,
closedBuilder: (_, openContainer) => Column( closedBuilder: (_, openContainer) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildThumbnail().paddingOnly(bottom: 4),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -310,7 +388,7 @@ class _PostItemState extends State<PostItem> {
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
context: context, context: context,
builder: (context) => AccountProfilePopup( builder: (context) => AccountProfilePopup(
account: item.author, name: item.author.name,
), ),
); );
}, },
@ -320,13 +398,48 @@ class _PostItemState extends State<PostItem> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildHeader(), _buildHeader(),
_buildHeaderDivider(),
Stack(
children: [
SizedContainer( SizedContainer(
maxWidth: 640, maxWidth: 640,
maxHeight:
widget.isFullContent ? double.infinity : 320,
child: _MeasureSize(
onChange: (size) {
setState(() => _contentHeight = size.height);
},
child: MarkdownTextContent( child: MarkdownTextContent(
parentId: 'p${item.id}-embed',
content: item.body['content'], content: item.body['content'],
isSelectable: widget.isContentSelectable, isSelectable: widget.isContentSelectable,
).paddingOnly(left: 12, right: 8), ).paddingOnly(left: 12, right: 8),
), ),
),
if (_contentHeight >= 320 && !widget.isFullContent)
Align(
alignment: Alignment.bottomCenter,
child: IgnorePointer(
child: Container(
height: 320,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context)
.colorScheme
.surface
.withOpacity(0),
],
),
),
),
),
),
],
),
if (widget.item.replyTo != null && widget.isShowEmbed) if (widget.item.replyTo != null && widget.isShowEmbed)
_buildReply(context).paddingOnly(top: 4), _buildReply(context).paddingOnly(top: 4),
if (widget.item.repostTo != null && widget.isShowEmbed) if (widget.item.repostTo != null && widget.isShowEmbed)
@ -343,11 +456,13 @@ class _PostItemState extends State<PostItem> {
left: 16, left: 16,
), ),
AttachmentList( AttachmentList(
flatMaxHeight: MediaQuery.of(context).size.width,
parentId: widget.item.id.toString(), parentId: widget.item.id.toString(),
attachmentsId: attachments, attachmentsId: attachments,
autoload: true,
isGrid: attachments.length > 1, isGrid: attachments.length > 1,
), ),
if (widget.isShowReply && widget.isReactable) if (widget.isShowReply || widget.isReactable)
PostQuickAction( PostQuickAction(
isShowReply: widget.isShowReply, isShowReply: widget.isShowReply,
isReactable: widget.isReactable, isReactable: widget.isReactable,
@ -377,8 +492,51 @@ class _PostItemState extends State<PostItem> {
), ),
closedElevation: 0, closedElevation: 0,
openElevation: 0, openElevation: 0,
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface, closedColor:
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface, openColor: Theme.of(context).colorScheme.surface,
); );
} }
} }
typedef _OnWidgetSizeChange = void Function(Size size);
class _MeasureSizeRenderObject extends RenderProxyBox {
Size? oldSize;
_OnWidgetSizeChange onChange;
_MeasureSizeRenderObject(this.onChange);
@override
void performLayout() {
super.performLayout();
Size newSize = child!.size;
if (oldSize == newSize) return;
oldSize = newSize;
WidgetsBinding.instance.addPostFrameCallback((_) {
onChange(newSize);
});
}
}
class _MeasureSize extends SingleChildRenderObjectWidget {
final _OnWidgetSizeChange onChange;
const _MeasureSize({
required this.onChange,
required Widget super.child,
});
@override
RenderObject createRenderObject(BuildContext context) {
return _MeasureSizeRenderObject(onChange);
}
@override
void updateRenderObject(
BuildContext context, covariant _MeasureSizeRenderObject renderObject) {
renderObject.onChange = onChange;
}
}

View File

@ -85,7 +85,13 @@ class PostListEntryWidget extends StatelessWidget {
context: context, context: context,
builder: (context) => PostAction(item: item), builder: (context) => PostAction(item: item),
).then((value) { ).then((value) {
if (value != null) onUpdate(); if (value is Future) {
value.then((_) {
onUpdate();
});
} else if (value != null) {
onUpdate();
}
}); });
}, },
); );

View File

@ -6,18 +6,21 @@ import 'package:solian/widgets/posts/post_item.dart';
class PostOwnedListEntry extends StatelessWidget { class PostOwnedListEntry extends StatelessWidget {
final Post item; final Post item;
final Function onTap; final Function onTap;
final bool isFullContent;
final Color? backgroundColor;
const PostOwnedListEntry({ const PostOwnedListEntry({
super.key, super.key,
required this.item, required this.item,
required this.onTap, required this.onTap,
this.isFullContent = false,
this.backgroundColor,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
child: InkWell( child: GestureDetector(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -28,6 +31,8 @@ class PostOwnedListEntry extends StatelessWidget {
isClickable: false, isClickable: false,
isShowReply: false, isShowReply: false,
isReactable: false, isReactable: false,
isFullContent: isFullContent,
backgroundColor: backgroundColor,
).paddingSymmetric(vertical: 8), ).paddingSymmetric(vertical: 8),
], ],
), ),

View File

@ -9,6 +9,7 @@ class PostWarpedListWidget extends StatelessWidget {
final bool isNestedClickable; final bool isNestedClickable;
final bool isPinned; final bool isPinned;
final PagingController<int, Post> controller; final PagingController<int, Post> controller;
final Function? onUpdate;
const PostWarpedListWidget({ const PostWarpedListWidget({
super.key, super.key,
@ -17,6 +18,7 @@ class PostWarpedListWidget extends StatelessWidget {
this.isClickable = true, this.isClickable = true,
this.isNestedClickable = true, this.isNestedClickable = true,
this.isPinned = true, this.isPinned = true,
this.onUpdate,
}); });
@override @override
@ -35,9 +37,7 @@ class PostWarpedListWidget extends StatelessWidget {
isNestedClickable: isNestedClickable, isNestedClickable: isNestedClickable,
isClickable: isClickable, isClickable: isClickable,
item: item, item: item,
onUpdate: () { onUpdate: onUpdate ?? () {},
controller.refresh();
},
); );
}, },
), ),

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