DM groups

This commit is contained in:
LittleSheep 2025-05-17 23:13:08 +08:00
parent 7ccaf737a9
commit 81d5083908
13 changed files with 276 additions and 122 deletions

View File

@ -10,8 +10,8 @@ part 'chat.g.dart';
abstract class SnChatRoom with _$SnChatRoom { abstract class SnChatRoom with _$SnChatRoom {
const factory SnChatRoom({ const factory SnChatRoom({
required String id, required String id,
required String name, required String? name,
required String description, required String? description,
required int type, required int type,
required bool isPublic, required bool isPublic,
required String? pictureId, required String? pictureId,

View File

@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$SnChatRoom { 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<SnChatMember>? 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<SnChatMember>? get members;
/// Create a copy of SnChatRoom /// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -49,7 +49,7 @@ abstract mixin class $SnChatRoomCopyWith<$Res> {
factory $SnChatRoomCopyWith(SnChatRoom value, $Res Function(SnChatRoom) _then) = _$SnChatRoomCopyWithImpl; factory $SnChatRoomCopyWith(SnChatRoom value, $Res Function(SnChatRoom) _then) = _$SnChatRoomCopyWithImpl;
@useResult @useResult
$Res call({ $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<SnChatMember>? 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<SnChatMember>? members
}); });
@ -66,12 +66,12 @@ class _$SnChatRoomCopyWithImpl<$Res>
/// Create a copy of SnChatRoom /// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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,name: freezed == 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?,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 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 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 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 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<String, dynamic> json) => _$SnChatRoomFromJson(json); factory _SnChatRoom.fromJson(Map<String, dynamic> json) => _$SnChatRoomFromJson(json);
@override final String id; @override final String id;
@override final String name; @override final String? name;
@override final String description; @override final String? description;
@override final int type; @override final int type;
@override final bool isPublic; @override final bool isPublic;
@override final String? pictureId; @override final String? pictureId;
@ -190,7 +190,7 @@ abstract mixin class _$SnChatRoomCopyWith<$Res> implements $SnChatRoomCopyWith<$
factory _$SnChatRoomCopyWith(_SnChatRoom value, $Res Function(_SnChatRoom) _then) = __$SnChatRoomCopyWithImpl; factory _$SnChatRoomCopyWith(_SnChatRoom value, $Res Function(_SnChatRoom) _then) = __$SnChatRoomCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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<SnChatMember>? 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<SnChatMember>? members
}); });
@ -207,12 +207,12 @@ class __$SnChatRoomCopyWithImpl<$Res>
/// Create a copy of SnChatRoom /// Create a copy of SnChatRoom
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_SnChatRoom(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable 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,name: freezed == 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?,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 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 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 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 as String?,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable

View File

@ -8,8 +8,8 @@ part of 'chat.dart';
_SnChatRoom _$SnChatRoomFromJson(Map<String, dynamic> json) => _SnChatRoom( _SnChatRoom _$SnChatRoomFromJson(Map<String, dynamic> json) => _SnChatRoom(
id: json['id'] as String, id: json['id'] as String,
name: json['name'] as String, name: json['name'] as String?,
description: json['description'] as String, description: json['description'] as String?,
type: (json['type'] as num).toInt(), type: (json['type'] as num).toInt(),
isPublic: json['is_public'] as bool, isPublic: json['is_public'] as bool,
pictureId: json['picture_id'] as String?, pictureId: json['picture_id'] as String?,

View File

@ -23,6 +23,7 @@ class _CaptchaScreenState extends ConsumerState<CaptchaScreen> {
final message = event.data as String; final message = event.data as String;
if (message.startsWith("captcha_tk=")) { if (message.startsWith("captcha_tk=")) {
String token = message.replaceFirst("captcha_tk=", ""); String token = message.replaceFirst("captcha_tk=", "");
// ignore: use_build_context_synchronously
if (context.mounted) Navigator.pop(context, token); if (context.mounted) Navigator.pop(context, token);
} }
} }

View File

@ -4,7 +4,6 @@ import 'package:croppy/croppy.dart' hide cropImage;
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -48,20 +47,27 @@ class ChatRoomListTile extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
leading: leading:
isDirect (isDirect && room.pictureId == null)
? ProfilePictureWidget( ? SplitAvatarWidget(
fileId: room.members!.first.account.profile.pictureId, filesId:
room.members!
.map((e) => e.account.profile.pictureId)
.toList(),
) )
: room.pictureId == null : room.pictureId == null
? CircleAvatar(child: Text(room.name[0].toUpperCase())) ? CircleAvatar(child: Text(room.name![0].toUpperCase()))
: ProfilePictureWidget(fileId: room.pictureId), : 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:
subtitle != null subtitle != null
? subtitle! ? subtitle!
: isDirect : (isDirect && room.description == null)
? Text('@${room.members!.first.account.name}') ? Text(room.members!.map((e) => '@${e.account.name}').join(', '))
: Text(room.description), : Text(room.description ?? 'descriptionNone'.tr()),
trailing: trailing, trailing: trailing,
onTap: onTap:
onTap ?? onTap ??
@ -82,8 +88,6 @@ Future<List<SnChatRoom>> chatroomsJoined(Ref ref) async {
.toList(); .toList();
} }
final chatFabKey = GlobalKey<ExpandableFabState>();
@RoutePage() @RoutePage()
class ChatListScreen extends HookConsumerWidget { class ChatListScreen extends HookConsumerWidget {
const ChatListScreen({super.key}); const ChatListScreen({super.key});
@ -139,69 +143,41 @@ class ChatListScreen extends HookConsumerWidget {
const Gap(8), const Gap(8),
], ],
), ),
floatingActionButtonLocation: ExpandableFab.location, floatingActionButton: FloatingActionButton(
floatingActionButton: ExpandableFab( onPressed: () {
key: chatFabKey, showModalBottomSheet(
distance: 75, context: context,
type: ExpandableFabType.up, builder:
childrenAnimation: ExpandableFabAnimation.none, (context) => Column(
overlayStyle: ExpandableFabOverlayStyle( mainAxisSize: MainAxisSize.min,
color: Theme.of( crossAxisAlignment: CrossAxisAlignment.stretch,
context, children: [
).colorScheme.surface.withAlpha((255 * 0.5).round()), ListTile(
), title: Text('createChatRoom').tr(),
openButtonBuilder: RotateFloatingActionButtonBuilder( leading: const Icon(Symbols.add),
child: const Icon(Icons.add), onTap: () {
fabSize: ExpandableFabSize.regular, Navigator.pop(context);
foregroundColor: context.pushRoute(NewChatRoute()).then((value) {
Theme.of(context).floatingActionButtonTheme.foregroundColor, if (value != null) {
backgroundColor: ref.invalidate(chatroomsJoinedProvider);
Theme.of(context).floatingActionButtonTheme.backgroundColor, }
), });
closeButtonBuilder: DefaultFloatingActionButtonBuilder( },
child: const Icon(Icons.close), ),
fabSize: ExpandableFabSize.regular, ListTile(
foregroundColor: title: Text('createDirectMessage').tr(),
Theme.of(context).floatingActionButtonTheme.foregroundColor, leading: const Icon(Symbols.person),
backgroundColor: onTap: () {
Theme.of(context).floatingActionButtonTheme.backgroundColor, Navigator.pop(context);
), createDirectMessage();
children: [ },
Row( ),
children: [ Gap(MediaQuery.of(context).padding.bottom + 16),
Text('createChatRoom').tr(), ],
const Gap(20), ),
FloatingActionButton( );
heroTag: null, },
tooltip: 'createChatRoom'.tr(), child: const Icon(Symbols.add),
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),
),
],
),
],
), ),
body: chats.when( body: chats.when(
data: data:
@ -281,8 +257,8 @@ class EditChatScreen extends HookConsumerWidget {
useEffect(() { useEffect(() {
if (chat.value != null) { if (chat.value != null) {
nameController.text = chat.value!.name; nameController.text = chat.value!.name ?? '';
descriptionController.text = chat.value!.description; descriptionController.text = chat.value!.description ?? '';
picture.value = chat.value!.picture; picture.value = chat.value!.picture;
background.value = chat.value!.background; background.value = chat.value!.background;
currentRealm.value = joinedRealms.value?.firstWhereOrNull( currentRealm.value = joinedRealms.value?.firstWhereOrNull(

View File

@ -442,10 +442,12 @@ class ChatRoomScreen extends HookConsumerWidget {
height: 26, height: 26,
width: 26, width: 26,
child: child:
room!.type == 1 (room!.type == 1 && room.pictureId == null)
? ProfilePictureWidget( ? SplitAvatarWidget(
fileId: filesId:
room.members!.first.account.profile.pictureId, room.members!
.map((e) => e.account.profile.pictureId)
.toList(),
) )
: room.pictureId != null : room.pictureId != null
? ProfilePictureWidget( ? ProfilePictureWidget(
@ -454,15 +456,15 @@ class ChatRoomScreen extends HookConsumerWidget {
) )
: CircleAvatar( : CircleAvatar(
child: Text( child: Text(
room.name[0].toUpperCase(), room.name![0].toUpperCase(),
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
), ),
), ),
), ),
Text( Text(
room.type == 1 (room.type == 1 && room.name == null)
? room.members!.first.account.nick ? room.members!.map((e) => e.account.nick).join(', ')
: room.name, : room.name!,
).fontSize(19), ).fontSize(19),
], ],
), ),
@ -763,7 +765,7 @@ class _ChatInput extends StatelessWidget {
? 'chatDirectMessageHint'.tr( ? 'chatDirectMessageHint'.tr(
args: [chatRoom.members!.first.account.nick], args: [chatRoom.members!.first.account.nick],
) )
: 'chatMessageHint'.tr(args: [chatRoom.name]), : 'chatMessageHint'.tr(args: [chatRoom.name!]),
border: InputBorder.none, border: InputBorder.none,
isDense: true, isDense: true,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(

View File

@ -54,14 +54,20 @@ class ChatDetailScreen extends HookConsumerWidget {
leading: PageBackButton(shadows: [iconShadow]), leading: PageBackButton(shadows: [iconShadow]),
flexibleSpace: FlexibleSpaceBar( flexibleSpace: FlexibleSpaceBar(
background: background:
currentRoom!.type == 1 && (currentRoom!.type == 1 &&
currentRoom.backgroundId != null)
? CloudImageWidget(
fileId: currentRoom.backgroundId!,
)
: (currentRoom.type == 1 &&
currentRoom.members!.length == 1 &&
currentRoom currentRoom
.members! .members!
.first .first
.account .account
.profile .profile
.backgroundId != .backgroundId !=
null null)
? CloudImageWidget( ? CloudImageWidget(
fileId: fileId:
currentRoom currentRoom
@ -81,9 +87,11 @@ class ChatDetailScreen extends HookConsumerWidget {
Theme.of(context).appBarTheme.backgroundColor, Theme.of(context).appBarTheme.backgroundColor,
), ),
title: Text( title: Text(
currentRoom.type == 1 (currentRoom.type == 1 && currentRoom.name == null)
? currentRoom.members!.first.account.nick ? currentRoom.members!
: currentRoom.name, .map((e) => e.account.name)
.join(', ')
: currentRoom.name!,
style: TextStyle( style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor, color: Theme.of(context).appBarTheme.foregroundColor,
shadows: [iconShadow], shadows: [iconShadow],
@ -114,7 +122,7 @@ class ChatDetailScreen extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
currentRoom.description, currentRoom.description ?? 'descriptionNone'.tr(),
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
), ),
], ],

View File

@ -73,7 +73,11 @@ class RealmListScreen extends HookConsumerWidget {
heroTag: Key("realms-page-fab"), heroTag: Key("realms-page-fab"),
child: const Icon(Symbols.add), child: const Icon(Symbols.add),
onPressed: () { onPressed: () {
context.router.push(NewRealmRoute()); context.router.push(NewRealmRoute()).then((value) {
if (value != null) {
ref.invalidate(realmsJoinedProvider);
}
});
}, },
), ),
body: RefreshIndicator( body: RefreshIndicator(

View File

@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/wallet.dart'; import 'package:island/models/wallet.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.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()), loading: () => const Center(child: CircularProgressIndicator()),
), ),
); );

View File

@ -64,6 +64,14 @@ class CloudImageWidget extends ConsumerWidget {
child: UniversalImage(uri: uri, blurHash: blurHash), 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 { class ProfilePictureWidget extends ConsumerWidget {
@ -104,3 +112,153 @@ class ProfilePictureWidget extends ConsumerWidget {
); );
} }
} }
class SplitAvatarWidget extends ConsumerWidget {
final List<String?> 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),
);
}
}

View File

@ -4,7 +4,7 @@ import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
class ResponseErrorWidget extends StatelessWidget { class ResponseErrorWidget extends StatelessWidget {
final Error error; final dynamic error;
final VoidCallback onRetry; final VoidCallback onRetry;
const ResponseErrorWidget({ const ResponseErrorWidget({
super.key, super.key,
@ -18,13 +18,13 @@ class ResponseErrorWidget extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Symbols.error_outline, size: 48), const Icon(Symbols.error_outline, size: 48),
const Gap(16), const Gap(4),
Text( Text(
error.toString(), error.toString(),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle(color: Color(0xFF757575)), style: const TextStyle(color: Color(0xFF757575)),
), ),
const SizedBox(height: 16), const Gap(8),
TextButton(onPressed: onRetry, child: const Text('retry').tr()), TextButton(onPressed: onRetry, child: const Text('retry').tr()),
], ],
); );

View File

@ -81,6 +81,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.1" 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: bitsdojo_window:
dependency: "direct main" dependency: "direct main"
description: description:
@ -630,14 +638,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.1" 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: flutter_highlight:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -88,7 +88,6 @@ dependencies:
drift_flutter: ^0.2.4 drift_flutter: ^0.2.4
path: ^1.9.1 path: ^1.9.1
collection: ^1.19.1 collection: ^1.19.1
flutter_expandable_fab: ^2.5.0
markdown_editor_plus: ^0.2.15 markdown_editor_plus: ^0.2.15
croppy: ^1.3.6 croppy: ^1.3.6
table_calendar: ^3.1.3 table_calendar: ^3.1.3
@ -96,6 +95,7 @@ dependencies:
dropdown_button2: ^2.3.9 dropdown_button2: ^2.3.9
riverpod_paging_utils: ^0.8.0 riverpod_paging_utils: ^0.8.0
crypto: ^3.0.6 crypto: ^3.0.6
avatar_stack: ^3.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: