Compare commits

..

46 Commits

Author SHA1 Message Date
989b5babd9 Auto update checking 2024-08-03 01:14:42 +08:00
9ea364640d 🚀 Launch 1.2.0+8 2024-08-02 23:24:36 +08:00
a9f55a489d ⬆️ Clean and upgrade packages 2024-08-02 23:22:50 +08:00
4616f3a3e2 Friend request indicator 2024-08-02 23:15:28 +08:00
425bae9d13 💄 Better friend page loading indicator 2024-08-02 22:54:56 +08:00
07771e8979 Improve the speed of fetching attachments meta via batch api 2024-08-02 22:46:48 +08:00
0ad4854443 💄 Grid view in call 2024-08-02 21:12:37 +08:00
4238ea6fdc Call grid layout 2024-08-02 18:49:28 +08:00
7d45c06302 💄 Optimized signal indicator 2024-08-02 18:29:01 +08:00
7e8993fbd2 💫 Auto hide or show call controls 2024-08-02 18:09:07 +08:00
c88fcc84da Show call participants 2024-08-02 17:14:23 +08:00
11fb79623e Attachment can link exists things
 Optimize upload progress
2024-08-02 15:49:32 +08:00
98cc313a91 💫 Optimize chat event list animation 2024-08-02 14:14:09 +08:00
bc3401a897 🐛 Fix post item color mismatch 2024-08-02 05:10:10 +08:00
5b6a5d9046 🐛 Fix post popup color mismatch 2024-08-02 05:04:31 +08:00
6cbd78e836 💫 Optimize post editor transition 2024-08-02 04:59:35 +08:00
aefcbad02f 💫 Better animated post list 2024-08-02 04:42:38 +08:00
70617be687 💫 Animated chat 2024-08-02 04:24:12 +08:00
cccb3d5c16 🐛 Fix post won't refresh after post 2024-08-02 01:00:31 +08:00
a0a3a8d182 DM message last preview 2024-08-02 00:54:19 +08:00
c6b2ef8459 💄 Better about 2024-08-02 00:41:12 +08:00
34a2fe3988 Move about page link from account to settings 2024-08-02 00:29:51 +08:00
0a5604d0ff Crop image in personalize 2024-08-02 00:12:16 +08:00
5e754ad233 💫 About page icon will rotate 2024-08-01 23:51:03 +08:00
5b9c92e4d3 Crop image 2024-08-01 23:44:07 +08:00
b2a6ca7244 Improve attachments queue performance 2024-08-01 23:10:19 +08:00
27c60fc8cb Block user action when attachments isn't ready 2024-08-01 22:36:00 +08:00
8b3c45ab29 Queued upload 2024-08-01 22:13:08 +08:00
adb415700a 💄 Optimized attachment edit action 2024-08-01 17:19:55 +08:00
1e4b44a78b 💄 Better attachment editor previewing 2024-08-01 16:45:18 +08:00
9765b200b9 🐛 Fix content previewing will show attachments 2024-08-01 16:28:48 +08:00
47d03ce1e5 🐛 Bug fixes 2024-08-01 16:09:09 +08:00
c41a71388d Post with publish at and until 2024-08-01 15:49:42 +08:00
7655dfdf37 Post publish zone 2024-08-01 15:21:43 +08:00
190bb34958 Markdown toolbar 2024-08-01 14:46:01 +08:00
d02ed68afa Mention user in chat 2024-08-01 14:01:12 +08:00
2bc4513bb6 🐛 Fix post tag input issue 2024-08-01 11:49:28 +08:00
f10393f6d0 Download attachment 2024-08-01 02:10:57 +08:00
ecef8dab0c Fix post list ui jank 2024-08-01 01:21:27 +08:00
52e58fce3d Make theme switcher easier to use 2024-07-31 22:48:22 +08:00
31d50bfb1f 🐛 Fix web url issue 2024-07-31 21:01:32 +08:00
ca8ad12d93 🍱 Update font 2024-07-31 20:45:36 +08:00
f799900450 🐛 Fix crash on ratio 1 in attachment 2024-07-31 20:45:16 +08:00
dfdf7b23c8 🐛 Fix theme switching 2024-07-31 13:29:26 +08:00
771b2029b0 🍱 Add fonts 2024-07-31 13:29:17 +08:00
cc9c99f375 Global theme color 2024-07-31 02:44:49 +08:00
79 changed files with 3566 additions and 1642 deletions

View File

@@ -16,7 +16,7 @@ jobs:
channel: stable channel: stable
cache: true cache: true
- run: flutter pub get - run: flutter pub get
- run: flutter build web - run: flutter build web --release --base-href=/
- name: Archive production artifacts - name: Archive production artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:

View File

@@ -70,6 +70,12 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data

BIN
assets/fonts/Comfortaa-Bold.ttf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/NotoSansHK-Bold.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/NotoSansJP-Bold.ttf Executable file

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/NotoSansSC-Bold.ttf Executable file

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +1,4 @@
description: This file stores settings for Dart & Flutter DevTools. description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions: extensions:
- provider: true

View File

@@ -38,19 +38,19 @@ PODS:
- file_picker (0.0.1): - file_picker (0.0.1):
- DKImagePickerController/PhotoGallery - DKImagePickerController/PhotoGallery
- Flutter - Flutter
- Firebase/CoreOnly (10.28.0): - Firebase/CoreOnly (10.29.0):
- FirebaseCore (= 10.28.0) - FirebaseCore (= 10.29.0)
- Firebase/Messaging (10.28.0): - Firebase/Messaging (10.29.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 10.28.0) - FirebaseMessaging (~> 10.29.0)
- firebase_core (3.2.0): - firebase_core (3.3.0):
- Firebase/CoreOnly (= 10.28.0) - Firebase/CoreOnly (= 10.29.0)
- Flutter - Flutter
- firebase_messaging (15.0.3): - firebase_messaging (15.0.4):
- Firebase/Messaging (= 10.28.0) - Firebase/Messaging (= 10.29.0)
- firebase_core - firebase_core
- Flutter - Flutter
- FirebaseCore (10.28.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)
@@ -61,7 +61,7 @@ PODS:
- GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8)
- PromisesObjC (~> 2.1) - PromisesObjC (~> 2.1)
- FirebaseMessaging (10.28.0): - FirebaseMessaging (10.29.0):
- FirebaseCore (~> 10.0) - FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0) - FirebaseInstallations (~> 10.0)
- GoogleDataTransport (~> 9.3) - GoogleDataTransport (~> 9.3)
@@ -76,6 +76,9 @@ PODS:
- flutter_webrtc (0.11.3): - flutter_webrtc (0.11.3):
- Flutter - Flutter
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
- gal (1.0.0):
- Flutter
- FlutterMacOS
- 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)
@@ -105,9 +108,12 @@ PODS:
- GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/UserDefaults (7.13.3):
- GoogleUtilities/Logger - GoogleUtilities/Logger
- GoogleUtilities/Privacy - GoogleUtilities/Privacy
- image_cropper (0.0.4):
- Flutter
- TOCropViewController (~> 2.7.4)
- image_picker_ios (0.0.1): - image_picker_ios (0.0.1):
- Flutter - Flutter
- livekit_client (2.2.2): - livekit_client (2.2.3):
- Flutter - Flutter
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
- media_kit_libs_ios_video (1.0.4): - media_kit_libs_ios_video (1.0.4):
@@ -139,7 +145,7 @@ PODS:
- SDWebImage/Core (= 5.19.4) - SDWebImage/Core (= 5.19.4)
- SDWebImage/Core (5.19.4) - SDWebImage/Core (5.19.4)
- Sentry/HybridSDK (8.32.0) - Sentry/HybridSDK (8.32.0)
- sentry_flutter (8.5.0): - sentry_flutter (8.6.0):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- Sentry/HybridSDK (= 8.32.0) - Sentry/HybridSDK (= 8.32.0)
@@ -152,6 +158,7 @@ PODS:
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- SwiftyGif (5.4.5) - SwiftyGif (5.4.5)
- TOCropViewController (2.7.4)
- url_launcher_ios (0.0.1): - url_launcher_ios (0.0.1):
- Flutter - Flutter
- volume_controller (0.0.1): - volume_controller (0.0.1):
@@ -169,6 +176,8 @@ DEPENDENCIES:
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- 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`)
- 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_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
@@ -204,6 +213,7 @@ SPEC REPOS:
- SDWebImage - SDWebImage
- Sentry - Sentry
- SwiftyGif - SwiftyGif
- TOCropViewController
- WebRTC-SDK - WebRTC-SDK
EXTERNAL SOURCES: EXTERNAL SOURCES:
@@ -223,6 +233,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage/ios" :path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_webrtc: flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios" :path: ".symlinks/plugins/flutter_webrtc/ios"
gal:
:path: ".symlinks/plugins/gal/darwin"
image_cropper:
:path: ".symlinks/plugins/image_cropper/ios"
image_picker_ios: image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios" :path: ".symlinks/plugins/image_picker_ios/ios"
livekit_client: livekit_client:
@@ -266,20 +280,22 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Firebase: 5121c624121af81cbc81df3bda414b3c28c4f3c3 Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d
firebase_core: a9d0180d5285527884d07a41eb4a9ec9ed12cdb6 firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb
firebase_messaging: ccc82a143a74de75f440a4e413dbbb37ec3fddbc firebase_messaging: c862b3d2b973ecc769194dc8de09bd22c77ae757
FirebaseCore: 857dc1c6dd1255675047404d8466f7dfaac5d779 FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16
FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
FirebaseMessaging: 087a7c7cadef7b9239f005bc4db823894844f323 FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
image_cropper: 37d40f62177c101ff4c164906d259ea2c3aa70cf
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
livekit_client: c767049a635d5b6d43de3273dca3c439b8a6e970 livekit_client: bad83a7776a41abc42e1f26d903eeac9164c8a9f
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
@@ -293,11 +309,12 @@ SPEC CHECKSUMS:
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb
sentry_flutter: f1d86adcb93a959bc47a40d8d55059bdf7569bc5 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
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1

View File

@@ -1,10 +1,15 @@
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:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/auth.dart'; 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/relation.dart'; import 'package:solian/providers/relation.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';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
@@ -21,6 +26,7 @@ class BootstrapperShell extends StatefulWidget {
class _BootstrapperShellState extends State<BootstrapperShell> { class _BootstrapperShellState extends State<BootstrapperShell> {
bool _isBusy = true; bool _isBusy = true;
bool _isErrored = false; bool _isErrored = false;
bool _isDismissable = true;
String? _subtitle; String? _subtitle;
Color get _unFocusColor => Color get _unFocusColor =>
@@ -29,6 +35,38 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
int _periodCursor = 0; int _periodCursor = 0;
late final List<({String label, Future<void> Function() action})> _periods = [ late final List<({String label, Future<void> Function() action})> _periods = [
(
label: 'bsLoadingTheme',
action: () async {
await context.read<ThemeSwitcher>().restoreTheme();
},
),
(
label: 'bsCheckForUpdate',
action: () async {
if (PlatformInfo.isWeb) return;
try {
final info = await PackageInfo.fromPlatform();
final localVersionString = '${info.version}+${info.buildNumber}';
final resp = await GetConnect().get(
'https://git.solsynth.dev/api/v1/repos/hydrogen/solian/tags?limit=1',
);
if (resp.body[0]['name'] != localVersionString) {
setState(() {
_isErrored = true;
_subtitle = PlatformInfo.isIOS || PlatformInfo.isMacOS
? 'bsCheckForUpdateDescApple'.tr
: 'bsCheckForUpdateDescCommon'.tr;
});
}
} catch (e) {
setState(() {
_isErrored = true;
_subtitle = 'bsCheckForUpdateFailed'.tr;
});
}
},
),
( (
label: 'bsCheckingServer', label: 'bsCheckingServer',
action: () async { action: () async {
@@ -38,12 +76,14 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
setState(() { setState(() {
_isErrored = true; _isErrored = true;
_subtitle = 'bsCheckingServerDown'.tr; _subtitle = 'bsCheckingServerDown'.tr;
_isDismissable = false;
}); });
throw Exception('unable connect to server'); throw Exception('unable connect to server');
} else if (resp.statusCode == null) { } else if (resp.statusCode == null) {
setState(() { setState(() {
_isErrored = true; _isErrored = true;
_subtitle = 'bsCheckingServerFail'.tr; _subtitle = 'bsCheckingServerFail'.tr;
_isDismissable = false;
}); });
throw Exception('unable connect to server'); throw Exception('unable connect to server');
} }
@@ -74,8 +114,9 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isTrue) { if (auth.isAuthorized.isTrue) {
await Future.wait([ await Future.wait([
Get.find<RealmProvider>().refreshAvailableRealms(),
Get.find<ChannelProvider>().refreshAvailableChannel(), Get.find<ChannelProvider>().refreshAvailableChannel(),
Get.find<RelationshipProvider>().refreshFriendList(), Get.find<RelationshipProvider>().refreshRelativeList(),
]); ]);
} }
}, },
@@ -138,9 +179,11 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
GestureDetector( GestureDetector(
child: Column( child: Column(
children: [ children: [
if (_isErrored) if (_isErrored && !_isDismissable)
const Icon(Icons.cancel, size: 24) const Icon(Icons.cancel, size: 24),
else if (_isErrored && _isDismissable)
const Icon(Icons.warning, size: 24),
if (!_isErrored && _isBusy)
const SizedBox( const SizedBox(
width: 24, width: 24,
height: 24, height: 24,
@@ -151,15 +194,24 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
maxWidth: 280, maxWidth: 280,
child: Column( child: Column(
children: [ children: [
Text( if (_subtitle == null)
_subtitle ?? Text(
'${_periods[_periodCursor].label.tr} (${_periodCursor + 1}/${_periods.length})', '${_periods[_periodCursor].label.tr} (${_periodCursor + 1}/${_periods.length})',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: _unFocusColor, color: _unFocusColor,
),
), ),
), if (_subtitle != null)
Text(
_subtitle!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: _unFocusColor,
),
).paddingOnly(bottom: 4),
Text( Text(
'2024 © Solsynth LLC', '2024 © Solsynth LLC',
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -175,12 +227,19 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
), ),
onTap: () { onTap: () {
if (_isBusy) return; if (_isBusy) return;
setState(() { if (_isDismissable) {
_isBusy = true; setState(() {
_isErrored = false; _isBusy = false;
_periodCursor = 0; _isErrored = false;
}); });
_runPeriods(); } else {
setState(() {
_isBusy = true;
_isErrored = false;
_periodCursor = 0;
});
_runPeriods();
}
}, },
) )
], ],

View File

@@ -16,7 +16,7 @@ class ChatEventController {
Channel? channel; Channel? channel;
String? scope; String? scope;
initialize() async { Future<void> initialize() async {
if (!PlatformInfo.isWeb) { if (!PlatformInfo.isWeb) {
database = await createHistoryDb(); database = await createHistoryDb();
} }
@@ -131,6 +131,8 @@ class ChatEventController {
} }
insertEvent(LocalEvent entry) { insertEvent(LocalEvent entry) {
if (entry.channelId != channel?.id) return;
final idx = currentEvents.indexWhere((x) => x.data.uuid == entry.data.uuid); final idx = currentEvents.indexWhere((x) => x.data.uuid == entry.data.uuid);
if (idx != -1) { if (idx != -1) {
currentEvents[idx] = entry; currentEvents[idx] = entry;

View File

@@ -7,7 +7,9 @@ import 'package:solian/models/post.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart'; import 'package:solian/widgets/attachments/attachment_editor.dart';
import 'package:solian/widgets/posts/editor/post_editor_categories_tags.dart'; import 'package:solian/widgets/posts/editor/post_editor_categories_tags.dart';
import 'package:solian/widgets/posts/editor/post_editor_date.dart';
import 'package:solian/widgets/posts/editor/post_editor_overview.dart'; import 'package:solian/widgets/posts/editor/post_editor_overview.dart';
import 'package:solian/widgets/posts/editor/post_editor_publish_zone.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';
@@ -25,6 +27,8 @@ class PostEditorController extends GetxController {
Rx<Post?> replyTo = Rx(null); Rx<Post?> replyTo = Rx(null);
Rx<Post?> repostTo = Rx(null); Rx<Post?> repostTo = Rx(null);
Rx<Realm?> realmZone = Rx(null); Rx<Realm?> realmZone = Rx(null);
Rx<DateTime?> publishedAt = Rx(null);
Rx<DateTime?> publishedUntil = Rx(null);
RxList<int> attachments = RxList<int>.empty(growable: true); RxList<int> attachments = RxList<int>.empty(growable: true);
RxList<String> tags = RxList<String>.empty(growable: true); RxList<String> tags = RxList<String>.empty(growable: true);
@@ -41,7 +45,6 @@ class PostEditorController extends GetxController {
PostEditorController() { PostEditorController() {
SharedPreferences.getInstance().then((inst) { SharedPreferences.getInstance().then((inst) {
_prefs = inst; _prefs = inst;
localRead();
_saveTimer = Timer.periodic( _saveTimer = Timer.periodic(
const Duration(seconds: 3), const Duration(seconds: 3),
(Timer t) { (Timer t) {
@@ -88,16 +91,35 @@ class PostEditorController extends GetxController {
); );
} }
Future<void> editPublishZone(BuildContext context) {
return showDialog(
context: context,
builder: (context) => PostEditorPublishZoneDialog(
controller: this,
),
);
}
Future<void> editPublishDate(BuildContext context) {
return showDialog(
context: context,
builder: (context) => PostEditorDateDialog(
controller: this,
),
);
}
Future<void> editAttachment(BuildContext context) { Future<void> editAttachment(BuildContext context) {
return showModalBottomSheet( return showModalBottomSheet(
context: context, context: context,
isScrollControlled: true,
builder: (context) => AttachmentEditorPopup( builder: (context) => AttachmentEditorPopup(
usage: 'i.attachment', usage: 'i.attachment',
current: attachments, initialAttachments: attachments,
onUpdate: (value) { onAdd: (int value) {
attachments.value = value; attachments.add(value);
attachments.refresh(); },
onRemove: (int value) {
attachments.remove(value);
}, },
), ),
); );
@@ -122,10 +144,12 @@ class PostEditorController extends GetxController {
} }
void localRead() { void localRead() {
if (_prefs.containsKey('post_editor_local_save')) { SharedPreferences.getInstance().then((inst) {
isRestoreFromLocal.value = true; if (inst.containsKey('post_editor_local_save')) {
payload = jsonDecode(_prefs.getString('post_editor_local_save')!); isRestoreFromLocal.value = true;
} payload = jsonDecode(inst.getString('post_editor_local_save')!);
}
});
} }
void localClear() { void localClear() {
@@ -141,6 +165,8 @@ class PostEditorController extends GetxController {
visibleUsers.clear(); visibleUsers.clear();
invisibleUsers.clear(); invisibleUsers.clear();
visibility.value = 0; visibility.value = 0;
publishedAt.value = null;
publishedUntil.value = null;
isDraft.value = false; isDraft.value = false;
isRestoreFromLocal.value = false; isRestoreFromLocal.value = false;
lastSaveTime.value = null; lastSaveTime.value = null;
@@ -163,6 +189,11 @@ class PostEditorController extends GetxController {
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;
publishedUntil.value = value.publishedUntil;
tags.value =
value.body['tags']?.map((x) => x['alias']).toList() ?? List.empty();
tags.refresh();
attachments.value = value.body['attachments']?.cast<int>() ?? List.empty(); attachments.value = value.body['attachments']?.cast<int>() ?? List.empty();
attachments.refresh(); attachments.refresh();
@@ -215,11 +246,14 @@ class PostEditorController extends GetxController {
'title': title, 'title': title,
'description': description, 'description': description,
'content': contentController.text, 'content': contentController.text,
'tags': tags, 'tags': tags.map((x) => {'alias': x}).toList(),
'attachments': attachments, 'attachments': attachments,
'visible_users': visibleUsers, 'visible_users': visibleUsers,
'invisible_users': invisibleUsers, 'invisible_users': invisibleUsers,
'visibility': visibility.value, 'visibility': visibility.value,
'published_at': publishedAt.value?.toUtc().toIso8601String() ??
DateTime.now().toUtc().toIso8601String(),
'published_until': publishedUntil.value?.toUtc().toIso8601String(),
'is_draft': isDraft.value, 'is_draft': isDraft.value,
if (replyTo.value != null) 'reply_to': replyTo.value!.id, if (replyTo.value != null) 'reply_to': replyTo.value!.id,
if (repostTo.value != null) 'repost_to': repostTo.value!.id, if (repostTo.value != null) 'repost_to': repostTo.value!.id,
@@ -229,6 +263,7 @@ 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>();
titleController.text = value['title'] ?? ''; titleController.text = value['title'] ?? '';
descriptionController.text = value['description'] ?? ''; descriptionController.text = value['description'] ?? '';
contentController.text = value['content'] ?? ''; contentController.text = value['content'] ?? '';
@@ -242,6 +277,12 @@ class PostEditorController extends GetxController {
if (value['invisible_users'] != null) { if (value['invisible_users'] != null) {
invisibleUsers.value = value['invisible_users'].cast<int>(); invisibleUsers.value = value['invisible_users'].cast<int>();
} }
if (value['published_at'] != null) {
publishedAt.value = DateTime.parse(value['published_at']).toLocal();
}
if (value['published_until'] != null) {
publishedAt.value = DateTime.parse(value['published_until']).toLocal();
}
if (value['reply_to'] != null) { if (value['reply_to'] != null) {
replyTo.value = Post.fromJson(value['reply_to']); replyTo.value = Post.fromJson(value['reply_to']);
} }

View File

@@ -2,9 +2,10 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
extension SolianExtenions on BuildContext { extension SolianExtenions on BuildContext {
void showSnackbar(String content) { void showSnackbar(String content, {SnackBarAction? action}) {
ScaffoldMessenger.of(this).showSnackBar(SnackBar( ScaffoldMessenger.of(this).showSnackBar(SnackBar(
content: Text(content), content: Text(content),
action: action,
)); ));
} }
@@ -29,6 +30,23 @@ extension SolianExtenions on BuildContext {
); );
} }
Future<void> showInfoDialog(String title, body) {
return showDialog<void>(
useRootNavigator: true,
context: this,
builder: (ctx) => AlertDialog(
title: Text(title),
content: Text(body),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text('okay'.tr),
)
],
),
);
}
Future<void> showErrorDialog(dynamic exception) { Future<void> showErrorDialog(dynamic exception) {
var stack = StackTrace.current; var stack = StackTrace.current;
var stackTrace = '$stack'; var stackTrace = '$stack';

View File

@@ -5,10 +5,13 @@ 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: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:sentry_flutter/sentry_flutter.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/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';
import 'package:solian/providers/content/attachment.dart'; import 'package:solian/providers/content/attachment.dart';
@@ -71,33 +74,45 @@ Future<void> _initializePlatformComponents() async {
} }
} }
final themeSwitcher = ThemeSwitcher(
lightThemeData: SolianTheme.build(Brightness.light),
darkThemeData: SolianTheme.build(Brightness.dark),
);
class SolianApp extends StatelessWidget { class SolianApp extends StatelessWidget {
const SolianApp({super.key}); const SolianApp({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetMaterialApp.router( return ChangeNotifierProvider.value(
title: 'Solian', value: themeSwitcher,
theme: SolianTheme.build(Brightness.light), child: Builder(builder: (context) {
darkTheme: SolianTheme.build(Brightness.dark), final theme = Provider.of<ThemeSwitcher>(context);
themeMode: ThemeMode.system,
routerDelegate: AppRouter.instance.routerDelegate, return GetMaterialApp.router(
routeInformationParser: AppRouter.instance.routeInformationParser, title: 'Solian',
routeInformationProvider: AppRouter.instance.routeInformationProvider, theme: theme.lightThemeData,
backButtonDispatcher: AppRouter.instance.backButtonDispatcher, darkTheme: theme.darkThemeData,
translations: SolianMessages(), themeMode: ThemeMode.system,
locale: Get.deviceLocale, routerDelegate: AppRouter.instance.routerDelegate,
fallbackLocale: const Locale('en', 'US'), routeInformationParser: AppRouter.instance.routeInformationParser,
onInit: () => _initializeProviders(context), routeInformationProvider: AppRouter.instance.routeInformationProvider,
builder: (context, child) { backButtonDispatcher: AppRouter.instance.backButtonDispatcher,
return SystemShell( translations: SolianMessages(),
child: ScaffoldMessenger( locale: Get.deviceLocale,
child: BootstrapperShell( fallbackLocale: const Locale('en', 'US'),
child: child ?? const SizedBox(), onInit: () => _initializeProviders(context),
), builder: (context, child) {
), return SystemShell(
child: ScaffoldMessenger(
child: BootstrapperShell(
child: child ?? const SizedBox(),
),
),
);
},
); );
}, }),
); );
} }
@@ -111,5 +126,6 @@ class SolianApp extends StatelessWidget {
Get.lazyPut(() => ChannelProvider()); Get.lazyPut(() => ChannelProvider());
Get.lazyPut(() => RealmProvider()); Get.lazyPut(() => RealmProvider());
Get.lazyPut(() => ChatCallProvider()); Get.lazyPut(() => ChatCallProvider());
Get.lazyPut(() => AttachmentUploaderController());
} }
} }

View File

@@ -10,6 +10,7 @@ class Call {
String externalId; String externalId;
int founderId; int founderId;
int channelId; int channelId;
List<dynamic> participants;
Channel channel; Channel channel;
Call({ Call({
@@ -21,6 +22,7 @@ class Call {
required this.externalId, required this.externalId,
required this.founderId, required this.founderId,
required this.channelId, required this.channelId,
required this.participants,
required this.channel, required this.channel,
}); });
@@ -34,6 +36,7 @@ class Call {
externalId: json['external_id'], externalId: json['external_id'],
founderId: json['founder_id'], founderId: json['founder_id'],
channelId: json['channel_id'], channelId: json['channel_id'],
participants: json['participants'] ?? List.empty(),
channel: Channel.fromJson(json['channel']), channel: Channel.fromJson(json['channel']),
); );
@@ -46,6 +49,7 @@ class Call {
'external_id': externalId, 'external_id': externalId,
'founder_id': founderId, 'founder_id': founderId,
'channel_id': channelId, 'channel_id': channelId,
'participants': participants,
'channel': channel.toJson(), 'channel': channel.toJson(),
}; };
} }
@@ -63,6 +67,7 @@ class ParticipantTrack {
{required this.participant, {required this.participant,
required this.videoTrack, required this.videoTrack,
required this.isScreenShare}); required this.isScreenShare});
VideoTrack? videoTrack; VideoTrack? videoTrack;
Participant participant; Participant participant;
bool isScreenShare; bool isScreenShare;

View File

@@ -20,6 +20,7 @@ class Post {
Post? repostTo; Post? repostTo;
Realm? realm; Realm? realm;
DateTime? publishedAt; DateTime? publishedAt;
DateTime? publishedUntil;
DateTime? pinnedAt; DateTime? pinnedAt;
bool? isDraft; bool? isDraft;
int authorId; int authorId;
@@ -44,6 +45,7 @@ class Post {
required this.repostTo, required this.repostTo,
required this.realm, required this.realm,
required this.publishedAt, required this.publishedAt,
required this.publishedUntil,
required this.pinnedAt, required this.pinnedAt,
required this.isDraft, required this.isDraft,
required this.authorId, required this.authorId,
@@ -80,6 +82,9 @@ class Post {
publishedAt: json['published_at'] != null publishedAt: json['published_at'] != null
? DateTime.parse(json['published_at']) ? DateTime.parse(json['published_at'])
: null, : null,
publishedUntil: json['published_until'] != null
? DateTime.parse(json['published_until'])
: null,
pinnedAt: json['pinned_at'] != null pinnedAt: json['pinned_at'] != null
? DateTime.parse(json['pinned_at']) ? DateTime.parse(json['pinned_at'])
: null, : null,
@@ -108,6 +113,7 @@ class Post {
'repost_to': repostTo?.toJson(), 'repost_to': repostTo?.toJson(),
'realm': realm?.toJson(), 'realm': realm?.toJson(),
'published_at': publishedAt?.toIso8601String(), 'published_at': publishedAt?.toIso8601String(),
'published_until': publishedUntil?.toIso8601String(),
'pinned_at': pinnedAt?.toIso8601String(), 'pinned_at': pinnedAt?.toIso8601String(),
'is_draft': isDraft, 'is_draft': isDraft,
'author_id': authorId, 'author_id': authorId,

View File

@@ -0,0 +1,195 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:get/get.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/providers/content/attachment.dart';
class AttachmentUploadTask {
File file;
String usage;
Map<String, dynamic>? metadata;
double progress = 0;
bool isUploading = false;
bool isCompleted = false;
AttachmentUploadTask({
required this.file,
required this.usage,
this.metadata,
});
}
class AttachmentUploaderController extends GetxController {
RxBool isUploading = false.obs;
RxDouble progressOfUpload = 0.0.obs;
RxList<AttachmentUploadTask> queueOfUpload = RxList.empty(growable: true);
Timer? _progressSyncTimer;
double _progressOfUpload = 0.0;
void _syncProgress() {
progressOfUpload.value = _progressOfUpload;
queueOfUpload.refresh();
}
void _startProgressSyncTimer() {
if (_progressSyncTimer != null) {
_progressSyncTimer!.cancel();
}
_progressSyncTimer = Timer.periodic(
const Duration(milliseconds: 500),
(_) => _syncProgress(),
);
}
void _stopProgressSyncTimer() {
if (_progressSyncTimer == null) return;
_progressSyncTimer!.cancel();
_progressSyncTimer = null;
}
void enqueueTask(AttachmentUploadTask task) {
if (isUploading.value) throw Exception('uploading blocked');
queueOfUpload.add(task);
}
void enqueueTaskBatch(Iterable<AttachmentUploadTask> tasks) {
if (isUploading.value) throw Exception('uploading blocked');
queueOfUpload.addAll(tasks);
}
void dequeueTask(AttachmentUploadTask task) {
if (isUploading.value) throw Exception('uploading blocked');
queueOfUpload.remove(task);
}
Future<Attachment> performSingleTask(int queueIndex) async {
isUploading.value = true;
progressOfUpload.value = 0;
_startProgressSyncTimer();
queueOfUpload[queueIndex].isUploading = true;
final task = queueOfUpload[queueIndex];
final result = await _rawUploadAttachment(
await task.file.readAsBytes(),
task.file.path,
task.usage,
null,
onProgress: (value) {
queueOfUpload[queueIndex].progress = value;
_progressOfUpload = value;
},
);
queueOfUpload.removeAt(queueIndex);
_stopProgressSyncTimer();
_syncProgress();
isUploading.value = false;
return result;
}
Future<void> performUploadQueue({
required Function(Attachment item) onData,
}) async {
isUploading.value = true;
progressOfUpload.value = 0;
_startProgressSyncTimer();
for (var idx = 0; idx < queueOfUpload.length; idx++) {
queueOfUpload[idx].isUploading = true;
final task = queueOfUpload[idx];
final result = await _rawUploadAttachment(
await task.file.readAsBytes(),
task.file.path,
task.usage,
null,
onProgress: (value) {
queueOfUpload[idx].progress = value;
_progressOfUpload = (idx + value) / queueOfUpload.length;
},
);
_progressOfUpload = (idx + 1) / queueOfUpload.length;
onData(result);
queueOfUpload[idx].isUploading = false;
queueOfUpload[idx].isCompleted = false;
}
queueOfUpload.clear();
_stopProgressSyncTimer();
_syncProgress();
isUploading.value = false;
}
Future<void> uploadAttachmentWithCallback(
Uint8List data,
String path,
String usage,
Map<String, dynamic>? metadata,
Function(Attachment) callback,
) async {
if (isUploading.value) throw Exception('uploading blocked');
isUploading.value = true;
final result = await _rawUploadAttachment(
data,
path,
usage,
metadata,
onProgress: (progress) {
progressOfUpload.value = progress;
},
);
isUploading.value = false;
callback(result);
}
Future<Attachment> uploadAttachment(
Uint8List data,
String path,
String usage,
Map<String, dynamic>? metadata,
) async {
if (isUploading.value) throw Exception('uploading blocked');
isUploading.value = true;
final result = await _rawUploadAttachment(
data,
path,
usage,
metadata,
onProgress: (progress) {
progressOfUpload.value = progress;
},
);
isUploading.value = false;
return result;
}
Future<Attachment> _rawUploadAttachment(
Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
{Function(double)? onProgress}) async {
final AttachmentProvider provider = Get.find();
try {
final result = await provider.createAttachment(
data,
path,
usage,
metadata,
onProgress: onProgress,
);
return result;
} catch (err) {
rethrow;
}
}
}

View File

@@ -16,6 +16,7 @@ class ChatCallProvider extends GetxController {
RxBool isReady = false.obs; RxBool isReady = false.obs;
RxBool isMounted = false.obs; RxBool isMounted = false.obs;
RxBool isInitialized = false.obs;
String? token; String? token;
String? endpoint; String? endpoint;
@@ -151,6 +152,8 @@ class ChatCallProvider extends GetxController {
void onRoomDidUpdate() => sortParticipants(); void onRoomDidUpdate() => sortParticipants();
void setupRoom() { void setupRoom() {
if(isInitialized.value) return;
sortParticipants(); sortParticipants();
room.addListener(onRoomDidUpdate); room.addListener(onRoomDidUpdate);
WidgetsBindingCompatible.instance?.addPostFrameCallback( WidgetsBindingCompatible.instance?.addPostFrameCallback(
@@ -160,6 +163,8 @@ class ChatCallProvider extends GetxController {
if (lkPlatformIsMobile()) { if (lkPlatformIsMobile()) {
Hardware.instance.setSpeakerphoneOn(true); Hardware.instance.setSpeakerphoneOn(true);
} }
isInitialized.value = true;
} }
void setupRoomListeners({ void setupRoomListeners({
@@ -362,6 +367,7 @@ class ChatCallProvider extends GetxController {
void disposeRoom() { void disposeRoom() {
isMounted.value = false; isMounted.value = false;
isInitialized.value = false;
current.value = null; current.value = null;
channel.value = null; channel.value = null;
room.removeListener(onRoomDidUpdate); room.removeListener(onRoomDidUpdate);

View File

@@ -4,8 +4,10 @@ import 'dart:typed_data';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:solian/models/attachment.dart'; import 'package:solian/models/attachment.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
import 'package:dio/dio.dart' as dio;
class AttachmentProvider extends GetConnect { class AttachmentProvider extends GetConnect {
static Map<String, String> mimetypeOverrides = { static Map<String, String> mimetypeOverrides = {
@@ -20,6 +22,48 @@ class AttachmentProvider extends GetConnect {
final Map<int, Attachment> _cachedResponses = {}; final Map<int, Attachment> _cachedResponses = {};
Future<List<Attachment?>> listMetadata(
List<int> id, {
noCache = false,
}) async {
List<Attachment?> result = List.filled(id.length, null);
List<int> pendingQuery = List.empty(growable: true);
if (!noCache) {
for (var idx = 0; idx < id.length; idx++) {
if (_cachedResponses.containsKey(id[idx])) {
result[idx] = _cachedResponses[id[idx]];
} else {
pendingQuery.add(id[idx]);
}
}
}
final resp = await get(
'/attachments?take=${pendingQuery.length}&id=${pendingQuery.join(',')}',
);
if (resp.statusCode != 200) return result;
final rawOut = PaginationResult.fromJson(resp.body);
if (rawOut.data == null) return result;
final List<Attachment> out =
rawOut.data!.map((x) => Attachment.fromJson(x)).toList();
for (final item in out) {
if (item.destination != 0 && item.isAnalyzed) {
_cachedResponses[item.id] = item;
}
}
for (var i = 0; i < out.length; i++) {
for (var j = 0; j < id.length; j++) {
if (out[i].id == id[j]) {
result[j] = out[i];
}
}
}
return result;
}
Future<Attachment?> getMetadata(int id, {noCache = false}) async { Future<Attachment?> getMetadata(int id, {noCache = false}) async {
if (!noCache && _cachedResponses.containsKey(id)) { if (!noCache && _cachedResponses.containsKey(id)) {
return _cachedResponses[id]!; return _cachedResponses[id]!;
@@ -37,21 +81,14 @@ class AttachmentProvider extends GetConnect {
return null; return null;
} }
Future<Response> createAttachment( Future<Attachment> createAttachment(
Uint8List data, Uint8List data, String path, String usage, Map<String, dynamic>? metadata,
String path, {Function(double)? onProgress}) async {
String usage,
Map<String, dynamic>? metadata,
) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized'); if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
final client = auth.configureClient( final filePayload =
'files', dio.MultipartFile.fromBytes(data, filename: basename(path));
timeout: const Duration(minutes: 3),
);
final filePayload = MultipartFile(data, filename: basename(path));
final fileAlt = basename(path).contains('.') final fileAlt = basename(path).contains('.')
? basename(path).substring(0, basename(path).lastIndexOf('.')) ? basename(path).substring(0, basename(path).lastIndexOf('.'))
: basename(path); : basename(path);
@@ -64,19 +101,30 @@ class AttachmentProvider extends GetConnect {
if (mimetypeOverrides.keys.contains(fileExt)) { if (mimetypeOverrides.keys.contains(fileExt)) {
mimetypeOverride = mimetypeOverrides[fileExt]; mimetypeOverride = mimetypeOverrides[fileExt];
} }
final payload = FormData({ final payload = dio.FormData.fromMap({
'alt': fileAlt, 'alt': fileAlt,
'file': filePayload, 'file': filePayload,
'usage': usage, 'usage': usage,
if (mimetypeOverride != null) 'mimetype': mimetypeOverride, if (mimetypeOverride != null) 'mimetype': mimetypeOverride,
'metadata': jsonEncode(metadata), 'metadata': jsonEncode(metadata),
}); });
final resp = await client.post('/attachments', payload); final resp = await dio.Dio(
dio.BaseOptions(
baseUrl: ServiceFinder.buildUrl('files', null),
headers: {'Authorization': 'Bearer ${auth.credentials!.accessToken}'},
),
).post(
'/attachments',
data: payload,
onSendProgress: (count, total) {
if (onProgress != null) onProgress(count / total);
},
);
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception(resp.bodyString); throw Exception(resp.data);
} }
return resp; return Attachment.fromJson(resp.data);
} }
Future<Response> updateAttachment( Future<Response> updateAttachment(

View File

@@ -1,7 +1,24 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/realm.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
class RealmProvider extends GetxController { class RealmProvider extends GetxController {
RxBool isLoading = false.obs;
RxList<Realm> availableRealms = RxList.empty(growable: true);
Future<void> refreshAvailableRealms() async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
isLoading.value = true;
final resp = await listAvailableRealm();
isLoading.value = false;
availableRealms.value =
resp.body.map((x) => Realm.fromJson(x)).toList().cast<Realm>();
availableRealms.refresh();
}
Future<Response> getRealm(String alias) async { Future<Response> getRealm(String alias) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) throw Exception('unauthorized'); if (auth.isAuthorized.isFalse) throw Exception('unauthorized');

View File

@@ -4,15 +4,19 @@ import 'package:solian/models/relations.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
class RelationshipProvider extends GetxController { class RelationshipProvider extends GetxController {
final RxInt friendRequestCount = 0.obs;
final RxList<Relationship> _friends = RxList.empty(growable: true); final RxList<Relationship> _friends = RxList.empty(growable: true);
Future<void> refreshFriendList() async { Future<void> refreshRelativeList() async {
final resp = await listRelationWithStatus(1); final resp = await listRelation();
_friends.value = resp.body final List<Relationship> result = resp.body
.map((e) => Relationship.fromJson(e)) .map((e) => Relationship.fromJson(e))
.toList() .toList()
.cast<Relationship>(); .cast<Relationship>();
_friends.value = result.where((x) => x.status == 1).toList();
_friends.refresh(); _friends.refresh();
friendRequestCount.value = result.where((x) => x.status == 0).length;
} }
bool hasFriend(Account account) { bool hasFriend(Account account) {

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/theme.dart';
class ThemeSwitcher extends ChangeNotifier {
ThemeData lightThemeData;
ThemeData darkThemeData;
ThemeSwitcher({
required this.lightThemeData,
required this.darkThemeData,
});
Future<void> restoreTheme() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('global_theme_color')) {
final value = prefs.getInt('global_theme_color')!;
final color = Color(value);
lightThemeData = SolianTheme.build(Brightness.light, seedColor: color);
darkThemeData = SolianTheme.build(Brightness.dark, seedColor: color);
notifyListeners();
}
}
void setTheme(ThemeData light, dark) {
lightThemeData = light;
darkThemeData = dark;
notifyListeners();
}
}

View File

@@ -1,3 +1,5 @@
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
import 'package:solian/screens/about.dart'; import 'package:solian/screens/about.dart';
@@ -18,6 +20,7 @@ import 'package:solian/screens/realms/realm_organize.dart';
import 'package:solian/screens/realms/realm_view.dart'; import 'package:solian/screens/realms/realm_view.dart';
import 'package:solian/screens/home.dart'; import 'package:solian/screens/home.dart';
import 'package:solian/screens/posts/post_editor.dart'; import 'package:solian/screens/posts/post_editor.dart';
import 'package:solian/screens/settings.dart';
import 'package:solian/shells/root_shell.dart'; import 'package:solian/shells/root_shell.dart';
import 'package:solian/shells/title_shell.dart'; import 'package:solian/shells/title_shell.dart';
@@ -34,6 +37,22 @@ abstract class AppRouter {
_chatRoute, _chatRoute,
_realmRoute, _realmRoute,
_accountRoute, _accountRoute,
GoRoute(
path: '/about',
name: 'about',
builder: (context, state) => TitleShell(
state: state,
child: const AboutScreen(),
),
),
GoRoute(
path: '/settings',
name: 'settings',
builder: (context, state) => TitleShell(
state: state,
child: const SettingScreen(),
),
),
], ],
), ),
], ],
@@ -76,14 +95,26 @@ abstract class AppRouter {
GoRoute( GoRoute(
path: '/posts/editor', path: '/posts/editor',
name: 'postEditor', name: 'postEditor',
builder: (context, state) { pageBuilder: (context, state) {
final arguments = state.extra as PostPublishArguments?; final arguments = state.extra as PostPublishArguments?;
return PostPublishScreen( return CustomTransitionPage(
edit: arguments?.edit, child: PostPublishScreen(
reply: arguments?.reply, edit: arguments?.edit,
repost: arguments?.repost, reply: arguments?.reply,
realm: arguments?.realm, repost: arguments?.repost,
mode: int.tryParse(state.uri.queryParameters['mode'] ?? '0') ?? 0, realm: arguments?.realm,
postListController: arguments?.postListController,
mode: int.tryParse(state.uri.queryParameters['mode'] ?? '0') ?? 0,
),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeThroughTransition(
fillColor: Theme.of(context).scaffoldBackgroundColor,
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
); );
}, },
), ),
@@ -210,14 +241,6 @@ abstract class AppRouter {
name: state.pathParameters['name']!, name: state.pathParameters['name']!,
), ),
), ),
GoRoute(
path: '/about',
name: 'about',
builder: (context, state) => TitleShell(
state: state,
child: const AboutScreen(),
),
),
], ],
); );
} }

View File

@@ -1,4 +1,5 @@
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';
@@ -17,7 +18,9 @@ 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), Image.asset('assets/logo.png', width: 64, height: 64)
.animate(onPlay: (c) => c.repeat())
.rotate(duration: 1000.ms),
Text( Text(
'Solian', 'Solian',
style: Theme.of(context).textTheme.headlineMedium, style: Theme.of(context).textTheme.headlineMedium,
@@ -44,17 +47,29 @@ class AboutScreen extends StatelessWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
TextButton( TextButton(
style: denseButtonStyle, style: denseButtonStyle,
child: const Text('More Information'), child: const Text('App Details'),
onPressed: () { onPressed: () async {
launchUrlString('https://solsynth.dev/products/solar-network'); final info = await PackageInfo.fromPlatform();
showAboutDialog(
context: context,
applicationVersion: '${info.version} (${info.buildNumber})',
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.',
applicationIcon: Image.asset(
'assets/logo.png',
width: 56,
height: 56,
),
);
}, },
), ),
TextButton( TextButton(
style: denseButtonStyle, style: denseButtonStyle,
child: const Text('Project Website'),
onPressed: () { onPressed: () {
launchUrlString('https://solsynth.dev'); launchUrlString('https://solsynth.dev/products/solar-network');
}, },
child: const Text('Official Website'),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text( const Text(

View File

@@ -3,11 +3,13 @@ import 'package:get/get.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/relation.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/auth/signin.dart'; import 'package:solian/screens/auth/signin.dart';
import 'package:solian/screens/auth/signup.dart'; import 'package:solian/screens/auth/signup.dart';
import 'package:solian/widgets/account/account_heading.dart'; import 'package:solian/widgets/account/account_heading.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
import 'package:badges/badges.dart' as badges;
class AccountScreen extends StatefulWidget { class AccountScreen extends StatefulWidget {
const AccountScreen({super.key}); const AccountScreen({super.key});
@@ -23,10 +25,27 @@ class _AccountScreenState extends State<AccountScreen> {
( (
const Icon(Icons.color_lens), const Icon(Icons.color_lens),
'accountPersonalize'.tr, 'accountPersonalize'.tr,
'accountPersonalize' 'accountPersonalize',
),
(
Obx(() {
final RelationshipProvider relations = Get.find();
return badges.Badge(
badgeContent: Text(
relations.friendRequestCount.value.toString(),
style: const TextStyle(color: Colors.white),
),
showBadge: relations.friendRequestCount.value > 0,
position: badges.BadgePosition.topEnd(
top: -12,
end: -8,
),
child: const Icon(Icons.diversity_1),
);
}),
'accountFriend'.tr,
'accountFriend',
), ),
(const Icon(Icons.diversity_1), 'accountFriend'.tr, 'accountFriend'),
(const Icon(Icons.info_outline), 'about'.tr, 'about'),
]; ];
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
@@ -41,7 +60,10 @@ class _AccountScreenState extends State<AccountScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ActionCard( ActionCard(
icon: const Icon(Icons.login, color: Colors.white), icon: Icon(
Icons.login,
color: Theme.of(context).colorScheme.onPrimary,
),
title: 'signin'.tr, title: 'signin'.tr,
caption: 'signinCaption'.tr, caption: 'signinCaption'.tr,
onTap: () { onTap: () {
@@ -59,7 +81,10 @@ class _AccountScreenState extends State<AccountScreen> {
}, },
), ),
ActionCard( ActionCard(
icon: const Icon(Icons.add, color: Colors.white), icon: Icon(
Icons.add,
color: Theme.of(context).colorScheme.onPrimary,
),
title: 'signup'.tr, title: 'signup'.tr,
caption: 'signupCaption'.tr, caption: 'signupCaption'.tr,
onTap: () { onTap: () {
@@ -184,7 +209,7 @@ class ActionCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CircleAvatar( CircleAvatar(
backgroundColor: Colors.indigo, backgroundColor: Theme.of(context).colorScheme.primary,
child: icon, child: icon,
).paddingOnly(bottom: 12), ).paddingOnly(bottom: 12),
Text( Text(

View File

@@ -1,9 +1,9 @@
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:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/relations.dart'; import 'package:solian/models/relations.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/theme.dart';
import 'package:solian/widgets/account/relative_list.dart'; import 'package:solian/widgets/account/relative_list.dart';
class FriendScreen extends StatefulWidget { class FriendScreen extends StatefulWidget {
@@ -21,15 +21,15 @@ class _FriendScreenState extends State<FriendScreen>
List<Relationship> _relations = List.empty(); List<Relationship> _relations = List.empty();
List<Relationship> filterByStatus(int status) { List<Relationship> _filterByStatus(int status) {
return _relations.where((x) => x.status == status).toList(); return _relations.where((x) => x.status == status).toList();
} }
Future<void> loadRelations() async { Future<void> _loadRelations() async {
setState(() => _isBusy = true); setState(() => _isBusy = true);
final RelationshipProvider provider = Get.find(); final RelationshipProvider relations = Get.find();
final resp = await provider.listRelation(); final resp = await relations.listRelation();
setState(() { setState(() {
_relations = resp.body _relations = resp.body
@@ -38,6 +38,9 @@ class _FriendScreenState extends State<FriendScreen>
.cast<Relationship>(); .cast<Relationship>();
_isBusy = false; _isBusy = false;
}); });
relations.friendRequestCount.value =
_relations.where((x) => x.status == 0).length;
} }
void promptAddFriend() async { void promptAddFriend() async {
@@ -104,8 +107,8 @@ class _FriendScreenState extends State<FriendScreen>
super.initState(); super.initState();
_tabController = TabController(length: 3, vsync: this); _tabController = TabController(length: 3, vsync: this);
loadRelations().then((_) { _loadRelations().then((_) {
if (filterByStatus(0).isEmpty) { if (_filterByStatus(0).isEmpty) {
_tabController.animateTo(1); _tabController.animateTo(1);
} }
}); });
@@ -119,6 +122,19 @@ class _FriendScreenState extends State<FriendScreen>
appBar: AppBar( appBar: AppBar(
centerTitle: false, centerTitle: false,
title: Text('accountFriend'.tr), title: Text('accountFriend'.tr),
actions: [
if (_isBusy)
SizedBox(
height: 48,
width: 48,
child: const CircularProgressIndicator(
strokeWidth: 3,
).paddingAll(14),
),
SizedBox(
width: SolianTheme.isLargeScreen(context) ? 8 : 16,
),
],
bottom: TabBar( bottom: TabBar(
controller: _tabController, controller: _tabController,
tabs: const [ tabs: const [
@@ -136,46 +152,34 @@ class _FriendScreenState extends State<FriendScreen>
controller: _tabController, controller: _tabController,
children: [ children: [
RefreshIndicator( RefreshIndicator(
onRefresh: () => loadRelations(), onRefresh: () => _loadRelations(),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
SilverRelativeList( SilverRelativeList(
items: filterByStatus(0), items: _filterByStatus(0),
onUpdate: () => loadRelations(), onUpdate: () => _loadRelations(),
), ),
], ],
), ),
), ),
RefreshIndicator( RefreshIndicator(
onRefresh: () => loadRelations(), onRefresh: () => _loadRelations(),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
SilverRelativeList( SilverRelativeList(
items: filterByStatus(1), items: _filterByStatus(1),
onUpdate: () => loadRelations(), onUpdate: () => _loadRelations(),
), ),
], ],
), ),
), ),
RefreshIndicator( RefreshIndicator(
onRefresh: () => loadRelations(), onRefresh: () => _loadRelations(),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
if (_isBusy)
SliverToBoxAdapter(
child: const LinearProgressIndicator().animate().scaleX(),
),
SilverRelativeList( SilverRelativeList(
items: filterByStatus(3), items: _filterByStatus(3),
onUpdate: () => loadRelations(), onUpdate: () => _loadRelations(),
), ),
], ],
), ),

View File

@@ -3,9 +3,11 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/attachment.dart'; import 'package:solian/providers/content/attachment.dart';
import 'package:solian/services.dart'; import 'package:solian/services.dart';
@@ -34,7 +36,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
bool _isBusy = false; bool _isBusy = false;
void selectBirthday() async { void _selectBirthday() async {
final DateTime? picked = await showDatePicker( final DateTime? picked = await showDatePicker(
context: context, context: context,
initialDate: _birthday?.toLocal(), initialDate: _birthday?.toLocal(),
@@ -49,48 +51,67 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
} }
} }
void syncWidget() async { void _syncWidget() async {
setState(() => _isBusy = true); _isBusy = true;
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
final prof = auth.userProfile.value!; final prof = auth.userProfile.value!;
setState(() { _usernameController.text = prof['name'];
_usernameController.text = prof['name']; _nicknameController.text = prof['nick'];
_nicknameController.text = prof['nick']; _descriptionController.text = prof['description'];
_descriptionController.text = prof['description']; _firstNameController.text = prof['profile']['first_name'];
_firstNameController.text = prof['profile']['first_name']; _lastNameController.text = prof['profile']['last_name'];
_lastNameController.text = prof['profile']['last_name']; _avatar = prof['avatar'];
_avatar = prof['avatar']; _banner = prof['banner'];
_banner = prof['banner']; if (prof['profile']['birthday'] != null) {
if (prof['profile']['birthday'] != null) { _birthday = DateTime.parse(prof['profile']['birthday']);
_birthday = DateTime.parse(prof['profile']['birthday']); _birthdayController.text =
_birthdayController.text = DateFormat('yyyy-MM-dd').format(_birthday!.toLocal());
DateFormat('yyyy-MM-dd').format(_birthday!.toLocal()); }
}
_isBusy = false; _isBusy = false;
});
} }
Future<void> updateImage(String position) async { Future<void> _editImage(String position) async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
final image = await _imagePicker.pickImage(source: ImageSource.gallery); final image = await _imagePicker.pickImage(source: ImageSource.gallery);
if (image == null) return; if (image == null) return;
CroppedFile? croppedFile = await ImageCropper().cropImage(
sourcePath: image.path,
uiSettings: [
AndroidUiSettings(
toolbarTitle: 'cropImage'.tr,
toolbarColor: Theme.of(context).colorScheme.primary,
toolbarWidgetColor: Theme.of(context).colorScheme.onPrimary,
aspectRatioPresets: [CropAspectRatioPreset.square],
),
IOSUiSettings(
title: 'cropImage'.tr,
aspectRatioPresets: [CropAspectRatioPreset.square],
),
WebUiSettings(
context: context,
),
],
);
if (croppedFile == null) return;
final file = File(croppedFile.path);
setState(() => _isBusy = true); setState(() => _isBusy = true);
final AttachmentProvider provider = Get.find(); final AttachmentProvider provider = Get.find();
Response? attachResp; Attachment? attachResult;
try { try {
final file = File(image.path); attachResult = await provider.createAttachment(
attachResp = await provider.createAttachment(
await file.readAsBytes(), await file.readAsBytes(),
file.path, file.path,
'p.$position', 'p.$position',
null null,
); );
} catch (e) { } catch (e) {
setState(() => _isBusy = false); setState(() => _isBusy = false);
@@ -102,10 +123,10 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
final resp = await client.put( final resp = await client.put(
'/users/me/$position', '/users/me/$position',
{'attachment': attachResp.body['id']}, {'attachment': attachResult.id},
); );
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
syncWidget(); _syncWidget();
context.showSnackbar('accountPersonalizeApplied'.tr); context.showSnackbar('accountPersonalizeApplied'.tr);
} else { } else {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);
@@ -114,7 +135,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void updatePersonalize() async { void _editUserInfo() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
@@ -134,7 +155,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
}, },
); );
if (resp.statusCode == 200) { if (resp.statusCode == 200) {
syncWidget(); _syncWidget();
context.showSnackbar('accountPersonalizeApplied'.tr); context.showSnackbar('accountPersonalizeApplied'.tr);
} else { } else {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);
@@ -146,8 +167,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_syncWidget();
Future.delayed(Duration.zero, () => syncWidget());
} }
@override @override
@@ -168,7 +188,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
left: 40, left: 40,
child: FloatingActionButton.small( child: FloatingActionButton.small(
heroTag: const Key('avatar-editor'), heroTag: const Key('avatar-editor'),
onPressed: () => updateImage('avatar'), onPressed: () => _editImage('avatar'),
child: const Icon( child: const Icon(
Icons.camera, Icons.camera,
), ),
@@ -187,7 +207,8 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
color: Theme.of(context).colorScheme.surfaceContainerHigh, color: Theme.of(context).colorScheme.surfaceContainerHigh,
child: _banner != null child: _banner != null
? Image.network( ? Image.network(
ServiceFinder.buildUrl('files', '/attachments/$_banner'), ServiceFinder.buildUrl(
'files', '/attachments/$_banner'),
fit: BoxFit.cover, fit: BoxFit.cover,
loadingBuilder: (BuildContext context, Widget child, loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) { ImageChunkEvent? loadingProgress) {
@@ -212,7 +233,7 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
right: 16, right: 16,
child: FloatingActionButton( child: FloatingActionButton(
heroTag: const Key('banner-editor'), heroTag: const Key('banner-editor'),
onPressed: () => updateImage('banner'), onPressed: () => _editImage('banner'),
child: const Icon( child: const Icon(
Icons.camera_alt, Icons.camera_alt,
), ),
@@ -293,18 +314,18 @@ class _PersonalizeScreenState extends State<PersonalizeScreen> {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
labelText: 'birthday'.tr, labelText: 'birthday'.tr,
), ),
onTap: () => selectBirthday(), onTap: () => _selectBirthday(),
).paddingSymmetric(horizontal: padding), ).paddingSymmetric(horizontal: padding),
const SizedBox(height: 16), const SizedBox(height: 16),
Row( Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
TextButton( TextButton(
onPressed: _isBusy ? null : () => syncWidget(), onPressed: _isBusy ? null : () => _syncWidget(),
child: Text('reset'.tr), child: Text('reset'.tr),
), ),
ElevatedButton( ElevatedButton(
onPressed: _isBusy ? null : () => updatePersonalize(), onPressed: _isBusy ? null : () => _editUserInfo(),
child: Text('apply'.tr), child: Text('apply'.tr),
), ),
], ],

View File

@@ -13,8 +13,8 @@ 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/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/feed/feed_list.dart';
import 'package:solian/widgets/posts/post_list.dart'; import 'package:solian/widgets/posts/post_list.dart';
import 'package:solian/widgets/posts/post_warped_list.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
class AccountProfilePage extends StatefulWidget { class AccountProfilePage extends StatefulWidget {
@@ -292,7 +292,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
child: Center(child: CircularProgressIndicator()), child: Center(child: CircularProgressIndicator()),
), ),
if (_userinfo != null) if (_userinfo != null)
FeedListWidget( PostWarpedListWidget(
isPinned: false, isPinned: false,
controller: _postController.pagingController, controller: _postController.pagingController,
), ),

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:async/async.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/providers/call.dart'; import 'package:solian/providers/call.dart';
@@ -16,11 +17,26 @@ class CallScreen extends StatefulWidget {
State<CallScreen> createState() => _CallScreenState(); State<CallScreen> createState() => _CallScreenState();
} }
class _CallScreenState extends State<CallScreen> { class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
Timer? timer; Timer? _timer;
String currentDuration = '00:00:00'; String _currentDuration = '00:00:00';
String parseDuration() { int _layoutMode = 0;
bool _showControls = true;
CancelableOperation? _hideControlsOperation;
late final AnimationController _controlsAnimationController =
AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
late final Animation<double> _controlsAnimation = CurvedAnimation(
parent: _controlsAnimationController,
curve: Curves.fastOutSlowIn,
);
String _parseDuration() {
final ChatCallProvider provider = Get.find(); final ChatCallProvider provider = Get.find();
if (provider.current.value == null) return '00:00:00'; if (provider.current.value == null) return '00:00:00';
Duration duration = Duration duration =
@@ -34,9 +50,142 @@ class _CallScreenState extends State<CallScreen> {
return formattedTime; return formattedTime;
} }
void updateDuration() { void _updateDuration() {
setState(() { setState(() {
currentDuration = parseDuration(); _currentDuration = _parseDuration();
});
}
void _switchLayout() {
if (_layoutMode < 1) {
setState(() => _layoutMode++);
} else {
setState(() => _layoutMode = 0);
}
}
void _toggleControls() {
if (_showControls) {
setState(() => _showControls = false);
_controlsAnimationController.animateTo(0);
_hideControlsOperation?.cancel();
} else {
setState(() => _showControls = true);
_controlsAnimationController.animateTo(1);
_planAutoHideControls();
}
}
void _planAutoHideControls() {
_hideControlsOperation = CancelableOperation.fromFuture(
Future.delayed(const Duration(seconds: 3), () {
if (!mounted) return;
if (_showControls) _toggleControls();
}),
);
}
Widget _buildListLayout() {
final ChatCallProvider call = Get.find();
return Obx(
() => Stack(
children: [
Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: call.focusTrack.value != null
? InteractiveParticipantWidget(
isFixedAvatar: false,
participant: call.focusTrack.value!,
onTap: () {},
)
: const SizedBox(),
),
Positioned(
left: 0,
right: 0,
top: 0,
child: SizedBox(
height: 128,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: math.max(0, call.participantTracks.length),
itemBuilder: (BuildContext context, int index) {
final track = call.participantTracks[index];
if (track.participant.sid ==
call.focusTrack.value?.participant.sid) {
return Container();
}
return Padding(
padding: const EdgeInsets.only(top: 8, left: 8),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget(
isFixedAvatar: true,
width: 120,
height: 120,
color: Theme.of(context).cardColor,
participant: track,
onTap: () {
if (track.participant.sid !=
call.focusTrack.value?.participant.sid) {
call.changeFocusTrack(track);
}
},
),
),
);
},
),
),
),
],
),
);
}
Widget _buildGridLayout() {
final ChatCallProvider call = Get.find();
return LayoutBuilder(builder: (context, constraints) {
double screenWidth = constraints.maxWidth;
double screenHeight = constraints.maxHeight;
int columns = (math.sqrt(call.participantTracks.length)).ceil();
int rows = (call.participantTracks.length / columns).ceil();
double tileWidth = screenWidth / columns;
double tileHeight = screenHeight / rows;
return Obx(
() => GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
childAspectRatio: tileWidth / tileHeight,
),
itemCount: math.max(0, call.participantTracks.length),
itemBuilder: (BuildContext context, int index) {
final track = call.participantTracks[index];
return Padding(
padding: const EdgeInsets.all(16),
child: Card(
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
participant: track,
onTap: () {
if (track.participant.sid !=
call.focusTrack.value?.participant.sid) {
call.changeFocusTrack(track);
}
},
),
),
),
);
},
),
);
}); });
} }
@@ -45,7 +194,18 @@ class _CallScreenState extends State<CallScreen> {
Get.find<ChatCallProvider>().setupRoom(); Get.find<ChatCallProvider>().setupRoom();
super.initState(); super.initState();
timer = Timer.periodic(const Duration(seconds: 1), (_) => updateDuration()); _updateDuration();
_planAutoHideControls();
_timer = Timer.periodic(
const Duration(seconds: 1),
(_) => _updateDuration(),
);
}
@override
void dispose() {
_controlsAnimationController.dispose();
super.dispose();
} }
@override @override
@@ -68,80 +228,71 @@ class _CallScreenState extends State<CallScreen> {
), ),
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
TextSpan( TextSpan(
text: currentDuration, text: _currentDuration,
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
]), ]),
), ),
), ),
body: SafeArea( body: SafeArea(
child: Obx( child: GestureDetector(
() => Stack( behavior: HitTestBehavior.translucent,
child: Column(
children: [ children: [
Column( SizeTransition(
children: [ sizeFactor: _controlsAnimation,
Expanded( axis: Axis.vertical,
child: Container(
color: Theme.of(context).colorScheme.surfaceContainer,
child: provider.focusTrack.value != null
? InteractiveParticipantWidget(
isFixed: false,
participant: provider.focusTrack.value!,
onTap: () {},
)
: const SizedBox(),
),
),
if (provider.room.localParticipant != null)
ControlsWidget(
provider.room,
provider.room.localParticipant!,
),
],
),
Positioned(
left: 0,
right: 0,
top: 0,
child: SizedBox( child: SizedBox(
height: 128, width: MediaQuery.of(context).size.width,
child: ListView.builder( height: 64,
scrollDirection: Axis.horizontal, child: Row(
itemCount: math.max(0, provider.participantTracks.length), children: [
itemBuilder: (BuildContext context, int index) { const Expanded(child: SizedBox()),
final track = provider.participantTracks[index]; IconButton(
if (track.participant.sid == icon: _layoutMode == 0
provider.focusTrack.value?.participant.sid) { ? const Icon(Icons.view_list)
return Container(); : const Icon(Icons.grid_view),
onPressed: () {
_switchLayout();
},
),
],
).paddingSymmetric(horizontal: 10),
),
),
Expanded(
child: Material(
color: Theme.of(context).colorScheme.surfaceContainerLow,
elevation: 2,
child: Builder(
builder: (context) {
switch (_layoutMode) {
case 1:
return _buildGridLayout();
default:
return _buildListLayout();
} }
return Padding(
padding: const EdgeInsets.only(top: 8, left: 8),
child: ClipRRect(
borderRadius:
const BorderRadius.all(Radius.circular(8)),
child: InteractiveParticipantWidget(
isFixed: true,
width: 120,
height: 120,
color: Theme.of(context).cardColor,
participant: track,
onTap: () {
if (track.participant.sid !=
provider
.focusTrack.value?.participant.sid) {
provider.changeFocusTrack(track);
}
},
),
),
);
}, },
), ),
), ),
), ),
if (provider.room.localParticipant != null)
SizeTransition(
sizeFactor: _controlsAnimation,
axis: Axis.vertical,
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: ControlsWidget(
provider.room,
provider.room.localParticipant!,
),
),
),
], ],
), ),
onTap: () {
_toggleControls();
},
), ),
), ),
), ),
@@ -150,16 +301,16 @@ class _CallScreenState extends State<CallScreen> {
@override @override
void deactivate() { void deactivate() {
timer?.cancel(); _timer?.cancel();
timer = null; _timer = null;
super.deactivate(); super.deactivate();
} }
@override @override
void activate() { void activate() {
timer ??= Timer.periodic( _timer ??= Timer.periodic(
const Duration(seconds: 1), const Duration(seconds: 1),
(_) => updateDuration(), (_) => _updateDuration(),
); );
super.activate(); super.activate();
} }

View File

@@ -11,7 +11,6 @@ import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart'; import 'package:solian/models/event.dart';
import 'package:solian/models/packet.dart'; import 'package:solian/models/packet.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/call.dart';
import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/websocket.dart'; import 'package:solian/providers/websocket.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
@@ -19,7 +18,7 @@ import 'package:solian/screens/channel/channel_detail.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/chat/call/call_prejoin.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.dart';
import 'package:solian/widgets/chat/chat_event_list.dart'; import 'package:solian/widgets/chat/chat_event_list.dart';
@@ -53,7 +52,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
late final ChatEventController _chatController; late final ChatEventController _chatController;
getChannel({String? alias}) async { _getChannel({String? alias}) async {
final ChannelProvider provider = Get.find(); final ChannelProvider provider = Get.find();
setState(() => _isBusy = true); setState(() => _isBusy = true);
@@ -80,7 +79,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
getOngoingCall() async { _getOngoingCall() async {
final ChannelProvider provider = Get.find(); final ChannelProvider provider = Get.find();
setState(() => _isBusy = true); setState(() => _isBusy = true);
@@ -100,7 +99,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void listenMessages() { void _listenMessages() {
final WebSocketProvider provider = Get.find(); final WebSocketProvider provider = Get.find();
_subscription = provider.stream.stream.listen((event) { _subscription = provider.stream.stream.listen((event) {
switch (event.method) { switch (event.method) {
@@ -110,26 +109,20 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
break; break;
case 'calls.new': case 'calls.new':
final payload = Call.fromJson(event.payload!); final payload = Call.fromJson(event.payload!);
setState(() => _ongoingCall = payload); if (payload.channel.id == _channel!.id) {
setState(() => _ongoingCall = payload);
}
break; break;
case 'calls.end': case 'calls.end':
setState(() => _ongoingCall = null); final payload = Call.fromJson(event.payload!);
if (payload.channel.id == _channel!.id) {
setState(() => _ongoingCall = null);
}
break; break;
} }
}); });
} }
void showCallPrejoin() {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => ChatCallPrejoinPopup(
ongoingCall: _ongoingCall!,
channel: _channel!,
),
);
}
Event? _messageToReplying; Event? _messageToReplying;
Event? _messageToEditing; Event? _messageToEditing;
@@ -149,13 +142,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_chatController = ChatEventController(); _chatController = ChatEventController();
_chatController.initialize(); _chatController.initialize();
getChannel().then((_) { _getOngoingCall();
_getChannel().then((_) {
_chatController.getEvents(_channel!, widget.realm); _chatController.getEvents(_channel!, widget.realm);
listenMessages(); _listenMessages();
}); });
getOngoingCall();
super.initState(); super.initState();
} }
@@ -183,8 +175,6 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
); );
} }
final ChatCallProvider call = Get.find();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: AppBarLeadingButton.adaptive(context), leading: AppBarLeadingButton.adaptive(context),
@@ -219,7 +209,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
if (value == false) AppRouter.instance.pop(); if (value == false) AppRouter.instance.pop();
if (value != null) { if (value != null) {
final resp = Channel.fromJson(value as Map<String, dynamic>); final resp = Channel.fromJson(value as Map<String, dynamic>);
getChannel(alias: resp.alias); _getChannel(alias: resp.alias);
} }
}); });
}, },
@@ -232,32 +222,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
body: Column( body: Column(
children: [ children: [
if (_ongoingCall != null) if (_ongoingCall != null)
MaterialBanner( ChannelCallIndicator(
padding: const EdgeInsets.only(left: 16, top: 4, bottom: 4), channel: _channel!,
leading: const Icon(Icons.call_received), ongoingCall: _ongoingCall!,
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
dividerColor: Colors.transparent,
content: Text('callOngoing'.tr),
actions: [
Obx(() {
if (call.current.value == null) {
return TextButton(
onPressed: showCallPrejoin,
child: Text('callJoin'.tr),
);
} else if (call.channel.value?.id == _channel?.id) {
return TextButton(
onPressed: () => call.gotoScreen(context),
child: Text('callResume'.tr),
);
} else {
return TextButton(
onPressed: null,
child: Text('callJoin'.tr),
);
}
})
],
), ),
Expanded( Expanded(
child: ChatEventList( child: ChatEventList(

View File

@@ -3,7 +3,7 @@ import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/pagination.dart'; import 'package:solian/models/pagination.dart';
import 'package:solian/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/widgets/feed/feed_list.dart'; import 'package:solian/widgets/posts/post_warped_list.dart';
import '../../models/post.dart'; import '../../models/post.dart';
@@ -77,7 +77,7 @@ class _FeedSearchScreenState extends State<FeedSearchScreen> {
onRefresh: () => Future.sync(() => _pagingController.refresh()), onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
FeedListWidget(controller: _pagingController), PostWarpedListWidget(controller: _pagingController),
], ],
), ),
), ),

View File

@@ -4,12 +4,13 @@ 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/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';
import 'package:solian/widgets/feed/feed_list.dart';
import 'package:solian/widgets/posts/post_shuffle_swiper.dart'; import 'package:solian/widgets/posts/post_shuffle_swiper.dart';
import 'package:solian/widgets/posts/post_warped_list.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@@ -51,7 +52,9 @@ class _HomeScreenState extends State<HomeScreen>
useRootNavigator: true, useRootNavigator: true,
isScrollControlled: true, isScrollControlled: true,
context: context, context: context,
builder: (context) => const PostCreatePopup(), builder: (context) => PostCreatePopup(
controller: _postController,
),
); );
}, },
), ),
@@ -95,7 +98,7 @@ class _HomeScreenState extends State<HomeScreen>
RefreshIndicator( RefreshIndicator(
onRefresh: () => _postController.reloadAllOver(), onRefresh: () => _postController.reloadAllOver(),
child: CustomScrollView(slivers: [ child: CustomScrollView(slivers: [
FeedListWidget( PostWarpedListWidget(
controller: _postController.pagingController, controller: _postController.pagingController,
), ),
]), ]),
@@ -118,12 +121,12 @@ class _HomeScreenState extends State<HomeScreen>
class PostCreatePopup extends StatelessWidget { class PostCreatePopup extends StatelessWidget {
final bool hideDraftBox; final bool hideDraftBox;
final Function? onCreated; final PostListController controller;
const PostCreatePopup({ const PostCreatePopup({
super.key, super.key,
this.hideDraftBox = false, this.hideDraftBox = false,
this.onCreated, required this.controller,
}); });
@override @override
@@ -140,11 +143,13 @@ class PostCreatePopup extends StatelessWidget {
label: 'postEditorModeStory'.tr, label: 'postEditorModeStory'.tr,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
AppRouter.instance.pushNamed('postEditor', queryParameters: { AppRouter.instance.pushNamed(
'mode': 0.toString(), 'postEditor',
}).then((val) { extra: PostPublishArguments(postListController: controller),
if (val != null && onCreated != null) onCreated!(); queryParameters: {
}); 'mode': 0.toString(),
},
);
}, },
), ),
( (
@@ -152,11 +157,13 @@ class PostCreatePopup extends StatelessWidget {
label: 'postEditorModeArticle'.tr, label: 'postEditorModeArticle'.tr,
onTap: () { onTap: () {
Navigator.pop(context); Navigator.pop(context);
AppRouter.instance.pushNamed('postEditor', queryParameters: { AppRouter.instance.pushNamed(
'mode': 1.toString(), 'postEditor',
}).then((val) { extra: PostPublishArguments(postListController: controller),
if (val != null && onCreated != null) onCreated!(); queryParameters: {
}); 'mode': 1.toString(),
},
);
}, },
), ),
( (

View File

@@ -3,14 +3,18 @@ import 'package:get/get.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/providers/content/posts.dart'; import 'package:solian/providers/content/posts.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:solian/widgets/posts/post_item.dart'; import 'package:solian/widgets/posts/post_item.dart';
import 'package:solian/widgets/posts/post_replies.dart'; import 'package:solian/widgets/posts/post_replies.dart';
class PostDetailScreen extends StatefulWidget { class PostDetailScreen extends StatefulWidget {
final String id; final String id;
final Post? post;
const PostDetailScreen({super.key, required this.id}); const PostDetailScreen({
super.key,
required this.id,
this.post,
});
@override @override
State<PostDetailScreen> createState() => _PostDetailScreenState(); State<PostDetailScreen> createState() => _PostDetailScreenState();
@@ -20,6 +24,11 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
Post? item; Post? item;
Future<Post?> getDetail() async { Future<Post?> getDetail() async {
if (widget.post != null) {
item = widget.post;
return widget.post;
}
final PostProvider provider = Get.find(); final PostProvider provider = Get.find();
try { try {
@@ -48,14 +57,12 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
return CustomScrollView( return CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
child: CenteredContainer( child: PostItem(
child: PostItem( item: item!,
item: item!, isClickable: true,
isClickable: true, isFullDate: true,
isFullDate: true, isShowReply: false,
isShowReply: false, isContentSelectable: true,
isContentSelectable: true,
),
), ),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
@@ -63,14 +70,12 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
.paddingOnly(top: 4), .paddingOnly(top: 4),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: CenteredContainer( child: Align(
child: Align( alignment: Alignment.centerLeft,
alignment: Alignment.centerLeft, child: Text(
child: Text( 'postReplies'.tr,
'postReplies'.tr, style: Theme.of(context).textTheme.headlineSmall,
style: Theme.of(context).textTheme.headlineSmall, ).paddingOnly(left: 24, right: 24, top: 16),
).paddingOnly(left: 24, right: 24, top: 16),
),
), ),
), ),
PostReplyList(item: item!), PostReplyList(item: item!),

View File

@@ -3,10 +3,13 @@ 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';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.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';
import 'package:solian/providers/attachment_uploader.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/theme.dart'; import 'package:solian/theme.dart';
@@ -20,8 +23,15 @@ class PostPublishArguments {
final Post? reply; final Post? reply;
final Post? repost; final Post? repost;
final Realm? realm; final Realm? realm;
final PostListController? postListController;
PostPublishArguments({this.edit, this.reply, this.repost, this.realm}); PostPublishArguments({
this.edit,
this.reply,
this.repost,
this.realm,
this.postListController,
});
} }
class PostPublishScreen extends StatefulWidget { class PostPublishScreen extends StatefulWidget {
@@ -29,6 +39,7 @@ 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({
@@ -37,6 +48,7 @@ class PostPublishScreen extends StatefulWidget {
this.reply, this.reply,
this.repost, this.repost,
this.realm, this.realm,
this.postListController,
required this.mode, required this.mode,
}); });
@@ -46,6 +58,7 @@ class PostPublishScreen extends StatefulWidget {
class _PostPublishScreenState extends State<PostPublishScreen> { class _PostPublishScreenState extends State<PostPublishScreen> {
final _editorController = PostEditorController(); final _editorController = PostEditorController();
final _contentFocusNode = FocusNode();
bool _isBusy = false; bool _isBusy = false;
@@ -54,6 +67,14 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
if (_editorController.isEmpty) return; if (_editorController.isEmpty) return;
final AttachmentUploaderController uploader = Get.find();
if (uploader.queueOfUpload.any(
((x) => x.usage == 'i.attachment' && x.isUploading),
)) {
context.showErrorDialog('attachmentUploadInProgress'.tr);
return;
}
setState(() => _isBusy = true); setState(() => _isBusy = true);
final client = auth.configureClient('interactive'); final client = auth.configureClient('interactive');
@@ -74,34 +95,49 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
context.showErrorDialog(resp.bodyString); context.showErrorDialog(resp.bodyString);
} else { } else {
_editorController.localClear(); _editorController.localClear();
if (widget.postListController != null) {
widget.postListController!.reloadAllOver();
}
AppRouter.instance.pop(resp.body); AppRouter.instance.pop(resp.body);
} }
setState(() => _isBusy = false); setState(() => _isBusy = false);
} }
void syncWidget() { void _syncWidget() {
_editorController.mode.value = widget.mode; _editorController.mode.value = widget.mode;
if (widget.edit != null) { if (widget.edit != null) {
_editorController.editTarget = widget.edit; _editorController.editTarget = widget.edit;
} }
if (widget.realm != null) {
_editorController.realmZone.value = widget.realm;
}
} }
void cancelAction() { void _cancelAction() {
_editorController.localClear();
AppRouter.instance.pop(); AppRouter.instance.pop();
} }
Post? get _editTo => _editorController.editTo.value;
Post? get _replyTo => _editorController.replyTo.value;
Post? get _repostTo => _editorController.repostTo.value;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
syncWidget(); if (widget.edit == null) _editorController.localRead();
_editorController.contentController.addListener(() => setState(() {}));
_syncWidget();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final notifyBannerActions = [ final notifyBannerActions = [
TextButton( TextButton(
onPressed: cancelAction, onPressed: _cancelAction,
child: Text('cancel'.tr), child: Text('cancel'.tr),
) )
]; ];
@@ -134,6 +170,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
], ],
), ),
body: Column( body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ListTile( ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerLow, tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
@@ -162,68 +199,55 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
}, },
), ),
), ),
if (_editTo != null && _editTo!.isDraft != true)
MaterialBanner(
leading: const Icon(Icons.edit),
leadingPadding: const EdgeInsets.only(left: 10, right: 20),
dividerColor: Colors.transparent,
content: Text('postEditingNotify'.tr),
actions: notifyBannerActions,
),
if (_replyTo != null)
ExpansionTile(
leading: const FaIcon(
FontAwesomeIcons.reply,
size: 18,
).paddingOnly(left: 2),
title: Text('postReplyingNotify'.trParams(
{'username': '@${widget.reply!.author.name}'},
)),
collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer,
children: [
PostItem(
item: _replyTo!,
isReactable: false,
).paddingOnly(bottom: 8),
],
),
if (_repostTo != null)
ExpansionTile(
leading: const FaIcon(
FontAwesomeIcons.retweet,
size: 18,
).paddingOnly(left: 2),
title: Text('postRepostingNotify'.trParams(
{'username': '@${widget.repost!.author.name}'},
)),
collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer,
children: [
PostItem(
item: _repostTo!,
isReactable: false,
).paddingOnly(bottom: 8),
],
),
Expanded( Expanded(
child: ListView( child: ListView(
children: [ children: [
if (_isBusy) if (_isBusy)
const LinearProgressIndicator().animate().scaleX(), const LinearProgressIndicator().animate().scaleX(),
if (widget.edit != null && widget.edit!.isDraft != true)
MaterialBanner(
leading: const Icon(Icons.edit),
leadingPadding:
const EdgeInsets.only(left: 10, right: 20),
dividerColor: Colors.transparent,
content: Text('postEditingNotify'.tr),
actions: notifyBannerActions,
),
if (widget.reply != null)
ExpansionTile(
leading: const FaIcon(
FontAwesomeIcons.reply,
size: 18,
).paddingOnly(left: 2),
title: Text('postReplyingNotify'.trParams(
{'username': '@${widget.reply!.author.name}'},
)),
collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer,
children: [
PostItem(
item: widget.reply!,
isReactable: false,
).paddingOnly(bottom: 8),
],
),
if (widget.repost != null)
ExpansionTile(
leading: const FaIcon(
FontAwesomeIcons.retweet,
size: 18,
).paddingOnly(left: 2),
title: Text('postRepostingNotify'.trParams(
{'username': '@${widget.repost!.author.name}'},
)),
collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainer,
children: [
PostItem(
item: widget.repost!,
isReactable: false,
).paddingOnly(bottom: 8),
],
),
if (widget.realm != null)
MaterialBanner(
leading: const Icon(Icons.group),
leadingPadding:
const EdgeInsets.only(left: 10, right: 20),
dividerColor: Colors.transparent,
content: Text(
'postInRealmNotify'
.trParams({'realm': '#${widget.realm!.alias}'}),
),
actions: notifyBannerActions,
),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
@@ -235,8 +259,8 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
autocorrect: true, autocorrect: true,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
controller: _editorController.contentController, controller: _editorController.contentController,
decoration: InputDecoration( focusNode: _contentFocusNode,
border: InputBorder.none, decoration: InputDecoration.collapsed(
hintText: 'postContentPlaceholder'.tr, hintText: 'postContentPlaceholder'.tr,
), ),
onTapOutside: (_) => onTapOutside: (_) =>
@@ -383,12 +407,72 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
}, },
), ),
IconButton( IconButton(
icon: const Icon(Icons.tag), icon: Obx(() {
return badges.Badge(
badgeContent: Text(
_editorController.tags.length.toString(),
style: const TextStyle(color: Colors.white),
),
showBadge: _editorController.tags.isNotEmpty,
position: badges.BadgePosition.topEnd(
top: -12,
end: -8,
),
child: const Icon(Icons.label),
);
}),
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
onPressed: () { onPressed: () {
_editorController.editCategoriesAndTags(context); _editorController.editCategoriesAndTags(context);
}, },
), ),
IconButton(
icon: Obx(() {
return badges.Badge(
showBadge:
_editorController.realmZone.value != null,
position: badges.BadgePosition.topEnd(
top: -4,
end: -6,
),
child: const Icon(Icons.workspaces),
);
}),
color: Theme.of(context).colorScheme.primary,
onPressed: () {
_editorController.editPublishZone(context);
},
),
IconButton(
icon: Obx(() {
return badges.Badge(
showBadge:
_editorController.publishedAt.value != null ||
_editorController.publishedUntil.value !=
null,
position: badges.BadgePosition.topEnd(
top: -4,
end: -6,
),
child: const Icon(Icons.schedule),
);
}),
color: Theme.of(context).colorScheme.primary,
onPressed: () {
_editorController.editPublishDate(context);
},
),
MarkdownToolbar(
hideImage: true,
useIncludedTextField: false,
backgroundColor: Colors.transparent,
iconColor: Theme.of(context).colorScheme.onSurface,
controller: _editorController.contentController,
focusNode: _contentFocusNode,
borderRadius:
const BorderRadius.all(Radius.circular(20)),
width: 40,
).paddingSymmetric(horizontal: 4),
], ],
).paddingSymmetric(horizontal: 6, vertical: 8), ).paddingSymmetric(horizontal: 6, vertical: 8),
), ),
@@ -403,6 +487,7 @@ class _PostPublishScreenState extends State<PostPublishScreen> {
@override @override
void dispose() { void dispose() {
_contentFocusNode.dispose();
_editorController.dispose(); _editorController.dispose();
super.dispose(); super.dispose();
} }

View File

@@ -26,7 +26,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
final List<Realm> _realms = List.empty(growable: true); final List<Realm> _realms = List.empty(growable: true);
getRealms() async { _getRealms() async {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
@@ -48,7 +48,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
getRealms(); _getRealms();
} }
@override @override
@@ -71,7 +71,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
onPressed: () { onPressed: () {
AppRouter.instance.pushNamed('realmOrganizing').then( AppRouter.instance.pushNamed('realmOrganizing').then(
(value) { (value) {
if (value != null) getRealms(); if (value != null) _getRealms();
}, },
); );
}, },
@@ -84,7 +84,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
body: Obx(() { body: Obx(() {
if (auth.isAuthorized.isFalse) { if (auth.isAuthorized.isFalse) {
return SigninRequiredOverlay( return SigninRequiredOverlay(
onSignedIn: () => getRealms(), onSignedIn: () => _getRealms(),
); );
} }
@@ -94,12 +94,12 @@ class _RealmListScreenState extends State<RealmListScreen> {
Expanded( Expanded(
child: CenteredContainer( child: CenteredContainer(
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () => getRealms(), onRefresh: () => _getRealms(),
child: ListView.builder( child: ListView.builder(
itemCount: _realms.length, itemCount: _realms.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final element = _realms[index]; final element = _realms[index];
return buildRealm(element); return _buildEntry(element);
}, },
), ),
), ),
@@ -112,7 +112,7 @@ class _RealmListScreenState extends State<RealmListScreen> {
); );
} }
Widget buildRealm(Realm element) { Widget _buildEntry(Realm element) {
return Card( return Card(
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)), borderRadius: const BorderRadius.all(Radius.circular(8)),

View File

@@ -12,7 +12,6 @@ import 'package:solian/providers/content/posts.dart';
import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/content/realm.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/screens/channel/channel_organize.dart'; import 'package:solian/screens/channel/channel_organize.dart';
import 'package:solian/screens/posts/post_editor.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/channel/channel_list.dart'; import 'package:solian/widgets/channel/channel_list.dart';
@@ -136,6 +135,7 @@ class _RealmViewScreenState extends State<RealmViewScreen> {
} }
return TabBarView( return TabBarView(
physics: const NeverScrollableScrollPhysics(),
children: [ children: [
RealmPostListWidget(realm: _realm!), RealmPostListWidget(realm: _realm!),
RealmChannelListWidget( RealmChannelListWidget(
@@ -189,7 +189,6 @@ class _RealmPostListWidgetState extends State<RealmPostListWidget> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_pagingController.addPageRequestListener(getPosts); _pagingController.addPageRequestListener(getPosts);
} }
@@ -199,28 +198,6 @@ class _RealmPostListWidgetState extends State<RealmPostListWidget> {
onRefresh: () => Future.sync(() => _pagingController.refresh()), onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
SliverToBoxAdapter(
child: ListTile(
leading: const Icon(Icons.post_add),
contentPadding: const EdgeInsets.only(left: 24, right: 8),
tileColor: Theme.of(context).colorScheme.surfaceContainer,
title: Text('postNew'.tr),
subtitle: Text(
'postNewInRealmHint'
.trParams({'realm': '#${widget.realm.alias}'}),
),
onTap: () {
AppRouter.instance
.pushNamed(
'postEditor',
extra: PostPublishArguments(realm: widget.realm),
)
.then((value) {
if (value != null) _pagingController.refresh();
});
},
),
),
PostListWidget(controller: _pagingController), PostListWidget(controller: _pagingController),
], ],
), ),

102
lib/screens/settings.dart Normal file
View File

@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:solian/exts.dart';
import 'package:solian/providers/theme_switcher.dart';
import 'package:solian/router.dart';
import 'package:solian/theme.dart';
class SettingScreen extends StatefulWidget {
const SettingScreen({super.key});
@override
State<SettingScreen> createState() => _SettingScreenState();
}
class _SettingScreenState extends State<SettingScreen> {
late final SharedPreferences _prefs;
Widget _buildCaptionHeader(String title) {
return Container(
width: MediaQuery.of(context).size.width,
color: Theme.of(context).colorScheme.surfaceContainer,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Text(title),
);
}
Widget _buildThemeColorButton(String label, Color color) {
return IconButton(
icon: Icon(Icons.circle, color: color),
tooltip: label,
onPressed: () {
context.read<ThemeSwitcher>().setTheme(
SolianTheme.build(
Brightness.light,
seedColor: color,
),
SolianTheme.build(
Brightness.dark,
seedColor: color,
),
);
_prefs.setInt('global_theme_color', color.value);
context.clearSnackbar();
context.showSnackbar('themeColorApplied'.tr);
},
);
}
static final List<(String, Color)> _presentTheme = [
('themeColorRed', const Color.fromRGBO(154, 98, 91, 1)),
('themeColorBlue', const Color.fromRGBO(103, 96, 193, 1)),
('themeColorMiku', const Color.fromRGBO(56, 120, 126, 1)),
('themeColorKagamine', const Color.fromRGBO(244, 183, 63, 1)),
('themeColorLuka', const Color.fromRGBO(243, 174, 218, 1)),
];
@override
void initState() {
super.initState();
SharedPreferences.getInstance().then((inst) {
_prefs = inst;
});
}
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: ListView(
children: [
_buildCaptionHeader('themeColor'.tr),
SizedBox(
height: 56,
child: ListView(
scrollDirection: Axis.horizontal,
children: _presentTheme
.map((x) => _buildThemeColorButton(x.$1, x.$2))
.toList(),
).paddingSymmetric(horizontal: 12, vertical: 8),
),
_buildCaptionHeader('more'.tr),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
TextButton(
style: const ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
),
child: Text('about'.tr),
onPressed: () {
AppRouter.instance.pushNamed('about');
},
),
],
).paddingSymmetric(horizontal: 12, vertical: 8),
],
),
);
}
}

View File

@@ -8,13 +8,15 @@ import 'package:solian/widgets/app_bar_leading.dart';
class TitleShell extends StatelessWidget { class TitleShell extends StatelessWidget {
final bool showAppBar; final bool showAppBar;
final bool isCenteredTitle; final bool isCenteredTitle;
final GoRouterState state; final String? title;
final GoRouterState? state;
final Widget child; final Widget child;
const TitleShell({ const TitleShell({
super.key, super.key,
required this.child, required this.child,
required this.state, this.title,
this.state,
this.showAppBar = true, this.showAppBar = true,
this.isCenteredTitle = false, this.isCenteredTitle = false,
}); });
@@ -25,7 +27,9 @@ class TitleShell extends StatelessWidget {
appBar: showAppBar appBar: showAppBar
? AppBar( ? AppBar(
leading: AppBarLeadingButton.adaptive(context), leading: AppBarLeadingButton.adaptive(context),
title: AppBarTitle(state.topRoute?.name?.tr ?? 'page'.tr), title: AppBarTitle(
title ?? (state!.topRoute?.name?.tr ?? 'page'.tr),
),
centerTitle: isCenteredTitle, centerTitle: isCenteredTitle,
toolbarHeight: SolianTheme.toolbarHeight(context), toolbarHeight: SolianTheme.toolbarHeight(context),
) )

View File

@@ -27,13 +27,25 @@ abstract class SolianTheme {
} }
} }
static ThemeData build(Brightness brightness) { static ThemeData build(Brightness brightness, {Color? seedColor}) {
return ThemeData( return ThemeData(
brightness: brightness, brightness: brightness,
useMaterial3: true, useMaterial3: true,
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
brightness: brightness, brightness: brightness,
seedColor: const Color.fromRGBO(154, 98, 91, 1), seedColor: seedColor ?? const Color.fromRGBO(154, 98, 91, 1),
),
fontFamily: 'Comfortaa',
fontFamilyFallback: [
'NotoSansSC',
'NotoSansHK',
'NotoSansJP',
if (PlatformInfo.isWeb) 'NotoSansEmoji',
],
typography: Typography.material2021(
colorScheme: brightness == Brightness.light
? const ColorScheme.light()
: const ColorScheme.dark(),
), ),
); );
} }

View File

@@ -10,8 +10,10 @@ const i18nEnglish = {
'draft': 'Draft', 'draft': 'Draft',
'draftSave': 'Save', 'draftSave': 'Save',
'draftBox': 'Draft Box', 'draftBox': 'Draft Box',
'more': 'More',
'share': 'Share', 'share': 'Share',
'feed': 'Feed', 'feed': 'Feed',
'unlink': 'Unlink',
'feedSearch': 'Search Feed', 'feedSearch': 'Search Feed',
'feedSearchWithTag': 'Searching with tag #@key', 'feedSearchWithTag': 'Searching with tag #@key',
'feedSearchWithCategory': 'Searching in category @category', 'feedSearchWithCategory': 'Searching in category @category',
@@ -27,11 +29,13 @@ const i18nEnglish = {
'about': 'About', 'about': 'About',
'edit': 'Edit', 'edit': 'Edit',
'delete': 'Delete', 'delete': 'Delete',
'settings': 'Settings',
'search': 'Search', 'search': 'Search',
'post': 'Post', 'post': 'Post',
'article': 'Article', 'article': 'Article',
'reply': 'Reply', 'reply': 'Reply',
'repost': 'Repost', 'repost': 'Repost',
'openInAlbum': 'Open in album',
'openInBrowser': 'Open in browser', 'openInBrowser': 'Open in browser',
'notification': 'Notification', 'notification': 'Notification',
'errorHappened': 'An error occurred', 'errorHappened': 'An error occurred',
@@ -99,6 +103,11 @@ const i18nEnglish = {
'postRestoreFromLocal': 'Restore from local', 'postRestoreFromLocal': 'Restore from local',
'postAutoSaveAt': 'Auto saved at @date', 'postAutoSaveAt': 'Auto saved at @date',
'postCategoriesAndTags': 'Categories n\' Tags', 'postCategoriesAndTags': 'Categories n\' Tags',
'postPublishDate': 'Publish Date',
'postPublishAt': 'Publish At',
'postPublishedUntil': 'Publish Until',
'postPublishZone': 'Publish Zone',
'postPublishZoneNone': 'None',
'postVisibility': 'Visibility', 'postVisibility': 'Visibility',
'postVisibilityAll': 'Everyone', 'postVisibilityAll': 'Everyone',
'postVisibilityFriends': 'Friends', 'postVisibilityFriends': 'Friends',
@@ -122,7 +131,7 @@ const i18nEnglish = {
'postAction': 'Post', 'postAction': 'Post',
'postEdited': 'Edited at @date', 'postEdited': 'Edited at @date',
'postNewCreated': 'Created at @date', 'postNewCreated': 'Created at @date',
'postAttachmentTip': '@count attachment(s)', 'attachmentHint': '@count attachment(s)',
'postInRealm': 'In @realm', 'postInRealm': 'In @realm',
'postDetail': 'Post', 'postDetail': 'Post',
'postReplies': 'Replies', 'postReplies': 'Replies',
@@ -150,6 +159,12 @@ const i18nEnglish = {
'reactCompleted': 'Your reaction has been added', 'reactCompleted': 'Your reaction has been added',
'reactUncompleted': 'Your reaction has been removed', 'reactUncompleted': 'Your reaction has been removed',
'attachmentUploadBy': 'Upload by', 'attachmentUploadBy': 'Upload by',
'attachmentAutoUpload': 'Auto Upload',
'attachmentUploadQueue': 'Upload Queue',
'attachmentUploadQueueStart': 'Start All',
'attachmentUploadInProgress': 'There are attachments being uploaded. Please wait until all attachments have been uploaded before proceeding...',
'attachmentAttached': 'Exists Files',
'attachmentUploadBlocked': 'Upload blocked, there is currently a task in progress...',
'attachmentAdd': 'Attach attachments', 'attachmentAdd': 'Attach attachments',
'attachmentAddGalleryPhoto': 'Gallery photo', 'attachmentAddGalleryPhoto': 'Gallery photo',
'attachmentAddGalleryVideo': 'Gallery video', 'attachmentAddGalleryVideo': 'Gallery video',
@@ -157,6 +172,9 @@ const i18nEnglish = {
'attachmentAddCameraVideo': 'Capture video', 'attachmentAddCameraVideo': 'Capture video',
'attachmentAddClipboard': 'Paste file', 'attachmentAddClipboard': 'Paste file',
'attachmentAddFile': 'Attach file', 'attachmentAddFile': 'Attach file',
'attachmentAddLink': 'Link attachments',
'attachmentAddLinkHint': 'Enter attachment serial number to link that attachment',
'attachmentAddLinkInput': 'Serial number',
'attachmentSetting': 'Adjust attachment', 'attachmentSetting': 'Adjust attachment',
'attachmentAlt': 'Alternative text', 'attachmentAlt': 'Alternative text',
'attachmentLoadFailed': 'Load Attachment Failed', 'attachmentLoadFailed': 'Load Attachment Failed',
@@ -239,6 +257,8 @@ const i18nEnglish = {
'Are your sure to delete message @id? This action cannot be undone!', 'Are your sure to delete message @id? This action cannot be undone!',
'call': 'Call', 'call': 'Call',
'callOngoing': 'A call is ongoing...', 'callOngoing': 'A call is ongoing...',
'callOngoingEmpty': 'A call is on hold...',
'callOngoingParticipants': '@count people are calling...',
'callJoin': 'Join', 'callJoin': 'Join',
'callResume': 'Resume', 'callResume': 'Resume',
'callMicrophone': 'Microphone', 'callMicrophone': 'Microphone',
@@ -293,6 +313,12 @@ const i18nEnglish = {
'accountStatusNegative': 'Negative', 'accountStatusNegative': 'Negative',
'accountStatusNeutral': 'Neutral', 'accountStatusNeutral': 'Neutral',
'accountStatusPositive': 'Positive', 'accountStatusPositive': 'Positive',
'bsLoadingTheme': 'Loading Theme',
'bsCheckForUpdate': 'Checking For Updates',
'bsCheckForUpdateFailed': 'Unable to Check Updates',
'bsCheckForUpdateNew': 'Found New Version',
'bsCheckForUpdateDescApple': 'Please head to TestFlight and update your app to latest version to prevent error happens and get latest functions.',
'bsCheckForUpdateDescCommon': 'Please head to our website download and install latest version of application to prevent error happens and get latest functions.',
'bsCheckingServer': 'Checking Server Status', 'bsCheckingServer': 'Checking Server Status',
'bsCheckingServerFail': 'bsCheckingServerFail':
'Unable connect to server, check your network connection', 'Unable connect to server, check your network connection',
@@ -304,4 +330,13 @@ const i18nEnglish = {
'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',
'themeColor': 'Global Theme Color',
'themeColorRed': 'Modern Red',
'themeColorBlue': 'Classic Blue',
'themeColorMiku': 'Miku Blue',
'themeColorKagamine': 'Kagamine Yellow',
'themeColorLuka': 'Luka Pink',
'themeColorApplied': 'Global theme color has been applied.',
'attachmentSaved': 'Attachment saved to your system album.',
'cropImage': 'Crop Image',
}; };

View File

@@ -13,12 +13,15 @@ const i18nSimplifiedChinese = {
'about': '关于', 'about': '关于',
'edit': '编辑', 'edit': '编辑',
'delete': '删除', 'delete': '删除',
'settings': '设置',
'page': '页面', 'page': '页面',
'draft': '草稿', 'draft': '草稿',
'draftSave': '存为草稿', 'draftSave': '存为草稿',
'draftBox': '草稿箱', 'draftBox': '草稿箱',
'more': '更多',
'share': '分享', 'share': '分享',
'feed': '资讯', 'feed': '资讯',
'unlink': '移除链接',
'feedSearch': '搜索资讯', 'feedSearch': '搜索资讯',
'feedSearchWithTag': '检索带有 #@key 标签的资讯', 'feedSearchWithTag': '检索带有 #@key 标签的资讯',
'feedSearchWithCategory': '检索位于分类 @category 的资讯', 'feedSearchWithCategory': '检索位于分类 @category 的资讯',
@@ -32,6 +35,7 @@ const i18nSimplifiedChinese = {
'article': '文章', 'article': '文章',
'reply': '回复', 'reply': '回复',
'repost': '转帖', 'repost': '转帖',
'openInAlbum': '在相簿中打开',
'openInBrowser': '在浏览器中打开', 'openInBrowser': '在浏览器中打开',
'notification': '通知', 'notification': '通知',
'errorHappened': '发生错误了', 'errorHappened': '发生错误了',
@@ -93,6 +97,11 @@ const i18nSimplifiedChinese = {
'postRestoreFromLocal': '内容从本地暂存回复', 'postRestoreFromLocal': '内容从本地暂存回复',
'postAutoSaveAt': '已自动保存于 @date', 'postAutoSaveAt': '已自动保存于 @date',
'postCategoriesAndTags': '分类与标签', 'postCategoriesAndTags': '分类与标签',
'postPublishDate': '发布时间',
'postPublishAt': '发布帖子于',
'postPublishedUntil': '取消发布于',
'postPublishZone': '帖子发布区',
'postPublishZoneNone': '无所属领域',
'postVisibility': '帖子可见性', 'postVisibility': '帖子可见性',
'postVisibilityAll': '所有人可见', 'postVisibilityAll': '所有人可见',
'postVisibilityFriends': '仅好友可见', 'postVisibilityFriends': '仅好友可见',
@@ -117,7 +126,7 @@ const i18nSimplifiedChinese = {
'postEdited': '编辑于 @date', 'postEdited': '编辑于 @date',
'postNewCreated': '创建于 @date', 'postNewCreated': '创建于 @date',
'postInRealm': '发表于 @realm', 'postInRealm': '发表于 @realm',
'postAttachmentTip': '@count 个附件', 'attachmentHint': '@count 个附件',
'postDetail': '帖子详情', 'postDetail': '帖子详情',
'postReplies': '帖子回复', 'postReplies': '帖子回复',
'postPublish': '编辑帖子', 'postPublish': '编辑帖子',
@@ -139,6 +148,12 @@ const i18nSimplifiedChinese = {
'reactCompleted': '你的反应已被添加', 'reactCompleted': '你的反应已被添加',
'reactUncompleted': '你的反应已被移除', 'reactUncompleted': '你的反应已被移除',
'attachmentUploadBy': '由上传', 'attachmentUploadBy': '由上传',
'attachmentAutoUpload': '自动上传',
'attachmentUploadQueue': '上传队列',
'attachmentUploadQueueStart': '整队上传',
'attachmentUploadInProgress': '有附件正在上传,请等待所有附件上传完毕后再进行操作……',
'attachmentAttached': '已附附件',
'attachmentUploadBlocked': '上传受阻,当前已有任务进行中……',
'attachmentAdd': '附加附件', 'attachmentAdd': '附加附件',
'attachmentAddGalleryPhoto': '相册照片', 'attachmentAddGalleryPhoto': '相册照片',
'attachmentAddGalleryVideo': '相册视频', 'attachmentAddGalleryVideo': '相册视频',
@@ -146,6 +161,9 @@ const i18nSimplifiedChinese = {
'attachmentAddCameraVideo': '拍摄视频', 'attachmentAddCameraVideo': '拍摄视频',
'attachmentAddClipboard': '粘贴文件', 'attachmentAddClipboard': '粘贴文件',
'attachmentAddFile': '附加文件', 'attachmentAddFile': '附加文件',
'attachmentAddLink': '链接附件',
'attachmentAddLinkHint': '输入附件的神秘代号来链接对应附件',
'attachmentAddLinkInput': '神秘代号',
'attachmentSetting': '调整附件', 'attachmentSetting': '调整附件',
'attachmentAlt': '替代文字', 'attachmentAlt': '替代文字',
'attachmentLoadFailed': '加载失败', 'attachmentLoadFailed': '加载失败',
@@ -220,6 +238,8 @@ const i18nSimplifiedChinese = {
'messageDeletionConfirmCaption': '你确定要删除消息 @id 吗?该操作不可撤销。', 'messageDeletionConfirmCaption': '你确定要删除消息 @id 吗?该操作不可撤销。',
'call': '通话', 'call': '通话',
'callOngoing': '一则通话正在进行中…', 'callOngoing': '一则通话正在进行中…',
'callOngoingEmpty': '一则通话待机中…',
'callOngoingParticipants': '@count 人正在进行通话…',
'callJoin': '加入', 'callJoin': '加入',
'callResume': '恢复', 'callResume': '恢复',
'callMicrophone': '麦克风', 'callMicrophone': '麦克风',
@@ -272,6 +292,12 @@ const i18nSimplifiedChinese = {
'accountStatusNegative': '负面', 'accountStatusNegative': '负面',
'accountStatusNeutral': '中性', 'accountStatusNeutral': '中性',
'accountStatusPositive': '积极', 'accountStatusPositive': '积极',
'bsLoadingTheme': '正在装载主题',
'bsCheckForUpdate': '正在检查更新',
'bsCheckForUpdateFailed': '无法检查更新',
'bsCheckForUpdateNew': '发现新版本',
'bsCheckForUpdateDescApple': '请前往 TestFlight 并将您的应用程序更新到最新版本,以防止出现错误并获取最新功能。',
'bsCheckForUpdateDescCommon': '请前往我们的网站下载并安装最新版本的应用程序,以防止出现错误并获取最新功能。',
'bsCheckingServer': '检查服务器状态中', 'bsCheckingServer': '检查服务器状态中',
'bsCheckingServerFail': '无法连接至服务器,请检查你的网络连接状态', 'bsCheckingServerFail': '无法连接至服务器,请检查你的网络连接状态',
'bsCheckingServerDown': '当前服务器不可用,请稍后重试', 'bsCheckingServerDown': '当前服务器不可用,请稍后重试',
@@ -281,4 +307,13 @@ const i18nSimplifiedChinese = {
'bsRegisteringPushNotify': '正在启用推送通知', 'bsRegisteringPushNotify': '正在启用推送通知',
'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': '全局主题色',
'themeColorRed': '现代红',
'themeColorBlue': '经典蓝',
'themeColorMiku': '未来蓝',
'themeColorKagamine': '镜音黄',
'themeColorLuka': '流音粉',
'themeColorApplied': '全局主题颜色已应用',
'attachmentSaved': '附件已保存到系统相册',
'cropImage': '裁剪图片',
}; };

View File

@@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/providers/content/attachment.dart';
class AttachmentAttrEditorDialog extends StatefulWidget {
final Attachment item;
final Function(Attachment item) onUpdate;
const AttachmentAttrEditorDialog({
super.key,
required this.item,
required this.onUpdate,
});
@override
State<AttachmentAttrEditorDialog> createState() => _AttachmentAttrEditorDialogState();
}
class _AttachmentAttrEditorDialogState extends State<AttachmentAttrEditorDialog> {
final _altController = TextEditingController();
bool _isBusy = false;
bool _isMature = false;
Future<Attachment?> _updateAttachment() async {
final AttachmentProvider provider = Get.find();
setState(() => _isBusy = true);
try {
final resp = await provider.updateAttachment(
widget.item.id,
_altController.value.text,
widget.item.usage,
isMature: _isMature,
);
Get.find<AttachmentProvider>().clearCache(id: widget.item.id);
setState(() => _isBusy = false);
return Attachment.fromJson(resp.body);
} catch (e) {
context.showErrorDialog(e);
setState(() => _isBusy = false);
return null;
}
}
void syncWidget() {
_isMature = widget.item.isMature;
_altController.text = widget.item.alt;
}
@override
void initState() {
syncWidget();
super.initState();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('attachmentSetting'.tr),
content: Container(
constraints: const BoxConstraints(minWidth: 400),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_isBusy)
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: const LinearProgressIndicator().animate().scaleX(),
),
const SizedBox(height: 18),
TextField(
controller: _altController,
decoration: InputDecoration(
isDense: true,
prefixIcon: const Icon(Icons.image_not_supported),
border: const OutlineInputBorder(),
labelText: 'attachmentAlt'.tr,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 8),
CheckboxListTile(
contentPadding: const EdgeInsets.only(left: 4, right: 18),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))),
title: Text('matureContent'.tr),
secondary: const Icon(Icons.visibility_off),
value: _isMature,
onChanged: (newValue) {
setState(() => _isMature = newValue ?? false);
},
controlAffinity: ListTileControlAffinity.leading,
),
],
),
),
actionsAlignment: MainAxisAlignment.spaceBetween,
actions: <Widget>[
Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onSurfaceVariant),
onPressed: () => Navigator.pop(context),
child: Text('cancel'.tr),
),
TextButton(
child: Text('apply'.tr),
onPressed: () {
_updateAttachment().then((value) {
if (value != null) {
widget.onUpdate(value);
Navigator.pop(context);
}
});
},
),
],
),
],
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,296 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:dio/dio.dart';
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:gal/gal.dart';
import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/platform.dart';
import 'package:solian/services.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/attachments/attachment_item.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:path/path.dart' show extension;
class AttachmentFullScreen extends StatefulWidget {
final String parentId;
final Attachment item;
const AttachmentFullScreen(
{super.key, required this.parentId, required this.item});
@override
State<AttachmentFullScreen> createState() => _AttachmentFullScreenState();
}
class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
bool _showDetails = true;
bool _isDownloading = false;
bool _isCompletedDownload = false;
double? _progressOfDownload = 0;
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
String _formatBytes(int bytes, {int decimals = 2}) {
if (bytes == 0) return '0 Bytes';
const k = 1024;
final dm = decimals < 0 ? 0 : decimals;
final sizes = [
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB'
];
final i = (math.log(bytes) / math.log(k)).floor().toInt();
return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
}
double _getRatio() {
final value = widget.item.metadata?['ratio'];
if (value == null) return 1;
if (value is int) return value.toDouble();
if (value is double) return value;
return 1;
}
Future<void> _saveToAlbum() async {
final url = ServiceFinder.buildUrl(
'files',
'/attachments/${widget.item.id}',
);
if (PlatformInfo.isWeb) {
await launchUrlString(url);
return;
}
if (!await Gal.hasAccess(toAlbum: true)) {
if (!await Gal.requestAccess(toAlbum: true)) return;
}
setState(() => _isDownloading = true);
var extName = extension(widget.item.name);
if (extName.isEmpty) extName = '.png';
final imagePath =
'${Directory.systemTemp.path}/${widget.item.uuid}$extName';
await Dio().download(
url,
imagePath,
onReceiveProgress: (count, total) {
setState(() => _progressOfDownload = count / total);
},
);
bool isSuccess = false;
try {
await Gal.putImage(imagePath);
isSuccess = true;
} on GalException catch (e) {
context.showErrorDialog(e.type.message);
}
context.showSnackbar(
'attachmentSaved'.tr,
action: SnackBarAction(
label: 'openInAlbum'.tr,
onPressed: () async => Gal.open(),
),
);
setState(() {
_isDownloading = false;
_isCompletedDownload = isSuccess;
});
}
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final metaTextStyle = TextStyle(
fontSize: 12,
color: _unFocusColor,
);
return DismissiblePage(
key: Key('attachment-dismissible${widget.item.id}'),
direction: DismissiblePageDismissDirection.vertical,
onDismissed: () => Navigator.pop(context),
dismissThresholds: const {
DismissiblePageDismissDirection.vertical: 0.0,
},
onDragStart: () {
setState(() => _showDetails = false);
},
onDragEnd: () {
setState(() => _showDetails = true);
},
child: GestureDetector(
child: Stack(
fit: StackFit.loose,
children: [
SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: InteractiveViewer(
boundaryMargin: EdgeInsets.zero,
minScale: 1,
maxScale: 16,
panEnabled: true,
scaleEnabled: true,
child: AttachmentItem(
parentId: widget.parentId,
showHideButton: false,
item: widget.item,
fit: BoxFit.contain,
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: IgnorePointer(
child: Container(
height: 300,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surface.withOpacity(0),
],
),
),
),
),
)
.animate(target: _showDetails ? 1 : 0)
.fadeIn(curve: Curves.fastEaseInToSlowEaseOut),
Positioned(
bottom: math.max(MediaQuery.of(context).padding.bottom, 16),
left: 16,
right: 16,
child: Material(
color: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.item.account != null)
Row(
children: [
IgnorePointer(
child: AccountAvatar(
content: widget.item.account!.avatar,
radius: 19,
),
),
const IgnorePointer(child: SizedBox(width: 8)),
Expanded(
child: IgnorePointer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'attachmentUploadBy'.tr,
style:
Theme.of(context).textTheme.bodySmall,
),
Text(
widget.item.account!.nick,
style:
Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
IconButton(
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -2,
),
icon: !_isDownloading
? !_isCompletedDownload
? const Icon(Icons.save_alt)
: const Icon(Icons.download_done)
: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
value: _progressOfDownload,
strokeWidth: 3,
),
),
onPressed:
_isDownloading ? null : () => _saveToAlbum(),
),
],
),
const IgnorePointer(child: SizedBox(height: 4)),
IgnorePointer(
child: Text(
widget.item.alt,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
),
const IgnorePointer(child: SizedBox(height: 2)),
IgnorePointer(
child: Wrap(
spacing: 6,
children: [
if (widget.item.metadata?['width'] != null &&
widget.item.metadata?['height'] != null)
Text(
'${widget.item.metadata?['width']}x${widget.item.metadata?['height']}',
style: metaTextStyle,
),
if (widget.item.metadata?['ratio'] != null)
Text(
'${_getRatio().toPrecision(2)}',
style: metaTextStyle,
),
Text(
_formatBytes(widget.item.size),
style: metaTextStyle,
),
Text(
widget.item.mimetype,
style: metaTextStyle,
),
],
),
),
],
),
),
)
.animate(target: _showDetails ? 1 : 0)
.fadeIn(curve: Curves.fastEaseInToSlowEaseOut),
],
),
onTap: () {
setState(() => _showDetails = !_showDetails);
},
),
);
}
}

View File

@@ -9,7 +9,7 @@ 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_list_fullscreen.dart'; import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
class AttachmentList extends StatefulWidget { class AttachmentList extends StatefulWidget {
final String parentId; final String parentId;
@@ -45,7 +45,7 @@ class _AttachmentListState extends State<AttachmentList> {
List<Attachment?> _attachmentsMeta = List.empty(); List<Attachment?> _attachmentsMeta = List.empty();
void _getMetadataList() { void _getMetadataList() {
final AttachmentProvider provider = Get.find(); final AttachmentProvider attach = Get.find();
if (widget.attachmentsId.isEmpty) { if (widget.attachmentsId.isEmpty) {
return; return;
@@ -53,25 +53,16 @@ class _AttachmentListState extends State<AttachmentList> {
_attachmentsMeta = List.filled(widget.attachmentsId.length, null); _attachmentsMeta = List.filled(widget.attachmentsId.length, null);
} }
int progress = 0; attach.listMetadata(widget.attachmentsId).then((result) {
for (var idx = 0; idx < widget.attachmentsId.length; idx++) { setState(() {
provider.getMetadata(widget.attachmentsId[idx]).then((resp) { _attachmentsMeta = result;
progress++; _isLoading = false;
if (resp != null) {
_attachmentsMeta[idx] = resp;
}
if (progress == widget.attachmentsId.length) {
calculateAspectRatio();
if (mounted) {
setState(() => _isLoading = false);
}
}
}); });
} _calculateAspectRatio();
});
} }
void calculateAspectRatio() { void _calculateAspectRatio() {
bool isConsistent = true; bool isConsistent = true;
double? consistentValue; double? consistentValue;
int portrait = 0, square = 0, landscape = 0; int portrait = 0, square = 0, landscape = 0;
@@ -320,9 +311,9 @@ class AttachmentListEntry extends StatelessWidget {
onReveal(true); onReveal(true);
} else if (['image'].contains(item!.mimetype.split('/').first)) { } else if (['image'].contains(item!.mimetype.split('/').first)) {
context.pushTransparentRoute( context.pushTransparentRoute(
AttachmentListFullScreen( AttachmentFullScreen(
parentId: parentId, parentId: parentId,
attachment: item!, item: item!,
), ),
rootNavigator: true, rootNavigator: true,
); );

View File

@@ -1,197 +0,0 @@
import 'dart:math' as math;
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:get/get.dart';
import 'package:solian/models/attachment.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/attachments/attachment_item.dart';
class AttachmentListFullScreen extends StatefulWidget {
final String parentId;
final Attachment attachment;
const AttachmentListFullScreen(
{super.key, required this.parentId, required this.attachment});
@override
State<AttachmentListFullScreen> createState() =>
_AttachmentListFullScreenState();
}
class _AttachmentListFullScreenState extends State<AttachmentListFullScreen> {
bool _showDetails = true;
Color get _unFocusColor =>
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
String _formatBytes(int bytes, {int decimals = 2}) {
if (bytes == 0) return '0 Bytes';
const k = 1024;
final dm = decimals < 0 ? 0 : decimals;
final sizes = [
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB'
];
final i = (math.log(bytes) / math.log(k)).floor().toInt();
return '${(bytes / math.pow(k, i)).toStringAsFixed(dm)} ${sizes[i]}';
}
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return DismissiblePage(
key: Key('attachment-dismissible${widget.attachment.id}'),
direction: DismissiblePageDismissDirection.vertical,
onDismissed: () => Navigator.pop(context),
dismissThresholds: const {
DismissiblePageDismissDirection.vertical: 0.0,
},
onDragStart: () {
setState(() => _showDetails = false);
},
onDragEnd: () {
setState(() => _showDetails = true);
},
child: GestureDetector(
child: Stack(
fit: StackFit.loose,
children: [
SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: InteractiveViewer(
boundaryMargin: EdgeInsets.zero,
minScale: 1,
maxScale: 16,
panEnabled: true,
scaleEnabled: true,
child: AttachmentItem(
parentId: widget.parentId,
showHideButton: false,
item: widget.attachment,
fit: BoxFit.contain,
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: IgnorePointer(
child: Container(
height: 300,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context).colorScheme.surface,
Theme.of(context).colorScheme.surface.withOpacity(0),
],
),
),
),
),
)
.animate(target: _showDetails ? 1 : 0)
.fadeIn(curve: Curves.fastEaseInToSlowEaseOut),
Positioned(
bottom: math.max(MediaQuery.of(context).padding.bottom, 16),
left: 16,
right: 16,
child: IgnorePointer(
child: Material(
color: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.attachment.account != null)
Row(
children: [
AccountAvatar(
content: widget.attachment.account!.avatar,
radius: 19,
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'attachmentUploadBy'.tr,
style: Theme.of(context).textTheme.bodySmall,
),
Text(
widget.attachment.account!.nick,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
],
),
const SizedBox(height: 4),
Text(
widget.attachment.alt,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Wrap(
spacing: 6,
children: [
if (widget.attachment.metadata?['width'] != null &&
widget.attachment.metadata?['height'] != null)
Text(
'${widget.attachment.metadata?['width']}x${widget.attachment.metadata?['height']}',
style: TextStyle(
fontSize: 12,
color: _unFocusColor,
),
),
if (widget.attachment.metadata?['ratio'] != null)
Text(
'${(widget.attachment.metadata?['ratio'] as double).toPrecision(2)}',
style: TextStyle(
fontSize: 12,
color: _unFocusColor,
),
),
Text(
_formatBytes(widget.attachment.size),
style: TextStyle(
fontSize: 12,
color: _unFocusColor,
),
)
],
),
],
),
),
),
)
.animate(target: _showDetails ? 1 : 0)
.fadeIn(curve: Curves.fastEaseInToSlowEaseOut),
],
),
onTap: () {
setState(() => _showDetails = !_showDetails);
},
),
);
}
}

View File

@@ -0,0 +1,89 @@
import 'dart:convert';
import 'package:avatar_stack/avatar_stack.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/models/account.dart';
import 'package:solian/models/call.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/call.dart';
import 'package:solian/widgets/chat/call/call_prejoin.dart';
class ChannelCallIndicator extends StatelessWidget {
final Channel channel;
final Call ongoingCall;
const ChannelCallIndicator(
{super.key, required this.channel, required this.ongoingCall});
void _showCallPrejoin(BuildContext context) {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => ChatCallPrejoinPopup(
ongoingCall: ongoingCall,
channel: channel,
),
);
}
@override
Widget build(BuildContext context) {
final ChatCallProvider call = Get.find();
return MaterialBanner(
padding: const EdgeInsets.only(left: 16, top: 4, bottom: 4),
leading: const Icon(Icons.call_received),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
dividerColor: Colors.transparent,
content: Row(
children: [
if (ongoingCall.participants.isEmpty) Text('callOngoingEmpty'.tr),
if (ongoingCall.participants.isNotEmpty)
Text('callOngoingParticipants'.trParams({
'count': ongoingCall.participants.length.toString(),
})),
const SizedBox(width: 6),
if (ongoingCall.participants.isNotEmpty)
Container(
height: 28,
constraints: const BoxConstraints(maxWidth: 120),
child: AvatarStack(
height: 28,
borderWidth: 0,
avatars: ongoingCall.participants.map((x) {
final userinfo = Account.fromJson(jsonDecode(x['metadata']));
return PlatformInfo.canCacheImage
? CachedNetworkImageProvider(userinfo.avatar)
as ImageProvider
: NetworkImage(userinfo.avatar) as ImageProvider;
}).toList(),
),
),
],
),
actions: [
Obx(() {
if (call.current.value == null) {
return TextButton(
onPressed: () => _showCallPrejoin(context),
child: Text('callJoin'.tr),
);
} else if (call.channel.value?.id == channel.id) {
return TextButton(
onPressed: () => call.gotoScreen(context),
child: Text('callResume'.tr),
);
} else {
return TextButton(
onPressed: null,
child: Text('callJoin'.tr),
);
}
})
],
);
}
}

View File

@@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.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/controllers/chat_events_controller.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
import 'package:solian/platform.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
@@ -31,7 +33,9 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
final List<Channel> _globalChannels = List.empty(growable: true); final List<Channel> _globalChannels = List.empty(growable: true);
final Map<String, List<Channel>> _inRealms = {}; final Map<String, List<Channel>> _inRealms = {};
void mapChannels() { final ChatEventController _eventController = ChatEventController();
void _mapChannels() {
_inRealms.clear(); _inRealms.clear();
_globalChannels.clear(); _globalChannels.clear();
@@ -55,16 +59,17 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
@override @override
void didUpdateWidget(covariant ChannelListWidget oldWidget) { void didUpdateWidget(covariant ChannelListWidget oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
setState(() => mapChannels()); setState(() => _mapChannels());
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
mapChannels(); _mapChannels();
_eventController.initialize();
} }
void gotoChannel(Channel item) { void _gotoChannel(Channel item) {
if (widget.useReplace) { if (widget.useReplace) {
AppRouter.instance.pushReplacementNamed( AppRouter.instance.pushReplacementNamed(
'channelChat', 'channelChat',
@@ -88,7 +93,35 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
} }
} }
Widget buildItem(Channel item) { Widget _buildDirectMessageDescription(Channel item, ChannelMember otherside) {
if (PlatformInfo.isWeb) {
return Text('channelDirectDescription'.trParams(
{'username': '@${otherside.account.name}'},
));
}
return FutureBuilder(
future: Future.delayed(
const Duration(milliseconds: 500),
() => _eventController.database.localEvents.findLastByChannel(item.id),
),
builder: (context, snapshot) {
if (!snapshot.hasData && snapshot.data == null) {
return Text('channelDirectDescription'.trParams(
{'username': '@${otherside.account.name}'},
));
}
return Text(
'${snapshot.data!.data.sender.account.nick}: ${snapshot.data!.data.body['text'] ?? 'Unsupported message to preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
},
);
}
Widget _buildEntry(Channel item) {
final padding = widget.isDense final padding = widget.isDense
? const EdgeInsets.symmetric(horizontal: 20) ? const EdgeInsets.symmetric(horizontal: 20)
: const EdgeInsets.symmetric(horizontal: 16); : const EdgeInsets.symmetric(horizontal: 16);
@@ -102,37 +135,36 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
leading: AccountAvatar( leading: AccountAvatar(
content: otherside.account.avatar, content: otherside.account.avatar,
radius: widget.isDense ? 12 : 20, radius: widget.isDense ? 12 : 20,
bgColor: Colors.indigo, bgColor: Theme.of(context).colorScheme.primary,
feColor: Colors.white, feColor: Theme.of(context).colorScheme.onPrimary,
), ),
contentPadding: padding, contentPadding: padding,
title: Text(otherside.account.nick), title: Text(otherside.account.nick),
subtitle: !widget.isDense subtitle: !widget.isDense
? Text( ? _buildDirectMessageDescription(item, otherside)
'channelDirectDescription'.trParams(
{'username': '@${otherside.account.name}'},
),
)
: null, : null,
onTap: () => gotoChannel(item), onTap: () => _gotoChannel(item),
); );
} else { } else {
return ListTile( return ListTile(
minTileHeight: widget.isDense ? 48 : null, minTileHeight: widget.isDense ? 48 : null,
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: backgroundColor: item.realmId == null
item.realmId == null ? Colors.indigo : Colors.transparent, ? Theme.of(context).colorScheme.primary
: Colors.transparent,
radius: widget.isDense ? 12 : 20, radius: widget.isDense ? 12 : 20,
child: FaIcon( child: FaIcon(
FontAwesomeIcons.hashtag, FontAwesomeIcons.hashtag,
color: item.realmId == null ? Colors.white : Colors.indigo, color: item.realmId == null
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.primary,
size: widget.isDense ? 12 : 16, size: widget.isDense ? 12 : 16,
), ),
), ),
contentPadding: padding, contentPadding: padding,
title: Text(item.name), title: Text(item.name),
subtitle: !widget.isDense ? Text(item.description) : null, subtitle: !widget.isDense ? Text(item.description) : null,
onTap: () => gotoChannel(item), onTap: () => _gotoChannel(item),
); );
} }
} }
@@ -146,7 +178,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
itemCount: _globalChannels.length, itemCount: _globalChannels.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final element = _globalChannels[index]; final element = _globalChannels[index];
return buildItem(element); return _buildEntry(element);
}, },
), ),
], ],
@@ -159,13 +191,13 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
itemCount: _globalChannels.length, itemCount: _globalChannels.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final element = _globalChannels[index]; final element = _globalChannels[index];
return buildItem(element); return _buildEntry(element);
}, },
), ),
SliverList.list( SliverList.list(
children: _inRealms.entries.map((element) { children: _inRealms.entries.map((element) {
return ExpansionTile( return ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 20), tilePadding: const EdgeInsets.only(left: 20, right: 24),
minTileHeight: 48, minTileHeight: 48,
title: Text(element.value.first.realm!.name), title: Text(element.value.first.realm!.name),
leading: CircleAvatar( leading: CircleAvatar(
@@ -177,7 +209,7 @@ class _ChannelListWidgetState extends State<ChannelListWidget> {
size: widget.isDense ? 12 : 16, size: widget.isDense ? 12 : 16,
), ),
), ),
children: element.value.map((x) => buildItem(x)).toList(), children: element.value.map((x) => _buildEntry(x)).toList(),
); );
}).toList(), }).toList(),
), ),

View File

@@ -23,7 +23,7 @@ class ControlsWidget extends StatefulWidget {
} }
class _ControlsWidgetState extends State<ControlsWidget> { class _ControlsWidgetState extends State<ControlsWidget> {
CameraPosition position = CameraPosition.front; CameraPosition _position = CameraPosition.front;
List<MediaDevice>? _audioInputs; List<MediaDevice>? _audioInputs;
List<MediaDevice>? _audioOutputs; List<MediaDevice>? _audioOutputs;
@@ -36,25 +36,25 @@ class _ControlsWidgetState extends State<ControlsWidget> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
participant.addListener(onChange); _participant.addListener(onChange);
_subscription = Hardware.instance.onDeviceChange.stream _subscription = Hardware.instance.onDeviceChange.stream
.listen((List<MediaDevice> devices) { .listen((List<MediaDevice> devices) {
revertDevices(devices); _revertDevices(devices);
}); });
Hardware.instance.enumerateDevices().then(revertDevices); Hardware.instance.enumerateDevices().then(_revertDevices);
_speakerphoneOn = Hardware.instance.speakerOn ?? false; _speakerphoneOn = Hardware.instance.speakerOn ?? false;
} }
@override @override
void dispose() { void dispose() {
_subscription?.cancel(); _subscription?.cancel();
participant.removeListener(onChange); _participant.removeListener(onChange);
super.dispose(); super.dispose();
} }
LocalParticipant get participant => widget.participant; LocalParticipant get _participant => widget.participant;
void revertDevices(List<MediaDevice> devices) async { void _revertDevices(List<MediaDevice> devices) async {
_audioInputs = devices.where((d) => d.kind == 'audioinput').toList(); _audioInputs = devices.where((d) => d.kind == 'audioinput').toList();
_audioOutputs = devices.where((d) => d.kind == 'audiooutput').toList(); _audioOutputs = devices.where((d) => d.kind == 'audiooutput').toList();
_videoInputs = devices.where((d) => d.kind == 'videoinput').toList(); _videoInputs = devices.where((d) => d.kind == 'videoinput').toList();
@@ -63,7 +63,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
void onChange() => setState(() {}); void onChange() => setState(() {});
bool get isMuted => participant.isMuted; bool get isMuted => _participant.isMuted;
Future<bool?> showDisconnectDialog() { Future<bool?> showDisconnectDialog() {
return showDialog<bool>( return showDialog<bool>(
@@ -85,7 +85,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
); );
} }
void disconnect() async { void _disconnect() async {
if (await showDisconnectDialog() != true) return; if (await showDisconnectDialog() != true) return;
final ChatCallProvider provider = Get.find(); final ChatCallProvider provider = Get.find();
@@ -95,59 +95,59 @@ class _ControlsWidgetState extends State<ControlsWidget> {
} }
} }
void disableAudio() async { void _disableAudio() async {
await participant.setMicrophoneEnabled(false); await _participant.setMicrophoneEnabled(false);
} }
void enableAudio() async { void _enableAudio() async {
await participant.setMicrophoneEnabled(true); await _participant.setMicrophoneEnabled(true);
} }
void disableVideo() async { void _disableVideo() async {
await participant.setCameraEnabled(false); await _participant.setCameraEnabled(false);
} }
void enableVideo() async { void _enableVideo() async {
await participant.setCameraEnabled(true); await _participant.setCameraEnabled(true);
} }
void selectAudioOutput(MediaDevice device) async { void _selectAudioOutput(MediaDevice device) async {
await widget.room.setAudioOutputDevice(device); await widget.room.setAudioOutputDevice(device);
setState(() {}); setState(() {});
} }
void selectAudioInput(MediaDevice device) async { void _selectAudioInput(MediaDevice device) async {
await widget.room.setAudioInputDevice(device); await widget.room.setAudioInputDevice(device);
setState(() {}); setState(() {});
} }
void selectVideoInput(MediaDevice device) async { void _selectVideoInput(MediaDevice device) async {
await widget.room.setVideoInputDevice(device); await widget.room.setVideoInputDevice(device);
setState(() {}); setState(() {});
} }
void setSpeakerphoneOn() { void _setSpeakerphoneOn() {
_speakerphoneOn = !_speakerphoneOn; _speakerphoneOn = !_speakerphoneOn;
Hardware.instance.setSpeakerphoneOn(_speakerphoneOn); Hardware.instance.setSpeakerphoneOn(_speakerphoneOn);
setState(() {}); setState(() {});
} }
void toggleCamera() async { void _toggleCamera() async {
final track = participant.videoTrackPublications.firstOrNull?.track; final track = _participant.videoTrackPublications.firstOrNull?.track;
if (track == null) return; if (track == null) return;
try { try {
final newPosition = position.switched(); final newPosition = _position.switched();
await track.setCameraPosition(newPosition); await track.setCameraPosition(newPosition);
setState(() { setState(() {
position = newPosition; _position = newPosition;
}); });
} catch (error) { } catch (error) {
return; return;
} }
} }
void enableScreenShare() async { void _enableScreenShare() async {
if (lkPlatformIsDesktop()) { if (lkPlatformIsDesktop()) {
try { try {
final source = await showDialog<DesktopCapturerSource>( final source = await showDialog<DesktopCapturerSource>(
@@ -163,7 +163,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
maxFrameRate: 15.0, maxFrameRate: 15.0,
), ),
); );
await participant.publishVideoTrack(track); await _participant.publishVideoTrack(track);
} catch (e) { } catch (e) {
final message = e.toString(); final message = e.toString();
context.showErrorDialog(message); context.showErrorDialog(message);
@@ -177,7 +177,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
maxFrameRate: 30.0, maxFrameRate: 30.0,
), ),
); );
await participant.publishVideoTrack(track); await _participant.publishVideoTrack(track);
return; return;
} }
@@ -188,11 +188,11 @@ class _ControlsWidgetState extends State<ControlsWidget> {
return; return;
} }
await participant.setScreenShareEnabled(true, captureScreenAudio: true); await _participant.setScreenShareEnabled(true, captureScreenAudio: true);
} }
void disableScreenShare() async { void _disableScreenShare() async {
await participant.setScreenShareEnabled(false); await _participant.setScreenShareEnabled(false);
} }
@override @override
@@ -210,12 +210,12 @@ class _ControlsWidgetState extends State<ControlsWidget> {
icon: Transform.flip( icon: Transform.flip(
flipX: true, child: const Icon(Icons.exit_to_app)), flipX: true, child: const Icon(Icons.exit_to_app)),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
onPressed: disconnect, onPressed: _disconnect,
), ),
if (participant.isMicrophoneEnabled()) if (_participant.isMicrophoneEnabled())
if (lkPlatformIs(PlatformType.android)) if (lkPlatformIs(PlatformType.android))
IconButton( IconButton(
onPressed: disableAudio, onPressed: _disableAudio,
icon: const Icon(Icons.mic), icon: const Icon(Icons.mic),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
tooltip: 'callMicrophoneOff'.tr, tooltip: 'callMicrophoneOff'.tr,
@@ -227,7 +227,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
return [ return [
PopupMenuItem<MediaDevice>( PopupMenuItem<MediaDevice>(
value: null, value: null,
onTap: isMuted ? enableAudio : disableAudio, onTap: isMuted ? _enableAudio : _disableAudio,
child: ListTile( child: ListTile(
leading: const Icon(Icons.mic_off), leading: const Icon(Icons.mic_off),
title: Text(isMuted title: Text(isMuted
@@ -246,7 +246,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
: const Icon(Icons.check_box_outline_blank), : const Icon(Icons.check_box_outline_blank),
title: Text(device.label), title: Text(device.label),
), ),
onTap: () => selectAudioInput(device), onTap: () => _selectAudioInput(device),
); );
}) })
]; ];
@@ -254,19 +254,19 @@ class _ControlsWidgetState extends State<ControlsWidget> {
) )
else else
IconButton( IconButton(
onPressed: enableAudio, onPressed: _enableAudio,
icon: const Icon(Icons.mic_off), icon: const Icon(Icons.mic_off),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
tooltip: 'callMicrophoneOn'.tr, tooltip: 'callMicrophoneOn'.tr,
), ),
if (participant.isCameraEnabled()) if (_participant.isCameraEnabled())
PopupMenuButton<MediaDevice>( PopupMenuButton<MediaDevice>(
icon: const Icon(Icons.videocam_sharp), icon: const Icon(Icons.videocam_sharp),
itemBuilder: (BuildContext context) { itemBuilder: (BuildContext context) {
return [ return [
PopupMenuItem<MediaDevice>( PopupMenuItem<MediaDevice>(
value: null, value: null,
onTap: disableVideo, onTap: _disableVideo,
child: ListTile( child: ListTile(
leading: const Icon(Icons.videocam_off), leading: const Icon(Icons.videocam_off),
title: Text('callCameraOff'.tr), title: Text('callCameraOff'.tr),
@@ -283,7 +283,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
: const Icon(Icons.check_box_outline_blank), : const Icon(Icons.check_box_outline_blank),
title: Text(device.label), title: Text(device.label),
), ),
onTap: () => selectVideoInput(device), onTap: () => _selectVideoInput(device),
); );
}) })
]; ];
@@ -291,17 +291,17 @@ class _ControlsWidgetState extends State<ControlsWidget> {
) )
else else
IconButton( IconButton(
onPressed: enableVideo, onPressed: _enableVideo,
icon: const Icon(Icons.videocam_off), icon: const Icon(Icons.videocam_off),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
tooltip: 'callCameraOn'.tr, tooltip: 'callCameraOn'.tr,
), ),
IconButton( IconButton(
icon: Icon(position == CameraPosition.back icon: Icon(_position == CameraPosition.back
? Icons.video_camera_back ? Icons.video_camera_back
: Icons.video_camera_front), : Icons.video_camera_front),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
onPressed: () => toggleCamera(), onPressed: () => _toggleCamera(),
tooltip: 'callVideoFlip'.tr, tooltip: 'callVideoFlip'.tr,
), ),
if (!lkPlatformIs(PlatformType.iOS)) if (!lkPlatformIs(PlatformType.iOS))
@@ -327,7 +327,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
: const Icon(Icons.check_box_outline_blank), : const Icon(Icons.check_box_outline_blank),
title: Text(device.label), title: Text(device.label),
), ),
onTap: () => selectAudioOutput(device), onTap: () => _selectAudioOutput(device),
); );
}) })
]; ];
@@ -336,7 +336,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
if (!kIsWeb && lkPlatformIs(PlatformType.iOS)) if (!kIsWeb && lkPlatformIs(PlatformType.iOS))
IconButton( IconButton(
onPressed: Hardware.instance.canSwitchSpeakerphone onPressed: Hardware.instance.canSwitchSpeakerphone
? setSpeakerphoneOn ? _setSpeakerphoneOn
: null, : null,
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
icon: Icon( icon: Icon(
@@ -344,18 +344,18 @@ class _ControlsWidgetState extends State<ControlsWidget> {
), ),
tooltip: 'callSpeakerphoneToggle'.tr, tooltip: 'callSpeakerphoneToggle'.tr,
), ),
if (participant.isScreenShareEnabled()) if (_participant.isScreenShareEnabled())
IconButton( IconButton(
icon: const Icon(Icons.monitor_outlined), icon: const Icon(Icons.monitor_outlined),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
onPressed: () => disableScreenShare(), onPressed: () => _disableScreenShare(),
tooltip: 'callScreenOff'.tr, tooltip: 'callScreenOff'.tr,
) )
else else
IconButton( IconButton(
icon: const Icon(Icons.monitor), icon: const Icon(Icons.monitor),
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
onPressed: () => enableScreenShare(), onPressed: () => _enableScreenShare(),
tooltip: 'callScreenOn'.tr, tooltip: 'callScreenOn'.tr,
), ),
], ],

View File

@@ -201,7 +201,7 @@ class InteractiveParticipantWidget extends StatelessWidget {
final double? width; final double? width;
final double? height; final double? height;
final Color? color; final Color? color;
final bool isFixed; final bool isFixedAvatar;
final ParticipantTrack participant; final ParticipantTrack participant;
final Function() onTap; final Function() onTap;
@@ -210,35 +210,32 @@ class InteractiveParticipantWidget extends StatelessWidget {
this.width, this.width,
this.height, this.height,
this.color, this.color,
this.isFixed = false, this.isFixedAvatar = false,
required this.participant, required this.participant,
required this.onTap, required this.onTap,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Material( return GestureDetector(
color: Colors.transparent, child: Container(
child: InkWell( width: width,
child: Container( height: height,
width: width, color: color,
height: height, child: ParticipantWidget.widgetFor(participant, isFixed: isFixedAvatar),
color: color,
child: ParticipantWidget.widgetFor(participant, isFixed: isFixed),
),
onTap: () => onTap(),
onLongPress: () {
if (participant.participant is LocalParticipant) return;
showModalBottomSheet(
context: context,
builder: (context) => ParticipantMenu(
participant: participant.participant as RemoteParticipant,
videoTrack: participant.videoTrack,
isScreenShare: participant.isScreenShare,
),
);
},
), ),
onTap: () => onTap(),
onLongPress: () {
if (participant.participant is LocalParticipant) return;
showModalBottomSheet(
context: context,
builder: (context) => ParticipantMenu(
participant: participant.participant as RemoteParticipant,
videoTrack: participant.videoTrack,
isScreenShare: participant.isScreenShare,
),
);
},
); );
} }
} }

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:livekit_client/livekit_client.dart'; import 'package:livekit_client/livekit_client.dart';
class ParticipantInfoWidget extends StatelessWidget { class ParticipantInfoWidget extends StatelessWidget {
@@ -34,38 +35,42 @@ class ParticipantInfoWidget extends StatelessWidget {
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
), ),
const SizedBox(width: 5),
isScreenShare isScreenShare
? const Padding( ? const Icon(
padding: EdgeInsets.only(left: 5), Icons.monitor,
child: Icon( color: Colors.white,
Icons.monitor, size: 16,
color: Colors.white,
size: 16,
),
) )
: Padding( : Icon(
padding: const EdgeInsets.only(left: 5), audioAvailable ? Icons.mic : Icons.mic_off,
child: Icon( color: audioAvailable ? Colors.white : Colors.red,
audioAvailable ? Icons.mic : Icons.mic_off, size: 16,
color: audioAvailable ? Colors.white : Colors.red,
size: 16,
),
), ),
const SizedBox(width: 3),
if (connectionQuality != ConnectionQuality.unknown) if (connectionQuality != ConnectionQuality.unknown)
Padding( Icon(
padding: const EdgeInsets.only(left: 5), {
child: Icon( ConnectionQuality.excellent: Icons.signal_cellular_alt,
connectionQuality == ConnectionQuality.poor ConnectionQuality.good: Icons.signal_cellular_alt_2_bar,
? Icons.wifi_off_outlined ConnectionQuality.poor: Icons.signal_cellular_alt_1_bar,
: Icons.wifi, }[connectionQuality],
color: { color: {
ConnectionQuality.excellent: Colors.green, ConnectionQuality.excellent: Colors.green,
ConnectionQuality.good: Colors.orange, ConnectionQuality.good: Colors.orange,
ConnectionQuality.poor: Colors.red, ConnectionQuality.poor: Colors.red,
}[connectionQuality], }[connectionQuality],
size: 16, size: 16,
)
else
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
), ),
), ).paddingAll(3),
], ],
), ),
); );

View File

@@ -9,19 +9,21 @@ class ChatCallCurrentIndicator extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ChatCallProvider provider = Get.find(); final ChatCallProvider provider = Get.find();
if (provider.current.value == null || provider.channel.value == null) { return Obx(() {
return const SizedBox(); if (provider.current.value == null || provider.channel.value == null) {
} return const SizedBox();
}
return ListTile( return ListTile(
tileColor: Theme.of(context).colorScheme.surfaceContainerHigh, tileColor: Theme.of(context).colorScheme.surfaceContainerHigh,
contentPadding: const EdgeInsets.symmetric(horizontal: 32), contentPadding: const EdgeInsets.symmetric(horizontal: 32),
leading: const Icon(Icons.call), leading: const Icon(Icons.call),
title: Text(provider.channel.value!.name), title: Text(provider.channel.value!.name),
subtitle: Text('callAlreadyOngoing'.tr), subtitle: Text('callAlreadyOngoing'.tr),
onTap: () { onTap: () {
provider.gotoScreen(context); provider.gotoScreen(context);
}, },
); );
});
} }
} }

View File

@@ -37,13 +37,33 @@ class ChatEvent extends StatelessWidget {
return '$negativeSign${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds'; return '$negativeSign${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds';
} }
Widget _buildAttachment(BuildContext context) { 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<int>.from(item.body['attachments'].map((x) => x))
: List<int>.empty(); : List<int>.empty();
if (attachments.isEmpty) return const SizedBox(); if (attachments.isEmpty) return const SizedBox();
if (isMinimal) {
final unFocusColor =
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
return Row(
children: [
Icon(
Icons.attachment,
size: 18,
color: unFocusColor,
).paddingOnly(right: 6),
Text(
'attachmentHint'.trParams(
{'count': attachments.length.toString()},
),
style: TextStyle(color: unFocusColor),
)
],
);
}
return Container( return Container(
key: Key('m${item.uuid}attachments-box'), key: Key('m${item.uuid}attachments-box'),
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
@@ -92,7 +112,9 @@ class ChatEvent extends StatelessWidget {
case 'messages.edit': case 'messages.edit':
return ChatEventMessageActionLog( return ChatEventMessageActionLog(
icon: const Icon(Icons.edit_note, size: 16), icon: const Icon(Icons.edit_note, size: 16),
text: 'messageEditDesc'.trParams({'id': '#${item.id}'}), text: 'messageEditDesc'.trParams({
'id': '#${item.body['related_event']}',
}),
isMerged: isMerged, isMerged: isMerged,
isHasMerged: isHasMerged, isHasMerged: isHasMerged,
isQuote: isQuote, isQuote: isQuote,
@@ -100,7 +122,9 @@ class ChatEvent extends StatelessWidget {
case 'messages.delete': case 'messages.delete':
return ChatEventMessageActionLog( return ChatEventMessageActionLog(
icon: const Icon(Icons.cancel_schedule_send, size: 16), icon: const Icon(Icons.cancel_schedule_send, size: 16),
text: 'messageDeleteDesc'.trParams({'id': '#${item.id}'}), text: 'messageDeleteDesc'.trParams({
'id': '#${item.body['related_event']}',
}),
isMerged: isMerged, isMerged: isMerged,
isHasMerged: isHasMerged, isHasMerged: isHasMerged,
isQuote: isQuote, isQuote: isQuote,
@@ -164,7 +188,8 @@ class ChatEvent extends StatelessWidget {
), ),
], ],
).paddingOnly(right: 12), ).paddingOnly(right: 12),
_buildAttachment(context), _buildAttachment(context, isMinimal: isContentPreviewing)
.paddingOnly(left: isContentPreviewing ? 12 : 0),
], ],
); );
} else if (isQuote) { } else if (isQuote) {

View File

@@ -1,4 +1,5 @@
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:solian/controllers/chat_events_controller.dart'; import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
@@ -23,7 +24,7 @@ class ChatEventList extends StatelessWidget {
required this.onReply, required this.onReply,
}); });
bool checkMessageMergeable(Event? a, Event? b) { bool _checkMessageMergeable(Event? a, Event? b) {
if (a == null || b == null) return false; if (a == null || b == null) return false;
if (a.sender.account.id != b.sender.account.id) return false; if (a.sender.account.id != b.sender.account.id) return false;
return a.createdAt.difference(b.createdAt).inMinutes <= 3; return a.createdAt.difference(b.createdAt).inMinutes <= 3;
@@ -41,13 +42,13 @@ class ChatEventList extends StatelessWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
bool isMerged = false, hasMerged = false; bool isMerged = false, hasMerged = false;
if (index > 0) { if (index > 0) {
hasMerged = checkMessageMergeable( hasMerged = _checkMessageMergeable(
chatController.currentEvents[index - 1].data, chatController.currentEvents[index - 1].data,
chatController.currentEvents[index].data, chatController.currentEvents[index].data,
); );
} }
if (index + 1 < chatController.currentEvents.length) { if (index + 1 < chatController.currentEvents.length) {
isMerged = checkMessageMergeable( isMerged = _checkMessageMergeable(
chatController.currentEvents[index].data, chatController.currentEvents[index].data,
chatController.currentEvents[index + 1].data, chatController.currentEvents[index + 1].data,
); );
@@ -82,7 +83,12 @@ class ChatEventList extends StatelessWidget {
), ),
); );
}, },
); ).animate(key: Key('m-animation${item.uuid}')).slideY(
duration: 250.ms,
curve: Curves.fastEaseInToSlowEaseOut,
end: 0,
begin: 0.5,
);
}, },
); );
}), }),

View File

@@ -24,7 +24,8 @@ class ChatEventMessage extends StatelessWidget {
final hasAttachment = body.attachments?.isNotEmpty ?? false; final hasAttachment = body.attachments?.isNotEmpty ?? false;
if (body.text.isEmpty && hasAttachment) { if (body.text.isEmpty && hasAttachment) {
final unFocusColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.75); final unFocusColor =
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
return Row( return Row(
children: [ children: [
Icon( Icon(
@@ -33,7 +34,7 @@ class ChatEventMessage extends StatelessWidget {
color: unFocusColor, color: unFocusColor,
).paddingOnly(right: 6), ).paddingOnly(right: 6),
Text( Text(
'postAttachmentTip'.trParams( 'attachmentHint'.trParams(
{'count': body.attachments?.length.toString() ?? 0.toString()}, {'count': body.attachments?.length.toString() ?? 0.toString()},
), ),
style: TextStyle(color: unFocusColor), style: TextStyle(color: unFocusColor),
@@ -46,9 +47,7 @@ class ChatEventMessage extends StatelessWidget {
} }
Widget _buildBody(BuildContext context) { Widget _buildBody(BuildContext context) {
if (isContentPreviewing) { if (isMerged) {
return _buildContent(context);
} else if (isMerged) {
return _buildContent(context).paddingOnly(left: 52); return _buildContent(context).paddingOnly(left: 52);
} else { } else {
return _buildContent(context); return _buildContent(context);
@@ -64,7 +63,7 @@ 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 ? 4 : (isHasMerged ? 2 : 0), bottom: hasAttachment && !isContentPreviewing ? 4 : (isHasMerged ? 2 : 0),
); );
} }
} }

View File

@@ -5,9 +5,11 @@ 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/providers/attachment_uploader.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.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:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ChatMessageInput extends StatefulWidget { class ChatMessageInput extends StatefulWidget {
@@ -38,47 +40,92 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
final TextEditingController _textController = TextEditingController(); final TextEditingController _textController = TextEditingController();
final FocusNode _focusNode = FocusNode(); final FocusNode _focusNode = FocusNode();
List<int> _attachments = List.empty(growable: true); final List<int> _attachments = List.empty(growable: true);
Event? _editTo; Event? _editTo;
Event? _replyTo; Event? _replyTo;
void showAttachments() { void _editAttachments() {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true,
builder: (context) => AttachmentEditorPopup( builder: (context) => AttachmentEditorPopup(
usage: 'm.attachment', usage: 'm.attachment',
current: _attachments, initialAttachments: _attachments,
onUpdate: (value) => _attachments = value, onAdd: (value) {
setState(() {
_attachments.add(value);
});
},
onRemove: (value) {
setState(() {
_attachments.remove(value);
});
},
), ),
); );
} }
Future<void> sendMessage() async { List<String> _findMentionedUsers(String text) {
RegExp regExp = RegExp(r'@[a-zA-Z0-9_]+');
Iterable<RegExpMatch> matches = regExp.allMatches(text);
List<String> mentionedUsers =
matches.map((match) => match.group(0)!.substring(1)).toList();
return mentionedUsers;
}
Future<void> _sendMessage() async {
_focusNode.requestFocus(); _focusNode.requestFocus();
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
final prof = auth.userProfile.value!; final prof = auth.userProfile.value!;
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;
final client = auth.configureClient('messaging'); final AttachmentUploaderController uploader = Get.find();
if (uploader.queueOfUpload.any(
((x) => x.usage == 'm.attachment' && x.isUploading),
)) {
context.showErrorDialog('attachmentUploadInProgress'.tr);
return;
}
// TODO Deal with the @ ping (query uid with username), and then add into related_user and replace the @ with internal link in body Response resp;
final mentionedUserNames = _findMentionedUsers(_textController.text);
final mentionedUserIds = List<int>.empty(growable: true);
var client = auth.configureClient('auth');
if (mentionedUserNames.isNotEmpty) {
resp = await client.get('/users?name=${mentionedUserNames.join(',')}');
if (resp.statusCode != 200) {
context.showErrorDialog(resp.bodyString);
return;
} else {
mentionedUserIds.addAll(
resp.body.map((x) => Account.fromJson(x).id).toList().cast<int>(),
);
}
}
client = auth.configureClient('messaging');
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.value.text, 'text': _textController.text,
'algorithm': 'plain', 'algorithm': 'plain',
'attachments': List.from(_attachments), 'attachments': List.from(_attachments),
'related_users': [ 'related_users': [
if (_replyTo != null) _replyTo!.sender.accountId, if (_replyTo != null) _replyTo!.sender.accountId,
...mentionedUserIds,
], ],
if (_replyTo != null) 'quote_event': _replyTo!.id, if (_replyTo != null) 'quote_event': _replyTo!.id,
if (_editTo != null) 'related_event': _editTo!.id, if (_editTo != null) 'related_event': _editTo!.id,
if (_editTo != null && _editTo!.body['quote_event'] != null)
'quote_event': _editTo!.body['quote_event'],
} }
}; };
@@ -111,7 +158,6 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
resetInput(); resetInput();
Response resp;
if (_editTo != null) { if (_editTo != null) {
resp = await client.put( resp = await client.put(
'/channels/${widget.realm}/${widget.channel.alias}/messages/${_editTo!.id}', '/channels/${widget.realm}/${widget.channel.alias}/messages/${_editTo!.id}',
@@ -217,20 +263,31 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
{'channel': '#${widget.channel.alias}'}, {'channel': '#${widget.channel.alias}'},
), ),
), ),
onSubmitted: (_) => sendMessage(), onSubmitted: (_) => _sendMessage(),
onTapOutside: (_) => onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(), FocusManager.instance.primaryFocus?.unfocus(),
), ),
), ),
IconButton( IconButton(
icon: const Icon(Icons.attach_file), icon: badges.Badge(
badgeContent: Text(
_attachments.length.toString(),
style: const TextStyle(color: Colors.white),
),
showBadge: _attachments.isNotEmpty,
position: badges.BadgePosition.topEnd(
top: -12,
end: -8,
),
child: const Icon(Icons.file_present_rounded),
),
color: Colors.teal, color: Colors.teal,
onPressed: () => showAttachments(), onPressed: () => _editAttachments(),
), ),
IconButton( IconButton(
icon: const Icon(Icons.send), icon: const Icon(Icons.send),
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
onPressed: () => sendMessage(), onPressed: () => _sendMessage(),
) )
], ],
).paddingOnly(left: 20, right: 16), ).paddingOnly(left: 20, right: 16),

View File

@@ -19,7 +19,9 @@ class MarkdownTextContent extends StatelessWidget {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
data: content, data: content,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith( styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context),
).copyWith(
horizontalRuleDecoration: BoxDecoration( horizontalRuleDecoration: BoxDecoration(
border: Border( border: Border(
top: BorderSide( top: BorderSide(

View File

@@ -4,6 +4,7 @@ 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/content/channel.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';
@@ -29,7 +30,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
late final ChannelProvider _channels; late final ChannelProvider _channels;
void getStatus() async { Future<void> _getStatus() async {
final StatusProvider provider = Get.find(); final StatusProvider provider = Get.find();
final resp = await provider.getCurrentStatus(); final resp = await provider.getCurrentStatus();
@@ -40,7 +41,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
}); });
} }
void detectSelectedIndex() { void _detectSelectedIndex() {
if (widget.routeName == null) return; if (widget.routeName == null) return;
final nameList = AppNavigation.destinations.map((x) => x.page).toList(); final nameList = AppNavigation.destinations.map((x) => x.page).toList();
@@ -49,22 +50,32 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
_selectedIndex = idx != -1 ? idx : null; _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(); _channels = Get.find();
detectSelectedIndex(); _detectSelectedIndex();
getStatus(); _getStatus();
} }
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
detectSelectedIndex(); _detectSelectedIndex();
} }
@override @override
@@ -78,7 +89,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
onDestinationSelected: (idx) { onDestinationSelected: (idx) {
setState(() => _selectedIndex = idx); setState(() => _selectedIndex = idx);
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page); AppRouter.instance.goNamed(AppNavigation.destinations[idx].page);
closeDrawer(); _closeDrawer();
}, },
children: [ children: [
Obx(() { Obx(() {
@@ -88,10 +99,11 @@ 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); setState(() => _selectedIndex = null);
closeDrawer(); _closeDrawer();
}, },
); );
} }
@@ -118,43 +130,55 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
); );
}, },
), ),
leading: Builder(builder: (context) { leading: Obx(() {
final badgeColor = _accountStatus != null final statusBadgeColor = _accountStatus != null
? StatusProvider.determineStatus( ? StatusProvider.determineStatus(
_accountStatus!, _accountStatus!,
).$2 ).$2
: Colors.grey; : Colors.grey;
final RelationshipProvider relations = Get.find();
final accountNotifications = relations.friendRequestCount.value;
return badges.Badge( return badges.Badge(
showBadge: _accountStatus != null, badgeContent: Text(
badgeStyle: badges.BadgeStyle(badgeColor: badgeColor), accountNotifications.toString(),
position: badges.BadgePosition.bottomEnd( style: const TextStyle(color: Colors.white),
bottom: 0,
end: -2,
), ),
child: AccountAvatar( showBadge: accountNotifications > 0,
content: auth.userProfile.value!['avatar'], position: badges.BadgePosition.topEnd(
top: -10,
end: -6,
),
child: badges.Badge(
showBadge: _accountStatus != null,
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
position: badges.BadgePosition.bottomEnd(
bottom: 0,
end: -2,
),
child: AccountAvatar(
content: auth.userProfile.value!['avatar'],
),
), ),
); );
}), }),
trailing: IconButton( trailing: _buildSettingButton(),
icon: const Icon(Icons.face_retouching_natural),
onPressed: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => AccountStatusAction(
currentStatus: _accountStatus!.status,
),
).then((val) {
if (val == true) getStatus();
});
},
),
onTap: () { onTap: () {
AppRouter.instance.goNamed('account'); AppRouter.instance.goNamed('account');
setState(() => _selectedIndex = null); setState(() => _selectedIndex = null);
closeDrawer(); _closeDrawer();
},
onLongPress: () {
showModalBottomSheet(
useRootNavigator: true,
context: context,
builder: (context) => AccountStatusAction(
currentStatus: _accountStatus!.status,
),
).then((val) {
if (val == true) _getStatus();
});
}, },
); );
}).paddingOnly(top: 8), }).paddingOnly(top: 8),
@@ -199,7 +223,7 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
useReplace: true, useReplace: true,
onSelected: (_) { onSelected: (_) {
setState(() => _selectedIndex = null); setState(() => _selectedIndex = null);
closeDrawer(); _closeDrawer();
}, },
), ),
), ),

View File

@@ -16,8 +16,7 @@ class PostEditorCategoriesDialog extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TagsField( TagsField(
initialTags: initialTags: controller.tags,
controller.editTo.value?.tags?.map((x) => x.alias).toList(),
hintText: 'postTagsPlaceholder'.tr, hintText: 'postTagsPlaceholder'.tr,
onUpdate: (value) { onUpdate: (value) {
controller.tags.value = value; controller.tags.value = value;

View File

@@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:solian/controllers/post_editor_controller.dart';
class PostEditorDateDialog extends StatefulWidget {
final PostEditorController controller;
const PostEditorDateDialog({super.key, required this.controller});
@override
State<PostEditorDateDialog> createState() => _PostEditorDateDialogState();
}
class _PostEditorDateDialogState extends State<PostEditorDateDialog> {
final TextEditingController _publishedAtController = TextEditingController();
final TextEditingController _publishedUntilController =
TextEditingController();
final _dateFormatter = DateFormat('yyyy-MM-dd HH:mm:ss');
void _selectDate(int mode) async {
final initial = mode == 0
? widget.controller.publishedAt.value
: widget.controller.publishedUntil.value;
final DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: initial?.toLocal(),
firstDate: DateTime(DateTime.now().year),
lastDate: DateTime(DateTime.now().year + 5),
);
if (pickedDate == null) return;
final TimeOfDay? pickedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (pickedTime == null) return;
final picked = pickedDate.copyWith(
hour: pickedTime.hour,
minute: pickedTime.minute,
);
if (mode == 0) {
setState(() {
widget.controller.publishedAt.value = picked;
_publishedAtController.text = _dateFormatter.format(picked);
});
} else {
widget.controller.publishedUntil.value = pickedDate;
_publishedUntilController.text = _dateFormatter.format(picked);
}
}
@override
void initState() {
super.initState();
if (widget.controller.publishedAt.value != null) {
_publishedAtController.text =
_dateFormatter.format(widget.controller.publishedAt.value!);
}
if (widget.controller.publishedUntil.value != null) {
_publishedUntilController.text =
_dateFormatter.format(widget.controller.publishedUntil.value!);
}
}
@override
void dispose() {
super.dispose();
_publishedAtController.dispose();
_publishedUntilController.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('postPublishDate'.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _publishedAtController,
readOnly: true,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'postPublishAt'.tr,
),
onTap: () => _selectDate(0),
),
const SizedBox(height: 16),
TextField(
controller: _publishedUntilController,
readOnly: true,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'postPublishedUntil'.tr,
),
onTap: () => _selectDate(1),
),
],
),
actions: [
TextButton(
onPressed: () {
widget.controller.publishedAt.value = null;
widget.controller.publishedUntil.value = null;
_publishedAtController.clear();
_publishedUntilController.clear();
},
child: Text('clear'.tr),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('confirm'.tr),
),
],
);
}
}

View File

@@ -0,0 +1,70 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:solian/controllers/post_editor_controller.dart';
import 'package:solian/providers/content/realm.dart';
class PostEditorPublishZoneDialog extends StatelessWidget {
final PostEditorController controller;
const PostEditorPublishZoneDialog({super.key, required this.controller});
@override
Widget build(BuildContext context) {
final RealmProvider realms = Get.find();
return AlertDialog(
title: Text('postPublishZone'.tr),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() {
return DropdownButtonFormField2<int>(
isExpanded: true,
decoration: const InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8)),
),
),
items: [
DropdownMenuItem<int>(
value: null,
child: Text(
'postPublishZoneNone'.tr,
style: const TextStyle(fontSize: 14),
),
),
...realms.availableRealms.map(
(x) => DropdownMenuItem<int>(
value: x.id,
child: Text(
'${x.name} (${x.alias})',
style: const TextStyle(fontSize: 14),
),
),
),
],
value: controller.realmZone.value?.id,
onChanged: (int? value) {
if (value == null) {
controller.realmZone.value = null;
} else {
controller.realmZone.value =
realms.availableRealms.firstWhere((x) => x.id == value);
}
},
buttonStyleData: const ButtonStyleData(height: 20),
menuItemStyleData: const MenuItemStyleData(height: 40),
);
}),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('confirm'.tr),
),
],
);
}
}

View File

@@ -1,14 +1,16 @@
import 'package:animations/animations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.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';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/router.dart'; import 'package:solian/screens/posts/post_detail.dart';
import 'package:solian/shells/title_shell.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_profile_popup.dart'; import 'package:solian/widgets/account/account_profile_popup.dart';
import 'package:solian/widgets/attachments/attachment_list.dart'; import 'package:solian/widgets/attachments/attachment_list.dart';
import 'package:solian/widgets/markdown_text_content.dart'; import 'package:solian/widgets/markdown_text_content.dart';
import 'package:solian/widgets/feed/feed_tags.dart'; import 'package:solian/widgets/posts/post_tags.dart';
import 'package:solian/widgets/posts/post_quick_action.dart'; import 'package:solian/widgets/posts/post_quick_action.dart';
import 'package:solian/widgets/sized_container.dart'; import 'package:solian/widgets/sized_container.dart';
import 'package:timeago/timeago.dart' show format; import 'package:timeago/timeago.dart' show format;
@@ -22,7 +24,8 @@ class PostItem extends StatefulWidget {
final bool isShowEmbed; final bool isShowEmbed;
final bool isFullDate; final bool isFullDate;
final bool isContentSelectable; final bool isContentSelectable;
final String? overrideAttachmentParent; final String? attachmentParent;
final Color? backgroundColor;
const PostItem({ const PostItem({
super.key, super.key,
@@ -34,7 +37,8 @@ class PostItem extends StatefulWidget {
this.isShowEmbed = true, this.isShowEmbed = true,
this.isFullDate = false, this.isFullDate = false,
this.isContentSelectable = false, this.isContentSelectable = false,
this.overrideAttachmentParent, this.attachmentParent,
this.backgroundColor,
}); });
@override @override
@@ -129,7 +133,7 @@ class _PostItemState extends State<PostItem> {
List<Widget> widgets = List.empty(growable: true); List<Widget> widgets = List.empty(growable: true);
if (widget.item.tags?.isNotEmpty ?? false) { if (widget.item.tags?.isNotEmpty ?? false) {
widgets.add(FeedTagsList(tags: widget.item.tags!)); widgets.add(PostTagsList(tags: widget.item.tags!));
} }
if (labels.isNotEmpty) { if (labels.isNotEmpty) {
widgets.add(Text( widgets.add(Text(
@@ -159,66 +163,92 @@ class _PostItemState extends State<PostItem> {
} }
Widget _buildReply(BuildContext context) { Widget _buildReply(BuildContext context) {
return Column( return OpenContainer(
children: [ closedBuilder: (_, openContainer) => Column(
Row( children: [
children: [ Row(
FaIcon( children: [
FontAwesomeIcons.reply, FaIcon(
size: 16, FontAwesomeIcons.reply,
color: _unFocusColor, size: 16,
), color: _unFocusColor,
Expanded( ),
child: Text( Expanded(
'postRepliedNotify'.trParams( child: Text(
{'username': '@${widget.item.replyTo!.author.name}'}, 'postRepliedNotify'.trParams(
), {'username': '@${widget.item.replyTo!.author.name}'},
style: TextStyle(color: _unFocusColor), ),
).paddingOnly(left: 6), style: TextStyle(color: _unFocusColor),
), ).paddingOnly(left: 6),
], ),
).paddingOnly(left: 12), ],
Card( ).paddingOnly(left: 12),
elevation: 1, Card(
child: PostItem( elevation: 1,
item: widget.item.replyTo!, child: PostItem(
isCompact: true, item: widget.item.replyTo!,
overrideAttachmentParent: widget.item.id.toString(), isCompact: true,
).paddingSymmetric(vertical: 8), attachmentParent: widget.item.id.toString(),
).paddingSymmetric(vertical: 8),
),
],
),
openBuilder: (_, __) => TitleShell(
title: 'postDetail'.tr,
child: PostDetailScreen(
id: widget.item.replyTo!.id.toString(),
post: widget.item.replyTo!,
), ),
], ),
closedElevation: 0,
openElevation: 0,
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface,
); );
} }
Widget _buildRepost(BuildContext context) { Widget _buildRepost(BuildContext context) {
return Column( return OpenContainer(
children: [ closedBuilder: (_, openContainer) => Column(
Row( children: [
children: [ Row(
FaIcon( children: [
FontAwesomeIcons.retweet, FaIcon(
size: 16, FontAwesomeIcons.retweet,
color: _unFocusColor, size: 16,
), color: _unFocusColor,
Expanded( ),
child: Text( Expanded(
'postRepostedNotify'.trParams( child: Text(
{'username': '@${widget.item.repostTo!.author.name}'}, 'postRepostedNotify'.trParams(
), {'username': '@${widget.item.repostTo!.author.name}'},
style: TextStyle(color: _unFocusColor), ),
).paddingOnly(left: 6), style: TextStyle(color: _unFocusColor),
), ).paddingOnly(left: 6),
], ),
).paddingOnly(left: 12), ],
Card( ).paddingOnly(left: 12),
elevation: 1, Card(
child: PostItem( elevation: 1,
item: widget.item.repostTo!, child: PostItem(
isCompact: true, item: widget.item.repostTo!,
overrideAttachmentParent: widget.item.id.toString(), isCompact: true,
).paddingSymmetric(vertical: 8), attachmentParent: widget.item.id.toString(),
).paddingSymmetric(vertical: 8),
),
],
),
openBuilder: (_, __) => TitleShell(
title: 'postDetail'.tr,
child: PostDetailScreen(
id: widget.item.repostTo!.id.toString(),
post: widget.item.repostTo!,
), ),
], ),
closedElevation: 0,
openElevation: 0,
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface,
); );
} }
@@ -253,7 +283,7 @@ class _PostItemState extends State<PostItem> {
color: _unFocusColor, color: _unFocusColor,
).paddingOnly(right: 6), ).paddingOnly(right: 6),
Text( Text(
'postAttachmentTip'.trParams( 'attachmentHint'.trParams(
{'count': attachments.length.toString()}, {'count': attachments.length.toString()},
), ),
style: TextStyle(color: _unFocusColor), style: TextStyle(color: _unFocusColor),
@@ -264,100 +294,91 @@ class _PostItemState extends State<PostItem> {
); );
} }
return Column( return OpenContainer(
crossAxisAlignment: CrossAxisAlignment.start, closedBuilder: (_, openContainer) => Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Row( children: [
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
GestureDetector( children: [
child: AccountAvatar(content: item.author.avatar.toString()), GestureDetector(
onTap: () { child: AccountAvatar(content: item.author.avatar.toString()),
showModalBottomSheet( onTap: () {
useRootNavigator: true, showModalBottomSheet(
isScrollControlled: true, useRootNavigator: true,
backgroundColor: Theme.of(context).colorScheme.surface, isScrollControlled: true,
context: context, backgroundColor: Theme.of(context).colorScheme.surface,
builder: (context) => AccountProfilePopup( context: context,
account: item.author, builder: (context) => AccountProfilePopup(
), account: item.author,
);
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
SizedContainer(
maxWidth: 640,
child: MarkdownTextContent(
content: item.body['content'],
isSelectable: widget.isContentSelectable,
).paddingOnly(left: 12, right: 8),
),
if (widget.item.replyTo != null && widget.isShowEmbed)
GestureDetector(
child: _buildReply(context).paddingOnly(top: 4),
onTap: () {
if (!widget.isClickable) return;
AppRouter.instance.pushNamed(
'postDetail',
pathParameters: {
'id': widget.item.replyTo!.id.toString(),
},
);
},
), ),
if (widget.item.repostTo != null && widget.isShowEmbed) );
GestureDetector( },
child: _buildRepost(context).paddingOnly(top: 4),
onTap: () {
if (!widget.isClickable) return;
AppRouter.instance.pushNamed(
'postDetail',
pathParameters: {
'alias': widget.item.repostTo!.id.toString(),
},
);
},
),
_buildFooter().paddingOnly(left: 12),
],
), ),
) Expanded(
], child: Column(
).paddingOnly( crossAxisAlignment: CrossAxisAlignment.start,
top: 10, children: [
bottom: hasAttachment ? 10 : 0, _buildHeader(),
right: 16, SizedContainer(
left: 16, maxWidth: 640,
), child: MarkdownTextContent(
AttachmentList( content: item.body['content'],
parentId: widget.item.id.toString(), isSelectable: widget.isContentSelectable,
attachmentsId: attachments, ).paddingOnly(left: 12, right: 8),
isGrid: attachments.length > 1, ),
), if (widget.item.replyTo != null && widget.isShowEmbed)
if (widget.isShowReply && widget.isReactable) _buildReply(context).paddingOnly(top: 4),
PostQuickAction( if (widget.item.repostTo != null && widget.isShowEmbed)
isShowReply: widget.isShowReply, _buildRepost(context).paddingOnly(top: 4),
isReactable: widget.isReactable, _buildFooter().paddingOnly(left: 12),
item: widget.item, ],
onReact: (symbol, changes) { ),
setState(() { ),
item.metric!.reactionList[symbol] = ],
(item.metric!.reactionList[symbol] ?? 0) + changes;
});
},
).paddingOnly( ).paddingOnly(
top: hasAttachment ? 10 : 6, top: 10,
left: hasAttachment ? 24 : 60, bottom: hasAttachment ? 10 : 0,
right: 16, right: 16,
bottom: 10, left: 16,
) ),
else AttachmentList(
const SizedBox(height: 10), parentId: widget.item.id.toString(),
], attachmentsId: attachments,
isGrid: attachments.length > 1,
),
if (widget.isShowReply && widget.isReactable)
PostQuickAction(
isShowReply: widget.isShowReply,
isReactable: widget.isReactable,
item: widget.item,
onReact: (symbol, changes) {
setState(() {
item.metric!.reactionList[symbol] =
(item.metric!.reactionList[symbol] ?? 0) + changes;
});
},
).paddingOnly(
top: hasAttachment ? 10 : 6,
left: hasAttachment ? 24 : 60,
right: 16,
bottom: 10,
)
else
const SizedBox(height: 10),
],
),
openBuilder: (_, __) => TitleShell(
title: 'postDetail'.tr,
child: PostDetailScreen(
id: item.id.toString(),
post: item,
),
),
closedElevation: 0,
openElevation: 0,
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
openColor: Theme.of(context).colorScheme.surface,
); );
} }
} }

View File

@@ -3,8 +3,6 @@ import 'package:get/get.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/router.dart';
import 'package:solian/widgets/sized_container.dart';
import 'package:solian/widgets/posts/post_action.dart'; import 'package:solian/widgets/posts/post_action.dart';
import 'package:solian/widgets/posts/post_item.dart'; import 'package:solian/widgets/posts/post_item.dart';
@@ -13,6 +11,7 @@ class PostListWidget extends StatelessWidget {
final bool isClickable; final bool isClickable;
final bool isNestedClickable; final bool isNestedClickable;
final PagingController<int, Post> controller; final PagingController<int, Post> controller;
final Color? backgroundColor;
const PostListWidget({ const PostListWidget({
super.key, super.key,
@@ -20,6 +19,7 @@ class PostListWidget extends StatelessWidget {
this.isShowEmbed = true, this.isShowEmbed = true,
this.isClickable = true, this.isClickable = true,
this.isNestedClickable = true, this.isNestedClickable = true,
this.backgroundColor,
}); });
@override @override
@@ -29,16 +29,15 @@ class PostListWidget extends StatelessWidget {
pagingController: controller, pagingController: controller,
builderDelegate: PagedChildBuilderDelegate<Post>( builderDelegate: PagedChildBuilderDelegate<Post>(
itemBuilder: (context, item, index) { itemBuilder: (context, item, index) {
return CenteredContainer( return PostListEntryWidget(
child: PostListEntryWidget( isShowEmbed: isShowEmbed,
isShowEmbed: isShowEmbed, isNestedClickable: isNestedClickable,
isNestedClickable: isNestedClickable, isClickable: isClickable,
isClickable: isClickable, item: item,
item: item, backgroundColor: backgroundColor,
onUpdate: () { onUpdate: () {
controller.refresh(); controller.refresh();
}, },
),
); );
}, },
), ),
@@ -48,19 +47,23 @@ class PostListWidget extends StatelessWidget {
} }
class PostListEntryWidget extends StatelessWidget { class PostListEntryWidget extends StatelessWidget {
final int renderOrder;
final bool isShowEmbed; final bool isShowEmbed;
final bool isNestedClickable; final bool isNestedClickable;
final bool isClickable; final bool isClickable;
final Post item; final Post item;
final Function onUpdate; final Function onUpdate;
final Color? backgroundColor;
const PostListEntryWidget({ const PostListEntryWidget({
super.key, super.key,
this.renderOrder = 0,
required this.isShowEmbed, required this.isShowEmbed,
required this.isNestedClickable, required this.isNestedClickable,
required this.isClickable, required this.isClickable,
required this.item, required this.item,
required this.onUpdate, required this.onUpdate,
this.backgroundColor,
}); });
@override @override
@@ -71,14 +74,8 @@ class PostListEntryWidget extends StatelessWidget {
item: item, item: item,
isShowEmbed: isShowEmbed, isShowEmbed: isShowEmbed,
isClickable: isNestedClickable, isClickable: isNestedClickable,
backgroundColor: backgroundColor,
).paddingSymmetric(vertical: 8), ).paddingSymmetric(vertical: 8),
onTap: () {
if (!isClickable) return;
AppRouter.instance.pushNamed(
'postDetail',
pathParameters: {'id': item.id.toString()},
);
},
onLongPress: () { onLongPress: () {
final AuthProvider auth = Get.find(); final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return; if (auth.isAuthorized.isFalse) return;

View File

@@ -85,65 +85,65 @@ class _PostQuickActionState extends State<PostQuickAction> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
const density = VisualDensity(horizontal: -4, vertical: -3); const density = VisualDensity(horizontal: -4, vertical: -3);
final reactionEntries = widget.item.metric!.reactionList.entries.toList();
return SizedBox( return SizedBox(
height: 32, height: 32,
width: double.infinity, width: double.infinity,
child: Row( child: CustomScrollView(
mainAxisAlignment: MainAxisAlignment.start, scrollDirection: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.center, slivers: [
children: [
if (widget.isReactable && widget.isShowReply) if (widget.isReactable && widget.isShowReply)
ActionChip( SliverToBoxAdapter(
avatar: const Icon(Icons.comment), child: ActionChip(
label: Text(widget.item.metric!.replyCount.toString()), avatar: const Icon(Icons.comment),
visualDensity: density, label: Text(widget.item.metric!.replyCount.toString()),
onPressed: () { visualDensity: density,
showModalBottomSheet( onPressed: () {
useRootNavigator: true, showModalBottomSheet(
context: context, useRootNavigator: true,
builder: (context) { context: context,
return PostReplyListPopup(item: widget.item); builder: (context) {
}, return PostReplyListPopup(item: widget.item);
); },
},
),
if (widget.isReactable && widget.isShowReply)
const VerticalDivider(
thickness: 0.3,
width: 0.3,
indent: 8,
endIndent: 8,
).paddingSymmetric(horizontal: 8),
Expanded(
child: ListView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: [
...widget.item.metric!.reactionList.entries.map((x) {
final info = reactions[x.key];
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ActionChip(
avatar: Text(info!.icon),
label: Text(x.value.toString()),
tooltip: ':${x.key}:',
visualDensity: density,
onPressed: _isSubmitting
? null
: () => doWidgetReact(x.key, info.attitude),
),
); );
}), },
if (widget.isReactable) ),
ActionChip( ),
avatar: const Icon(Icons.add_reaction, color: Colors.teal), if (widget.isReactable && widget.isShowReply)
label: Text('reactAdd'.tr), SliverToBoxAdapter(
visualDensity: density, child: const VerticalDivider(
onPressed: () => showReactMenu(), thickness: 0.3,
), width: 0.3,
], indent: 8,
endIndent: 8,
).paddingSymmetric(horizontal: 8),
),
SliverList.builder(
itemCount: reactionEntries.length,
itemBuilder: (context, index) {
final x = reactionEntries[index];
final info = reactions[x.key];
return ActionChip(
avatar: Text(info!.icon),
label: Text(x.value.toString()),
tooltip: ':${x.key}:',
visualDensity: density,
onPressed: _isSubmitting
? null
: () => doWidgetReact(x.key, info.attitude),
).paddingOnly(right: 8);
},
),
if (widget.isReactable)
SliverToBoxAdapter(
child: ActionChip(
avatar: const Icon(Icons.add_reaction, color: Colors.teal),
label: Text('reactAdd'.tr),
visualDensity: density,
onPressed: () => showReactMenu(),
),
), ),
)
], ],
), ),
); );

View File

@@ -8,10 +8,12 @@ import 'package:solian/widgets/posts/post_list.dart';
class PostReplyList extends StatefulWidget { class PostReplyList extends StatefulWidget {
final Post item; final Post item;
final Color? backgroundColor;
const PostReplyList({ const PostReplyList({
super.key, super.key,
required this.item, required this.item,
this.backgroundColor,
}); });
@override @override
@@ -45,7 +47,6 @@ class _PostReplyListState extends State<PostReplyList> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_pagingController.addPageRequestListener(getReplies); _pagingController.addPageRequestListener(getReplies);
} }
@@ -54,6 +55,7 @@ class _PostReplyListState extends State<PostReplyList> {
return PostListWidget( return PostListWidget(
isShowEmbed: false, isShowEmbed: false,
controller: _pagingController, controller: _pagingController,
backgroundColor: widget.backgroundColor,
); );
} }
} }
@@ -74,7 +76,12 @@ class PostReplyListPopup extends StatelessWidget {
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded( Expanded(
child: CustomScrollView( child: CustomScrollView(
slivers: [PostReplyList(item: item)], slivers: [
PostReplyList(
item: item,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
),
],
), ),
), ),
], ],

View File

@@ -24,6 +24,7 @@ class PostSingleDisplay extends StatelessWidget {
isShowEmbed: true, isShowEmbed: true,
isNestedClickable: true, isNestedClickable: true,
onUpdate: onUpdate, onUpdate: onUpdate,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLow,
), ),
), ),
), ),

View File

@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:solian/models/feed.dart'; import 'package:solian/models/feed.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
class FeedTagsList extends StatelessWidget { class PostTagsList extends StatelessWidget {
final List<Tag> tags; final List<Tag> tags;
const FeedTagsList({ const PostTagsList({
super.key, super.key,
required this.tags, required this.tags,
}); });

View File

@@ -3,14 +3,14 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/widgets/posts/post_list.dart'; import 'package:solian/widgets/posts/post_list.dart';
class FeedListWidget extends StatelessWidget { class PostWarpedListWidget extends StatelessWidget {
final bool isShowEmbed; final bool isShowEmbed;
final bool isClickable; final bool isClickable;
final bool isNestedClickable; final bool isNestedClickable;
final bool isPinned; final bool isPinned;
final PagingController<int, Post> controller; final PagingController<int, Post> controller;
const FeedListWidget({ const PostWarpedListWidget({
super.key, super.key,
required this.controller, required this.controller,
this.isShowEmbed = true, this.isShowEmbed = true,
@@ -30,6 +30,7 @@ class FeedListWidget extends StatelessWidget {
return const SizedBox(); return const SizedBox();
} }
return PostListEntryWidget( return PostListEntryWidget(
renderOrder: index,
isShowEmbed: isShowEmbed, isShowEmbed: isShowEmbed,
isNestedClickable: isNestedClickable, isNestedClickable: isNestedClickable,
isClickable: isClickable, isClickable: isClickable,

View File

@@ -13,6 +13,7 @@ import firebase_core
import firebase_messaging import firebase_messaging
import flutter_secure_storage_macos import flutter_secure_storage_macos
import flutter_webrtc import flutter_webrtc
import gal
import livekit_client import livekit_client
import macos_window_utils import macos_window_utils
import media_kit_libs_macos_video import media_kit_libs_macos_video
@@ -38,6 +39,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))

View File

@@ -8,20 +8,20 @@ PODS:
- FlutterMacOS - FlutterMacOS
- file_selector_macos (0.0.1): - file_selector_macos (0.0.1):
- FlutterMacOS - FlutterMacOS
- Firebase/CoreOnly (10.28.1): - Firebase/CoreOnly (10.29.0):
- FirebaseCore (= 10.28.1) - FirebaseCore (= 10.29.0)
- Firebase/Messaging (10.28.1): - Firebase/Messaging (10.29.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 10.28.0) - FirebaseMessaging (~> 10.29.0)
- firebase_core (3.2.0): - firebase_core (3.3.0):
- Firebase/CoreOnly (~> 10.28.0) - Firebase/CoreOnly (~> 10.29.0)
- FlutterMacOS - FlutterMacOS
- firebase_messaging (15.0.3): - firebase_messaging (15.0.4):
- Firebase/CoreOnly (~> 10.28.0) - Firebase/CoreOnly (~> 10.29.0)
- Firebase/Messaging (~> 10.28.0) - Firebase/Messaging (~> 10.29.0)
- firebase_core - firebase_core
- FlutterMacOS - FlutterMacOS
- FirebaseCore (10.28.1): - 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)
@@ -32,7 +32,7 @@ PODS:
- GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8)
- PromisesObjC (~> 2.1) - PromisesObjC (~> 2.1)
- FirebaseMessaging (10.28.0): - FirebaseMessaging (10.29.0):
- FirebaseCore (~> 10.0) - FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0) - FirebaseInstallations (~> 10.0)
- GoogleDataTransport (~> 9.3) - GoogleDataTransport (~> 9.3)
@@ -47,6 +47,9 @@ PODS:
- FlutterMacOS - FlutterMacOS
- WebRTC-SDK (= 125.6422.04) - WebRTC-SDK (= 125.6422.04)
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- gal (1.0.0):
- Flutter
- FlutterMacOS
- 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)
@@ -111,6 +114,9 @@ PODS:
- Sentry/HybridSDK (= 8.32.0) - Sentry/HybridSDK (= 8.32.0)
- share_plus (0.0.1): - share_plus (0.0.1):
- FlutterMacOS - FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite (0.0.3): - sqflite (0.0.3):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@@ -130,6 +136,7 @@ DEPENDENCIES:
- flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`) - flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`)
- livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`) - livekit_client (from `Flutter/ephemeral/.symlinks/plugins/livekit_client/macos`)
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`) - macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
@@ -142,6 +149,7 @@ DEPENDENCIES:
- screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`) - screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`)
- sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`) - sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`)
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
- wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`)
@@ -179,6 +187,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos :path: Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :path: Flutter/ephemeral
gal:
:path: Flutter/ephemeral/.symlinks/plugins/gal/darwin
livekit_client: livekit_client:
:path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos :path: Flutter/ephemeral/.symlinks/plugins/livekit_client/macos
macos_window_utils: macos_window_utils:
@@ -203,6 +213,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos :path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos
share_plus: share_plus:
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
sqflite: sqflite:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
url_launcher_macos: url_launcher_macos:
@@ -215,16 +227,17 @@ SPEC CHECKSUMS:
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720
file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2 file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2
Firebase: 49e62242b3ae422a002ab647a7e62a332a8c3ec1 Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d
firebase_core: d8af40a9c8a9ce3112a94692aac83675627c0486 firebase_core: 73185b844efc8a534e5744d68152e75e740922d2
firebase_messaging: 7871cfa8af1e863324e46ae9e90343c452626c02 firebase_messaging: 167fdd90971720e0b62ccd6fa8d430b8af4ca6e9
FirebaseCore: dfc33f0dffba05f76181da9cc0151171ebb3bd10 FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16
FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
FirebaseMessaging: 087a7c7cadef7b9239f005bc4db823894844f323 FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9 flutter_webrtc: 2b4e4a2de70a1485836e40fd71a7a94c77d49bd9
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
livekit_client: c24af2b8474a39325596e714118e05551ec5eacc livekit_client: c24af2b8474a39325596e714118e05551ec5eacc
@@ -242,6 +255,7 @@ SPEC CHECKSUMS:
Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb
sentry_flutter: f1d86adcb93a959bc47a40d8d55059bdf7569bc5 sentry_flutter: f1d86adcb93a959bc47a40d8d55059bdf7569bc5
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399
wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269

View File

@@ -13,10 +13,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _flutterfire_internals name: _flutterfire_internals
sha256: b46f62516902afb04befa4b30eb6a12ac1f58ca8cb25fb9d632407259555dd3d sha256: b1595874fbc8f7a50da90f5d8f327bb0bfd6a95dc906c390efe991540c3b54aa
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.39" version: "1.3.40"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
@@ -25,6 +25,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.4.1" version: "6.4.1"
animations:
dependency: "direct main"
description:
name: animations
sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb
url: "https://pub.dev"
source: hosted
version: "2.0.11"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@@ -42,13 +50,21 @@ packages:
source: hosted source: hosted
version: "2.5.0" version: "2.5.0"
async: async:
dependency: transitive dependency: "direct main"
description: description:
name: async name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" version: "2.11.0"
avatar_stack:
dependency: "direct main"
description:
name: avatar_stack
sha256: e4a1576f7478add964bbb8aa5e530db39288fbbf81c30c4fb4b81162dd68aa49
url: "https://pub.dev"
source: hosted
version: "1.2.0"
badges: badges:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -133,26 +149,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cached_network_image name: cached_network_image
sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" version: "3.4.0"
cached_network_image_platform_interface: cached_network_image_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: cached_network_image_platform_interface name: cached_network_image_platform_interface
sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" sha256: ff0c949e323d2a1b52be73acce5b4a7b04063e61414c8ca542dbba47281630a7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.1.0"
cached_network_image_web: cached_network_image_web:
dependency: transitive dependency: transitive
description: description:
name: cached_network_image_web name: cached_network_image_web
sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
carousel_slider: carousel_slider:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -245,10 +261,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: cross_file name: cross_file
sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.4+1" version: "0.3.4+2"
crypto: crypto:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -301,10 +317,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dev_build name: dev_build
sha256: "5600100e28f7424ed53728e8e7aa6bc0e0506ec04bb49a82616f62112c1822c3" sha256: f526d1fbe68875f6119ffc333f114dfe6aa93ad04439276d53968f7977cc410e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0+8" version: "1.0.0+11"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -321,6 +337,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.1" version: "7.0.1"
dio:
dependency: "direct main"
description:
name: dio
sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714
url: "https://pub.dev"
source: hosted
version: "5.5.0+1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
dismissible_page: dismissible_page:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -337,14 +369,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.9" version: "2.3.9"
easy_debounce:
dependency: "direct main"
description:
name: easy_debounce
sha256: f082609cfb8f37defb9e37fc28bc978c6712dedf08d4c5a26f820fa10165a236
url: "https://pub.dev"
source: hosted
version: "2.0.3"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -413,50 +437,50 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_core name: firebase_core
sha256: "5159984ce9b70727473eb388394650677c02c925aaa6c9439905e1f30966a4d5" sha256: "3187f4f8e49968573fd7403011dca67ba95aae419bc0d8131500fae160d94f92"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.0" version: "3.3.0"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_core_platform_interface name: firebase_core_platform_interface
sha256: "1003a5a03a61fc9a22ef49f37cbcb9e46c86313a7b2e7029b9390cf8c6fc32cb" sha256: "3c3a1e92d6f4916c32deea79c4a7587aa0e9dbbe5889c7a16afcf005a485ee02"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.0" version: "5.2.0"
firebase_core_web: firebase_core_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_core_web name: firebase_core_web
sha256: "23509cb3cddfb3c910c143279ac3f07f06d3120f7d835e4a5d4b42558e978712" sha256: e8d1e22de72cb21cdcfc5eed7acddab3e99cd83f3b317f54f7a96c32f25fd11e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.17.3" version: "2.17.4"
firebase_messaging: firebase_messaging:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_messaging name: firebase_messaging
sha256: "156c4292aa63a6a7d508c68ded984cb38730d2823c3265e573cb1e94983e2025" sha256: "1b0a4f9ecbaf9007771bac152afad738ddfacc4b8431a7591c00829480d99553"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.3" version: "15.0.4"
firebase_messaging_platform_interface: firebase_messaging_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_platform_interface name: firebase_messaging_platform_interface
sha256: "10408c5ca242b7fc632dd5eab4caf8fdf18ebe88db6052980fa71a18d88bd200" sha256: c5a6443e66ae064fe186901d740ee7ce648ca2a6fd0484b8c5e963849ac0fc28
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.41" version: "4.5.42"
firebase_messaging_web: firebase_messaging_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_web name: firebase_messaging_web
sha256: c7a756e3750679407948de665735e69a368cb902940466e5d68a00ea7aba1aaa sha256: "232ef63b986467ae5b5577a09c2502b26e2e2aebab5b85e6c966a5ca9b038b89"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.8.11" version: "3.8.12"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@@ -522,10 +546,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_cache_manager name: flutter_cache_manager
sha256: ceff65d74d907b1b772e22cf04daad60fb472461638977d9fae8b00a63e01e3d sha256: a77f77806a790eb9ba0118a5a3a936e81c4fea2b61533033b2b0c3d50bbde5ea
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.3" version: "3.4.0"
flutter_card_swiper: flutter_card_swiper:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -652,10 +676,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_webrtc name: flutter_webrtc
sha256: d305793e6737c59a81c45b18484e1f985710827704eeb9092573387efcbae272 sha256: f46bd76cef6e8d787dc707d0c591f0e89c912a2970c7b5e68a55b9cca1bdde4c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.5" version: "0.11.6"
font_awesome_flutter: font_awesome_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -672,6 +696,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
gal:
dependency: "direct main"
description:
name: gal
sha256: "54c9b72528efce7c66234f3b6dd01cb0304fd8af8196de15571d7bdddb940977"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
get: get:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -692,10 +724,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: "39dd52168d6c59984454183148dc3a5776960c61083adfc708cc79a7b3ce1ba8" sha256: d380de0355788c5c784fe9f81b43fc833b903991c25ecc4e2a416a67faefa722
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.1" version: "14.2.2"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@@ -729,13 +761,37 @@ packages:
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
image: image:
dependency: "direct main" dependency: transitive
description: description:
name: image name: image
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.0" version: "4.2.0"
image_cropper:
dependency: "direct main"
description:
name: image_cropper
sha256: "7c3a85e54ec591738ee68f3cc9b8849bab0ec8e908bf8970645be959a079e17f"
url: "https://pub.dev"
source: hosted
version: "8.0.1"
image_cropper_for_web:
dependency: transitive
description:
name: image_cropper_for_web
sha256: "65f3f23fb82ff153601f22864e07e2ec14a7859db9e6ee5f643f0cde5243e9f2"
url: "https://pub.dev"
source: hosted
version: "6.0.1"
image_cropper_platform_interface:
dependency: transitive
description:
name: image_cropper_platform_interface
sha256: e8e9d2ca36360387aee39295ce49029362ae4df3071f23e8e71f2b81e40b7531
url: "https://pub.dev"
source: hosted
version: "7.0.0"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -756,10 +812,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_for_web name: image_picker_for_web
sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3" sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.4" version: "3.0.5"
image_picker_ios: image_picker_ios:
dependency: transitive dependency: transitive
description: description:
@@ -884,10 +940,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: livekit_client name: livekit_client
sha256: e6b1e8a3cdcae95f7e62c0371590648444bac245fce3a1bcfb4ec05889ad82f3 sha256: be2a3375851a6147d5de94a870edd6e831ab8d3d793e3563ba1ff1b05490b3de
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.2" version: "2.2.3"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@@ -912,6 +968,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.2.2" version: "7.2.2"
markdown_toolbar:
dependency: "direct main"
description:
name: markdown_toolbar
sha256: "5f4e2548afe96cc2ccdad22da87d380e4726d2d3385ddbb7035aa94daf9a4d47"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@@ -1016,6 +1080,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
nm: nm:
dependency: transitive dependency: transitive
description: description:
@@ -1028,10 +1100,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: octo_image name: octo_image
sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.1.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@@ -1076,10 +1148,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.4"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
@@ -1272,6 +1344,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.0" version: "0.2.0"
provider:
dependency: "direct main"
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.dev"
source: hosted
version: "6.1.2"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -1364,18 +1444,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sentry name: sentry
sha256: "60756499f09c3ed944640d7993ac527a89f7c3033f13ec12ae145706b269b5c8" sha256: "76ad4fab90ff82427c26939bd79dc4df345a081e2b1cd5954b947e340b9af9a5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.5.0" version: "8.6.0"
sentry_flutter: sentry_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
name: sentry_flutter name: sentry_flutter
sha256: "26cfe89cb08a60d9bc0c4e748a0508e223ae378265aec8ed2a2b48f0d2c936b9" sha256: a7c92014701093a7c0a373e1a47c54ec428d8468d8bf2b793fee7ea1b085c21b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.5.0" version: "8.6.0"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1396,58 +1476,58 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.3" version: "2.3.1"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: "3d4571b3c5eb58ce52a419d86e655493d0bc3020672da79f72fa0c16ca3a8ec1" sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.4" version: "2.3.0"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_foundation name: shared_preferences_foundation
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.5.0"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_linux name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.4.0"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_platform_interface name: shared_preferences_platform_interface
sha256: "034650b71e73629ca08a0bd789fd1d83cc63c2d1e405946f7cef7bc37432f93a" sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.1"
shared_preferences_web: shared_preferences_web:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_web name: shared_preferences_web
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.4.1"
shared_preferences_windows: shared_preferences_windows:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_windows name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.4.0"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@@ -1613,14 +1693,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.0" version: "0.7.0"
textfield_tags:
dependency: "direct main"
description:
name: textfield_tags
sha256: d1f2204114157a1296bb97c20d7f8c8c7fd036212812afb2e19de7bb34acc55b
url: "https://pub.dev"
source: hosted
version: "3.0.1"
timeago: timeago:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1681,10 +1753,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "678979703e10d7862c551c736fe6b9f185261bddf141b46672063b99790bc700" sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.7" version: "6.3.8"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
@@ -1721,10 +1793,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" sha256: a36e2d7981122fa185006b216eb6b5b97ede3f9a54b7a511bc966971ab98d049
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.1" version: "2.3.2"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
@@ -1825,10 +1897,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.5.1" version: "5.5.3"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:

View File

@@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App" description: "The Solar Network App"
publish_to: "none" publish_to: "none"
version: 1.2.0+1 version: 1.2.0+9
environment: environment:
sdk: ">=3.3.4 <4.0.0" sdk: ">=3.3.4 <4.0.0"
@@ -27,7 +27,6 @@ dependencies:
crypto: ^3.0.3 crypto: ^3.0.3
path: ^1.9.0 path: ^1.9.0
intl: ^0.19.0 intl: ^0.19.0
image: ^4.1.7
font_awesome_flutter: ^10.7.0 font_awesome_flutter: ^10.7.0
web_socket_channel: ^3.0.0 web_socket_channel: ^3.0.0
permission_handler: ^11.3.1 permission_handler: ^11.3.1
@@ -50,7 +49,6 @@ dependencies:
media_kit: ^1.1.10+1 media_kit: ^1.1.10+1
media_kit_video: ^1.2.4 media_kit_video: ^1.2.4
media_kit_libs_video: ^1.0.4 media_kit_libs_video: ^1.0.4
textfield_tags: ^3.0.1
pasteboard: ^0.2.0 pasteboard: ^0.2.0
desktop_drop: ^0.4.4 desktop_drop: ^0.4.4
badges: ^3.1.2 badges: ^3.1.2
@@ -60,7 +58,14 @@ dependencies:
flutter_cache_manager: ^3.3.3 flutter_cache_manager: ^3.3.3
flutter_markdown_selectionarea: ^0.6.17+1 flutter_markdown_selectionarea: ^0.6.17+1
shared_preferences: ^2.2.3 shared_preferences: ^2.2.3
easy_debounce: ^2.0.3 provider: ^6.1.2
gal: ^2.3.0
dio: ^5.5.0+1
image_cropper: ^8.0.1
markdown_toolbar: ^0.5.0
animations: ^2.0.11
avatar_stack: ^1.2.0
async: ^2.11.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -80,6 +85,31 @@ flutter:
assets: assets:
- assets/logo.png - assets/logo.png
fonts:
- family: NotoSansEmoji
fonts:
- asset: assets/fonts/NotoColorEmoji-Regular.ttf
- family: NotoSansSC
fonts:
- asset: assets/fonts/NotoSansSC-Regular.ttf
- asset: assets/fonts/NotoSansSC-Bold.ttf
weight: 700
- family: NotoSansHK
fonts:
- asset: assets/fonts/NotoSansHK-Regular.ttf
- asset: assets/fonts/NotoSansHK-Bold.ttf
weight: 700
- family: NotoSansJP
fonts:
- asset: assets/fonts/NotoSansJP-Regular.ttf
- asset: assets/fonts/NotoSansJP-Bold.ttf
weight: 700
- family: Comfortaa
fonts:
- asset: assets/fonts/Comfortaa-Regular.ttf
- asset: assets/fonts/Comfortaa-Bold.ttf
weight: 700
flutter_launcher_icons: flutter_launcher_icons:
android: android:
generate: "launcher_icon" generate: "launcher_icon"

View File

@@ -1,8 +1,7 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head>
<head> <!--
<!--
If you are serving your web app in a path other than the root, change the If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from. href value below to reflect the base path you are serving from.
@@ -15,65 +14,68 @@
This is a placeholder for base href that will be replaced by the value of This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`. the `--base-href` argument provided to `flutter build`.
--> -->
<base href="$FLUTTER_BASE_HREF"> <base href="$FLUTTER_BASE_HREF" />
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta content="IE=Edge" http-equiv="X-UA-Compatible" />
<meta name="description" content="A new Flutter project."> <meta name="description" content="A new Flutter project." />
<!-- iOS meta tags & icons --> <!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="solian"> <meta name="apple-mobile-web-app-title" content="solian" />
<link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="apple-touch-icon" href="icons/Icon-192.png" />
<!-- Favicon --> <!-- Cropper.js -->
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.2/cropper.min.js"></script>
<!-- Loading styles --> <!-- Favicon -->
<style> <link rel="icon" type="image/png" href="favicon.png" />
.loader-container {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.loader { <!-- Loading styles -->
border: 10px solid #f3f3f3; <style>
border-top: 10px solid #8f94ca; .loader-container {
border-radius: 50%; display: flex;
width: 80px; justify-content: center;
height: 80px; align-items: center;
animation: spin .35s linear infinite; margin: 0;
} position: absolute;
top: 50%;
@keyframes spin { left: 50%;
0% { -ms-transform: translate(-50%, -50%);
transform: rotate(0deg); transform: translate(-50%, -50%);
} }
100% { .loader {
transform: rotate(360deg); border: 10px solid #f3f3f3;
border-top: 10px solid #8f94ca;
border-radius: 50%;
width: 80px;
height: 80px;
animation: spin 0.35s linear infinite;
} }
}
</style>
<title>Solian</title> @keyframes spin {
<link rel="manifest" href="manifest.json"> 0% {
</head> transform: rotate(0deg);
}
<body> 100% {
<div class="loader-container"> transform: rotate(360deg);
<div class="loader"></div> }
</div> }
</style>
<script src="flutter_bootstrap.js" async></script> <title>Solian</title>
</body> <link rel="manifest" href="manifest.json" />
</head>
<body>
<div class="loader-container">
<div class="loader"></div>
</div>
<script src="flutter_bootstrap.js" async></script>
</body>
</html> </html>

View File

@@ -13,6 +13,7 @@
#include <flutter_acrylic/flutter_acrylic_plugin.h> #include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h> #include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
#include <gal/gal_plugin_c_api.h>
#include <livekit_client/live_kit_plugin.h> #include <livekit_client/live_kit_plugin.h>
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h> #include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
#include <media_kit_video/media_kit_video_plugin_c_api.h> #include <media_kit_video/media_kit_video_plugin_c_api.h>
@@ -39,6 +40,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
FlutterWebRTCPluginRegisterWithRegistrar( FlutterWebRTCPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
GalPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GalPluginCApi"));
LiveKitPluginRegisterWithRegistrar( LiveKitPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LiveKitPlugin")); registry->GetRegistrarForPlugin("LiveKitPlugin"));
MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(

View File

@@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
flutter_acrylic flutter_acrylic
flutter_secure_storage_windows flutter_secure_storage_windows
flutter_webrtc flutter_webrtc
gal
livekit_client livekit_client
media_kit_libs_windows_video media_kit_libs_windows_video
media_kit_video media_kit_video