Chat message uploading file progress

This commit is contained in:
LittleSheep 2025-05-18 02:21:14 +08:00
parent 81d5083908
commit 3737de6936
6 changed files with 323 additions and 111 deletions

View File

@ -236,5 +236,6 @@
}, },
"levelingProgress": "Leveling Progress", "levelingProgress": "Leveling Progress",
"levelingProgressExperience": "{} EXP", "levelingProgressExperience": "{} EXP",
"levelingProgressLevel": "Level {}" "levelingProgressLevel": "Level {}",
"fileUploadingProgress": "Uploading file #{}: {}%"
} }

View File

@ -1,5 +1,6 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:island/models/chat.dart'; import 'package:island/models/chat.dart';
import 'package:island/models/file.dart';
class ChatMessages extends Table { class ChatMessages extends Table {
TextColumn get id => text()(); TextColumn get id => text()();
@ -23,6 +24,7 @@ class LocalChatMessage {
final DateTime createdAt; final DateTime createdAt;
MessageStatus status; MessageStatus status;
final String? nonce; final String? nonce;
List<UniversalFile>? localAttachments;
LocalChatMessage({ LocalChatMessage({
required this.id, required this.id,
@ -30,8 +32,9 @@ class LocalChatMessage {
required this.senderId, required this.senderId,
required this.data, required this.data,
required this.createdAt, required this.createdAt,
required this.nonce,
required this.status, required this.status,
this.nonce, this.localAttachments,
}); });
SnChatMessage toRemoteMessage() { SnChatMessage toRemoteMessage() {

View File

@ -14,6 +14,7 @@ class MessageRepository {
final AppDatabase _database; final AppDatabase _database;
final Map<String, LocalChatMessage> pendingMessages = {}; final Map<String, LocalChatMessage> pendingMessages = {};
final Map<String, Map<int, double>> fileUploadProgress = {};
MessageRepository(this.room, this.identity, this._apiClient, this._database); MessageRepository(this.room, this.identity, this._apiClient, this._database);
@ -181,6 +182,7 @@ class MessageRepository {
SnChatMessage? forwardingTo, SnChatMessage? forwardingTo,
SnChatMessage? editingTo, SnChatMessage? editingTo,
Function(LocalChatMessage)? onPending, Function(LocalChatMessage)? onPending,
Function(String, Map<int, double>)? onProgress,
}) async { }) async {
// Generate a unique nonce for this message // Generate a unique nonce for this message
final nonce = const Uuid().v4(); final nonce = const Uuid().v4();
@ -204,6 +206,7 @@ class MessageRepository {
// Store in memory and database // Store in memory and database
pendingMessages[localMessage.id] = localMessage; pendingMessages[localMessage.id] = localMessage;
fileUploadProgress[localMessage.id] = {};
await _database.saveMessage(_database.messageToCompanion(localMessage)); await _database.saveMessage(_database.messageToCompanion(localMessage));
onPending?.call(localMessage); onPending?.call(localMessage);
@ -225,6 +228,13 @@ class MessageRepository {
UniversalFileType.audio => 'audio/unknown', UniversalFileType.audio => 'audio/unknown',
UniversalFileType.file => 'application/octet-stream', UniversalFileType.file => 'application/octet-stream',
}, },
onProgress: (progress, _) {
fileUploadProgress[localMessage.id]?[idx] = progress;
onProgress?.call(
localMessage.id,
fileUploadProgress[localMessage.id] ?? {},
);
},
).future; ).future;
if (cloudFile == null) { if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...'); throw ArgumentError('Failed to upload the file...');

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.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';
@ -23,6 +24,10 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:super_context_menu/super_context_menu.dart'; import 'package:super_context_menu/super_context_menu.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'chat.dart'; import 'chat.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'room.g.dart';
final messageRepositoryProvider = final messageRepositoryProvider =
FutureProvider.family<MessageRepository, String>((ref, roomId) async { FutureProvider.family<MessageRepository, String>((ref, roomId) async {
@ -33,29 +38,22 @@ final messageRepositoryProvider =
return MessageRepository(room!, identity!, apiClient, database); return MessageRepository(room!, identity!, apiClient, database);
}); });
// Provider for messages with pagination @riverpod
final messagesProvider = StateNotifierProvider.family< class MessagesNotifier extends _$MessagesNotifier {
MessagesNotifier, late final String _roomId;
AsyncValue<List<LocalChatMessage>>,
String
>((ref, roomId) => MessagesNotifier(ref, roomId));
class MessagesNotifier
extends StateNotifier<AsyncValue<List<LocalChatMessage>>> {
final Ref _ref;
final String _roomId;
int _currentPage = 0; int _currentPage = 0;
static const int _pageSize = 20; static const int _pageSize = 20;
bool _hasMore = true; bool _hasMore = true;
MessagesNotifier(this._ref, this._roomId) @override
: super(const AsyncValue.loading()) { FutureOr<List<LocalChatMessage>> build(String roomId) async {
loadInitial(); _roomId = roomId;
return await loadInitial();
} }
Future<void> loadInitial() async { Future<List<LocalChatMessage>> loadInitial() async {
try { try {
final repository = await _ref.read( final repository = await ref.read(
messageRepositoryProvider(_roomId).future, messageRepositoryProvider(_roomId).future,
); );
final synced = await repository.syncMessages(); final synced = await repository.syncMessages();
@ -64,11 +62,11 @@ class MessagesNotifier
take: _pageSize, take: _pageSize,
synced: synced, synced: synced,
); );
state = AsyncValue.data(messages);
_currentPage = 0; _currentPage = 0;
_hasMore = messages.length == _pageSize; _hasMore = messages.length == _pageSize;
} catch (e, stack) { return messages;
state = AsyncValue.error(e, stack); } catch (_) {
rethrow;
} }
} }
@ -78,7 +76,7 @@ class MessagesNotifier
try { try {
final currentMessages = state.value ?? []; final currentMessages = state.value ?? [];
_currentPage++; _currentPage++;
final repository = await _ref.read( final repository = await ref.read(
messageRepositoryProvider(_roomId).future, messageRepositoryProvider(_roomId).future,
); );
final newMessages = await repository.listMessages( final newMessages = await repository.listMessages(
@ -100,61 +98,49 @@ class MessagesNotifier
Future<void> sendMessage( Future<void> sendMessage(
String content, String content,
List<UniversalFile> attachments, { List<UniversalFile> attachments, {
SnChatMessage? replyingTo,
SnChatMessage? forwardingTo,
SnChatMessage? editingTo, SnChatMessage? editingTo,
SnChatMessage? forwardingTo,
SnChatMessage? replyingTo,
Function(String, Map<int, double>)? onProgress,
}) async { }) async {
try { try {
final repository = await _ref.read( final repository = await ref.read(
messageRepositoryProvider(_roomId).future, messageRepositoryProvider(_roomId).future,
); );
final baseUrl = ref.read(serverUrlProvider);
final nonce = const Uuid().v4();
final baseUrl = _ref.read(serverUrlProvider);
final atk = await getFreshAtk( final atk = await getFreshAtk(
_ref.watch(tokenPairProvider), ref.watch(tokenPairProvider),
baseUrl, baseUrl,
onRefreshed: (atk, rtk) { onRefreshed: (atk, rtk) {
setTokenPair(_ref.watch(sharedPreferencesProvider), atk, rtk); setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
_ref.invalidate(tokenPairProvider); ref.invalidate(tokenPairProvider);
}, },
); );
if (atk == null) throw Exception("Unauthorized"); if (atk == null) throw ArgumentError('Access token is null');
LocalChatMessage? pendingMessage; final currentMessages = state.value ?? [];
final messageTask = repository.sendMessage( await repository.sendMessage(
atk, atk,
baseUrl, baseUrl,
_roomId, _roomId,
content, content,
nonce, const Uuid().v4(),
attachments: attachments, attachments: attachments,
replyingTo: replyingTo,
forwardingTo: forwardingTo,
editingTo: editingTo, editingTo: editingTo,
forwardingTo: forwardingTo,
replyingTo: replyingTo,
onPending: (pending) { onPending: (pending) {
pendingMessage = pending;
final currentMessages = state.value ?? [];
state = AsyncValue.data([pending, ...currentMessages]); state = AsyncValue.data([pending, ...currentMessages]);
}, },
onProgress: onProgress,
); );
final message = await messageTask; // Refresh messages
final messages = await repository.listMessages(
final updatedMessages = state.value ?? []; offset: 0,
if (pendingMessage != null) { take: _pageSize,
final index = updatedMessages.indexWhere(
(m) => m.id == pendingMessage!.id,
); );
if (index >= 0) { state = AsyncValue.data(messages);
final newList = [...updatedMessages];
newList[index] = message;
state = AsyncValue.data(newList);
}
} else {
state = AsyncValue.data([message, ...updatedMessages]);
}
} catch (err) { } catch (err) {
showErrorAlert(err); showErrorAlert(err);
} }
@ -162,7 +148,7 @@ class MessagesNotifier
Future<void> retryMessage(String pendingMessageId) async { Future<void> retryMessage(String pendingMessageId) async {
try { try {
final repository = await _ref.read( final repository = await ref.read(
messageRepositoryProvider(_roomId).future, messageRepositoryProvider(_roomId).future,
); );
final updatedMessage = await repository.retryMessage(pendingMessageId); final updatedMessage = await repository.retryMessage(pendingMessageId);
@ -182,7 +168,7 @@ class MessagesNotifier
Future<void> receiveMessage(SnChatMessage remoteMessage) async { Future<void> receiveMessage(SnChatMessage remoteMessage) async {
try { try {
final repository = await _ref.read( final repository = await ref.read(
messageRepositoryProvider(_roomId).future, messageRepositoryProvider(_roomId).future,
); );
@ -217,7 +203,7 @@ class MessagesNotifier
Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async { Future<void> receiveMessageUpdate(SnChatMessage remoteMessage) async {
try { try {
final repository = await _ref.read( final repository = await ref.read(
messageRepositoryProvider(_roomId).future, messageRepositoryProvider(_roomId).future,
); );
@ -246,7 +232,7 @@ class MessagesNotifier
Future<void> receiveMessageDeletion(String messageId) async { Future<void> receiveMessageDeletion(String messageId) async {
try { try {
final repository = await _ref.read( final repository = await ref.read(
messageRepositoryProvider(_roomId).future, messageRepositoryProvider(_roomId).future,
); );
@ -265,41 +251,9 @@ class MessagesNotifier
} }
} }
Future<void> updateMessage(
String messageId,
String content, {
List<SnCloudFile>? attachments,
Map<String, dynamic>? meta,
}) async {
try {
final repository = await _ref.read(
messageRepositoryProvider(_roomId).future,
);
final updatedMessage = await repository.updateMessage(
messageId,
content,
attachments: attachments,
meta: meta,
);
// Update the message in the list
final currentMessages = state.value ?? [];
final index = currentMessages.indexWhere((m) => m.id == messageId);
if (index >= 0) {
final newList = [...currentMessages];
newList[index] = updatedMessage;
state = AsyncValue.data(newList);
}
} catch (err) {
showErrorAlert(err);
}
}
Future<void> deleteMessage(String messageId) async { Future<void> deleteMessage(String messageId) async {
try { try {
final repository = await _ref.read( final repository = await ref.read(
messageRepositoryProvider(_roomId).future, messageRepositoryProvider(_roomId).future,
); );
@ -320,7 +274,7 @@ class MessagesNotifier
Future<LocalChatMessage?> fetchMessageById(String messageId) async { Future<LocalChatMessage?> fetchMessageById(String messageId) async {
try { try {
final repository = await _ref.read( final repository = await ref.read(
messageRepositoryProvider(_roomId).future, messageRepositoryProvider(_roomId).future,
); );
return await repository.getMessageById(messageId); return await repository.getMessageById(messageId);
@ -340,8 +294,8 @@ class ChatRoomScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final chatRoom = ref.watch(chatroomProvider(id)); final chatRoom = ref.watch(chatroomProvider(id));
final chatIdentity = ref.watch(chatroomIdentityProvider(id)); final chatIdentity = ref.watch(chatroomIdentityProvider(id));
final messages = ref.watch(messagesProvider(id)); final messages = ref.watch(messagesNotifierProvider(id));
final messagesNotifier = ref.read(messagesProvider(id).notifier); final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
final ws = ref.watch(websocketProvider); final ws = ref.watch(websocketProvider);
final messageController = useTextEditingController(); final messageController = useTextEditingController();
@ -350,6 +304,8 @@ class ChatRoomScreen extends HookConsumerWidget {
final messageReplyingTo = useState<SnChatMessage?>(null); final messageReplyingTo = useState<SnChatMessage?>(null);
final messageForwardingTo = useState<SnChatMessage?>(null); final messageForwardingTo = useState<SnChatMessage?>(null);
final messageEditingTo = useState<SnChatMessage?>(null); final messageEditingTo = useState<SnChatMessage?>(null);
final attachments = useState<List<UniversalFile>>([]);
final attachmentProgress = useState<Map<String, Map<int, double>>>({});
// Add scroll listener for pagination // Add scroll listener for pagination
useEffect(() { useEffect(() {
@ -385,8 +341,6 @@ class ChatRoomScreen extends HookConsumerWidget {
return () => subscription.cancel(); return () => subscription.cancel();
}, [ws, chatRoom]); }, [ws, chatRoom]);
final attachments = useState<List<UniversalFile>>([]);
Future<void> pickPhotoMedia() async { Future<void> pickPhotoMedia() async {
final result = await ref final result = await ref
.watch(imagePickerProvider) .watch(imagePickerProvider)
@ -420,6 +374,12 @@ class ChatRoomScreen extends HookConsumerWidget {
editingTo: messageEditingTo.value, editingTo: messageEditingTo.value,
forwardingTo: messageForwardingTo.value, forwardingTo: messageForwardingTo.value,
replyingTo: messageReplyingTo.value, replyingTo: messageReplyingTo.value,
onProgress: (messageId, progress) {
attachmentProgress.value = {
...attachmentProgress.value,
messageId: progress,
};
},
); );
messageController.clear(); messageController.clear();
messageEditingTo.value = null; messageEditingTo.value = null;
@ -542,12 +502,15 @@ class ChatRoomScreen extends HookConsumerWidget {
message.toRemoteMessage(); message.toRemoteMessage();
} }
}, },
progress:
attachmentProgress.value[message.id],
), ),
loading: loading:
() => _MessageBubble( () => _MessageBubble(
message: message, message: message,
isCurrentUser: false, isCurrentUser: false,
onAction: null, onAction: null,
progress: null,
), ),
error: (_, __) => const SizedBox.shrink(), error: (_, __) => const SizedBox.shrink(),
); );
@ -804,11 +767,13 @@ class _MessageBubble extends HookConsumerWidget {
final LocalChatMessage message; final LocalChatMessage message;
final bool isCurrentUser; final bool isCurrentUser;
final Function(String action)? onAction; final Function(String action)? onAction;
final Map<int, double>? progress;
const _MessageBubble({ const _MessageBubble({
required this.message, required this.message,
required this.isCurrentUser, required this.isCurrentUser,
required this.onAction, required this.onAction,
required this.progress,
}); });
@override @override
@ -914,9 +879,58 @@ class _MessageBubble extends HookConsumerWidget {
style: TextStyle(color: textColor), style: TextStyle(color: textColor),
), ),
if (message.toRemoteMessage().attachments.isNotEmpty) if (message.toRemoteMessage().attachments.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
CloudFileList( CloudFileList(
files: message.toRemoteMessage().attachments, files: message.toRemoteMessage().attachments,
maxWidth: MediaQuery.of(context).size.width * 0.8,
),
],
).padding(top: 4), ).padding(top: 4),
if (progress != null && progress!.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 8,
children: [
if ((message
.toRemoteMessage()
.content
?.isNotEmpty ??
false))
const Gap(0),
for (var entry in progress!.entries)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'fileUploadingProgress'.tr(
args: [
(entry.key + 1).toString(),
entry.value.toStringAsFixed(1),
],
),
style: TextStyle(
fontSize: 12,
color: textColor.withOpacity(0.8),
),
),
const Gap(4),
LinearProgressIndicator(
value: entry.value / 100,
backgroundColor:
Theme.of(
context,
).colorScheme.surfaceVariant,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
],
),
const Gap(0),
],
),
const Gap(4), const Gap(4),
Row( Row(
spacing: 4, spacing: 4,
@ -978,7 +992,7 @@ class _MessageBubble extends HookConsumerWidget {
(context, ref, _) => GestureDetector( (context, ref, _) => GestureDetector(
onTap: () { onTap: () {
ref ref
.read(messagesProvider(message.roomId).notifier) .read(messagesNotifierProvider(message.roomId).notifier)
.retryMessage(message.id); .retryMessage(message.id);
}, },
child: const Icon( child: const Icon(
@ -1006,7 +1020,7 @@ class _MessageQuoteWidget extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final messagesNotifier = ref.watch( final messagesNotifier = ref.watch(
messagesProvider(message.roomId).notifier, messagesNotifierProvider(message.roomId).notifier,
); );
return FutureBuilder<LocalChatMessage?>( return FutureBuilder<LocalChatMessage?>(

View File

@ -0,0 +1,179 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'room.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$messagesNotifierHash() => r'71a9fc1c6d024f6203f06225384c19335b9b6f2c';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$MessagesNotifier
extends BuildlessAutoDisposeAsyncNotifier<List<LocalChatMessage>> {
late final String roomId;
FutureOr<List<LocalChatMessage>> build(String roomId);
}
/// See also [MessagesNotifier].
@ProviderFor(MessagesNotifier)
const messagesNotifierProvider = MessagesNotifierFamily();
/// See also [MessagesNotifier].
class MessagesNotifierFamily
extends Family<AsyncValue<List<LocalChatMessage>>> {
/// See also [MessagesNotifier].
const MessagesNotifierFamily();
/// See also [MessagesNotifier].
MessagesNotifierProvider call(String roomId) {
return MessagesNotifierProvider(roomId);
}
@override
MessagesNotifierProvider getProviderOverride(
covariant MessagesNotifierProvider provider,
) {
return call(provider.roomId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'messagesNotifierProvider';
}
/// See also [MessagesNotifier].
class MessagesNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
MessagesNotifier,
List<LocalChatMessage>
> {
/// See also [MessagesNotifier].
MessagesNotifierProvider(String roomId)
: this._internal(
() => MessagesNotifier()..roomId = roomId,
from: messagesNotifierProvider,
name: r'messagesNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$messagesNotifierHash,
dependencies: MessagesNotifierFamily._dependencies,
allTransitiveDependencies:
MessagesNotifierFamily._allTransitiveDependencies,
roomId: roomId,
);
MessagesNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.roomId,
}) : super.internal();
final String roomId;
@override
FutureOr<List<LocalChatMessage>> runNotifierBuild(
covariant MessagesNotifier notifier,
) {
return notifier.build(roomId);
}
@override
Override overrideWith(MessagesNotifier Function() create) {
return ProviderOverride(
origin: this,
override: MessagesNotifierProvider._internal(
() => create()..roomId = roomId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
roomId: roomId,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
MessagesNotifier,
List<LocalChatMessage>
>
createElement() {
return _MessagesNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is MessagesNotifierProvider && other.roomId == roomId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, roomId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin MessagesNotifierRef
on AutoDisposeAsyncNotifierProviderRef<List<LocalChatMessage>> {
/// The parameter `roomId` of this provider.
String get roomId;
}
class _MessagesNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
MessagesNotifier,
List<LocalChatMessage>
>
with MessagesNotifierRef {
_MessagesNotifierProviderElement(super.provider);
@override
String get roomId => (origin as MessagesNotifierProvider).roomId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:island/models/file.dart'; import 'package:island/models/file.dart';
@ -7,7 +9,13 @@ import 'package:styled_widget/styled_widget.dart';
class CloudFileList extends StatelessWidget { class CloudFileList extends StatelessWidget {
final List<SnCloudFile> files; final List<SnCloudFile> files;
final double maxHeight; final double maxHeight;
const CloudFileList({super.key, required this.files, this.maxHeight = 360}); final double maxWidth;
const CloudFileList({
super.key,
required this.files,
this.maxHeight = 360,
this.maxWidth = double.infinity,
});
double calculateAspectRatio() { double calculateAspectRatio() {
double total = 0; double total = 0;
@ -44,14 +52,14 @@ class CloudFileList extends StatelessWidget {
if (allImages) { if (allImages) {
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(maxHeight: maxHeight, minWidth: maxWidth),
maxHeight: maxHeight,
minWidth: double.infinity,
),
child: AspectRatio( child: AspectRatio(
aspectRatio: calculateAspectRatio(), aspectRatio: calculateAspectRatio(),
child: CarouselView( child: CarouselView(
itemExtent: MediaQuery.of(context).size.width * 0.85, itemExtent: math.min(
MediaQuery.of(context).size.width * 0.85,
maxWidth * 0.85,
),
itemSnapping: true, itemSnapping: true,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(16)), borderRadius: const BorderRadius.all(Radius.circular(16)),
@ -63,10 +71,7 @@ class CloudFileList extends StatelessWidget {
} }
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(maxHeight: maxHeight, minWidth: maxWidth),
maxHeight: maxHeight,
minWidth: double.infinity,
),
child: AspectRatio( child: AspectRatio(
aspectRatio: calculateAspectRatio(), aspectRatio: calculateAspectRatio(),
child: ListView.separated( child: ListView.separated(