Compare commits

...

42 Commits

Author SHA1 Message Date
4a3e6a9e15 🚀 Launch 1.2.1+12 2024-08-11 00:50:25 +08:00
00092ba7b6 Some useful options 2024-08-11 00:36:27 +08:00
b5da8ece4a Use capital share link 2024-08-10 18:24:47 +08:00
dfe9165bc9 🐛 Bug fixes on upload attachment 2024-08-10 01:17:31 +08:00
3d45b54236 ⬆️ Upgrade flutter & deps 2024-08-10 01:16:40 +08:00
7f63fe7f0e 💄 Better sidebar navigation 2024-08-10 00:51:54 +08:00
bc5dbab9c5 Dismissible refresh notification 2024-08-10 00:49:21 +08:00
9910fc7a92 Channel content auto refresh after long time background activity 2024-08-10 00:43:55 +08:00
2356eac118 Better side navigation bar 2024-08-09 22:59:24 +08:00
0135b8d838 Better screenshare 2024-08-09 22:40:05 +08:00
8ec33ccbf4 🚨 Fix CarouselController import issue 2024-08-07 19:21:01 +08:00
d267316a35 💄 Better emotes 2024-08-07 19:11:52 +08:00
138da60e55 🚸 Prevent user from sending empty message 2024-08-07 19:02:49 +08:00
4562c2f991 🐛 Fix able send space message 2024-08-07 18:31:26 +08:00
8009f4ca9b 💄 Better sidebar navigation 2024-08-07 18:24:16 +08:00
54dee9702b 🐛 Fix attachments max width 2024-08-07 14:34:41 +08:00
94385564bd 🐛 Fix dupe attachment notification 2024-08-07 14:27:23 +08:00
0b2309816f 🐛 Fix desktop panic when download things 2024-08-07 13:50:50 +08:00
8283272a3b 🗑️ Fix mis-import 2024-08-07 01:49:03 +08:00
eb02a47e9a 💄 Fixes and improvements 2024-08-07 01:47:53 +08:00
7c0c1ec94f 💄 Optimize styles 2024-08-07 01:20:23 +08:00
272044a77e 💄 Optimize logo in signup & signin popup 2024-08-07 01:06:57 +08:00
39c22b1cf6 Sticker has pack id 2024-08-07 00:56:06 +08:00
98c3bb912d Stickers auto resize 2024-08-07 00:52:34 +08:00
035b92d9b8 Rollback sized container 2024-08-07 00:12:44 +08:00
0bfc0bd61b 🌐 Update en translation 2024-08-07 00:08:29 +08:00
de00a20eee 💄 Better call ui 2024-08-06 23:23:02 +08:00
73982f48d6 🐛 Bug fixes 2024-08-06 20:00:13 +08:00
1d36b30361 Video won't load until click 2024-08-06 19:39:07 +08:00
dea743a307 Username hint 2024-08-06 18:34:46 +08:00
c48bd3e758 Stickers hint 2024-08-06 18:18:40 +08:00
56bbf73b5e Better sticker & able embed attachment into markdown 2024-08-06 16:24:47 +08:00
4f6c5aa053 🐛 Bug fixes 2024-08-04 21:12:35 +08:00
d8e79fb4f9 🚀 Launch 1.2.1+5 2024-08-04 20:49:11 +08:00
06e0fa465b Article has special badge 2024-08-04 20:48:51 +08:00
895a257f50 Better overflow effect 2024-08-04 20:43:25 +08:00
d9804ba00b 🚸 Enhanced share feature 2024-08-04 18:32:16 +08:00
62ff1c2f1c 🚀 Launch 1.2.1+4 2024-08-04 18:14:28 +08:00
a157596a2e Optimize and fixes 2024-08-04 18:13:59 +08:00
12102bf527 Limit content and read more in posts 2024-08-04 17:39:22 +08:00
c00a018380 🐛 Fix draft box 2024-08-04 17:15:56 +08:00
53b3cac4ca Show hint when dismissible error 2024-08-04 16:26:05 +08:00
47 changed files with 1532 additions and 589 deletions

View File

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

View File

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

View File

@ -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();
}
},
); );
} }

View File

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

View File

@ -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']),

View File

@ -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();

View File

@ -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('.')

View File

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

View File

@ -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:

View File

@ -300,6 +300,7 @@ class _AccountProfilePageState extends State<AccountProfilePage> {
PostWarpedListWidget( PostWarpedListWidget(
isPinned: false, isPinned: false,
controller: _postController.pagingController, controller: _postController.pagingController,
onUpdate: () => _postController.reloadAllOver(),
), ),
]), ]),
), ),

View File

@ -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(),
); );
}, },

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import 'package:solian/theme.dart';
import 'package:solian/widgets/app_bar_leading.dart'; import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/chat/call/call_controls.dart'; import 'package:solian/widgets/chat/call/call_controls.dart';
import 'package:solian/widgets/chat/call/call_participant.dart'; import 'package:solian/widgets/chat/call/call_participant.dart';
import 'package:livekit_client/livekit_client.dart' as livekit;
class CallScreen extends StatefulWidget { class CallScreen extends StatefulWidget {
const CallScreen({super.key}); const CallScreen({super.key});
@ -161,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(

View File

@ -20,7 +20,6 @@ import 'package:solian/widgets/app_bar_leading.dart';
import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/app_bar_title.dart';
import 'package:solian/widgets/channel/channel_call_indicator.dart'; import 'package:solian/widgets/channel/channel_call_indicator.dart';
import 'package:solian/widgets/chat/call/chat_call_action.dart'; import 'package:solian/widgets/chat/call/chat_call_action.dart';
import 'package:solian/widgets/chat/chat_event.dart';
import 'package:solian/widgets/chat/chat_event_list.dart'; import 'package:solian/widgets/chat/chat_event_list.dart';
import 'package:solian/widgets/chat/chat_message_input.dart'; import 'package:solian/widgets/chat/chat_message_input.dart';
import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/current_state_action.dart';
@ -39,7 +38,10 @@ class ChannelChatScreen extends StatefulWidget {
State<ChannelChatScreen> createState() => _ChannelChatScreenState(); State<ChannelChatScreen> createState() => _ChannelChatScreenState();
} }
class _ChannelChatScreenState extends State<ChannelChatScreen> { class _ChannelChatScreenState extends State<ChannelChatScreen>
with WidgetsBindingObserver {
DateTime? _isOutOfSyncSince;
bool _isBusy = false; bool _isBusy = false;
int? _accountId; int? _accountId;
@ -123,20 +125,38 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
}); });
} }
void _keepUpdateWithServer() {
_getOngoingCall();
_chatController.getEvents(_channel!, widget.realm);
setState(() => _isOutOfSyncSince = null);
}
Event? _messageToReplying; Event? _messageToReplying;
Event? _messageToEditing; Event? _messageToEditing;
Widget buildHistoryBody(Event item, {bool isMerged = false}) { @override
return ChatEvent( void didChangeAppLifecycleState(AppLifecycleState state) {
key: Key('m${item.uuid}'), switch (state) {
item: item, case AppLifecycleState.resumed:
isMerged: isMerged, if (_isOutOfSyncSince == null) break;
chatController: _chatController, if (DateTime.now().difference(_isOutOfSyncSince!).inSeconds < 60) break;
); _keepUpdateWithServer();
break;
case AppLifecycleState.paused:
if (mounted) {
setState(() => _isOutOfSyncSince = DateTime.now());
}
break;
default:
break;
}
} }
@override @override
void initState() { void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_accountId = Get.find<AuthProvider>().userProfile.value!['id']; _accountId = Get.find<AuthProvider>().userProfile.value!['id'];
_chatController = ChatEventController(); _chatController = ChatEventController();
@ -147,21 +167,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_chatController.getEvents(_channel!, widget.realm); _chatController.getEvents(_channel!, widget.realm);
_listenMessages(); _listenMessages();
}); });
super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isBusy || _channel == null) {
return Material(
color: Theme.of(context).colorScheme.surface,
child: const Center(
child: CircularProgressIndicator(),
),
);
}
String title = _channel?.name ?? 'loading'.tr; String title = _channel?.name ?? 'loading'.tr;
String? placeholder; String? placeholder;
@ -185,7 +194,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
actions: [ actions: [
const BackgroundStateWidget(), const BackgroundStateWidget(),
Builder(builder: (context) { Builder(builder: (context) {
if (_isBusy) return const SizedBox(); if (_isBusy || _channel == null) return const SizedBox();
return ChatCallButton( return ChatCallButton(
realm: _channel!.realm, realm: _channel!.realm,
channel: _channel!, channel: _channel!,
@ -195,6 +205,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
IconButton( IconButton(
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
onPressed: () { onPressed: () {
if (_channel == null) return;
AppRouter.instance AppRouter.instance
.pushNamed( .pushNamed(
'channelDetail', 'channelDetail',
@ -219,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();
} }
} }

View File

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

View File

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

View File

@ -4,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'),
);
}, },
), ),
]; ];

View File

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

View File

@ -5,7 +5,6 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:markdown_toolbar/markdown_toolbar.dart'; import 'package:markdown_toolbar/markdown_toolbar.dart';
import 'package:solian/controllers/post_editor_controller.dart'; import 'package:solian/controllers/post_editor_controller.dart';
import 'package:solian/controllers/post_list_controller.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/post.dart'; import 'package:solian/models/post.dart';
import 'package:solian/models/realm.dart'; import 'package:solian/models/realm.dart';
@ -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);
} }

View File

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

View File

@ -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',
}; };

View File

@ -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': '清除消息记录',
}; };

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exts.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/account_status.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
@ -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(

View File

@ -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(

View File

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

View File

@ -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,
); );
} }

View File

@ -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);
},
);
},
);
}
}

View File

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

View File

@ -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),
], ],
), ),
), ),

View File

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

View File

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

View File

@ -1,17 +1,36 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/exts.dart'; import 'package:solian/exts.dart';
import 'package:solian/models/account.dart'; import 'package:solian/models/account.dart';
import 'package:solian/models/channel.dart'; import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart'; import 'package:solian/models/event.dart';
import 'package:solian/platform.dart';
import 'package:solian/providers/attachment_uploader.dart'; import 'package:solian/providers/attachment_uploader.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/stickers.dart';
import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/attachments/attachment_editor.dart'; import 'package:solian/widgets/attachments/attachment_editor.dart';
import 'package:solian/widgets/chat/chat_event.dart'; import 'package:solian/widgets/chat/chat_event.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class ChatMessageSuggestion {
final String type;
final Widget leading;
final String display;
final String content;
ChatMessageSuggestion({
required this.type,
required this.leading,
required this.display,
required this.content,
});
}
class ChatMessageInput extends StatefulWidget { class ChatMessageInput extends StatefulWidget {
final Event? edit; final Event? edit;
final Event? reply; final Event? reply;
@ -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(

View File

@ -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;

View File

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

View File

@ -1,18 +1,19 @@
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:solian/models/account_status.dart'; import 'package:solian/models/account_status.dart';
import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/account_status.dart';
import 'package:solian/providers/auth.dart'; import 'package:solian/providers/auth.dart';
import 'package:solian/providers/content/channel.dart';
import 'package:solian/providers/relation.dart'; import 'package:solian/providers/relation.dart';
import 'package:solian/router.dart'; import 'package:solian/router.dart';
import 'package:solian/shells/root_shell.dart'; import 'package:solian/shells/root_shell.dart';
import 'package:solian/theme.dart'; import 'package:solian/theme.dart';
import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_avatar.dart';
import 'package:solian/widgets/account/account_status_action.dart'; import 'package:solian/widgets/account/account_status_action.dart';
import 'package:solian/widgets/channel/channel_list.dart';
import 'package:solian/widgets/navigation/app_navigation.dart'; import 'package:solian/widgets/navigation/app_navigation.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
import 'package:solian/widgets/navigation/app_navigation_regions.dart';
class AppNavigationDrawer extends StatefulWidget { class AppNavigationDrawer extends StatefulWidget {
final String? routeName; final String? routeName;
@ -24,12 +25,8 @@ class AppNavigationDrawer extends StatefulWidget {
} }
class _AppNavigationDrawerState extends State<AppNavigationDrawer> { class _AppNavigationDrawerState extends State<AppNavigationDrawer> {
int? _selectedIndex = 0;
AccountStatus? _accountStatus; AccountStatus? _accountStatus;
late final ChannelProvider _channels;
Future<void> _getStatus() async { Future<void> _getStatus() async {
final StatusProvider provider = Get.find(); final StatusProvider provider = Get.find();
@ -41,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),
),
],
),
),
); );
} }
} }

View File

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

View File

@ -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)

View File

@ -1,5 +1,6 @@
import 'package:animations/animations.dart'; import 'package:animations/animations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get_utils/get_utils.dart'; import 'package:get/get_utils/get_utils.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -22,7 +23,9 @@ class PostItem extends StatefulWidget {
final bool isReactable; final bool isReactable;
final bool isShowReply; final bool isShowReply;
final bool isShowEmbed; final bool isShowEmbed;
final bool isOverrideEmbedClickable;
final bool isFullDate; final bool isFullDate;
final bool isFullContent;
final bool isContentSelectable; final bool isContentSelectable;
final String? attachmentParent; final String? attachmentParent;
final Color? backgroundColor; final Color? backgroundColor;
@ -35,7 +38,9 @@ class PostItem extends StatefulWidget {
this.isReactable = true, this.isReactable = true,
this.isShowReply = true, this.isShowReply = true,
this.isShowEmbed = true, this.isShowEmbed = true,
this.isOverrideEmbedClickable = false,
this.isFullDate = false, this.isFullDate = false,
this.isFullContent = false,
this.isContentSelectable = false, this.isContentSelectable = false,
this.attachmentParent, this.attachmentParent,
this.backgroundColor, this.backgroundColor,
@ -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;
}
}

View File

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

View File

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

View File

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

View File

@ -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,
), ),
); );

View File

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

View File

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

View File

@ -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"

View File

@ -2,7 +2,7 @@ name: solian
description: "The Solar Network App" description: "The Solar Network App"
publish_to: "none" publish_to: "none"
version: 1.2.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: