diff --git a/lib/models/chat.dart b/lib/models/chat.dart index 621df2d..9d54c41 100644 --- a/lib/models/chat.dart +++ b/lib/models/chat.dart @@ -10,8 +10,8 @@ part 'chat.g.dart'; abstract class SnChatRoom with _$SnChatRoom { const factory SnChatRoom({ required String id, - required String name, - required String description, + required String? name, + required String? description, required int type, required bool isPublic, required String? pictureId, diff --git a/lib/models/chat.freezed.dart b/lib/models/chat.freezed.dart index 1cf5137..c46e2d6 100644 --- a/lib/models/chat.freezed.dart +++ b/lib/models/chat.freezed.dart @@ -16,7 +16,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$SnChatRoom { - String get id; String get name; String get description; int get type; bool get isPublic; String? get pictureId; SnCloudFile? get picture; String? get backgroundId; SnCloudFile? get background; String? get realmId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List? get members; + String get id; String? get name; String? get description; int get type; bool get isPublic; String? get pictureId; SnCloudFile? get picture; String? get backgroundId; SnCloudFile? get background; String? get realmId; SnRealm? get realm; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; List? get members; /// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -49,7 +49,7 @@ abstract mixin class $SnChatRoomCopyWith<$Res> { factory $SnChatRoomCopyWith(SnChatRoom value, $Res Function(SnChatRoom) _then) = _$SnChatRoomCopyWithImpl; @useResult $Res call({ - String id, String name, String description, int type, bool isPublic, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members + String id, String? name, String? description, int type, bool isPublic, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members }); @@ -66,12 +66,12 @@ class _$SnChatRoomCopyWithImpl<$Res> /// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = null,Object? description = null,Object? type = null,Object? isPublic = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { return _then(_self.copyWith( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable -as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable -as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable -as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as int,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable as bool,pictureId: freezed == pictureId ? _self.pictureId : pictureId // ignore: cast_nullable_to_non_nullable as String?,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable @@ -134,8 +134,8 @@ class _SnChatRoom implements SnChatRoom { factory _SnChatRoom.fromJson(Map json) => _$SnChatRoomFromJson(json); @override final String id; -@override final String name; -@override final String description; +@override final String? name; +@override final String? description; @override final int type; @override final bool isPublic; @override final String? pictureId; @@ -190,7 +190,7 @@ abstract mixin class _$SnChatRoomCopyWith<$Res> implements $SnChatRoomCopyWith<$ factory _$SnChatRoomCopyWith(_SnChatRoom value, $Res Function(_SnChatRoom) _then) = __$SnChatRoomCopyWithImpl; @override @useResult $Res call({ - String id, String name, String description, int type, bool isPublic, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members + String id, String? name, String? description, int type, bool isPublic, String? pictureId, SnCloudFile? picture, String? backgroundId, SnCloudFile? background, String? realmId, SnRealm? realm, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, List? members }); @@ -207,12 +207,12 @@ class __$SnChatRoomCopyWithImpl<$Res> /// Create a copy of SnChatRoom /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = null,Object? description = null,Object? type = null,Object? isPublic = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? name = freezed,Object? description = freezed,Object? type = null,Object? isPublic = null,Object? pictureId = freezed,Object? picture = freezed,Object? backgroundId = freezed,Object? background = freezed,Object? realmId = freezed,Object? realm = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? members = freezed,}) { return _then(_SnChatRoom( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable -as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable -as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable -as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as int,isPublic: null == isPublic ? _self.isPublic : isPublic // ignore: cast_nullable_to_non_nullable as bool,pictureId: freezed == pictureId ? _self.pictureId : pictureId // ignore: cast_nullable_to_non_nullable as String?,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/chat.g.dart b/lib/models/chat.g.dart index cf559b0..08bd0a1 100644 --- a/lib/models/chat.g.dart +++ b/lib/models/chat.g.dart @@ -8,8 +8,8 @@ part of 'chat.dart'; _SnChatRoom _$SnChatRoomFromJson(Map json) => _SnChatRoom( id: json['id'] as String, - name: json['name'] as String, - description: json['description'] as String, + name: json['name'] as String?, + description: json['description'] as String?, type: (json['type'] as num).toInt(), isPublic: json['is_public'] as bool, pictureId: json['picture_id'] as String?, diff --git a/lib/screens/auth/captcha.web.dart b/lib/screens/auth/captcha.web.dart index 7d060d0..78b6fa4 100644 --- a/lib/screens/auth/captcha.web.dart +++ b/lib/screens/auth/captcha.web.dart @@ -23,6 +23,7 @@ class _CaptchaScreenState extends ConsumerState { final message = event.data as String; if (message.startsWith("captcha_tk=")) { String token = message.replaceFirst("captcha_tk=", ""); + // ignore: use_build_context_synchronously if (context.mounted) Navigator.pop(context, token); } } diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index 4d8810c..3377d48 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -4,7 +4,6 @@ import 'package:croppy/croppy.dart' hide cropImage; import 'package:dio/dio.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -48,20 +47,27 @@ class ChatRoomListTile extends StatelessWidget { Widget build(BuildContext context) { return ListTile( leading: - isDirect - ? ProfilePictureWidget( - fileId: room.members!.first.account.profile.pictureId, + (isDirect && room.pictureId == null) + ? SplitAvatarWidget( + filesId: + room.members! + .map((e) => e.account.profile.pictureId) + .toList(), ) : room.pictureId == null - ? CircleAvatar(child: Text(room.name[0].toUpperCase())) + ? CircleAvatar(child: Text(room.name![0].toUpperCase())) : ProfilePictureWidget(fileId: room.pictureId), - title: Text(isDirect ? room.members!.first.account.nick : room.name), + title: Text( + (isDirect && room.name == null) + ? room.members!.map((e) => e.account.nick).join(', ') + : room.name!, + ), subtitle: subtitle != null ? subtitle! - : isDirect - ? Text('@${room.members!.first.account.name}') - : Text(room.description), + : (isDirect && room.description == null) + ? Text(room.members!.map((e) => '@${e.account.name}').join(', ')) + : Text(room.description ?? 'descriptionNone'.tr()), trailing: trailing, onTap: onTap ?? @@ -82,8 +88,6 @@ Future> chatroomsJoined(Ref ref) async { .toList(); } -final chatFabKey = GlobalKey(); - @RoutePage() class ChatListScreen extends HookConsumerWidget { const ChatListScreen({super.key}); @@ -139,69 +143,41 @@ class ChatListScreen extends HookConsumerWidget { const Gap(8), ], ), - floatingActionButtonLocation: ExpandableFab.location, - floatingActionButton: ExpandableFab( - key: chatFabKey, - distance: 75, - type: ExpandableFabType.up, - childrenAnimation: ExpandableFabAnimation.none, - overlayStyle: ExpandableFabOverlayStyle( - color: Theme.of( - context, - ).colorScheme.surface.withAlpha((255 * 0.5).round()), - ), - openButtonBuilder: RotateFloatingActionButtonBuilder( - child: const Icon(Icons.add), - fabSize: ExpandableFabSize.regular, - foregroundColor: - Theme.of(context).floatingActionButtonTheme.foregroundColor, - backgroundColor: - Theme.of(context).floatingActionButtonTheme.backgroundColor, - ), - closeButtonBuilder: DefaultFloatingActionButtonBuilder( - child: const Icon(Icons.close), - fabSize: ExpandableFabSize.regular, - foregroundColor: - Theme.of(context).floatingActionButtonTheme.foregroundColor, - backgroundColor: - Theme.of(context).floatingActionButtonTheme.backgroundColor, - ), - children: [ - Row( - children: [ - Text('createChatRoom').tr(), - const Gap(20), - FloatingActionButton( - heroTag: null, - tooltip: 'createChatRoom'.tr(), - onPressed: () { - chatFabKey.currentState?.toggle(); - context.pushRoute(NewChatRoute()).then((value) { - if (value != null) { - ref.invalidate(chatroomsJoinedProvider); - } - }); - }, - child: const Icon(Symbols.chat_add_on), - ), - ], - ), - Row( - children: [ - Text('createDirectMessage').tr(), - const Gap(20), - FloatingActionButton( - heroTag: null, - tooltip: 'createDirectMessage'.tr(), - onPressed: () { - chatFabKey.currentState?.toggle(); - createDirectMessage(); - }, - child: const Icon(Symbols.communication), - ), - ], - ), - ], + floatingActionButton: FloatingActionButton( + onPressed: () { + showModalBottomSheet( + context: context, + builder: + (context) => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListTile( + title: Text('createChatRoom').tr(), + leading: const Icon(Symbols.add), + onTap: () { + Navigator.pop(context); + context.pushRoute(NewChatRoute()).then((value) { + if (value != null) { + ref.invalidate(chatroomsJoinedProvider); + } + }); + }, + ), + ListTile( + title: Text('createDirectMessage').tr(), + leading: const Icon(Symbols.person), + onTap: () { + Navigator.pop(context); + createDirectMessage(); + }, + ), + Gap(MediaQuery.of(context).padding.bottom + 16), + ], + ), + ); + }, + child: const Icon(Symbols.add), ), body: chats.when( data: @@ -281,8 +257,8 @@ class EditChatScreen extends HookConsumerWidget { useEffect(() { if (chat.value != null) { - nameController.text = chat.value!.name; - descriptionController.text = chat.value!.description; + nameController.text = chat.value!.name ?? ''; + descriptionController.text = chat.value!.description ?? ''; picture.value = chat.value!.picture; background.value = chat.value!.background; currentRealm.value = joinedRealms.value?.firstWhereOrNull( diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index a5f617e..3da5a94 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -442,10 +442,12 @@ class ChatRoomScreen extends HookConsumerWidget { height: 26, width: 26, child: - room!.type == 1 - ? ProfilePictureWidget( - fileId: - room.members!.first.account.profile.pictureId, + (room!.type == 1 && room.pictureId == null) + ? SplitAvatarWidget( + filesId: + room.members! + .map((e) => e.account.profile.pictureId) + .toList(), ) : room.pictureId != null ? ProfilePictureWidget( @@ -454,15 +456,15 @@ class ChatRoomScreen extends HookConsumerWidget { ) : CircleAvatar( child: Text( - room.name[0].toUpperCase(), + room.name![0].toUpperCase(), style: const TextStyle(fontSize: 12), ), ), ), Text( - room.type == 1 - ? room.members!.first.account.nick - : room.name, + (room.type == 1 && room.name == null) + ? room.members!.map((e) => e.account.nick).join(', ') + : room.name!, ).fontSize(19), ], ), @@ -763,7 +765,7 @@ class _ChatInput extends StatelessWidget { ? 'chatDirectMessageHint'.tr( args: [chatRoom.members!.first.account.nick], ) - : 'chatMessageHint'.tr(args: [chatRoom.name]), + : 'chatMessageHint'.tr(args: [chatRoom.name!]), border: InputBorder.none, isDense: true, contentPadding: const EdgeInsets.symmetric( diff --git a/lib/screens/chat/room_detail.dart b/lib/screens/chat/room_detail.dart index 7df73c5..64b95d2 100644 --- a/lib/screens/chat/room_detail.dart +++ b/lib/screens/chat/room_detail.dart @@ -54,14 +54,20 @@ class ChatDetailScreen extends HookConsumerWidget { leading: PageBackButton(shadows: [iconShadow]), flexibleSpace: FlexibleSpaceBar( background: - currentRoom!.type == 1 && + (currentRoom!.type == 1 && + currentRoom.backgroundId != null) + ? CloudImageWidget( + fileId: currentRoom.backgroundId!, + ) + : (currentRoom.type == 1 && + currentRoom.members!.length == 1 && currentRoom .members! .first .account .profile .backgroundId != - null + null) ? CloudImageWidget( fileId: currentRoom @@ -81,9 +87,11 @@ class ChatDetailScreen extends HookConsumerWidget { Theme.of(context).appBarTheme.backgroundColor, ), title: Text( - currentRoom.type == 1 - ? currentRoom.members!.first.account.nick - : currentRoom.name, + (currentRoom.type == 1 && currentRoom.name == null) + ? currentRoom.members! + .map((e) => e.account.name) + .join(', ') + : currentRoom.name!, style: TextStyle( color: Theme.of(context).appBarTheme.foregroundColor, shadows: [iconShadow], @@ -114,7 +122,7 @@ class ChatDetailScreen extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - currentRoom.description, + currentRoom.description ?? 'descriptionNone'.tr(), style: const TextStyle(fontSize: 16), ), ], diff --git a/lib/screens/realm/realms.dart b/lib/screens/realm/realms.dart index b2d4173..ebf7924 100644 --- a/lib/screens/realm/realms.dart +++ b/lib/screens/realm/realms.dart @@ -73,7 +73,11 @@ class RealmListScreen extends HookConsumerWidget { heroTag: Key("realms-page-fab"), child: const Icon(Symbols.add), onPressed: () { - context.router.push(NewRealmRoute()); + context.router.push(NewRealmRoute()).then((value) { + if (value != null) { + ref.invalidate(realmsJoinedProvider); + } + }); }, ), body: RefreshIndicator( diff --git a/lib/screens/wallet.dart b/lib/screens/wallet.dart index e1cf132..8b11070 100644 --- a/lib/screens/wallet.dart +++ b/lib/screens/wallet.dart @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/wallet.dart'; import 'package:island/pods/network.dart'; import 'package:island/widgets/app_scaffold.dart'; +import 'package:island/widgets/response.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; @@ -149,7 +150,11 @@ class WalletScreen extends HookConsumerWidget { ], ); }, - error: (error, stackTrace) => Center(child: Text('Error: $error')), + error: + (error, stackTrace) => ResponseErrorWidget( + error: error, + onRetry: () => ref.invalidate(walletCurrentProvider), + ), loading: () => const Center(child: CircularProgressIndicator()), ), ); diff --git a/lib/widgets/content/cloud_files.dart b/lib/widgets/content/cloud_files.dart index b0e4701..9b0f300 100644 --- a/lib/widgets/content/cloud_files.dart +++ b/lib/widgets/content/cloud_files.dart @@ -64,6 +64,14 @@ class CloudImageWidget extends ConsumerWidget { child: UniversalImage(uri: uri, blurHash: blurHash), ); } + + static ImageProvider provider({ + required String fileId, + required String serverUrl, + }) { + final uri = '$serverUrl/files/$fileId'; + return CachedNetworkImageProvider(uri); + } } class ProfilePictureWidget extends ConsumerWidget { @@ -104,3 +112,153 @@ class ProfilePictureWidget extends ConsumerWidget { ); } } + +class SplitAvatarWidget extends ConsumerWidget { + final List filesId; + final double radius; + final IconData fallbackIcon; + final Color? fallbackColor; + + const SplitAvatarWidget({ + super.key, + required this.filesId, + this.radius = 20, + this.fallbackIcon = Symbols.account_circle, + this.fallbackColor, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (filesId.isEmpty) { + return ProfilePictureWidget( + fileId: null, + radius: radius, + fallbackIcon: fallbackIcon, + fallbackColor: fallbackColor, + ); + } + if (filesId.length == 1) { + return ProfilePictureWidget( + fileId: filesId[0], + radius: radius, + fallbackIcon: fallbackIcon, + fallbackColor: fallbackColor, + ); + } + + return ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(radius)), + child: Container( + width: radius * 2, + height: radius * 2, + color: Theme.of(context).colorScheme.primaryContainer, + child: Stack( + children: [ + if (filesId.length == 2) + Row( + children: [ + Expanded( + child: _buildQuadrant(context, filesId[0], ref, radius), + ), + Expanded( + child: _buildQuadrant(context, filesId[1], ref, radius), + ), + ], + ) + else if (filesId.length == 3) + Row( + children: [ + Column( + children: [ + Expanded( + child: _buildQuadrant(context, filesId[0], ref, radius), + ), + Expanded( + child: _buildQuadrant(context, filesId[1], ref, radius), + ), + ], + ), + Expanded( + child: _buildQuadrant(context, filesId[2], ref, radius), + ), + ], + ) + else ...[ + Positioned( + top: 0, + left: 0, + child: _buildQuadrant(context, filesId[0], ref, radius), + ), + Positioned( + top: 0, + right: 0, + child: _buildQuadrant(context, filesId[1], ref, radius), + ), + Positioned( + bottom: 0, + left: 0, + child: _buildQuadrant(context, filesId[2], ref, radius), + ), + Positioned( + bottom: 0, + right: 0, + child: + filesId.length > 4 + ? Container( + width: radius, + height: radius, + color: Theme.of(context).colorScheme.primaryContainer, + child: Center( + child: Text( + '+${filesId.length - 3}', + style: TextStyle( + fontSize: radius * 0.4, + color: + Theme.of( + context, + ).colorScheme.onPrimaryContainer, + ), + ), + ), + ) + : _buildQuadrant(context, filesId[3], ref, radius), + ), + ], + ], + ), + ), + ); + } + + Widget _buildQuadrant( + BuildContext context, + String? fileId, + WidgetRef ref, + double radius, + ) { + if (fileId == null) { + return Container( + width: radius, + height: radius, + color: Theme.of(context).colorScheme.primaryContainer, + child: + Icon( + fallbackIcon, + size: radius * 0.6, + color: + fallbackColor ?? + Theme.of(context).colorScheme.onPrimaryContainer, + ).center(), + ); + } + + final serverUrl = ref.watch(serverUrlProvider); + final uri = '$serverUrl/files/$fileId'; + + return SizedBox( + width: radius, + height: radius, + child: CachedNetworkImage(imageUrl: uri, fit: BoxFit.cover), + ); + } +} diff --git a/lib/widgets/response.dart b/lib/widgets/response.dart index ac74d60..36263aa 100644 --- a/lib/widgets/response.dart +++ b/lib/widgets/response.dart @@ -4,7 +4,7 @@ import 'package:gap/gap.dart'; import 'package:material_symbols_icons/symbols.dart'; class ResponseErrorWidget extends StatelessWidget { - final Error error; + final dynamic error; final VoidCallback onRetry; const ResponseErrorWidget({ super.key, @@ -18,13 +18,13 @@ class ResponseErrorWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Symbols.error_outline, size: 48), - const Gap(16), + const Gap(4), Text( error.toString(), textAlign: TextAlign.center, style: const TextStyle(color: Color(0xFF757575)), ), - const SizedBox(height: 16), + const Gap(8), TextButton(onPressed: onRetry, child: const Text('retry').tr()), ], ); diff --git a/pubspec.lock b/pubspec.lock index 743e025..3a6c839 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "10.0.1" + avatar_stack: + dependency: "direct main" + description: + name: avatar_stack + sha256: "354527ba139956fd6439e2c49199d8298d72afdaa6c4cd6f37f26b97faf21f7e" + url: "https://pub.dev" + source: hosted + version: "3.0.0" bitsdojo_window: dependency: "direct main" description: @@ -630,14 +638,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" - flutter_expandable_fab: - dependency: "direct main" - description: - name: flutter_expandable_fab - sha256: c2936d398169166064d025df91a3bb417109a859e725d9b80c6ef7f04e01b6ab - url: "https://pub.dev" - source: hosted - version: "2.5.1" flutter_highlight: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f5bed0e..76a7382 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -88,7 +88,6 @@ dependencies: drift_flutter: ^0.2.4 path: ^1.9.1 collection: ^1.19.1 - flutter_expandable_fab: ^2.5.0 markdown_editor_plus: ^0.2.15 croppy: ^1.3.6 table_calendar: ^3.1.3 @@ -96,6 +95,7 @@ dependencies: dropdown_button2: ^2.3.9 riverpod_paging_utils: ^0.8.0 crypto: ^3.0.6 + avatar_stack: ^3.0.0 dev_dependencies: flutter_test: