Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
4a3e6a9e15 | |||
00092ba7b6 | |||
b5da8ece4a | |||
dfe9165bc9 | |||
3d45b54236 | |||
7f63fe7f0e | |||
bc5dbab9c5 | |||
9910fc7a92 | |||
2356eac118 | |||
0135b8d838 | |||
8ec33ccbf4 | |||
d267316a35 | |||
138da60e55 | |||
4562c2f991 | |||
8009f4ca9b | |||
54dee9702b | |||
94385564bd | |||
0b2309816f | |||
8283272a3b | |||
eb02a47e9a | |||
7c0c1ec94f | |||
272044a77e | |||
39c22b1cf6 | |||
98c3bb912d | |||
035b92d9b8 | |||
0bfc0bd61b | |||
de00a20eee | |||
73982f48d6 | |||
1d36b30361 | |||
dea743a307 | |||
c48bd3e758 | |||
56bbf73b5e | |||
4f6c5aa053 | |||
d8e79fb4f9 | |||
06e0fa465b | |||
895a257f50 | |||
d9804ba00b | |||
62ff1c2f1c | |||
a157596a2e | |||
12102bf527 | |||
c00a018380 | |||
53b3cac4ca |
@ -71,6 +71,8 @@ PODS:
|
|||||||
- GoogleUtilities/UserDefaults (~> 7.8)
|
- GoogleUtilities/UserDefaults (~> 7.8)
|
||||||
- nanopb (< 2.30911.0, >= 2.30908.0)
|
- nanopb (< 2.30911.0, >= 2.30908.0)
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
|
- flutter_keyboard_visibility (0.0.1):
|
||||||
|
- Flutter
|
||||||
- flutter_secure_storage (6.0.0):
|
- flutter_secure_storage (6.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_webrtc (0.11.3):
|
- flutter_webrtc (0.11.3):
|
||||||
@ -136,19 +138,21 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- pointer_interceptor_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- protocol_handler_ios (0.0.1):
|
- protocol_handler_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- screen_brightness_ios (0.1.0):
|
- screen_brightness_ios (0.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SDWebImage (5.19.4):
|
- SDWebImage (5.19.6):
|
||||||
- SDWebImage/Core (= 5.19.4)
|
- SDWebImage/Core (= 5.19.6)
|
||||||
- SDWebImage/Core (5.19.4)
|
- SDWebImage/Core (5.19.6)
|
||||||
- Sentry/HybridSDK (8.32.0)
|
- Sentry/HybridSDK (8.33.0)
|
||||||
- sentry_flutter (8.6.0):
|
- sentry_flutter (8.7.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- Sentry/HybridSDK (= 8.32.0)
|
- Sentry/HybridSDK (= 8.33.0)
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
@ -174,6 +178,7 @@ DEPENDENCIES:
|
|||||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
|
- flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||||
- gal (from `.symlinks/plugins/gal/darwin`)
|
- gal (from `.symlinks/plugins/gal/darwin`)
|
||||||
@ -187,6 +192,7 @@ DEPENDENCIES:
|
|||||||
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
|
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
||||||
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
|
- protocol_handler_ios (from `.symlinks/plugins/protocol_handler_ios/ios`)
|
||||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||||
@ -229,6 +235,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/firebase_messaging/ios"
|
:path: ".symlinks/plugins/firebase_messaging/ios"
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
|
flutter_keyboard_visibility:
|
||||||
|
:path: ".symlinks/plugins/flutter_keyboard_visibility/ios"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
flutter_webrtc:
|
flutter_webrtc:
|
||||||
@ -255,6 +263,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
|
pointer_interceptor_ios:
|
||||||
|
:path: ".symlinks/plugins/pointer_interceptor_ios/ios"
|
||||||
protocol_handler_ios:
|
protocol_handler_ios:
|
||||||
:path: ".symlinks/plugins/protocol_handler_ios/ios"
|
:path: ".symlinks/plugins/protocol_handler_ios/ios"
|
||||||
screen_brightness_ios:
|
screen_brightness_ios:
|
||||||
@ -288,6 +298,7 @@ SPEC CHECKSUMS:
|
|||||||
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
|
FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd
|
||||||
FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366
|
FirebaseMessaging: 7b5d8033e183ab59eb5b852a53201559e976d366
|
||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
|
flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069
|
||||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||||
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
|
flutter_webrtc: 75b868e4f9e817c7a9a42ca4b6169063de4eec9f
|
||||||
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
gal: 61e868295d28fe67ffa297fae6dacebf56fd53e1
|
||||||
@ -304,12 +315,13 @@ SPEC CHECKSUMS:
|
|||||||
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0
|
||||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||||
|
pointer_interceptor_ios: 508241697ff0947f853c061945a8b822463947c1
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990
|
protocol_handler_ios: a5db8abc38526ee326988b808be621e5fd568990
|
||||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||||
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
|
SDWebImage: a79252b60f4678812d94316c91da69ec83089c9f
|
||||||
Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb
|
Sentry: 8560050221424aef0bebc8e31eedf00af80f90a6
|
||||||
sentry_flutter: 090351ce1ff5f96a4b33ef9455b7e3b28185387d
|
sentry_flutter: e26b861f744e5037a3faf9bf56603ec65d658a61
|
||||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Flutter
|
import Flutter
|
||||||
|
|
||||||
@UIApplicationMain
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
override func application(
|
override func application(
|
||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
|
@ -163,24 +163,25 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isBusy || _isErrored) {
|
if (_isBusy || _isErrored) {
|
||||||
return Material(
|
return GestureDetector(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
child: Material(
|
||||||
child: Column(
|
color: Theme.of(context).colorScheme.surface,
|
||||||
mainAxisSize: MainAxisSize.max,
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
SizedBox(
|
children: [
|
||||||
height: 280,
|
SizedBox(
|
||||||
child: Align(
|
height: 280,
|
||||||
alignment: Alignment.bottomCenter,
|
child: Align(
|
||||||
child: ClipRRect(
|
alignment: Alignment.bottomCenter,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
child: ClipRRect(
|
||||||
child: Image.asset('assets/logo.png', width: 80, height: 80),
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
|
child:
|
||||||
|
Image.asset('assets/logo.png', width: 80, height: 80),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Column(
|
||||||
GestureDetector(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
if (_isErrored && !_isDismissable && !_isBusy)
|
if (_isErrored && !_isDismissable && !_isBusy)
|
||||||
const Icon(Icons.cancel, size: 24),
|
const Icon(Icons.cancel, size: 24),
|
||||||
@ -215,6 +216,15 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
color: _unFocusColor,
|
color: _unFocusColor,
|
||||||
),
|
),
|
||||||
).paddingOnly(bottom: 4),
|
).paddingOnly(bottom: 4),
|
||||||
|
if (!_isBusy && _isErrored && _isDismissable)
|
||||||
|
Text(
|
||||||
|
'bsDismissibleErrorHint'.tr,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: _unFocusColor,
|
||||||
|
),
|
||||||
|
).paddingOnly(bottom: 5),
|
||||||
Text(
|
Text(
|
||||||
'2024 © Solsynth LLC',
|
'2024 © Solsynth LLC',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@ -228,25 +238,25 @@ class _BootstrapperShellState extends State<BootstrapperShell> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
],
|
||||||
if (_isBusy) return;
|
),
|
||||||
if (_isDismissable) {
|
|
||||||
setState(() {
|
|
||||||
_isBusy = false;
|
|
||||||
_isErrored = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_isBusy = true;
|
|
||||||
_isErrored = false;
|
|
||||||
_periodCursor = 0;
|
|
||||||
});
|
|
||||||
_runPeriods();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (_isBusy) return;
|
||||||
|
if (_isDismissable) {
|
||||||
|
setState(() {
|
||||||
|
_isBusy = false;
|
||||||
|
_isErrored = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_isBusy = true;
|
||||||
|
_isErrored = false;
|
||||||
|
_periodCursor = 0;
|
||||||
|
});
|
||||||
|
_runPeriods();
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +52,17 @@ class Realm {
|
|||||||
'is_community': isCommunity,
|
'is_community': isCommunity,
|
||||||
'account_id': accountId,
|
'account_id': accountId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other is Realm) {
|
||||||
|
return other.id == id;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => id;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RealmMember {
|
class RealmMember {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:solian/models/account.dart';
|
import 'package:solian/models/account.dart';
|
||||||
import 'package:solian/models/attachment.dart';
|
import 'package:solian/models/attachment.dart';
|
||||||
|
import 'package:solian/services.dart';
|
||||||
|
|
||||||
class Sticker {
|
class Sticker {
|
||||||
int id;
|
int id;
|
||||||
@ -30,6 +31,14 @@ class Sticker {
|
|||||||
required this.account,
|
required this.account,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
String get textPlaceholder => '${pack?.prefix}$alias';
|
||||||
|
String get textWarpedPlaceholder => ':$textPlaceholder:';
|
||||||
|
|
||||||
|
String get imageUrl => ServiceFinder.buildUrl(
|
||||||
|
'files',
|
||||||
|
'/attachments/$attachmentId',
|
||||||
|
);
|
||||||
|
|
||||||
factory Sticker.fromJson(Map<String, dynamic> json) => Sticker(
|
factory Sticker.fromJson(Map<String, dynamic> json) => Sticker(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
createdAt: DateTime.parse(json['created_at']),
|
createdAt: DateTime.parse(json['created_at']),
|
||||||
|
@ -138,8 +138,7 @@ class AttachmentUploaderController extends GetxController {
|
|||||||
queueOfUpload[idx].isCompleted = true;
|
queueOfUpload[idx].isCompleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
queueOfUpload.value =
|
queueOfUpload.removeWhere((x) => x.error == null);
|
||||||
queueOfUpload.where((x) => x.error == null).toList(growable: true);
|
|
||||||
_stopProgressSyncTimer();
|
_stopProgressSyncTimer();
|
||||||
_syncProgress();
|
_syncProgress();
|
||||||
|
|
||||||
|
@ -89,6 +89,8 @@ class AttachmentProvider extends GetConnect {
|
|||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
if (auth.isAuthorized.isFalse) throw Exception('unauthorized');
|
||||||
|
|
||||||
|
await auth.ensureCredentials();
|
||||||
|
|
||||||
final filePayload =
|
final filePayload =
|
||||||
dio.MultipartFile.fromBytes(data, filename: basename(path));
|
dio.MultipartFile.fromBytes(data, filename: basename(path));
|
||||||
final fileAlt = basename(path).contains('.')
|
final fileAlt = basename(path).contains('.')
|
||||||
|
@ -5,9 +5,12 @@ import 'package:solian/services.dart';
|
|||||||
|
|
||||||
class StickerProvider extends GetxController {
|
class StickerProvider extends GetxController {
|
||||||
final RxMap<String, String> aliasImageMapping = RxMap();
|
final RxMap<String, String> aliasImageMapping = RxMap();
|
||||||
final RxMap<String, List<Sticker>> availableStickers = RxMap();
|
final RxList<Sticker> availableStickers = RxList.empty(growable: true);
|
||||||
|
|
||||||
Future<void> refreshAvailableStickers() async {
|
Future<void> refreshAvailableStickers() async {
|
||||||
|
availableStickers.clear();
|
||||||
|
aliasImageMapping.clear();
|
||||||
|
|
||||||
final client = ServiceFinder.configureClient('files');
|
final client = ServiceFinder.configureClient('files');
|
||||||
final resp = await client.get(
|
final resp = await client.get(
|
||||||
'/stickers/manifest?take=100',
|
'/stickers/manifest?take=100',
|
||||||
@ -20,16 +23,9 @@ class StickerProvider extends GetxController {
|
|||||||
for (final pack in out) {
|
for (final pack in out) {
|
||||||
for (final sticker in (pack.stickers ?? List<Sticker>.empty())) {
|
for (final sticker in (pack.stickers ?? List<Sticker>.empty())) {
|
||||||
sticker.pack = pack;
|
sticker.pack = pack;
|
||||||
final imageUrl = ServiceFinder.buildUrl(
|
aliasImageMapping[sticker.textPlaceholder.toUpperCase()] =
|
||||||
'files',
|
sticker.imageUrl;
|
||||||
'/attachments/${sticker.attachmentId}',
|
availableStickers.add(sticker);
|
||||||
);
|
|
||||||
aliasImageMapping['${pack.prefix}${sticker.alias}'.camelCase!] =
|
|
||||||
imageUrl;
|
|
||||||
if (availableStickers[pack.prefix] == null) {
|
|
||||||
availableStickers[pack.prefix] = List.empty(growable: true);
|
|
||||||
}
|
|
||||||
availableStickers[pack.prefix]!.add(sticker);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,6 @@ abstract class AppRouter {
|
|||||||
reply: arguments?.reply,
|
reply: arguments?.reply,
|
||||||
repost: arguments?.repost,
|
repost: arguments?.repost,
|
||||||
realm: arguments?.realm,
|
realm: arguments?.realm,
|
||||||
postListController: arguments?.postListController,
|
|
||||||
mode: int.tryParse(state.uri.queryParameters['mode'] ?? '0') ?? 0,
|
mode: int.tryParse(state.uri.queryParameters['mode'] ?? '0') ?? 0,
|
||||||
),
|
),
|
||||||
transitionsBuilder:
|
transitionsBuilder:
|
||||||
|
@ -300,6 +300,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
|
|||||||
PostWarpedListWidget(
|
PostWarpedListWidget(
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
controller: _postController.pagingController,
|
controller: _postController.pagingController,
|
||||||
|
onUpdate: () => _postController.reloadAllOver(),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
@ -70,7 +70,7 @@ class _StickerScreenState extends State<StickerScreen> {
|
|||||||
);
|
);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(item.name),
|
title: Text(item.name),
|
||||||
subtitle: Text(':${'$prefix${item.alias}'.camelCase}:'),
|
subtitle: Text(item.textWarpedPlaceholder),
|
||||||
contentPadding: const EdgeInsets.only(left: 16, right: 14),
|
contentPadding: const EdgeInsets.only(left: 16, right: 14),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -159,15 +159,25 @@ class _StickerScreenState extends State<StickerScreen> {
|
|||||||
builderDelegate: PagedChildBuilderDelegate(
|
builderDelegate: PagedChildBuilderDelegate(
|
||||||
itemBuilder: (BuildContext context, item, int index) {
|
itemBuilder: (BuildContext context, item, int index) {
|
||||||
return ExpansionTile(
|
return ExpansionTile(
|
||||||
title: Text(item.name),
|
title: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(item.name),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Badge(
|
||||||
|
label: Text('#${item.id}'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
item.description,
|
item.description,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
children: item.stickers
|
children: item.stickers?.map((x) {
|
||||||
?.map((x) => _buildEmoteEntry(x, item.prefix))
|
x.pack = item;
|
||||||
.toList() ??
|
return _buildEmoteEntry(x, item.prefix);
|
||||||
|
}).toList() ??
|
||||||
List.empty(),
|
List.empty(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -136,8 +136,10 @@ class _SignInPopupState extends State<SignInPopup> with ProtocolListener {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Image.asset('assets/logo.png', width: 64, height: 64)
|
ClipRRect(
|
||||||
.paddingOnly(bottom: 4),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Image.asset('assets/logo.png', width: 64, height: 64),
|
||||||
|
).paddingOnly(bottom: 4),
|
||||||
Text(
|
Text(
|
||||||
'signinGreeting'.tr,
|
'signinGreeting'.tr,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
@ -70,8 +70,10 @@ class _SignUpPopupState extends State<SignUpPopup> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Image.asset('assets/logo.png', width: 64, height: 64)
|
ClipRRect(
|
||||||
.paddingOnly(bottom: 4),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
child: Image.asset('assets/logo.png', width: 64, height: 64),
|
||||||
|
).paddingOnly(bottom: 4),
|
||||||
Text(
|
Text(
|
||||||
'signupGreeting'.tr,
|
'signupGreeting'.tr,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
@ -9,6 +9,7 @@ import 'package:solian/theme.dart';
|
|||||||
import 'package:solian/widgets/app_bar_leading.dart';
|
import 'package:solian/widgets/app_bar_leading.dart';
|
||||||
import 'package:solian/widgets/chat/call/call_controls.dart';
|
import 'package:solian/widgets/chat/call/call_controls.dart';
|
||||||
import 'package:solian/widgets/chat/call/call_participant.dart';
|
import 'package:solian/widgets/chat/call/call_participant.dart';
|
||||||
|
import 'package:livekit_client/livekit_client.dart' as livekit;
|
||||||
|
|
||||||
class CallScreen extends StatefulWidget {
|
class CallScreen extends StatefulWidget {
|
||||||
const CallScreen({super.key});
|
const CallScreen({super.key});
|
||||||
@ -161,31 +162,30 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
|||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: columns,
|
crossAxisCount: columns,
|
||||||
childAspectRatio: tileWidth / tileHeight,
|
childAspectRatio: tileWidth / tileHeight,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
),
|
),
|
||||||
itemCount: math.max(0, call.participantTracks.length),
|
itemCount: math.max(0, call.participantTracks.length),
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final track = call.participantTracks[index];
|
final track = call.participantTracks[index];
|
||||||
return Padding(
|
return Card(
|
||||||
padding: const EdgeInsets.all(16),
|
child: ClipRRect(
|
||||||
child: Card(
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: ClipRRect(
|
child: InteractiveParticipantWidget(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
child: InteractiveParticipantWidget(
|
participant: track,
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
onTap: () {
|
||||||
participant: track,
|
if (track.participant.sid !=
|
||||||
onTap: () {
|
call.focusTrack.value?.participant.sid) {
|
||||||
if (track.participant.sid !=
|
call.changeFocusTrack(track);
|
||||||
call.focusTrack.value?.participant.sid) {
|
}
|
||||||
call.changeFocusTrack(track);
|
},
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
).paddingAll(8);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,8 +246,77 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
|||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
height: 64,
|
height: 64,
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Expanded(child: SizedBox()),
|
Builder(builder: (context) {
|
||||||
|
final call = Get.find<ChatCallProvider>();
|
||||||
|
final connectionQuality =
|
||||||
|
call.room.localParticipant?.connectionQuality ??
|
||||||
|
livekit.ConnectionQuality.unknown;
|
||||||
|
return Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(call.room.serverRegion ?? 'unknown'),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(call.room.serverVersion ?? 'unknown')
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
{
|
||||||
|
livekit.ConnectionState.disconnected:
|
||||||
|
'callStatusDisconnected'.tr,
|
||||||
|
livekit.ConnectionState.connected:
|
||||||
|
'callStatusConnected'.tr,
|
||||||
|
livekit.ConnectionState.connecting:
|
||||||
|
'callStatusConnecting'.tr,
|
||||||
|
livekit.ConnectionState.reconnecting:
|
||||||
|
'callStatusReconnecting'.tr,
|
||||||
|
}[call.room.connectionState]!,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
if (connectionQuality !=
|
||||||
|
livekit.ConnectionQuality.unknown)
|
||||||
|
Icon(
|
||||||
|
{
|
||||||
|
livekit.ConnectionQuality.excellent:
|
||||||
|
Icons.signal_cellular_alt,
|
||||||
|
livekit.ConnectionQuality.good:
|
||||||
|
Icons.signal_cellular_alt_2_bar,
|
||||||
|
livekit.ConnectionQuality.poor:
|
||||||
|
Icons.signal_cellular_alt_1_bar,
|
||||||
|
}[connectionQuality],
|
||||||
|
color: {
|
||||||
|
livekit.ConnectionQuality.excellent:
|
||||||
|
Colors.green,
|
||||||
|
livekit.ConnectionQuality.good:
|
||||||
|
Colors.orange,
|
||||||
|
livekit.ConnectionQuality.poor:
|
||||||
|
Colors.red,
|
||||||
|
}[connectionQuality],
|
||||||
|
size: 16,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
).paddingAll(3),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: _layoutMode == 0
|
icon: _layoutMode == 0
|
||||||
? const Icon(Icons.view_list)
|
? const Icon(Icons.view_list)
|
||||||
@ -257,7 +326,7 @@ class _CallScreenState extends State<CallScreen> with TickerProviderStateMixin {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).paddingSymmetric(horizontal: 10),
|
).paddingOnly(left: 20, right: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
@ -20,7 +20,6 @@ import 'package:solian/widgets/app_bar_leading.dart';
|
|||||||
import 'package:solian/widgets/app_bar_title.dart';
|
import 'package:solian/widgets/app_bar_title.dart';
|
||||||
import 'package:solian/widgets/channel/channel_call_indicator.dart';
|
import 'package:solian/widgets/channel/channel_call_indicator.dart';
|
||||||
import 'package:solian/widgets/chat/call/chat_call_action.dart';
|
import 'package:solian/widgets/chat/call/chat_call_action.dart';
|
||||||
import 'package:solian/widgets/chat/chat_event.dart';
|
|
||||||
import 'package:solian/widgets/chat/chat_event_list.dart';
|
import 'package:solian/widgets/chat/chat_event_list.dart';
|
||||||
import 'package:solian/widgets/chat/chat_message_input.dart';
|
import 'package:solian/widgets/chat/chat_message_input.dart';
|
||||||
import 'package:solian/widgets/current_state_action.dart';
|
import 'package:solian/widgets/current_state_action.dart';
|
||||||
@ -39,7 +38,10 @@ class ChannelChatScreen extends StatefulWidget {
|
|||||||
State<ChannelChatScreen> createState() => _ChannelChatScreenState();
|
State<ChannelChatScreen> createState() => _ChannelChatScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
class _ChannelChatScreenState extends State<ChannelChatScreen>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
|
DateTime? _isOutOfSyncSince;
|
||||||
|
|
||||||
bool _isBusy = false;
|
bool _isBusy = false;
|
||||||
int? _accountId;
|
int? _accountId;
|
||||||
|
|
||||||
@ -123,20 +125,38 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _keepUpdateWithServer() {
|
||||||
|
_getOngoingCall();
|
||||||
|
_chatController.getEvents(_channel!, widget.realm);
|
||||||
|
setState(() => _isOutOfSyncSince = null);
|
||||||
|
}
|
||||||
|
|
||||||
Event? _messageToReplying;
|
Event? _messageToReplying;
|
||||||
Event? _messageToEditing;
|
Event? _messageToEditing;
|
||||||
|
|
||||||
Widget buildHistoryBody(Event item, {bool isMerged = false}) {
|
@override
|
||||||
return ChatEvent(
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
key: Key('m${item.uuid}'),
|
switch (state) {
|
||||||
item: item,
|
case AppLifecycleState.resumed:
|
||||||
isMerged: isMerged,
|
if (_isOutOfSyncSince == null) break;
|
||||||
chatController: _chatController,
|
if (DateTime.now().difference(_isOutOfSyncSince!).inSeconds < 60) break;
|
||||||
);
|
_keepUpdateWithServer();
|
||||||
|
break;
|
||||||
|
case AppLifecycleState.paused:
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _isOutOfSyncSince = DateTime.now());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
|
||||||
_accountId = Get.find<AuthProvider>().userProfile.value!['id'];
|
_accountId = Get.find<AuthProvider>().userProfile.value!['id'];
|
||||||
|
|
||||||
_chatController = ChatEventController();
|
_chatController = ChatEventController();
|
||||||
@ -147,21 +167,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
_chatController.getEvents(_channel!, widget.realm);
|
_chatController.getEvents(_channel!, widget.realm);
|
||||||
_listenMessages();
|
_listenMessages();
|
||||||
});
|
});
|
||||||
|
|
||||||
super.initState();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isBusy || _channel == null) {
|
|
||||||
return Material(
|
|
||||||
color: Theme.of(context).colorScheme.surface,
|
|
||||||
child: const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String title = _channel?.name ?? 'loading'.tr;
|
String title = _channel?.name ?? 'loading'.tr;
|
||||||
String? placeholder;
|
String? placeholder;
|
||||||
|
|
||||||
@ -185,7 +194,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
actions: [
|
actions: [
|
||||||
const BackgroundStateWidget(),
|
const BackgroundStateWidget(),
|
||||||
Builder(builder: (context) {
|
Builder(builder: (context) {
|
||||||
if (_isBusy) return const SizedBox();
|
if (_isBusy || _channel == null) return const SizedBox();
|
||||||
|
|
||||||
return ChatCallButton(
|
return ChatCallButton(
|
||||||
realm: _channel!.realm,
|
realm: _channel!.realm,
|
||||||
channel: _channel!,
|
channel: _channel!,
|
||||||
@ -195,6 +205,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
if (_channel == null) return;
|
||||||
|
|
||||||
AppRouter.instance
|
AppRouter.instance
|
||||||
.pushNamed(
|
.pushNamed(
|
||||||
'channelDetail',
|
'channelDetail',
|
||||||
@ -219,66 +231,94 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Builder(builder: (context) {
|
||||||
children: [
|
if (_isBusy || _channel == null) {
|
||||||
if (_ongoingCall != null)
|
return const Center(
|
||||||
ChannelCallIndicator(
|
child: CircularProgressIndicator(),
|
||||||
channel: _channel!,
|
);
|
||||||
ongoingCall: _ongoingCall!,
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
if (_ongoingCall != null)
|
||||||
|
ChannelCallIndicator(
|
||||||
|
channel: _channel!,
|
||||||
|
ongoingCall: _ongoingCall!,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ChatEventList(
|
||||||
|
scope: widget.realm,
|
||||||
|
channel: _channel!,
|
||||||
|
chatController: _chatController,
|
||||||
|
onEdit: (item) {
|
||||||
|
setState(() => _messageToEditing = item);
|
||||||
|
},
|
||||||
|
onReply: (item) {
|
||||||
|
setState(() => _messageToReplying = item);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
if (_isOutOfSyncSince != null)
|
||||||
child: ChatEventList(
|
ListTile(
|
||||||
scope: widget.realm,
|
contentPadding: const EdgeInsets.only(left: 16, right: 8),
|
||||||
channel: _channel!,
|
tileColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
chatController: _chatController,
|
leading: const Icon(Icons.history_toggle_off),
|
||||||
onEdit: (item) {
|
title: Text('messageOutOfSync'.tr),
|
||||||
setState(() => _messageToEditing = item);
|
subtitle: Text('messageOutOfSyncCaption'.tr),
|
||||||
},
|
trailing: IconButton(
|
||||||
onReply: (item) {
|
icon: const Icon(Icons.close),
|
||||||
setState(() => _messageToReplying = item);
|
onPressed: () {
|
||||||
},
|
setState(() => _isOutOfSyncSince = null);
|
||||||
),
|
|
||||||
),
|
|
||||||
Obx(() {
|
|
||||||
if (_chatController.isLoading.isTrue) {
|
|
||||||
return const LinearProgressIndicator().animate().slideY();
|
|
||||||
} else {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
ClipRect(
|
|
||||||
child: BackdropFilter(
|
|
||||||
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
|
||||||
child: SafeArea(
|
|
||||||
child: ChatMessageInput(
|
|
||||||
edit: _messageToEditing,
|
|
||||||
reply: _messageToReplying,
|
|
||||||
realm: widget.realm,
|
|
||||||
placeholder: placeholder,
|
|
||||||
channel: _channel!,
|
|
||||||
onSent: (Event item) {
|
|
||||||
setState(() {
|
|
||||||
_chatController.addPendingEvent(item);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onReset: () {
|
|
||||||
setState(() {
|
|
||||||
_messageToReplying = null;
|
|
||||||
_messageToEditing = null;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
onTap: _isBusy
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
_keepUpdateWithServer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Obx(() {
|
||||||
|
if (_chatController.isLoading.isTrue) {
|
||||||
|
return const LinearProgressIndicator().animate().slideY();
|
||||||
|
} else {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ClipRect(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
||||||
|
child: SafeArea(
|
||||||
|
child: ChatMessageInput(
|
||||||
|
edit: _messageToEditing,
|
||||||
|
reply: _messageToReplying,
|
||||||
|
realm: widget.realm,
|
||||||
|
placeholder: placeholder,
|
||||||
|
channel: _channel!,
|
||||||
|
onSent: (Event item) {
|
||||||
|
setState(() {
|
||||||
|
_chatController.addPendingEvent(item);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onReset: () {
|
||||||
|
setState(() {
|
||||||
|
_messageToReplying = null;
|
||||||
|
_messageToEditing = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_subscription?.cancel();
|
_subscription?.cancel();
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
|
|||||||
final PagingController<int, Post> _pagingController =
|
final PagingController<int, Post> _pagingController =
|
||||||
PagingController(firstPageKey: 0);
|
PagingController(firstPageKey: 0);
|
||||||
|
|
||||||
getPosts(int pageKey) async {
|
_getPosts(int pageKey) async {
|
||||||
final PostProvider provider = Get.find();
|
final PostProvider provider = Get.find();
|
||||||
|
|
||||||
Response resp;
|
Response resp;
|
||||||
@ -49,7 +49,7 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_pagingController.addPageRequestListener(getPosts);
|
_pagingController.addPageRequestListener(_getPosts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -76,6 +76,9 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
|
|||||||
itemBuilder: (context, item, index) {
|
itemBuilder: (context, item, index) {
|
||||||
return PostOwnedListEntry(
|
return PostOwnedListEntry(
|
||||||
item: item,
|
item: item,
|
||||||
|
isFullContent: true,
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
@ -85,7 +88,13 @@ class _DraftBoxScreenState extends State<DraftBoxScreen> {
|
|||||||
noReact: true,
|
noReact: true,
|
||||||
),
|
),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value != null) _pagingController.refresh();
|
if (value is Future) {
|
||||||
|
value.then((_) {
|
||||||
|
_pagingController.refresh();
|
||||||
|
});
|
||||||
|
} else if (value != null) {
|
||||||
|
_pagingController.refresh();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
).paddingOnly(left: 12, right: 12, bottom: 4);
|
).paddingOnly(left: 12, right: 12, bottom: 4);
|
||||||
|
@ -77,7 +77,10 @@ class _FeedSearchScreenState extends State<FeedSearchScreen> {
|
|||||||
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
onRefresh: () => Future.sync(() => _pagingController.refresh()),
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
PostWarpedListWidget(controller: _pagingController),
|
PostWarpedListWidget(
|
||||||
|
controller: _pagingController,
|
||||||
|
onUpdate: () => _pagingController.refresh(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -4,7 +4,6 @@ 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';
|
||||||
@ -47,15 +46,20 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
showModalBottomSheet(
|
final value = await showModalBottomSheet(
|
||||||
useRootNavigator: true,
|
useRootNavigator: true,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => PostCreatePopup(
|
builder: (context) => const PostCreatePopup(),
|
||||||
controller: _postController,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
if (value is Future) {
|
||||||
|
value.then((_) {
|
||||||
|
_postController.reloadAllOver();
|
||||||
|
});
|
||||||
|
} else if (value != null) {
|
||||||
|
_postController.reloadAllOver();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: NestedScrollView(
|
body: NestedScrollView(
|
||||||
@ -100,6 +104,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
child: CustomScrollView(slivers: [
|
child: CustomScrollView(slivers: [
|
||||||
PostWarpedListWidget(
|
PostWarpedListWidget(
|
||||||
controller: _postController.pagingController,
|
controller: _postController.pagingController,
|
||||||
|
onUpdate: () => _postController.reloadAllOver(),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
@ -121,12 +126,10 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
|
|
||||||
class PostCreatePopup extends StatelessWidget {
|
class PostCreatePopup extends StatelessWidget {
|
||||||
final bool hideDraftBox;
|
final bool hideDraftBox;
|
||||||
final PostListController controller;
|
|
||||||
|
|
||||||
const PostCreatePopup({
|
const PostCreatePopup({
|
||||||
super.key,
|
super.key,
|
||||||
this.hideDraftBox = false,
|
this.hideDraftBox = false,
|
||||||
required this.controller,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -142,13 +145,14 @@ class PostCreatePopup extends StatelessWidget {
|
|||||||
icon: const Icon(Icons.post_add),
|
icon: const Icon(Icons.post_add),
|
||||||
label: 'postEditorModeStory'.tr,
|
label: 'postEditorModeStory'.tr,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(
|
||||||
AppRouter.instance.pushNamed(
|
context,
|
||||||
'postEditor',
|
AppRouter.instance.pushNamed(
|
||||||
extra: PostPublishArguments(postListController: controller),
|
'postEditor',
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
'mode': 0.toString(),
|
'mode': 0.toString(),
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -156,13 +160,14 @@ class PostCreatePopup extends StatelessWidget {
|
|||||||
icon: const Icon(Icons.description),
|
icon: const Icon(Icons.description),
|
||||||
label: 'postEditorModeArticle'.tr,
|
label: 'postEditorModeArticle'.tr,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(
|
||||||
AppRouter.instance.pushNamed(
|
context,
|
||||||
'postEditor',
|
AppRouter.instance.pushNamed(
|
||||||
extra: PostPublishArguments(postListController: controller),
|
'postEditor',
|
||||||
queryParameters: {
|
queryParameters: {
|
||||||
'mode': 1.toString(),
|
'mode': 1.toString(),
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -170,8 +175,10 @@ class PostCreatePopup extends StatelessWidget {
|
|||||||
icon: const Icon(Icons.drafts),
|
icon: const Icon(Icons.drafts),
|
||||||
label: 'draftBoxOpen'.tr,
|
label: 'draftBoxOpen'.tr,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(
|
||||||
AppRouter.instance.pushNamed('draftBox');
|
context,
|
||||||
|
AppRouter.instance.pushNamed('draftBox'),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
@ -59,8 +59,10 @@ class _PostDetailScreenState extends State<PostDetailScreen> {
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: PostItem(
|
child: PostItem(
|
||||||
item: item!,
|
item: item!,
|
||||||
isClickable: true,
|
isClickable: false,
|
||||||
|
isOverrideEmbedClickable: true,
|
||||||
isFullDate: true,
|
isFullDate: true,
|
||||||
|
isFullContent: true,
|
||||||
isShowReply: false,
|
isShowReply: false,
|
||||||
isContentSelectable: true,
|
isContentSelectable: true,
|
||||||
),
|
),
|
||||||
|
@ -5,7 +5,6 @@ import 'package:get/get.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:markdown_toolbar/markdown_toolbar.dart';
|
import 'package:markdown_toolbar/markdown_toolbar.dart';
|
||||||
import 'package:solian/controllers/post_editor_controller.dart';
|
import 'package:solian/controllers/post_editor_controller.dart';
|
||||||
import 'package:solian/controllers/post_list_controller.dart';
|
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
import 'package:solian/models/realm.dart';
|
import 'package:solian/models/realm.dart';
|
||||||
@ -23,14 +22,12 @@ class PostPublishArguments {
|
|||||||
final Post? reply;
|
final Post? reply;
|
||||||
final Post? repost;
|
final Post? repost;
|
||||||
final Realm? realm;
|
final Realm? realm;
|
||||||
final PostListController? postListController;
|
|
||||||
|
|
||||||
PostPublishArguments({
|
PostPublishArguments({
|
||||||
this.edit,
|
this.edit,
|
||||||
this.reply,
|
this.reply,
|
||||||
this.repost,
|
this.repost,
|
||||||
this.realm,
|
this.realm,
|
||||||
this.postListController,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +36,6 @@ class PostPublishScreen extends StatefulWidget {
|
|||||||
final Post? reply;
|
final Post? reply;
|
||||||
final Post? repost;
|
final Post? repost;
|
||||||
final Realm? realm;
|
final Realm? realm;
|
||||||
final PostListController? postListController;
|
|
||||||
final int mode;
|
final int mode;
|
||||||
|
|
||||||
const PostPublishScreen({
|
const PostPublishScreen({
|
||||||
@ -48,7 +44,6 @@ class PostPublishScreen extends StatefulWidget {
|
|||||||
this.reply,
|
this.reply,
|
||||||
this.repost,
|
this.repost,
|
||||||
this.realm,
|
this.realm,
|
||||||
this.postListController,
|
|
||||||
required this.mode,
|
required this.mode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -95,9 +90,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:solian/controllers/chat_events_controller.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/providers/theme_switcher.dart';
|
import 'package:solian/providers/theme_switcher.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
@ -93,6 +94,18 @@ class _SettingScreenState extends State<SettingScreen> {
|
|||||||
AppRouter.instance.pushNamed('about');
|
AppRouter.instance.pushNamed('about');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
TextButton(
|
||||||
|
style: const ButtonStyle(
|
||||||
|
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
||||||
|
),
|
||||||
|
child: Text('messageHistoryWipe'.tr),
|
||||||
|
onPressed: () {
|
||||||
|
final chatHistory = ChatEventController();
|
||||||
|
chatHistory.initialize().then((_) async {
|
||||||
|
await chatHistory.database.localEvents.wipeLocalEvents();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
).paddingSymmetric(horizontal: 12, vertical: 8),
|
).paddingSymmetric(horizontal: 12, vertical: 8),
|
||||||
],
|
],
|
||||||
|
@ -12,6 +12,7 @@ const i18nEnglish = {
|
|||||||
'draftBox': 'Draft Box',
|
'draftBox': 'Draft Box',
|
||||||
'more': 'More',
|
'more': 'More',
|
||||||
'share': 'Share',
|
'share': 'Share',
|
||||||
|
'shareNoUri': 'Share text content',
|
||||||
'feed': 'Feed',
|
'feed': 'Feed',
|
||||||
'unlink': 'Unlink',
|
'unlink': 'Unlink',
|
||||||
'feedSearch': 'Search Feed',
|
'feedSearch': 'Search Feed',
|
||||||
@ -333,6 +334,7 @@ const i18nEnglish = {
|
|||||||
'bsEstablishingConn': 'Establishing Connection',
|
'bsEstablishingConn': 'Establishing Connection',
|
||||||
'bsPreparingData': 'Preparing User Data',
|
'bsPreparingData': 'Preparing User Data',
|
||||||
'bsRegisteringPushNotify': 'Enabling Push Notifications',
|
'bsRegisteringPushNotify': 'Enabling Push Notifications',
|
||||||
|
'bsDismissibleErrorHint': 'Click anywhere to ignore this error',
|
||||||
'postShareContent':
|
'postShareContent':
|
||||||
'@content\n\n@username on the Solar Network\nCheck it out: @link',
|
'@content\n\n@username on the Solar Network\nCheck it out: @link',
|
||||||
'postShareSubject': '@username posted a post on the Solar Network',
|
'postShareSubject': '@username posted a post on the Solar Network',
|
||||||
@ -360,4 +362,16 @@ const i18nEnglish = {
|
|||||||
'stickerUploaderName': 'Name',
|
'stickerUploaderName': 'Name',
|
||||||
'stickerUploaderNameHint':
|
'stickerUploaderNameHint':
|
||||||
'A human-friendly name given to the user in the sticker selection interface.',
|
'A human-friendly name given to the user in the sticker selection interface.',
|
||||||
|
'readMore': 'Read more',
|
||||||
|
'attachmentUnload': 'Not Loaded',
|
||||||
|
'attachmentUnloadCaption':
|
||||||
|
'In order to save traffic, this attachment is not loaded automatically. Click it to start loading.',
|
||||||
|
'callStatusConnected': 'Connected',
|
||||||
|
'callStatusDisconnected': 'Disconnected',
|
||||||
|
'callStatusConnecting': 'Connecting',
|
||||||
|
'callStatusReconnected': 'Reconnecting',
|
||||||
|
'messageOutOfSync': 'May Out of Sync with Server',
|
||||||
|
'messageOutOfSyncCaption':
|
||||||
|
'Since the App has entered the background, there may be a time difference between the message list and the server. Click to Refresh.',
|
||||||
|
'messageHistoryWipe': 'Wipe local message history',
|
||||||
};
|
};
|
||||||
|
@ -20,6 +20,7 @@ const i18nSimplifiedChinese = {
|
|||||||
'draftBox': '草稿箱',
|
'draftBox': '草稿箱',
|
||||||
'more': '更多',
|
'more': '更多',
|
||||||
'share': '分享',
|
'share': '分享',
|
||||||
|
'shareNoUri': '分享文字内容',
|
||||||
'feed': '资讯',
|
'feed': '资讯',
|
||||||
'unlink': '移除链接',
|
'unlink': '移除链接',
|
||||||
'feedSearch': '搜索资讯',
|
'feedSearch': '搜索资讯',
|
||||||
@ -306,6 +307,7 @@ const i18nSimplifiedChinese = {
|
|||||||
'bsEstablishingConn': '部署连接中',
|
'bsEstablishingConn': '部署连接中',
|
||||||
'bsPreparingData': '正在准备用户资料',
|
'bsPreparingData': '正在准备用户资料',
|
||||||
'bsRegisteringPushNotify': '正在启用推送通知',
|
'bsRegisteringPushNotify': '正在启用推送通知',
|
||||||
|
'bsDismissibleErrorHint': '点击任意地方忽略此错误',
|
||||||
'postShareContent': '@content\n\n@username 在 Solar Network\n原帖地址:@link',
|
'postShareContent': '@content\n\n@username 在 Solar Network\n原帖地址:@link',
|
||||||
'postShareSubject': '@username 在 Solar Network 上发布了一篇帖子',
|
'postShareSubject': '@username 在 Solar Network 上发布了一篇帖子',
|
||||||
'themeColor': '全局主题色',
|
'themeColor': '全局主题色',
|
||||||
@ -328,4 +330,14 @@ const i18nSimplifiedChinese = {
|
|||||||
'stickerUploaderAliasHint': '将会在输入时使用和贴图包前缀组成占位符。',
|
'stickerUploaderAliasHint': '将会在输入时使用和贴图包前缀组成占位符。',
|
||||||
'stickerUploaderName': '贴图名称',
|
'stickerUploaderName': '贴图名称',
|
||||||
'stickerUploaderNameHint': '在贴图选择界面提供给用户的人类友好名称。',
|
'stickerUploaderNameHint': '在贴图选择界面提供给用户的人类友好名称。',
|
||||||
|
'readMore': '阅读更多',
|
||||||
|
'attachmentUnload': '附件未加载',
|
||||||
|
'attachmentUnloadCaption': '为了节省流量,本附件未自动加载,点一下来开始加载。',
|
||||||
|
'callStatusConnected': '已连接',
|
||||||
|
'callStatusDisconnected': '已断开',
|
||||||
|
'callStatusConnecting': '连接中',
|
||||||
|
'callStatusReconnected': '重连中',
|
||||||
|
'messageOutOfSync': '消息可能与服务器脱节',
|
||||||
|
'messageOutOfSyncCaption': '由于 App 进入后台,消息列表可能与服务器存在时差,点击刷新。',
|
||||||
|
'messageHistoryWipe': '清除消息记录',
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/exts.dart';
|
|
||||||
import 'package:solian/models/account.dart';
|
import 'package:solian/models/account.dart';
|
||||||
import 'package:solian/providers/account_status.dart';
|
import 'package:solian/providers/account_status.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
@ -18,23 +17,38 @@ class AccountProfilePopup extends StatefulWidget {
|
|||||||
|
|
||||||
class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
||||||
bool _isBusy = true;
|
bool _isBusy = true;
|
||||||
|
dynamic _hasError;
|
||||||
|
|
||||||
Account? _userinfo;
|
Account? _userinfo;
|
||||||
|
|
||||||
void _getUserinfo() async {
|
void _getUserinfo() async {
|
||||||
setState(() => _isBusy = true);
|
setState(() => _isBusy = true);
|
||||||
|
|
||||||
final client = ServiceFinder.configureClient('auth');
|
try {
|
||||||
final resp = await client.get('/users/${widget.name}');
|
final client = ServiceFinder.configureClient('auth');
|
||||||
if (resp.statusCode == 200) {
|
final resp = await client.get('/users/${widget.name}');
|
||||||
_userinfo = Account.fromJson(resp.body);
|
if (resp.statusCode == 200) {
|
||||||
setState(() => _isBusy = false);
|
setState(() {
|
||||||
} else {
|
_userinfo = Account.fromJson(resp.body);
|
||||||
context.showErrorDialog(resp.bodyString);
|
_isBusy = false;
|
||||||
Navigator.pop(context);
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_hasError = resp.bodyString;
|
||||||
|
_isBusy = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_hasError = e;
|
||||||
|
_isBusy = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Color get _unFocusColor =>
|
||||||
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -43,13 +57,35 @@ class _AccountProfilePopupState extends State<AccountProfilePopup> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_isBusy || _userinfo == null) {
|
if (_isBusy) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: MediaQuery.of(context).size.height * 0.75,
|
height: MediaQuery.of(context).size.height * 0.75,
|
||||||
child: const Center(child: CircularProgressIndicator()),
|
child: const Center(child: CircularProgressIndicator()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_hasError != null) {
|
||||||
|
return SizedBox(
|
||||||
|
height: MediaQuery.of(context).size.height * 0.75,
|
||||||
|
width: double.infinity,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.cancel, size: 24),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
_hasError.toString(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: _unFocusColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: MediaQuery.of(context).size.height * 0.75,
|
height: MediaQuery.of(context).size.height * 0.75,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -597,30 +597,39 @@ class _AttachmentEditorPopupState extends State<AttachmentEditorPopup> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Row(
|
||||||
'attachmentAdd'.tr,
|
children: [
|
||||||
style: Theme.of(context).textTheme.headlineSmall,
|
Expanded(
|
||||||
maxLines: 2,
|
child: Text(
|
||||||
overflow: TextOverflow.fade,
|
'attachmentAdd'.tr,
|
||||||
|
style:
|
||||||
|
Theme.of(context).textTheme.headlineSmall,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Obx(() {
|
||||||
|
if (_uploadController.isUploading.value) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
value: _uploadController
|
||||||
|
.progressOfUpload.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox();
|
||||||
|
}),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
|
||||||
Obx(() {
|
|
||||||
if (_uploadController.isUploading.value) {
|
|
||||||
return SizedBox(
|
|
||||||
width: 18,
|
|
||||||
height: 18,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2.5,
|
|
||||||
value: _uploadController.progressOfUpload.value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return const SizedBox();
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 20),
|
||||||
Text('attachmentAutoUpload'.tr),
|
Text('attachmentAutoUpload'.tr),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Switch(
|
Switch(
|
||||||
|
@ -70,7 +70,7 @@ class _AttachmentFullScreenState extends State<AttachmentFullScreen> {
|
|||||||
'/attachments/${widget.item.id}',
|
'/attachments/${widget.item.id}',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (PlatformInfo.isWeb) {
|
if (PlatformInfo.isWeb || PlatformInfo.isDesktop) {
|
||||||
await launchUrlString(url);
|
await launchUrlString(url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import 'package:media_kit_video/media_kit_video.dart';
|
|||||||
import 'package:solian/models/attachment.dart';
|
import 'package:solian/models/attachment.dart';
|
||||||
import 'package:solian/platform.dart';
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/services.dart';
|
import 'package:solian/services.dart';
|
||||||
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class AttachmentItem extends StatefulWidget {
|
class AttachmentItem extends StatefulWidget {
|
||||||
@ -213,8 +214,12 @@ class _AttachmentItemImage extends StatelessWidget {
|
|||||||
|
|
||||||
class _AttachmentItemVideo extends StatefulWidget {
|
class _AttachmentItemVideo extends StatefulWidget {
|
||||||
final Attachment item;
|
final Attachment item;
|
||||||
|
final bool autoload;
|
||||||
|
|
||||||
const _AttachmentItemVideo({required this.item});
|
const _AttachmentItemVideo({
|
||||||
|
required this.item,
|
||||||
|
this.autoload = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_AttachmentItemVideo> createState() => _AttachmentItemVideoState();
|
State<_AttachmentItemVideo> createState() => _AttachmentItemVideoState();
|
||||||
@ -226,21 +231,65 @@ class _AttachmentItemVideoState extends State<_AttachmentItemVideo> {
|
|||||||
);
|
);
|
||||||
late final _controller = VideoController(_player);
|
late final _controller = VideoController(_player);
|
||||||
|
|
||||||
|
bool _showContent = false;
|
||||||
|
|
||||||
|
void _startLoad() {
|
||||||
|
_player.open(
|
||||||
|
Media(ServiceFinder.buildUrl('files', '/attachments/${widget.item.id}')),
|
||||||
|
play: false,
|
||||||
|
);
|
||||||
|
setState(() => _showContent = true);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_player.open(
|
_showContent = widget.autoload;
|
||||||
Media(
|
|
||||||
ServiceFinder.buildUrl('files', '/attachments/${widget.item.id}'),
|
|
||||||
),
|
|
||||||
play: false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final ratio = widget.item.metadata?['ratio'] ?? 16 / 9;
|
||||||
|
if (!_showContent) {
|
||||||
|
return GestureDetector(
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: ratio,
|
||||||
|
child: CenteredContainer(
|
||||||
|
maxWidth: 280,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.not_started,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'attachmentUnload'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'attachmentUnloadCaption'.tr,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
_startLoad();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Video(
|
return Video(
|
||||||
aspectRatio: widget.item.metadata?['ratio'] ?? 16 / 9,
|
aspectRatio: ratio,
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,14 @@ import 'dart:ui';
|
|||||||
|
|
||||||
import 'package:carousel_slider/carousel_slider.dart';
|
import 'package:carousel_slider/carousel_slider.dart';
|
||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart' hide CarouselController;
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/models/attachment.dart';
|
import 'package:solian/models/attachment.dart';
|
||||||
import 'package:solian/widgets/attachments/attachment_item.dart';
|
import 'package:solian/widgets/attachments/attachment_item.dart';
|
||||||
import 'package:solian/providers/content/attachment.dart';
|
import 'package:solian/providers/content/attachment.dart';
|
||||||
import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
|
import 'package:solian/widgets/attachments/attachment_fullscreen.dart';
|
||||||
|
import 'package:solian/widgets/sized_container.dart';
|
||||||
|
|
||||||
class AttachmentList extends StatefulWidget {
|
class AttachmentList extends StatefulWidget {
|
||||||
final String parentId;
|
final String parentId;
|
||||||
@ -272,35 +273,33 @@ class AttachmentListEntry extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (item!.isMature && !showMature)
|
if (item!.isMature && !showMature)
|
||||||
Center(
|
CenteredContainer(
|
||||||
child: Container(
|
maxWidth: 280,
|
||||||
constraints: const BoxConstraints(maxWidth: 280),
|
child: Column(
|
||||||
child: Column(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
children: [
|
const Icon(
|
||||||
const Icon(
|
Icons.visibility_off,
|
||||||
Icons.visibility_off,
|
color: Colors.white,
|
||||||
color: Colors.white,
|
size: 32,
|
||||||
size: 32,
|
),
|
||||||
|
if (!isDense) const SizedBox(height: 8),
|
||||||
|
if (!isDense)
|
||||||
|
Text(
|
||||||
|
'matureContent'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (!isDense) const SizedBox(height: 8),
|
if (!isDense)
|
||||||
if (!isDense)
|
Text(
|
||||||
Text(
|
'matureContentCaption'.tr,
|
||||||
'matureContent'.tr,
|
style: const TextStyle(color: Colors.white),
|
||||||
style: const TextStyle(
|
textAlign: TextAlign.center,
|
||||||
color: Colors.white,
|
),
|
||||||
fontWeight: FontWeight.bold,
|
],
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!isDense)
|
|
||||||
Text(
|
|
||||||
'matureContentCaption'.tr,
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -322,3 +321,50 @@ class AttachmentListEntry extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AttachmentSelfContainedEntry extends StatefulWidget {
|
||||||
|
final int id;
|
||||||
|
final String parentId;
|
||||||
|
final bool isDense;
|
||||||
|
|
||||||
|
const AttachmentSelfContainedEntry({
|
||||||
|
super.key,
|
||||||
|
required this.id,
|
||||||
|
required this.parentId,
|
||||||
|
this.isDense = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AttachmentSelfContainedEntry> createState() =>
|
||||||
|
_AttachmentSelfContainedEntryState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AttachmentSelfContainedEntryState
|
||||||
|
extends State<AttachmentSelfContainedEntry> {
|
||||||
|
bool _showMature = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final AttachmentProvider attachments = Get.find();
|
||||||
|
|
||||||
|
return FutureBuilder(
|
||||||
|
future: attachments.getMetadata(widget.id),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return const Text('Loading...');
|
||||||
|
}
|
||||||
|
|
||||||
|
return AttachmentListEntry(
|
||||||
|
item: snapshot.data,
|
||||||
|
isDense: widget.isDense,
|
||||||
|
parentId: widget.parentId,
|
||||||
|
showMature: _showMature,
|
||||||
|
showBorder: true,
|
||||||
|
onReveal: (value) {
|
||||||
|
setState(() => _showMature = value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -159,8 +159,9 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
|||||||
}
|
}
|
||||||
var track = await LocalVideoTrack.createScreenShareTrack(
|
var track = await LocalVideoTrack.createScreenShareTrack(
|
||||||
ScreenShareCaptureOptions(
|
ScreenShareCaptureOptions(
|
||||||
|
captureScreenAudio: true,
|
||||||
sourceId: source.id,
|
sourceId: source.id,
|
||||||
maxFrameRate: 15.0,
|
maxFrameRate: 30.0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await _participant.publishVideoTrack(track);
|
await _participant.publishVideoTrack(track);
|
||||||
@ -174,6 +175,7 @@ class _ControlsWidgetState extends State<ControlsWidget> {
|
|||||||
var track = await LocalVideoTrack.createScreenShareTrack(
|
var track = await LocalVideoTrack.createScreenShareTrack(
|
||||||
const ScreenShareCaptureOptions(
|
const ScreenShareCaptureOptions(
|
||||||
useiOSBroadcastExtension: true,
|
useiOSBroadcastExtension: true,
|
||||||
|
captureScreenAudio: true,
|
||||||
maxFrameRate: 30.0,
|
maxFrameRate: 30.0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -50,10 +50,10 @@ class ChatEvent extends StatelessWidget {
|
|||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.attachment,
|
Icons.file_copy,
|
||||||
size: 18,
|
size: 15,
|
||||||
color: unFocusColor,
|
color: unFocusColor,
|
||||||
).paddingOnly(right: 6),
|
).paddingOnly(right: 5),
|
||||||
Text(
|
Text(
|
||||||
'attachmentHint'.trParams(
|
'attachmentHint'.trParams(
|
||||||
{'count': attachments.length.toString()},
|
{'count': attachments.length.toString()},
|
||||||
@ -221,6 +221,8 @@ class ChatEvent extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
_buildContent().paddingOnly(left: 0.5),
|
_buildContent().paddingOnly(left: 0.5),
|
||||||
|
_buildAttachment(context, isMinimal: true)
|
||||||
|
.paddingOnly(left: 0),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -31,7 +31,11 @@ class ChatEventMessageActionLog extends StatelessWidget {
|
|||||||
).paddingOnly(
|
).paddingOnly(
|
||||||
left: isQuote ? 0 : (isMerged ? 64 : 12),
|
left: isQuote ? 0 : (isMerged ? 64 : 12),
|
||||||
top: 2,
|
top: 2,
|
||||||
bottom: isHasMerged ? 2 : 0,
|
bottom: isQuote
|
||||||
|
? 0
|
||||||
|
: isHasMerged
|
||||||
|
? 2
|
||||||
|
: 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -23,16 +23,19 @@ class ChatEventMessage extends StatelessWidget {
|
|||||||
final body = EventMessageBody.fromJson(item.body);
|
final body = EventMessageBody.fromJson(item.body);
|
||||||
final hasAttachment = body.attachments?.isNotEmpty ?? false;
|
final hasAttachment = body.attachments?.isNotEmpty ?? false;
|
||||||
|
|
||||||
if (body.text.isEmpty && hasAttachment) {
|
if (body.text.isEmpty &&
|
||||||
|
hasAttachment &&
|
||||||
|
!isContentPreviewing &&
|
||||||
|
!isQuote) {
|
||||||
final unFocusColor =
|
final unFocusColor =
|
||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
Theme.of(context).colorScheme.onSurface.withOpacity(0.75);
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.attachment,
|
Icons.file_copy,
|
||||||
size: 18,
|
size: 15,
|
||||||
color: unFocusColor,
|
color: unFocusColor,
|
||||||
).paddingOnly(right: 6),
|
).paddingOnly(right: 5),
|
||||||
Text(
|
Text(
|
||||||
'attachmentHint'.trParams(
|
'attachmentHint'.trParams(
|
||||||
{'count': body.attachments?.length.toString() ?? 0.toString()},
|
{'count': body.attachments?.length.toString() ?? 0.toString()},
|
||||||
@ -43,7 +46,11 @@ class ChatEventMessage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return MarkdownTextContent(content: body.text);
|
return MarkdownTextContent(
|
||||||
|
parentId: 'm${item.id}',
|
||||||
|
isSelectable: true,
|
||||||
|
content: body.text,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody(BuildContext context) {
|
Widget _buildBody(BuildContext context) {
|
||||||
@ -63,7 +70,9 @@ class ChatEventMessage extends StatelessWidget {
|
|||||||
left: isQuote ? 0 : 12,
|
left: isQuote ? 0 : 12,
|
||||||
right: isQuote ? 0 : 12,
|
right: isQuote ? 0 : 12,
|
||||||
top: body.quoteEvent == null ? 2 : 0,
|
top: body.quoteEvent == null ? 2 : 0,
|
||||||
bottom: hasAttachment && !isContentPreviewing ? 4 : (isHasMerged ? 2 : 0),
|
bottom: hasAttachment && !isContentPreviewing && !isQuote
|
||||||
|
? 4
|
||||||
|
: (isHasMerged ? 2 : 0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,36 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/account.dart';
|
import 'package:solian/models/account.dart';
|
||||||
import 'package:solian/models/channel.dart';
|
import 'package:solian/models/channel.dart';
|
||||||
import 'package:solian/models/event.dart';
|
import 'package:solian/models/event.dart';
|
||||||
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/attachment_uploader.dart';
|
import 'package:solian/providers/attachment_uploader.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
|
import 'package:solian/providers/stickers.dart';
|
||||||
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
import 'package:solian/widgets/attachments/attachment_editor.dart';
|
import 'package:solian/widgets/attachments/attachment_editor.dart';
|
||||||
import 'package:solian/widgets/chat/chat_event.dart';
|
import 'package:solian/widgets/chat/chat_event.dart';
|
||||||
import 'package:badges/badges.dart' as badges;
|
import 'package:badges/badges.dart' as badges;
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
class ChatMessageSuggestion {
|
||||||
|
final String type;
|
||||||
|
final Widget leading;
|
||||||
|
final String display;
|
||||||
|
final String content;
|
||||||
|
|
||||||
|
ChatMessageSuggestion({
|
||||||
|
required this.type,
|
||||||
|
required this.leading,
|
||||||
|
required this.display,
|
||||||
|
required this.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
class ChatMessageInput extends StatefulWidget {
|
class ChatMessageInput extends StatefulWidget {
|
||||||
final Event? edit;
|
final Event? edit;
|
||||||
final Event? reply;
|
final Event? reply;
|
||||||
@ -110,12 +129,14 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
|
|
||||||
client = auth.configureClient('messaging');
|
client = auth.configureClient('messaging');
|
||||||
|
|
||||||
|
if (_textController.text.trim().isEmpty && _attachments.isEmpty) return;
|
||||||
|
|
||||||
const uuid = Uuid();
|
const uuid = Uuid();
|
||||||
final payload = {
|
final payload = {
|
||||||
'uuid': uuid.v4(),
|
'uuid': uuid.v4(),
|
||||||
'type': _editTo == null ? 'messages.new' : 'messages.edit',
|
'type': _editTo == null ? 'messages.new' : 'messages.edit',
|
||||||
'body': {
|
'body': {
|
||||||
'text': _textController.text,
|
'text': _textController.text.trim(),
|
||||||
'algorithm': 'plain',
|
'algorithm': 'plain',
|
||||||
'attachments': List.from(_attachments),
|
'attachments': List.from(_attachments),
|
||||||
'related_users': [
|
'related_users': [
|
||||||
@ -156,7 +177,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
widget.onSent(message);
|
widget.onSent(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetInput();
|
_resetInput();
|
||||||
|
|
||||||
if (_editTo != null) {
|
if (_editTo != null) {
|
||||||
resp = await client.put(
|
resp = await client.put(
|
||||||
@ -175,7 +196,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetInput() {
|
void _resetInput() {
|
||||||
if (widget.onReset != null) widget.onReset!();
|
if (widget.onReset != null) widget.onReset!();
|
||||||
_editTo = null;
|
_editTo = null;
|
||||||
_replyTo = null;
|
_replyTo = null;
|
||||||
@ -184,11 +205,13 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
void syncWidget() {
|
void _syncWidget() {
|
||||||
if (widget.edit != null && widget.edit!.type.startsWith('messages')) {
|
if (widget.edit != null && widget.edit!.type.startsWith('messages')) {
|
||||||
final body = EventMessageBody.fromJson(widget.edit!.body);
|
final body = EventMessageBody.fromJson(widget.edit!.body);
|
||||||
_editTo = widget.edit!;
|
_editTo = widget.edit!;
|
||||||
_textController.text = body.text;
|
_textController.text = body.text;
|
||||||
|
_attachments.addAll(
|
||||||
|
widget.edit!.body['attachments']?.cast<int>() ?? List.empty());
|
||||||
}
|
}
|
||||||
if (widget.reply != null) {
|
if (widget.reply != null) {
|
||||||
_replyTo = widget.reply!;
|
_replyTo = widget.reply!;
|
||||||
@ -197,9 +220,52 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSuggestion(ChatMessageSuggestion suggestion) {
|
||||||
|
return ListTile(
|
||||||
|
leading: suggestion.leading,
|
||||||
|
title: Text(suggestion.display),
|
||||||
|
subtitle: Text(suggestion.content),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _insertSuggestion(ChatMessageSuggestion suggestion) {
|
||||||
|
final replaceText =
|
||||||
|
_textController.text.substring(0, _textController.selection.baseOffset);
|
||||||
|
var startText = '';
|
||||||
|
final afterText = replaceText == _textController.text
|
||||||
|
? ''
|
||||||
|
: _textController.text
|
||||||
|
.substring(_textController.selection.baseOffset + 1);
|
||||||
|
var insertText = '';
|
||||||
|
|
||||||
|
if (suggestion.type == 'emotes') {
|
||||||
|
insertText = suggestion.content;
|
||||||
|
startText = replaceText.replaceFirstMapped(
|
||||||
|
RegExp(r':(?:([-\w]+)~)?([-\w]+)$'),
|
||||||
|
(Match m) => insertText,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suggestion.type == 'users') {
|
||||||
|
insertText = suggestion.content;
|
||||||
|
startText = replaceText.replaceFirstMapped(
|
||||||
|
RegExp(r'(?:\s|^)@([-\w]+)$'),
|
||||||
|
(Match m) => insertText,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insertText.isNotEmpty && startText.isNotEmpty) {
|
||||||
|
_textController.text = startText + afterText;
|
||||||
|
_textController.selection = TextSelection(
|
||||||
|
baseOffset: startText.length,
|
||||||
|
extentOffset: startText.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant ChatMessageInput oldWidget) {
|
void didUpdateWidget(covariant ChatMessageInput oldWidget) {
|
||||||
syncWidget();
|
_syncWidget();
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +273,7 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final notifyBannerActions = [
|
final notifyBannerActions = [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: resetInput,
|
onPressed: _resetInput,
|
||||||
child: Text('cancel'.tr),
|
child: Text('cancel'.tr),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
@ -251,21 +317,101 @@ class _ChatMessageInputState extends State<ChatMessageInput> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TypeAheadField<ChatMessageSuggestion>(
|
||||||
|
direction: VerticalDirection.up,
|
||||||
|
hideOnEmpty: true,
|
||||||
|
hideOnLoading: true,
|
||||||
controller: _textController,
|
controller: _textController,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
maxLines: null,
|
hideOnSelect: false,
|
||||||
autocorrect: true,
|
debounceDuration: const Duration(milliseconds: 500),
|
||||||
keyboardType: TextInputType.text,
|
onSelected: (value) {
|
||||||
decoration: InputDecoration.collapsed(
|
_insertSuggestion(value);
|
||||||
hintText: widget.placeholder ??
|
},
|
||||||
'messageInputPlaceholder'.trParams(
|
itemBuilder: (context, item) => _buildSuggestion(item),
|
||||||
{'channel': '#${widget.channel.alias}'},
|
builder: (context, controller, focusNode) {
|
||||||
),
|
return TextField(
|
||||||
),
|
controller: _textController,
|
||||||
onSubmitted: (_) => _sendMessage(),
|
focusNode: _focusNode,
|
||||||
onTapOutside: (_) =>
|
maxLines: null,
|
||||||
FocusManager.instance.primaryFocus?.unfocus(),
|
autocorrect: true,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
decoration: InputDecoration.collapsed(
|
||||||
|
hintText: widget.placeholder ??
|
||||||
|
'messageInputPlaceholder'.trParams({
|
||||||
|
'channel': '#${widget.channel.alias}',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
onSubmitted: (_) => _sendMessage(),
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
suggestionsCallback: (search) async {
|
||||||
|
final searchText = _textController.text
|
||||||
|
.substring(0, _textController.selection.baseOffset);
|
||||||
|
|
||||||
|
final emojiMatch = RegExp(r':(?:([-\w]+)~)?([-\w]+)$')
|
||||||
|
.firstMatch(searchText);
|
||||||
|
if (emojiMatch != null) {
|
||||||
|
final StickerProvider stickers = Get.find();
|
||||||
|
final emoteSearch = emojiMatch[2]!;
|
||||||
|
return stickers.availableStickers
|
||||||
|
.where(
|
||||||
|
(x) => x.textWarpedPlaceholder
|
||||||
|
.toUpperCase()
|
||||||
|
.contains(emoteSearch.toUpperCase()),
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(x) => ChatMessageSuggestion(
|
||||||
|
type: 'emotes',
|
||||||
|
leading: PlatformInfo.canCacheImage
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: x.imageUrl,
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
)
|
||||||
|
: Image.network(
|
||||||
|
x.imageUrl,
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
),
|
||||||
|
display: x.name,
|
||||||
|
content: x.textWarpedPlaceholder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
final userMatch =
|
||||||
|
RegExp(r'(?:\s|^)@([-\w]+)$').firstMatch(searchText);
|
||||||
|
if (userMatch != null) {
|
||||||
|
final userSearch = userMatch[1]!.toLowerCase();
|
||||||
|
final AuthProvider auth = Get.find();
|
||||||
|
|
||||||
|
final client = auth.configureClient('auth');
|
||||||
|
final resp = await client.get(
|
||||||
|
'/users/search?probe=$userSearch',
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<Account> result = resp.body
|
||||||
|
.map((x) => Account.fromJson(x))
|
||||||
|
.toList()
|
||||||
|
.cast<Account>();
|
||||||
|
return result
|
||||||
|
.map(
|
||||||
|
(x) => ChatMessageSuggestion(
|
||||||
|
type: 'users',
|
||||||
|
leading: AccountAvatar(content: x.avatar),
|
||||||
|
display: x.nick,
|
||||||
|
content: '@${x.name}',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
@ -1,24 +1,33 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_markdown_selectionarea/flutter_markdown.dart';
|
import 'package:flutter_markdown_selectionarea/flutter_markdown.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:markdown/markdown.dart' as markdown;
|
import 'package:markdown/markdown.dart' as markdown;
|
||||||
import 'package:markdown/markdown.dart';
|
import 'package:markdown/markdown.dart';
|
||||||
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/stickers.dart';
|
import 'package:solian/providers/stickers.dart';
|
||||||
|
import 'package:solian/widgets/attachments/attachment_list.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
import 'account/account_profile_popup.dart';
|
import 'account/account_profile_popup.dart';
|
||||||
|
|
||||||
class MarkdownTextContent extends StatelessWidget {
|
class MarkdownTextContent extends StatelessWidget {
|
||||||
final String content;
|
final String content;
|
||||||
|
final String parentId;
|
||||||
final bool isSelectable;
|
final bool isSelectable;
|
||||||
|
|
||||||
const MarkdownTextContent({
|
const MarkdownTextContent({
|
||||||
super.key,
|
super.key,
|
||||||
required this.content,
|
required this.content,
|
||||||
|
required this.parentId,
|
||||||
this.isSelectable = false,
|
this.isSelectable = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
|
final emojiRegex = RegExp(r':([-\w]+):');
|
||||||
|
final emojiMatch = emojiRegex.allMatches(content);
|
||||||
|
final isOnlyEmoji = content.replaceAll(emojiRegex, '').trim().isEmpty;
|
||||||
|
|
||||||
return Markdown(
|
return Markdown(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
@ -42,6 +51,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
_UserNameCardInlineSyntax(),
|
_UserNameCardInlineSyntax(),
|
||||||
_CustomEmoteInlineSyntax(),
|
_CustomEmoteInlineSyntax(),
|
||||||
markdown.EmojiSyntax(),
|
markdown.EmojiSyntax(),
|
||||||
|
markdown.AutolinkSyntax(),
|
||||||
markdown.AutolinkExtensionSyntax(),
|
markdown.AutolinkExtensionSyntax(),
|
||||||
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
|
||||||
],
|
],
|
||||||
@ -70,6 +80,78 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
mode: LaunchMode.externalApplication,
|
mode: LaunchMode.externalApplication,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
imageBuilder: (uri, title, alt) {
|
||||||
|
var url = uri.toString();
|
||||||
|
double? width, height;
|
||||||
|
BoxFit? fit;
|
||||||
|
if (url.startsWith('solink://')) {
|
||||||
|
final segments = url.replaceFirst('solink://', '').split('/');
|
||||||
|
switch (segments[0]) {
|
||||||
|
case 'stickers':
|
||||||
|
double radius = 8;
|
||||||
|
final StickerProvider sticker = Get.find();
|
||||||
|
url = sticker.aliasImageMapping[segments[1].toUpperCase()]!;
|
||||||
|
if (emojiMatch.length <= 1 && isOnlyEmoji) {
|
||||||
|
width = 128;
|
||||||
|
height = 128;
|
||||||
|
} else if (emojiMatch.length <= 3 && isOnlyEmoji) {
|
||||||
|
width = 32;
|
||||||
|
height = 32;
|
||||||
|
} else {
|
||||||
|
radius = 4;
|
||||||
|
width = 16;
|
||||||
|
height = 16;
|
||||||
|
}
|
||||||
|
fit = BoxFit.contain;
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(radius)),
|
||||||
|
child: Container(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
child: PlatformInfo.canCacheImage
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: url,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: fit,
|
||||||
|
)
|
||||||
|
: Image.network(
|
||||||
|
url,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: fit,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).paddingSymmetric(vertical: 4);
|
||||||
|
case 'attachments':
|
||||||
|
const radius = BorderRadius.all(Radius.circular(8));
|
||||||
|
return LimitedBox(
|
||||||
|
maxHeight: MediaQuery.of(context).size.width,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: radius,
|
||||||
|
child: AttachmentSelfContainedEntry(
|
||||||
|
isDense: true,
|
||||||
|
parentId: parentId,
|
||||||
|
id: int.parse(segments[1]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).paddingSymmetric(vertical: 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PlatformInfo.canCacheImage
|
||||||
|
? CachedNetworkImage(
|
||||||
|
imageUrl: url,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: fit,
|
||||||
|
)
|
||||||
|
: Image.network(
|
||||||
|
url,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: fit,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,19 +181,19 @@ class _UserNameCardInlineSyntax extends InlineSyntax {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CustomEmoteInlineSyntax extends InlineSyntax {
|
class _CustomEmoteInlineSyntax extends InlineSyntax {
|
||||||
_CustomEmoteInlineSyntax() : super(r':([a-z0-9_+-]+):');
|
_CustomEmoteInlineSyntax() : super(r':([-\w]+):');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool onMatch(markdown.InlineParser parser, Match match) {
|
bool onMatch(markdown.InlineParser parser, Match match) {
|
||||||
final StickerProvider sticker = Get.find();
|
final StickerProvider sticker = Get.find();
|
||||||
final alias = match[1]!;
|
final alias = match[1]!.toUpperCase();
|
||||||
if (sticker.aliasImageMapping[alias] == null) {
|
if (sticker.aliasImageMapping[alias] == null) {
|
||||||
parser.advanceBy(1);
|
parser.advanceBy(1);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final element = markdown.Element.empty('img');
|
final element = markdown.Element.empty('img');
|
||||||
element.attributes['src'] = sticker.aliasImageMapping[alias]!;
|
element.attributes['src'] = 'solink://stickers/$alias';
|
||||||
parser.addNode(element);
|
parser.addNode(element);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -4,17 +4,17 @@ import 'package:get/utils.dart';
|
|||||||
abstract class AppNavigation {
|
abstract class AppNavigation {
|
||||||
static List<AppNavigationDestination> destinations = [
|
static List<AppNavigationDestination> destinations = [
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: const Icon(Icons.home),
|
icon: Icons.home,
|
||||||
label: 'home'.tr,
|
label: 'home'.tr,
|
||||||
page: 'home',
|
page: 'home',
|
||||||
),
|
),
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: const Icon(Icons.workspaces),
|
icon: Icons.workspaces,
|
||||||
label: 'realms'.tr,
|
label: 'realms'.tr,
|
||||||
page: 'realms',
|
page: 'realms',
|
||||||
),
|
),
|
||||||
AppNavigationDestination(
|
AppNavigationDestination(
|
||||||
icon: const Icon(Icons.forum),
|
icon: Icons.forum,
|
||||||
label: 'channelTypeDirect'.tr,
|
label: 'channelTypeDirect'.tr,
|
||||||
page: 'chat',
|
page: 'chat',
|
||||||
),
|
),
|
||||||
@ -25,7 +25,7 @@ abstract class AppNavigation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AppNavigationDestination {
|
class AppNavigationDestination {
|
||||||
final Widget icon;
|
final IconData icon;
|
||||||
final String label;
|
final String label;
|
||||||
final String page;
|
final String page;
|
||||||
|
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:solian/models/account_status.dart';
|
import 'package:solian/models/account_status.dart';
|
||||||
import 'package:solian/providers/account_status.dart';
|
import 'package:solian/providers/account_status.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/providers/content/channel.dart';
|
|
||||||
import 'package:solian/providers/relation.dart';
|
import 'package:solian/providers/relation.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/shells/root_shell.dart';
|
import 'package:solian/shells/root_shell.dart';
|
||||||
import 'package:solian/theme.dart';
|
import 'package:solian/theme.dart';
|
||||||
import 'package:solian/widgets/account/account_avatar.dart';
|
import 'package:solian/widgets/account/account_avatar.dart';
|
||||||
import 'package:solian/widgets/account/account_status_action.dart';
|
import 'package:solian/widgets/account/account_status_action.dart';
|
||||||
import 'package:solian/widgets/channel/channel_list.dart';
|
|
||||||
import 'package:solian/widgets/navigation/app_navigation.dart';
|
import 'package:solian/widgets/navigation/app_navigation.dart';
|
||||||
import 'package:badges/badges.dart' as badges;
|
import 'package:badges/badges.dart' as badges;
|
||||||
|
import 'package:solian/widgets/navigation/app_navigation_regions.dart';
|
||||||
|
|
||||||
class AppNavigationDrawer extends StatefulWidget {
|
class AppNavigationDrawer extends StatefulWidget {
|
||||||
final String? routeName;
|
final String? routeName;
|
||||||
@ -24,12 +25,8 @@ class AppNavigationDrawer extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
||||||
int? _selectedIndex = 0;
|
|
||||||
|
|
||||||
AccountStatus? _accountStatus;
|
AccountStatus? _accountStatus;
|
||||||
|
|
||||||
late final ChannelProvider _channels;
|
|
||||||
|
|
||||||
Future<void> _getStatus() async {
|
Future<void> _getStatus() async {
|
||||||
final StatusProvider provider = Get.find();
|
final StatusProvider provider = Get.find();
|
||||||
|
|
||||||
@ -41,201 +38,165 @@ class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _detectSelectedIndex() {
|
|
||||||
if (widget.routeName == null) return;
|
|
||||||
|
|
||||||
final nameList = AppNavigation.destinations.map((x) => x.page).toList();
|
|
||||||
final idx = nameList.indexOf(widget.routeName!);
|
|
||||||
|
|
||||||
_selectedIndex = idx != -1 ? idx : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _closeDrawer() {
|
void _closeDrawer() {
|
||||||
rootScaffoldKey.currentState!.closeDrawer();
|
rootScaffoldKey.currentState!.closeDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSettingButton() {
|
|
||||||
return IconButton(
|
|
||||||
icon: const Icon(Icons.settings),
|
|
||||||
onPressed: () {
|
|
||||||
AppRouter.instance.pushNamed('settings');
|
|
||||||
setState(() => _selectedIndex = null);
|
|
||||||
_closeDrawer();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_channels = Get.find();
|
|
||||||
_detectSelectedIndex();
|
|
||||||
_getStatus();
|
_getStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
_detectSelectedIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final AuthProvider auth = Get.find();
|
final AuthProvider auth = Get.find();
|
||||||
|
|
||||||
return NavigationDrawer(
|
return Drawer(
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
SolianTheme.isLargeScreen(context) ? Colors.transparent : null,
|
SolianTheme.isLargeScreen(context) ? Colors.transparent : null,
|
||||||
selectedIndex: _selectedIndex,
|
child: SafeArea(
|
||||||
onDestinationSelected: (idx) {
|
bottom: false,
|
||||||
setState(() => _selectedIndex = idx);
|
child: Column(
|
||||||
AppRouter.instance.goNamed(AppNavigation.destinations[idx].page);
|
children: [
|
||||||
_closeDrawer();
|
Obx(() {
|
||||||
},
|
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
|
||||||
children: [
|
return ListTile(
|
||||||
Obx(() {
|
contentPadding: const EdgeInsets.symmetric(horizontal: 28),
|
||||||
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
|
leading: const Icon(Icons.account_circle),
|
||||||
return ListTile(
|
title: Text('guest'.tr),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 28),
|
subtitle: Text('unsignedIn'.tr),
|
||||||
leading: const Icon(Icons.account_circle),
|
onTap: () {
|
||||||
title: Text('guest'.tr),
|
AppRouter.instance.goNamed('account');
|
||||||
subtitle: Text('unsignedIn'.tr),
|
_closeDrawer();
|
||||||
trailing: _buildSettingButton(),
|
},
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed('account');
|
|
||||||
setState(() => _selectedIndex = null);
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
title: Text(
|
|
||||||
auth.userProfile.value!['nick'],
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
subtitle: Builder(
|
|
||||||
builder: (context) {
|
|
||||||
if (_accountStatus == null) {
|
|
||||||
return Text('loading'.tr);
|
|
||||||
}
|
|
||||||
final info = StatusProvider.determineStatus(
|
|
||||||
_accountStatus!,
|
|
||||||
);
|
);
|
||||||
return Text(
|
}
|
||||||
info.$3,
|
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: const EdgeInsets.only(left: 20, right: 20),
|
||||||
|
title: Text(
|
||||||
|
auth.userProfile.value!['nick'],
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
);
|
),
|
||||||
},
|
subtitle: Builder(
|
||||||
),
|
builder: (context) {
|
||||||
leading: Obx(() {
|
if (_accountStatus == null) {
|
||||||
final statusBadgeColor = _accountStatus != null
|
return Text('loading'.tr);
|
||||||
? StatusProvider.determineStatus(
|
}
|
||||||
|
final info = StatusProvider.determineStatus(
|
||||||
_accountStatus!,
|
_accountStatus!,
|
||||||
).$2
|
);
|
||||||
: Colors.grey;
|
return Text(
|
||||||
|
info.$3,
|
||||||
final RelationshipProvider relations = Get.find();
|
maxLines: 1,
|
||||||
final accountNotifications = relations.friendRequestCount.value;
|
overflow: TextOverflow.fade,
|
||||||
|
);
|
||||||
return badges.Badge(
|
},
|
||||||
badgeContent: Text(
|
|
||||||
accountNotifications.toString(),
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
),
|
||||||
showBadge: accountNotifications > 0,
|
leading: Obx(() {
|
||||||
position: badges.BadgePosition.topEnd(
|
final statusBadgeColor = _accountStatus != null
|
||||||
top: -10,
|
? StatusProvider.determineStatus(
|
||||||
end: -6,
|
_accountStatus!,
|
||||||
),
|
).$2
|
||||||
child: badges.Badge(
|
: Colors.grey;
|
||||||
showBadge: _accountStatus != null,
|
|
||||||
badgeStyle: badges.BadgeStyle(badgeColor: statusBadgeColor),
|
|
||||||
position: badges.BadgePosition.bottomEnd(
|
|
||||||
bottom: 0,
|
|
||||||
end: -2,
|
|
||||||
),
|
|
||||||
child: AccountAvatar(
|
|
||||||
content: auth.userProfile.value!['avatar'],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
trailing: _buildSettingButton(),
|
|
||||||
onTap: () {
|
|
||||||
AppRouter.instance.goNamed('account');
|
|
||||||
setState(() => _selectedIndex = null);
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
onLongPress: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AccountStatusAction(
|
|
||||||
currentStatus: _accountStatus!.status,
|
|
||||||
),
|
|
||||||
).then((val) {
|
|
||||||
if (val == true) _getStatus();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}).paddingOnly(top: 8),
|
|
||||||
const Divider(thickness: 0.3, height: 1).paddingOnly(
|
|
||||||
bottom: 12,
|
|
||||||
top: 8,
|
|
||||||
),
|
|
||||||
...AppNavigation.destinations.map(
|
|
||||||
(e) => NavigationDrawerDestination(
|
|
||||||
icon: e.icon,
|
|
||||||
label: Text(e.label),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Divider(thickness: 0.3, height: 1).paddingOnly(
|
|
||||||
top: 12,
|
|
||||||
),
|
|
||||||
Obx(() {
|
|
||||||
if (auth.isAuthorized.isFalse || auth.userProfile.value == null) {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
final selfId = auth.userProfile.value!['id'];
|
final RelationshipProvider relations = Get.find();
|
||||||
|
final accountNotifications =
|
||||||
|
relations.friendRequestCount.value;
|
||||||
|
|
||||||
return Column(
|
return badges.Badge(
|
||||||
children: [
|
badgeContent: Text(
|
||||||
Theme(
|
accountNotifications.toString(),
|
||||||
data: Theme.of(context)
|
style: const TextStyle(color: Colors.white),
|
||||||
.copyWith(dividerColor: Colors.transparent),
|
),
|
||||||
child: ExpansionTile(
|
showBadge: accountNotifications > 0,
|
||||||
title: Text('channels'.tr),
|
position: badges.BadgePosition.topEnd(
|
||||||
tilePadding: const EdgeInsets.symmetric(horizontal: 24),
|
top: -10,
|
||||||
children: [
|
end: -6,
|
||||||
Obx(
|
),
|
||||||
() => SizedBox(
|
child: badges.Badge(
|
||||||
height: 360,
|
showBadge: _accountStatus != null,
|
||||||
child: RefreshIndicator(
|
badgeStyle:
|
||||||
onRefresh: () => _channels.refreshAvailableChannel(),
|
badges.BadgeStyle(badgeColor: statusBadgeColor),
|
||||||
child: ChannelListWidget(
|
position: badges.BadgePosition.bottomEnd(
|
||||||
channels: _channels.groupChannels,
|
bottom: 0,
|
||||||
selfId: selfId,
|
end: -2,
|
||||||
isDense: true,
|
),
|
||||||
useReplace: true,
|
child: AccountAvatar(
|
||||||
onSelected: (_) {
|
content: auth.userProfile.value!['avatar'],
|
||||||
setState(() => _selectedIndex = null);
|
|
||||||
_closeDrawer();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
}),
|
||||||
|
onTap: () {
|
||||||
|
AppRouter.instance.goNamed('account');
|
||||||
|
_closeDrawer();
|
||||||
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AccountStatusAction(
|
||||||
|
currentStatus: _accountStatus!.status,
|
||||||
|
),
|
||||||
|
).then((val) {
|
||||||
|
if (val == true) _getStatus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).paddingSymmetric(vertical: 8),
|
||||||
|
const Divider(thickness: 0.3, height: 1),
|
||||||
|
Column(
|
||||||
|
children: AppNavigation.destinations
|
||||||
|
.map(
|
||||||
|
(e) => ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
),
|
||||||
|
leading: Icon(e.icon, size: 20).paddingAll(2),
|
||||||
|
title: Text(e.label),
|
||||||
|
enabled: true,
|
||||||
|
onTap: () {
|
||||||
|
AppRouter.instance.goNamed(e.page);
|
||||||
|
_closeDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
).paddingSymmetric(vertical: 8),
|
||||||
|
const Divider(thickness: 0.3, height: 1),
|
||||||
|
Expanded(
|
||||||
|
child: AppNavigationRegions(
|
||||||
|
onSelected: (item) {
|
||||||
|
_closeDrawer();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
const Divider(thickness: 0.3, height: 1),
|
||||||
}),
|
Column(
|
||||||
],
|
children: [
|
||||||
|
ListTile(
|
||||||
|
minTileHeight: 0,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.settings, size: 20).paddingAll(2),
|
||||||
|
title: Text('settings'.tr),
|
||||||
|
onTap: () {
|
||||||
|
AppRouter.instance.pushNamed('settings');
|
||||||
|
_closeDrawer();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingOnly(
|
||||||
|
top: 8,
|
||||||
|
bottom: math.max(8, MediaQuery.of(context).padding.bottom),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
89
lib/widgets/navigation/app_navigation_regions.dart
Normal file
89
lib/widgets/navigation/app_navigation_regions.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:solian/models/channel.dart';
|
||||||
|
import 'package:solian/providers/content/channel.dart';
|
||||||
|
import 'package:solian/router.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
class AppNavigationRegions extends StatelessWidget {
|
||||||
|
final Function(Channel item) onSelected;
|
||||||
|
|
||||||
|
const AppNavigationRegions({super.key, required this.onSelected});
|
||||||
|
|
||||||
|
void _gotoChannel(Channel item) {
|
||||||
|
AppRouter.instance.pushReplacementNamed(
|
||||||
|
'channelChat',
|
||||||
|
pathParameters: {'alias': item.alias},
|
||||||
|
queryParameters: {
|
||||||
|
if (item.realmId != null) 'realm': item.realm!.alias,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
onSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEntry(BuildContext context, Channel item) {
|
||||||
|
const padding = EdgeInsets.symmetric(horizontal: 20);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
minTileHeight: 0,
|
||||||
|
leading: const Icon(Icons.tag_outlined),
|
||||||
|
contentPadding: padding,
|
||||||
|
title: Text(item.name),
|
||||||
|
subtitle: Text(
|
||||||
|
item.description,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
onTap: () => _gotoChannel(item),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ChannelProvider channels = Get.find();
|
||||||
|
|
||||||
|
return Obx(() {
|
||||||
|
final List<Channel> noRealmGroupChannels = channels.availableChannels
|
||||||
|
.where((x) => x.type == 0 && x.realmId == null)
|
||||||
|
.toList();
|
||||||
|
final List<Channel> hasRealmGroupChannels = channels.availableChannels
|
||||||
|
.where((x) => x.type == 0 && x.realmId != null)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
const SliverPadding(padding: EdgeInsets.only(top: 8)),
|
||||||
|
SliverList.builder(
|
||||||
|
itemCount: noRealmGroupChannels.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final element = noRealmGroupChannels[index];
|
||||||
|
return _buildEntry(context, element);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SliverList.list(
|
||||||
|
children: hasRealmGroupChannels
|
||||||
|
.groupListsBy((x) => x.realm)
|
||||||
|
.entries
|
||||||
|
.map((element) {
|
||||||
|
return ExpansionTile(
|
||||||
|
minTileHeight: 0,
|
||||||
|
initiallyExpanded: true,
|
||||||
|
tilePadding: const EdgeInsets.only(left: 20, right: 24),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
collapsedBackgroundColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainer,
|
||||||
|
title: Text(element.value.first.realm!.name),
|
||||||
|
leading: const Icon(Icons.workspaces, size: 16)
|
||||||
|
.paddingSymmetric(horizontal: 4),
|
||||||
|
children:
|
||||||
|
element.value.map((x) => _buildEntry(context, x)).toList(),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
const SliverPadding(padding: EdgeInsets.only(bottom: 8)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:solian/exts.dart';
|
import 'package:solian/exts.dart';
|
||||||
import 'package:solian/models/post.dart';
|
import 'package:solian/models/post.dart';
|
||||||
|
import 'package:solian/platform.dart';
|
||||||
import 'package:solian/providers/auth.dart';
|
import 'package:solian/providers/auth.dart';
|
||||||
import 'package:solian/router.dart';
|
import 'package:solian/router.dart';
|
||||||
import 'package:solian/screens/posts/post_editor.dart';
|
import 'package:solian/screens/posts/post_editor.dart';
|
||||||
@ -38,19 +39,32 @@ class _PostActionState extends State<PostAction> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _doShare() async {
|
Future<void> _doShare({bool noUri = false}) async {
|
||||||
final box = context.findRenderObject() as RenderBox?;
|
final box = context.findRenderObject() as RenderBox?;
|
||||||
await Share.share(
|
if ((PlatformInfo.isAndroid || PlatformInfo.isIOS) && !noUri) {
|
||||||
'postShareContent'.trParams({
|
await Share.shareUri(
|
||||||
'username': widget.item.author.nick,
|
Uri.parse('https://solsynth.dev/posts/${widget.item.id}'),
|
||||||
'content': widget.item.body['content'] ?? 'no content',
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||||
'link': 'https://sn.solsynth.dev/posts/view/${widget.item.id}',
|
);
|
||||||
}),
|
} else {
|
||||||
subject: 'postShareSubject'.trParams({
|
final extraContent = [
|
||||||
'username': widget.item.author.nick,
|
widget.item.body['title'],
|
||||||
}),
|
widget.item.body['description'],
|
||||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
];
|
||||||
);
|
final isExtraNotEmpty = extraContent.any((x) => x != null);
|
||||||
|
await Share.share(
|
||||||
|
'postShareContent'.trParams({
|
||||||
|
'username': widget.item.author.nick,
|
||||||
|
'content':
|
||||||
|
'${extraContent.join('\n')}${isExtraNotEmpty ? '\n\n' : ''}${widget.item.body['content'] ?? 'no content'}',
|
||||||
|
'link': 'https://solsynth.dev/posts/${widget.item.id}',
|
||||||
|
}),
|
||||||
|
subject: 'postShareSubject'.trParams({
|
||||||
|
'username': widget.item.author.nick,
|
||||||
|
}),
|
||||||
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -86,6 +100,16 @@ class _PostActionState extends State<PostAction> {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
leading: const Icon(Icons.share),
|
leading: const Icon(Icons.share),
|
||||||
title: Text('share'.tr),
|
title: Text('share'.tr),
|
||||||
|
trailing: PlatformInfo.isIOS || PlatformInfo.isAndroid
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.link_off),
|
||||||
|
tooltip: 'shareNoUri'.tr,
|
||||||
|
onPressed: () async {
|
||||||
|
await _doShare(noUri: true);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await _doShare();
|
await _doShare();
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
@ -97,13 +121,13 @@ class _PostActionState extends State<PostAction> {
|
|||||||
leading: const FaIcon(FontAwesomeIcons.reply, size: 20),
|
leading: const FaIcon(FontAwesomeIcons.reply, size: 20),
|
||||||
title: Text('reply'.tr),
|
title: Text('reply'.tr),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final value = await AppRouter.instance.pushNamed(
|
Navigator.pop(
|
||||||
'postEditor',
|
context,
|
||||||
extra: PostPublishArguments(reply: widget.item),
|
AppRouter.instance.pushNamed(
|
||||||
|
'postEditor',
|
||||||
|
extra: PostPublishArguments(reply: widget.item),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (value != null) {
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (!widget.noReact)
|
if (!widget.noReact)
|
||||||
@ -112,13 +136,13 @@ class _PostActionState extends State<PostAction> {
|
|||||||
leading: const FaIcon(FontAwesomeIcons.retweet, size: 20),
|
leading: const FaIcon(FontAwesomeIcons.retweet, size: 20),
|
||||||
title: Text('repost'.tr),
|
title: Text('repost'.tr),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final value = await AppRouter.instance.pushNamed(
|
Navigator.pop(
|
||||||
'postEditor',
|
context,
|
||||||
extra: PostPublishArguments(repost: widget.item),
|
AppRouter.instance.pushNamed(
|
||||||
|
'postEditor',
|
||||||
|
extra: PostPublishArguments(repost: widget.item),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (value != null) {
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (_canModifyContent && !widget.noReact)
|
if (_canModifyContent && !widget.noReact)
|
||||||
@ -146,13 +170,13 @@ class _PostActionState extends State<PostAction> {
|
|||||||
leading: const Icon(Icons.edit),
|
leading: const Icon(Icons.edit),
|
||||||
title: Text('edit'.tr),
|
title: Text('edit'.tr),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final value = await AppRouter.instance.pushNamed(
|
Navigator.pop(
|
||||||
'postEditor',
|
context,
|
||||||
extra: PostPublishArguments(edit: widget.item),
|
AppRouter.instance.pushNamed(
|
||||||
|
'postEditor',
|
||||||
|
extra: PostPublishArguments(edit: widget.item),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (value != null) {
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (_canModifyContent)
|
if (_canModifyContent)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:animations/animations.dart';
|
import 'package:animations/animations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:get/get_utils/get_utils.dart';
|
import 'package:get/get_utils/get_utils.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
@ -22,7 +23,9 @@ class PostItem extends StatefulWidget {
|
|||||||
final bool isReactable;
|
final bool isReactable;
|
||||||
final bool isShowReply;
|
final bool isShowReply;
|
||||||
final bool isShowEmbed;
|
final bool isShowEmbed;
|
||||||
|
final bool isOverrideEmbedClickable;
|
||||||
final bool isFullDate;
|
final bool isFullDate;
|
||||||
|
final bool isFullContent;
|
||||||
final bool isContentSelectable;
|
final bool isContentSelectable;
|
||||||
final String? attachmentParent;
|
final String? attachmentParent;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
@ -35,7 +38,9 @@ class PostItem extends StatefulWidget {
|
|||||||
this.isReactable = true,
|
this.isReactable = true,
|
||||||
this.isShowReply = true,
|
this.isShowReply = true,
|
||||||
this.isShowEmbed = true,
|
this.isShowEmbed = true,
|
||||||
|
this.isOverrideEmbedClickable = false,
|
||||||
this.isFullDate = false,
|
this.isFullDate = false,
|
||||||
|
this.isFullContent = false,
|
||||||
this.isContentSelectable = false,
|
this.isContentSelectable = false,
|
||||||
this.attachmentParent,
|
this.attachmentParent,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
@ -73,12 +78,13 @@ class _PostItemState extends State<PostItem> {
|
|||||||
|
|
||||||
Widget _buildHeader() {
|
Widget _buildHeader() {
|
||||||
return Row(
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.isCompact)
|
if (widget.isCompact)
|
||||||
AccountAvatar(
|
AccountAvatar(
|
||||||
content: item.author.avatar.toString(),
|
content: item.author.avatar.toString(),
|
||||||
radius: 10,
|
radius: 10,
|
||||||
).paddingOnly(left: 2),
|
).paddingOnly(left: 2, top: 1),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -105,18 +111,26 @@ class _PostItemState extends State<PostItem> {
|
|||||||
item.body['description'],
|
item.body['description'],
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
if (item.body['description'] != null ||
|
|
||||||
item.body['title'] != null)
|
|
||||||
const Divider(thickness: 0.3, height: 1).paddingSymmetric(
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
).paddingOnly(left: widget.isCompact ? 6 : 12),
|
).paddingOnly(left: widget.isCompact ? 6 : 12),
|
||||||
),
|
),
|
||||||
|
if (widget.item.type == 'article')
|
||||||
|
Badge(
|
||||||
|
label: Text('article'.tr),
|
||||||
|
).paddingOnly(top: 3),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildHeaderDivider() {
|
||||||
|
if (item.body['description'] != null || item.body['title'] != null) {
|
||||||
|
return const Divider(thickness: 0.3, height: 1).paddingSymmetric(
|
||||||
|
vertical: 8,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildFooter() {
|
Widget _buildFooter() {
|
||||||
List<String> labels = List.empty(growable: true);
|
List<String> labels = List.empty(growable: true);
|
||||||
if (widget.item.editedAt != null) {
|
if (widget.item.editedAt != null) {
|
||||||
@ -164,6 +178,7 @@ class _PostItemState extends State<PostItem> {
|
|||||||
|
|
||||||
Widget _buildReply(BuildContext context) {
|
Widget _buildReply(BuildContext context) {
|
||||||
return OpenContainer(
|
return OpenContainer(
|
||||||
|
tappable: widget.isClickable || widget.isOverrideEmbedClickable,
|
||||||
closedBuilder: (_, openContainer) => Column(
|
closedBuilder: (_, openContainer) => Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@ -202,13 +217,15 @@ class _PostItemState extends State<PostItem> {
|
|||||||
),
|
),
|
||||||
closedElevation: 0,
|
closedElevation: 0,
|
||||||
openElevation: 0,
|
openElevation: 0,
|
||||||
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
|
closedColor:
|
||||||
|
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
|
||||||
openColor: Theme.of(context).colorScheme.surface,
|
openColor: Theme.of(context).colorScheme.surface,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRepost(BuildContext context) {
|
Widget _buildRepost(BuildContext context) {
|
||||||
return OpenContainer(
|
return OpenContainer(
|
||||||
|
tappable: widget.isClickable || widget.isOverrideEmbedClickable,
|
||||||
closedBuilder: (_, openContainer) => Column(
|
closedBuilder: (_, openContainer) => Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@ -247,11 +264,14 @@ class _PostItemState extends State<PostItem> {
|
|||||||
),
|
),
|
||||||
closedElevation: 0,
|
closedElevation: 0,
|
||||||
openElevation: 0,
|
openElevation: 0,
|
||||||
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
|
closedColor:
|
||||||
|
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
|
||||||
openColor: Theme.of(context).colorScheme.surface,
|
openColor: Theme.of(context).colorScheme.surface,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _contentHeight = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final List<int> attachments = item.body['attachments'] is List
|
final List<int> attachments = item.body['attachments'] is List
|
||||||
@ -264,24 +284,61 @@ class _PostItemState extends State<PostItem> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildHeader().paddingSymmetric(horizontal: 12),
|
_buildHeader().paddingSymmetric(horizontal: 12),
|
||||||
MarkdownTextContent(
|
_buildHeaderDivider().paddingSymmetric(horizontal: 12),
|
||||||
content: item.body['content'],
|
Stack(
|
||||||
isSelectable: widget.isContentSelectable,
|
children: [
|
||||||
).paddingOnly(
|
SizedContainer(
|
||||||
left: 16,
|
maxWidth: 640,
|
||||||
right: 12,
|
maxHeight: widget.isFullContent ? double.infinity : 80,
|
||||||
top: 2,
|
child: _MeasureSize(
|
||||||
bottom: hasAttachment ? 4 : 0,
|
onChange: (size) {
|
||||||
|
setState(() => _contentHeight = size.height);
|
||||||
|
},
|
||||||
|
child: MarkdownTextContent(
|
||||||
|
parentId: 'p${item.id}',
|
||||||
|
content: item.body['content'],
|
||||||
|
isSelectable: widget.isContentSelectable,
|
||||||
|
).paddingOnly(
|
||||||
|
left: 16,
|
||||||
|
right: 12,
|
||||||
|
top: 2,
|
||||||
|
bottom: hasAttachment ? 4 : 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_contentHeight >= 80 && !widget.isFullContent)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surface
|
||||||
|
.withOpacity(0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
_buildFooter().paddingOnly(left: 16),
|
_buildFooter().paddingOnly(left: 16),
|
||||||
if (attachments.isNotEmpty)
|
if (attachments.isNotEmpty)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.attachment,
|
Icons.file_copy,
|
||||||
size: 18,
|
size: 15,
|
||||||
color: _unFocusColor,
|
color: _unFocusColor,
|
||||||
).paddingOnly(right: 6),
|
).paddingOnly(right: 5),
|
||||||
Text(
|
Text(
|
||||||
'attachmentHint'.trParams(
|
'attachmentHint'.trParams(
|
||||||
{'count': attachments.length.toString()},
|
{'count': attachments.length.toString()},
|
||||||
@ -295,6 +352,7 @@ class _PostItemState extends State<PostItem> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return OpenContainer(
|
return OpenContainer(
|
||||||
|
tappable: widget.isClickable,
|
||||||
closedBuilder: (_, openContainer) => Column(
|
closedBuilder: (_, openContainer) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -320,12 +378,47 @@ class _PostItemState extends State<PostItem> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildHeader(),
|
_buildHeader(),
|
||||||
SizedContainer(
|
_buildHeaderDivider(),
|
||||||
maxWidth: 640,
|
Stack(
|
||||||
child: MarkdownTextContent(
|
children: [
|
||||||
content: item.body['content'],
|
SizedContainer(
|
||||||
isSelectable: widget.isContentSelectable,
|
maxWidth: 640,
|
||||||
).paddingOnly(left: 12, right: 8),
|
maxHeight:
|
||||||
|
widget.isFullContent ? double.infinity : 320,
|
||||||
|
child: _MeasureSize(
|
||||||
|
onChange: (size) {
|
||||||
|
setState(() => _contentHeight = size.height);
|
||||||
|
},
|
||||||
|
child: MarkdownTextContent(
|
||||||
|
parentId: 'p${item.id}',
|
||||||
|
content: item.body['content'],
|
||||||
|
isSelectable: widget.isContentSelectable,
|
||||||
|
).paddingOnly(left: 12, right: 8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_contentHeight >= 320 && !widget.isFullContent)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: Container(
|
||||||
|
height: 320,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
Theme.of(context).colorScheme.surface,
|
||||||
|
Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surface
|
||||||
|
.withOpacity(0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (widget.item.replyTo != null && widget.isShowEmbed)
|
if (widget.item.replyTo != null && widget.isShowEmbed)
|
||||||
_buildReply(context).paddingOnly(top: 4),
|
_buildReply(context).paddingOnly(top: 4),
|
||||||
@ -343,11 +436,12 @@ class _PostItemState extends State<PostItem> {
|
|||||||
left: 16,
|
left: 16,
|
||||||
),
|
),
|
||||||
AttachmentList(
|
AttachmentList(
|
||||||
|
flatMaxHeight: MediaQuery.of(context).size.width,
|
||||||
parentId: widget.item.id.toString(),
|
parentId: widget.item.id.toString(),
|
||||||
attachmentsId: attachments,
|
attachmentsId: attachments,
|
||||||
isGrid: attachments.length > 1,
|
isGrid: attachments.length > 1,
|
||||||
),
|
),
|
||||||
if (widget.isShowReply && widget.isReactable)
|
if (widget.isShowReply || widget.isReactable)
|
||||||
PostQuickAction(
|
PostQuickAction(
|
||||||
isShowReply: widget.isShowReply,
|
isShowReply: widget.isShowReply,
|
||||||
isReactable: widget.isReactable,
|
isReactable: widget.isReactable,
|
||||||
@ -377,8 +471,51 @@ class _PostItemState extends State<PostItem> {
|
|||||||
),
|
),
|
||||||
closedElevation: 0,
|
closedElevation: 0,
|
||||||
openElevation: 0,
|
openElevation: 0,
|
||||||
closedColor: widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
|
closedColor:
|
||||||
|
widget.backgroundColor ?? Theme.of(context).colorScheme.surface,
|
||||||
openColor: Theme.of(context).colorScheme.surface,
|
openColor: Theme.of(context).colorScheme.surface,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef _OnWidgetSizeChange = void Function(Size size);
|
||||||
|
|
||||||
|
class _MeasureSizeRenderObject extends RenderProxyBox {
|
||||||
|
Size? oldSize;
|
||||||
|
_OnWidgetSizeChange onChange;
|
||||||
|
|
||||||
|
_MeasureSizeRenderObject(this.onChange);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void performLayout() {
|
||||||
|
super.performLayout();
|
||||||
|
|
||||||
|
Size newSize = child!.size;
|
||||||
|
if (oldSize == newSize) return;
|
||||||
|
|
||||||
|
oldSize = newSize;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
onChange(newSize);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MeasureSize extends SingleChildRenderObjectWidget {
|
||||||
|
final _OnWidgetSizeChange onChange;
|
||||||
|
|
||||||
|
const _MeasureSize({
|
||||||
|
required this.onChange,
|
||||||
|
required Widget super.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
|
return _MeasureSizeRenderObject(onChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(
|
||||||
|
BuildContext context, covariant _MeasureSizeRenderObject renderObject) {
|
||||||
|
renderObject.onChange = onChange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -85,7 +85,13 @@ class PostListEntryWidget extends StatelessWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) => PostAction(item: item),
|
builder: (context) => PostAction(item: item),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value != null) onUpdate();
|
if (value is Future) {
|
||||||
|
value.then((_) {
|
||||||
|
onUpdate();
|
||||||
|
});
|
||||||
|
} else if (value != null) {
|
||||||
|
onUpdate();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -6,18 +6,21 @@ import 'package:solian/widgets/posts/post_item.dart';
|
|||||||
class PostOwnedListEntry extends StatelessWidget {
|
class PostOwnedListEntry extends StatelessWidget {
|
||||||
final Post item;
|
final Post item;
|
||||||
final Function onTap;
|
final Function onTap;
|
||||||
|
final bool isFullContent;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
const PostOwnedListEntry({
|
const PostOwnedListEntry({
|
||||||
super.key,
|
super.key,
|
||||||
required this.item,
|
required this.item,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
|
this.isFullContent = false,
|
||||||
|
this.backgroundColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
child: InkWell(
|
child: GestureDetector(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -28,6 +31,8 @@ class PostOwnedListEntry extends StatelessWidget {
|
|||||||
isClickable: false,
|
isClickable: false,
|
||||||
isShowReply: false,
|
isShowReply: false,
|
||||||
isReactable: false,
|
isReactable: false,
|
||||||
|
isFullContent: isFullContent,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
).paddingSymmetric(vertical: 8),
|
).paddingSymmetric(vertical: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -9,6 +9,7 @@ class PostWarpedListWidget extends StatelessWidget {
|
|||||||
final bool isNestedClickable;
|
final bool isNestedClickable;
|
||||||
final bool isPinned;
|
final bool isPinned;
|
||||||
final PagingController<int, Post> controller;
|
final PagingController<int, Post> controller;
|
||||||
|
final Function? onUpdate;
|
||||||
|
|
||||||
const PostWarpedListWidget({
|
const PostWarpedListWidget({
|
||||||
super.key,
|
super.key,
|
||||||
@ -17,6 +18,7 @@ class PostWarpedListWidget extends StatelessWidget {
|
|||||||
this.isClickable = true,
|
this.isClickable = true,
|
||||||
this.isNestedClickable = true,
|
this.isNestedClickable = true,
|
||||||
this.isPinned = true,
|
this.isPinned = true,
|
||||||
|
this.onUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -35,9 +37,7 @@ class PostWarpedListWidget extends StatelessWidget {
|
|||||||
isNestedClickable: isNestedClickable,
|
isNestedClickable: isNestedClickable,
|
||||||
isClickable: isClickable,
|
isClickable: isClickable,
|
||||||
item: item,
|
item: item,
|
||||||
onUpdate: () {
|
onUpdate: onUpdate ?? () {},
|
||||||
controller.refresh();
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -3,11 +3,13 @@ import 'package:flutter/material.dart';
|
|||||||
class SizedContainer extends StatelessWidget {
|
class SizedContainer extends StatelessWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final double maxWidth;
|
final double maxWidth;
|
||||||
|
final double maxHeight;
|
||||||
|
|
||||||
const SizedContainer({
|
const SizedContainer({
|
||||||
super.key,
|
super.key,
|
||||||
required this.child,
|
required this.child,
|
||||||
this.maxWidth = 720,
|
this.maxWidth = 720,
|
||||||
|
this.maxHeight = double.infinity,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -15,7 +17,7 @@ class SizedContainer extends StatelessWidget {
|
|||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
constraints: BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -107,11 +107,11 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- screen_brightness_macos (0.1.0):
|
- screen_brightness_macos (0.1.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- Sentry/HybridSDK (8.32.0)
|
- Sentry/HybridSDK (8.33.0)
|
||||||
- sentry_flutter (8.6.0):
|
- sentry_flutter (8.7.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- Sentry/HybridSDK (= 8.32.0)
|
- Sentry/HybridSDK (= 8.33.0)
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
@ -252,8 +252,8 @@ SPEC CHECKSUMS:
|
|||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
|
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
|
||||||
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
|
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
|
||||||
Sentry: 96ae1dcdf01a644bc3a3b1dc279cecaf48a833fb
|
Sentry: 8560050221424aef0bebc8e31eedf00af80f90a6
|
||||||
sentry_flutter: 090351ce1ff5f96a4b33ef9455b7e3b28185387d
|
sentry_flutter: e26b861f744e5037a3faf9bf56603ec65d658a61
|
||||||
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
|
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
|
||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
|
|
||||||
@NSApplicationMain
|
@main
|
||||||
class AppDelegate: FlutterAppDelegate {
|
class AppDelegate: FlutterAppDelegate {
|
||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
|
209
pubspec.lock
209
pubspec.lock
@ -5,10 +5,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "67.0.0"
|
version: "72.0.0"
|
||||||
_flutterfire_internals:
|
_flutterfire_internals:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -17,14 +17,19 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.40"
|
version: "1.3.40"
|
||||||
|
_macros:
|
||||||
|
dependency: transitive
|
||||||
|
description: dart
|
||||||
|
source: sdk
|
||||||
|
version: "0.3.2"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.4.1"
|
version: "6.7.0"
|
||||||
animations:
|
animations:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -117,18 +122,18 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7"
|
sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.11"
|
version: "2.4.12"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_runner_core
|
name: build_runner_core
|
||||||
sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe
|
sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.3.1"
|
version: "7.3.2"
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -173,10 +178,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: carousel_slider
|
name: carousel_slider
|
||||||
sha256: "9c695cc963bf1d04a47bd6021f68befce8970bcd61d24938e1fb0918cf5d9c42"
|
sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.1"
|
version: "5.0.0"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -226,7 +231,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.10.0"
|
version: "4.10.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||||
@ -385,6 +390,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
field_suggestion:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: field_suggestion
|
||||||
|
sha256: "596362ed67a661a18e01e4f51b8b92424f333baf37cebd48cc647a4c82c858d5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.5"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -397,10 +410,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: file_picker
|
name: file_picker
|
||||||
sha256: "824f5b9f389bfc4dddac3dea76cd70c51092d9dff0b2ece7ef4f53db8547d258"
|
sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.0.6"
|
version: "8.0.7"
|
||||||
file_selector_linux:
|
file_selector_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -558,6 +571,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
flutter_keyboard_visibility:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility
|
||||||
|
sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.0"
|
||||||
|
flutter_keyboard_visibility_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_linux
|
||||||
|
sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
flutter_keyboard_visibility_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_macos
|
||||||
|
sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
flutter_keyboard_visibility_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_platform_interface
|
||||||
|
sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
flutter_keyboard_visibility_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_web
|
||||||
|
sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
flutter_keyboard_visibility_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_keyboard_visibility_windows
|
||||||
|
sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@ -578,10 +639,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_markdown
|
name: flutter_markdown
|
||||||
sha256: "2e8a801b1ded5ea001a4529c97b1f213dcb11c6b20668e081cafb23468593514"
|
sha256: a23c41ee57573e62fc2190a1f36a0480c4d90bde3a8a8d7126e5d5992fb53fb7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.3"
|
version: "0.7.3+1"
|
||||||
flutter_markdown_selectionarea:
|
flutter_markdown_selectionarea:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -667,6 +728,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_typeahead:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_typeahead
|
||||||
|
sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.2.0"
|
||||||
flutter_web_plugins:
|
flutter_web_plugins:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -676,10 +745,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_webrtc
|
name: flutter_webrtc
|
||||||
sha256: f46bd76cef6e8d787dc707d0c591f0e89c912a2970c7b5e68a55b9cca1bdde4c
|
sha256: "67faa07cf49392b50b1aa14590a83caa64d2109345fabd29899dcd8da8538348"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.6"
|
version: "0.11.6+hotfix.1"
|
||||||
font_awesome_flutter:
|
font_awesome_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -724,10 +793,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: d380de0355788c5c784fe9f81b43fc833b903991c25ecc4e2a416a67faefa722
|
sha256: ddc16d34b0d74cb313986918c0f0885a7ba2fc24d8fb8419de75f0015144ccfe
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.2"
|
version: "14.2.3"
|
||||||
graphs:
|
graphs:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -736,6 +805,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.2"
|
||||||
|
highlightable:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: highlightable
|
||||||
|
sha256: "526793e148c91977b694d75d99cd34401ea3b65efd223e7b539c76916af86ffd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -804,10 +881,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: "3fe99e3a459b969f657565a853353bdea7e3d3cae34f7dd124836267d426ec87"
|
sha256: c0e72ecd170b00a5590bb71238d57dc8ad22ee14c60c6b0d1a4e05cafbc5db4b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.12+10"
|
version: "0.8.12+11"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -900,18 +977,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.4"
|
version: "10.0.5"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.5"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -960,6 +1037,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.5.0"
|
||||||
|
macros:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: macros
|
||||||
|
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.2-main.4"
|
||||||
markdown:
|
markdown:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -988,10 +1073,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.0"
|
version: "0.11.1"
|
||||||
media_kit:
|
media_kit:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1068,10 +1153,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.0"
|
version: "1.15.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1204,10 +1289,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_android
|
name: permission_handler_android
|
||||||
sha256: b29a799ca03be9f999aa6c39f7de5209482d638e6f857f6b93b0875c618b7e54
|
sha256: eaf2a1ec4472775451e88ca6a7b86559ef2f1d1ed903942ed135e38ea0097dca
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.0.7"
|
version: "12.0.8"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1228,10 +1313,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_platform_interface
|
name: permission_handler_platform_interface
|
||||||
sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20"
|
sha256: fe0ffe274d665be8e34f9c59705441a7d248edebbe5d9e3ec2665f88b79358ea
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.1"
|
version: "4.2.2"
|
||||||
permission_handler_windows:
|
permission_handler_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1272,6 +1357,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
pointer_interceptor:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pointer_interceptor
|
||||||
|
sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.1+2"
|
||||||
|
pointer_interceptor_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pointer_interceptor_ios
|
||||||
|
sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.1"
|
||||||
|
pointer_interceptor_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pointer_interceptor_platform_interface
|
||||||
|
sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.0+1"
|
||||||
|
pointer_interceptor_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pointer_interceptor_web
|
||||||
|
sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.2+1"
|
||||||
pool:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1444,18 +1561,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sentry
|
name: sentry
|
||||||
sha256: "76ad4fab90ff82427c26939bd79dc4df345a081e2b1cd5954b947e340b9af9a5"
|
sha256: "0f787e27ff617e4f88f7074977240406a9c5509444bac64a4dfa5b3200fb5632"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.6.0"
|
version: "8.7.0"
|
||||||
sentry_flutter:
|
sentry_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: sentry_flutter
|
name: sentry_flutter
|
||||||
sha256: a7c92014701093a7c0a373e1a47c54ec428d8468d8bf2b793fee7ea1b085c21b
|
sha256: fbbb47d72ccca48be25bf3c2ced6ab6e872991af3a0ba78e54be8d138f2e053f
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.6.0"
|
version: "8.7.0"
|
||||||
share_plus:
|
share_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1689,10 +1806,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.0"
|
version: "0.7.2"
|
||||||
timeago:
|
timeago:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1769,10 +1886,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_linux
|
name: url_launcher_linux
|
||||||
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
|
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.2.0"
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1793,10 +1910,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_web
|
name: url_launcher_web
|
||||||
sha256: a36e2d7981122fa185006b216eb6b5b97ede3f9a54b7a511bc966971ab98d049
|
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.3"
|
||||||
url_launcher_windows:
|
url_launcher_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1825,10 +1942,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.1"
|
version: "14.2.4"
|
||||||
volume_controller:
|
volume_controller:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1934,5 +2051,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.4.0 <4.0.0"
|
dart: ">=3.5.0-259.0.dev <4.0.0"
|
||||||
flutter: ">=3.22.0"
|
flutter: ">=3.22.0"
|
||||||
|
@ -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.1+2
|
version: 1.2.1+12
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.4 <4.0.0"
|
sdk: ">=3.3.4 <4.0.0"
|
||||||
@ -19,7 +19,7 @@ dependencies:
|
|||||||
flutter_markdown: ^0.7.1
|
flutter_markdown: ^0.7.1
|
||||||
flutter_animate: ^4.5.0
|
flutter_animate: ^4.5.0
|
||||||
flutter_secure_storage: ^9.2.1
|
flutter_secure_storage: ^9.2.1
|
||||||
carousel_slider: ^4.2.1
|
carousel_slider: ^5.0.0
|
||||||
url_launcher: ^6.2.6
|
url_launcher: ^6.2.6
|
||||||
infinite_scroll_pagination: ^4.0.0
|
infinite_scroll_pagination: ^4.0.0
|
||||||
image_picker: ^1.1.1
|
image_picker: ^1.1.1
|
||||||
@ -66,6 +66,9 @@ dependencies:
|
|||||||
animations: ^2.0.11
|
animations: ^2.0.11
|
||||||
avatar_stack: ^1.2.0
|
avatar_stack: ^1.2.0
|
||||||
async: ^2.11.0
|
async: ^2.11.0
|
||||||
|
field_suggestion: ^0.2.5
|
||||||
|
flutter_typeahead: ^5.2.0
|
||||||
|
collection: ^1.18.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Reference in New Issue
Block a user