From 54560ad5d800b82d6d06607227d5e72a6a4688a4 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 27 Sep 2025 15:51:26 +0800 Subject: [PATCH] :bug: Fix some bugs in attachment upload sheet --- lib/screens/chat/room.dart | 347 +-------------------------- lib/widgets/attachment_uploader.dart | 54 +++-- lib/widgets/chat/chat_input.dart | 6 +- 3 files changed, 40 insertions(+), 367 deletions(-) diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index 0334de42..26855e94 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -1,7 +1,5 @@ import "dart:async"; import "dart:convert"; -import "dart:typed_data"; -import "package:cross_file/cross_file.dart"; import "package:easy_localization/easy_localization.dart"; import "package:file_picker/file_picker.dart"; import "package:flutter/material.dart"; @@ -12,9 +10,7 @@ import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:island/database/message.dart"; import "package:island/models/chat.dart"; import "package:island/models/file.dart"; -import "package:island/models/file_pool.dart"; import "package:island/pods/config.dart"; -import "package:island/pods/file_pool.dart"; import "package:island/pods/messages_notifier.dart"; import "package:island/pods/network.dart"; import "package:island/pods/websocket.dart"; @@ -26,9 +22,7 @@ import "package:island/widgets/app_scaffold.dart"; import "package:island/widgets/attachment_uploader.dart"; import "package:island/widgets/chat/call_overlay.dart"; import "package:island/widgets/chat/message_item.dart"; -import "package:island/widgets/content/attachment_preview.dart"; import "package:island/widgets/content/cloud_files.dart"; -import "package:island/widgets/content/sheet.dart"; import "package:island/widgets/post/compose_shared.dart"; import "package:island/widgets/response.dart"; import "package:material_symbols_icons/material_symbols_icons.dart"; @@ -482,7 +476,7 @@ class ChatRoomScreen extends HookConsumerWidget { context: context, isScrollControlled: true, builder: - (context) => ChatAttachmentUploaderSheet( + (context) => AttachmentUploaderSheet( ref: ref, attachments: attachments.value, index: index, @@ -904,342 +898,3 @@ class ChatRoomScreen extends HookConsumerWidget { ); } } - -class ChatAttachmentUploaderSheet extends StatefulWidget { - final WidgetRef ref; - final List attachments; - final int index; - - const ChatAttachmentUploaderSheet({ - super.key, - required this.ref, - required this.attachments, - required this.index, - }); - - @override - State createState() => - _ChatAttachmentUploaderSheetState(); -} - -class _ChatAttachmentUploaderSheetState - extends State { - String? selectedPoolId; - - @override - Widget build(BuildContext context) { - final attachment = widget.attachments[widget.index]; - - return SheetScaffold( - titleText: 'uploadAttachment'.tr(), - child: FutureBuilder>( - future: widget.ref.read(poolsProvider.future), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } - if (snapshot.hasError) { - return Center(child: Text('errorLoadingPools'.tr())); - } - final pools = snapshot.data!; - selectedPoolId ??= resolveDefaultPoolId(widget.ref, pools); - - return Column( - children: [ - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DropdownButtonFormField( - value: selectedPoolId, - items: - pools.map((pool) { - return DropdownMenuItem( - value: pool.id, - child: Text(pool.name), - ); - }).toList(), - onChanged: (value) { - setState(() { - selectedPoolId = value; - }); - }, - decoration: InputDecoration( - labelText: 'selectPool'.tr(), - border: const OutlineInputBorder(), - hintText: 'choosePool'.tr(), - ), - ), - const Gap(16), - FutureBuilder( - future: _getFileSize(attachment), - builder: (context, sizeSnapshot) { - if (!sizeSnapshot.hasData) { - return const SizedBox.shrink(); - } - final fileSize = sizeSnapshot.data!; - final selectedPool = pools.firstWhere( - (p) => p.id == selectedPoolId, - ); - - // Check file size limit - final maxFileSize = - selectedPool.policyConfig?['max_file_size'] - as int?; - final fileSizeExceeded = - maxFileSize != null && fileSize > maxFileSize; - - // Check accepted types - final acceptTypes = - selectedPool.policyConfig?['accept_types'] - as List?; - final mimeType = - attachment.data.mimeType ?? - ComposeLogic.getMimeTypeFromFileType( - attachment.type, - ); - final typeAccepted = - acceptTypes == null || - acceptTypes.isEmpty || - acceptTypes.any( - (type) => mimeType.startsWith(type), - ); - - final hasIssues = fileSizeExceeded || !typeAccepted; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (hasIssues) ...[ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: - Theme.of( - context, - ).colorScheme.errorContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Symbols.warning, - size: 18, - color: - Theme.of( - context, - ).colorScheme.error, - ), - const Gap(8), - Text( - 'uploadConstraints'.tr(), - style: Theme.of( - context, - ).textTheme.bodyMedium?.copyWith( - color: - Theme.of( - context, - ).colorScheme.error, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - if (fileSizeExceeded) ...[ - const Gap(4), - Text( - 'fileSizeExceeded'.tr( - args: [ - _formatFileSize(maxFileSize), - ], - ), - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith( - color: - Theme.of( - context, - ).colorScheme.error, - ), - ), - ], - if (!typeAccepted) ...[ - const Gap(4), - Text( - 'fileTypeNotAccepted'.tr(), - style: Theme.of( - context, - ).textTheme.bodySmall?.copyWith( - color: - Theme.of( - context, - ).colorScheme.error, - ), - ), - ], - ], - ), - ), - const Gap(12), - ], - Row( - spacing: 6, - children: [ - const Icon( - Symbols.account_balance_wallet, - size: 18, - ), - Expanded( - child: Text( - 'quotaCostInfo'.tr( - args: [ - _formatQuotaCost( - fileSize, - selectedPool, - ), - ], - ), - style: - Theme.of( - context, - ).textTheme.bodyMedium, - ).fontSize(13), - ), - ], - ).padding(horizontal: 4), - ], - ); - }, - ), - const Gap(4), - Row( - spacing: 6, - children: [ - const Icon(Symbols.info, size: 18), - Text( - 'attachmentPreview'.tr(), - style: Theme.of(context).textTheme.titleMedium, - ).fontSize(13), - ], - ).padding(horizontal: 4), - const Gap(8), - AttachmentPreview(item: attachment, isCompact: true), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton.icon( - onPressed: () => Navigator.pop(context), - icon: const Icon(Symbols.close), - label: Text('cancel').tr(), - ), - const Gap(8), - TextButton.icon( - onPressed: () => _confirmUpload(), - icon: const Icon(Symbols.upload), - label: Text('upload').tr(), - ), - ], - ), - ), - ], - ); - }, - ), - ); - } - - Future _getUploadConfig() async { - final attachment = widget.attachments[widget.index]; - final fileSize = await _getFileSize(attachment); - - if (fileSize == null) return null; - - // Get the selected pool to check constraints - final pools = await widget.ref.read(poolsProvider.future); - final selectedPool = pools.firstWhere((p) => p.id == selectedPoolId); - - // Check constraints - final maxFileSize = selectedPool.policyConfig?['max_file_size'] as int?; - final fileSizeExceeded = maxFileSize != null && fileSize > maxFileSize; - - final acceptTypes = selectedPool.policyConfig?['accept_types'] as List?; - final mimeType = - attachment.data.mimeType ?? - ComposeLogic.getMimeTypeFromFileType(attachment.type); - final typeAccepted = - acceptTypes == null || - acceptTypes.isEmpty || - acceptTypes.any((type) => mimeType.startsWith(type)); - - final hasConstraints = fileSizeExceeded || !typeAccepted; - - return AttachmentUploadConfig( - poolId: selectedPoolId!, - hasConstraints: hasConstraints, - ); - } - - Future _confirmUpload() async { - final config = await _getUploadConfig(); - if (config != null && mounted) { - Navigator.pop(context, config); - } - } - - Future _getFileSize(UniversalFile attachment) async { - if (attachment.data is XFile) { - try { - return await (attachment.data as XFile).length(); - } catch (e) { - return null; - } - } else if (attachment.data is SnCloudFile) { - return (attachment.data as SnCloudFile).size; - } else if (attachment.data is List) { - return (attachment.data as List).length; - } else if (attachment.data is Uint8List) { - return (attachment.data as Uint8List).length; - } - return null; - } - - String _formatNumber(int number) { - if (number >= 1000000) { - return '${(number / 1000000).toStringAsFixed(1)}M'; - } else if (number >= 1000) { - return '${(number / 1000).toStringAsFixed(1)}K'; - } else { - return number.toString(); - } - } - - String _formatFileSize(int bytes) { - if (bytes >= 1073741824) { - return '${(bytes / 1073741824).toStringAsFixed(1)} GB'; - } else if (bytes >= 1048576) { - return '${(bytes / 1048576).toStringAsFixed(1)} MB'; - } else if (bytes >= 1024) { - return '${(bytes / 1024).toStringAsFixed(1)} KB'; - } else { - return '$bytes bytes'; - } - } - - String _formatQuotaCost(int fileSize, SnFilePool pool) { - final costMultiplier = pool.billingConfig?['cost_multiplier'] ?? 1.0; - final quotaCost = ((fileSize / 1024 / 1024) * costMultiplier).round(); - return _formatNumber(quotaCost); - } -} diff --git a/lib/widgets/attachment_uploader.dart b/lib/widgets/attachment_uploader.dart index 4e872678..c443073f 100644 --- a/lib/widgets/attachment_uploader.dart +++ b/lib/widgets/attachment_uploader.dart @@ -26,15 +26,20 @@ class AttachmentUploadConfig { class AttachmentUploaderSheet extends StatefulWidget { final WidgetRef ref; - final ComposeState state; + final ComposeState? state; + final List? attachments; final int index; const AttachmentUploaderSheet({ super.key, required this.ref, - required this.state, + this.state, + this.attachments, required this.index, - }); + }) : assert( + state != null || attachments != null, + 'Either state or attachments must be provided', + ); @override State createState() => @@ -46,7 +51,9 @@ class _AttachmentUploaderSheetState extends State { @override Widget build(BuildContext context) { - final attachment = widget.state.attachments.value[widget.index]; + final attachment = + widget.attachments?[widget.index] ?? + widget.state!.attachments.value[widget.index]; return SheetScaffold( titleText: 'uploadAttachment'.tr(), @@ -111,19 +118,18 @@ class _AttachmentUploaderSheetState extends State { // Check accepted types final acceptTypes = - selectedPool.policyConfig?['accept_types'] - as List?; + (selectedPool.policyConfig?['accept_types'] + as List?) + ?.cast(); final mimeType = attachment.data.mimeType ?? ComposeLogic.getMimeTypeFromFileType( attachment.type, ); - final typeAccepted = - acceptTypes == null || - acceptTypes.isEmpty || - acceptTypes.any( - (type) => mimeType.startsWith(type), - ); + final typeAccepted = _isMimeTypeAccepted( + mimeType, + acceptTypes, + ); final hasIssues = fileSizeExceeded || !typeAccepted; @@ -279,7 +285,9 @@ class _AttachmentUploaderSheetState extends State { } Future _getUploadConfig() async { - final attachment = widget.state.attachments.value[widget.index]; + final attachment = + widget.attachments?[widget.index] ?? + widget.state!.attachments.value[widget.index]; final fileSize = await _getFileSize(attachment); if (fileSize == null) return null; @@ -292,14 +300,12 @@ class _AttachmentUploaderSheetState extends State { final maxFileSize = selectedPool.policyConfig?['max_file_size'] as int?; final fileSizeExceeded = maxFileSize != null && fileSize > maxFileSize; - final acceptTypes = selectedPool.policyConfig?['accept_types'] as List?; + final acceptTypes = + (selectedPool.policyConfig?['accept_types'] as List?)?.cast(); final mimeType = attachment.data.mimeType ?? ComposeLogic.getMimeTypeFromFileType(attachment.type); - final typeAccepted = - acceptTypes == null || - acceptTypes.isEmpty || - acceptTypes.any((type) => mimeType.startsWith(type)); + final typeAccepted = _isMimeTypeAccepted(mimeType, acceptTypes); final hasConstraints = fileSizeExceeded || !typeAccepted; @@ -360,4 +366,16 @@ class _AttachmentUploaderSheetState extends State { final quotaCost = ((fileSize / 1024 / 1024) * costMultiplier).round(); return _formatNumber(quotaCost); } + + bool _isMimeTypeAccepted(String mimeType, List? acceptTypes) { + if (acceptTypes == null || acceptTypes.isEmpty) return true; + return acceptTypes.any((type) { + if (type.endsWith('/*')) { + final mainType = type.substring(0, type.length - 2); + return mimeType.startsWith('$mainType/'); + } else { + return mimeType == type; + } + }); + } } diff --git a/lib/widgets/chat/chat_input.dart b/lib/widgets/chat/chat_input.dart index ccfc67c2..3c8aec53 100644 --- a/lib/widgets/chat/chat_input.dart +++ b/lib/widgets/chat/chat_input.dart @@ -127,16 +127,16 @@ class ChatInput extends HookConsumerWidget { children: [ if (attachments.isNotEmpty) SizedBox( - height: 280, + height: 180, child: ListView.separated( padding: EdgeInsets.symmetric(horizontal: 12), scrollDirection: Axis.horizontal, itemCount: attachments.length, itemBuilder: (context, idx) { return SizedBox( - height: 280, - width: 280, + width: 180, child: AttachmentPreview( + isCompact: true, item: attachments[idx], progress: attachmentProgress['chat-upload']?[idx], onRequestUpload: () => onUploadAttachment(idx),