From 8c6bd0feaad022a6dbe3bb8ca5980c1c379bb6f5 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 10 Oct 2025 23:58:33 +0800 Subject: [PATCH] :lipstick: Optimize cloud file text file --- assets/i18n/en-US.json | 3 +- assets/i18n/zh-CN.json | 2 +- lib/widgets/chat/chat_input.dart | 2 +- lib/widgets/chat/message_indicators.dart | 4 +- lib/widgets/content/cloud_files.dart | 188 ++++++++++++++++++++--- 5 files changed, 171 insertions(+), 28 deletions(-) diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 9c254397..7ac57a82 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -1212,5 +1212,6 @@ "transferCreatedSuccessfully": "Transfer created successfully!", "postUpdate": "Update", "fileMetadata": "File Metadata", - "resend": "Resend" + "resend": "Resend", + "fileInfoTitle": "File Information" } diff --git a/assets/i18n/zh-CN.json b/assets/i18n/zh-CN.json index 7c267ddb..5d52d283 100644 --- a/assets/i18n/zh-CN.json +++ b/assets/i18n/zh-CN.json @@ -1060,7 +1060,7 @@ "selectPool": "选择储存池", "choosePool": "选择一个储存池", "errorLoadingPools": "加载池时出错", - "quotaCostInfo": "此上传将消耗{} 配额点", + "quotaCostInfo": "此上传将消耗 {} 配额点", "uploadConstraints": "上传限制", "fileSizeExceeded": "文件大小超过了 {} 的最大限制", "fileTypeNotAccepted": "此储存池不接受该文件类型", diff --git a/lib/widgets/chat/chat_input.dart b/lib/widgets/chat/chat_input.dart index cfecc88f..4ef6a091 100644 --- a/lib/widgets/chat/chat_input.dart +++ b/lib/widgets/chat/chat_input.dart @@ -368,7 +368,7 @@ class ChatInput extends HookConsumerWidget { onLinkAttachment!, ), ], - iconColor: Colors.white, + iconColor: Theme.of(context).colorScheme.onSurface, ), ], ), diff --git a/lib/widgets/chat/message_indicators.dart b/lib/widgets/chat/message_indicators.dart index f981f0fa..ef4bd255 100644 --- a/lib/widgets/chat/message_indicators.dart +++ b/lib/widgets/chat/message_indicators.dart @@ -39,7 +39,7 @@ class MessageIndicators extends StatelessWidget { context, status!, textColor.withOpacity(0.7), - ).padding(bottom: 4), + ).padding(bottom: 2), ); } @@ -72,7 +72,7 @@ class MessageIndicators extends StatelessWidget { strokeWidth: 2, valueColor: AlwaysStoppedAnimation(textColor), ), - ); + ).padding(bottom: 2); case MessageStatus.sent: // Sent status is hidden return const SizedBox.shrink(); diff --git a/lib/widgets/content/cloud_files.dart b/lib/widgets/content/cloud_files.dart index 874ceee5..b8bf67a6 100644 --- a/lib/widgets/content/cloud_files.dart +++ b/lib/widgets/content/cloud_files.dart @@ -1,7 +1,9 @@ +import 'dart:io'; import 'dart:math' as math; import 'package:cached_network_image/cached_network_image.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; @@ -11,12 +13,16 @@ import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/services/time.dart'; import 'package:island/utils/format.dart'; +import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/audio.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:path/path.dart' show extension; +import 'package:path_provider/path_provider.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:island/widgets/data_saving_gate.dart'; +import 'package:island/widgets/content/file_info_sheet.dart'; import 'image.dart'; import 'video.dart'; @@ -93,31 +99,167 @@ class CloudFileWidget extends HookConsumerWidget { } if (item.mimeType?.startsWith('text/') == true) { - return SizedBox( + final textFuture = useMemoized( + () => ref + .read(apiClientProvider) + .get(uri) + .then((response) => response.data as String), + [uri], + ); + + Future downloadFile() async { + try { + showSnackBar('Downloading file...'); + + final client = ref.read(apiClientProvider); + final tempDir = await getTemporaryDirectory(); + var extName = extension(item.name).trim(); + if (extName.isEmpty) { + extName = item.mimeType?.split('/').lastOrNull ?? 'txt'; + } + final filePath = '${tempDir.path}/${item.id}.$extName'; + + await client.download( + '/drive/files/${item.id}', + filePath, + queryParameters: {'original': true}, + ); + + await FileSaver.instance.saveFile( + name: item.name.isEmpty ? '${item.id}.$extName' : item.name, + file: File(filePath), + ); + showSnackBar('File saved to downloads'); + } catch (e) { + showErrorAlert(e); + } + } + + return Container( height: 400, - child: FutureBuilder( - future: ref - .read(apiClientProvider) - .get(uri) - .then((response) => response.data as String), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); - } else if (snapshot.hasError) { - return Center( - child: Text('Error loading text: ${snapshot.error}'), - ); - } else if (snapshot.hasData) { - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: SelectableText( - snapshot.data!, - style: const TextStyle(fontFamily: 'monospace', fontSize: 14), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outline, + width: 1 / MediaQuery.devicePixelRatioOf(context), + ), + borderRadius: BorderRadius.circular(8), + ), + child: Stack( + children: [ + FutureBuilder( + future: textFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + return Center( + child: Text('Error loading text: ${snapshot.error}'), + ); + } else if (snapshot.hasData) { + return SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 20 + 48, 20, 20), + child: SelectableText( + snapshot.data!, + style: const TextStyle( + fontFamily: 'monospace', + fontSize: 14, + ), + ), + ); + } + return const Center(child: Text('No content')); + }, + ), + Positioned( + top: 8, + left: 8, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), ), - ); - } - return const Center(child: Text('No content')); - }, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 7, + children: [ + Icon( + Symbols.file_present, + size: 16, + color: Colors.white, + ).padding(top: 2), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.name, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + formatFileSize(item.size), + style: const TextStyle( + color: Colors.white, + fontSize: 9, + ), + ), + ], + ), + ], + ).padding(vertical: 4, horizontal: 8), + ), + ), + Positioned( + top: 8, + right: 8, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 4, + children: [ + IconButton( + icon: const Icon( + Symbols.download, + color: Colors.white, + size: 16, + ), + onPressed: downloadFile, + padding: EdgeInsets.all(4), + constraints: const BoxConstraints(), + ), + IconButton( + icon: const Icon( + Symbols.info, + color: Colors.white, + size: 16, + ), + onPressed: () { + showModalBottomSheet( + useRootNavigator: true, + context: context, + isScrollControlled: true, + builder: (context) => FileInfoSheet(item: item), + ); + }, + padding: EdgeInsets.all(4), + constraints: const BoxConstraints(), + ), + ], + ), + ), + ), + ], ), ); }