From 5541e704d827d97ceca2ab472dbb64ace44ccd8a Mon Sep 17 00:00:00 2001 From: ngfchl Date: Sat, 18 Jul 2026 16:58:07 +0800 Subject: [PATCH] feat: copy fast transfer json from file menus --- lib/pages/search_results_page.dart | 1 + lib/pages/workspace_page.dart | 102 +++++++++++++++------ lib/providers/file_provider.dart | 138 +++++++++++++++++++++++++++++ lib/widgets/file_list_tile.dart | 7 ++ 4 files changed, 219 insertions(+), 29 deletions(-) diff --git a/lib/pages/search_results_page.dart b/lib/pages/search_results_page.dart index 814e90f..5bc297b 100644 --- a/lib/pages/search_results_page.dart +++ b/lib/pages/search_results_page.dart @@ -127,6 +127,7 @@ class _FileSearchResultsPageState extends ConsumerState { onOpen: () => notifier.downloadFile(file), onCopy: () => notifier.copyToClipboard([file]), onCut: () => notifier.cutToClipboard([file]), + onCopyFastTransfer: () => notifier.copyFastTransferJSON(file), onDownload: () => notifier.downloadFile(file), onDelete: () => notifier.deleteFiles([file]), ); diff --git a/lib/pages/workspace_page.dart b/lib/pages/workspace_page.dart index 9aca613..c13bcf7 100644 --- a/lib/pages/workspace_page.dart +++ b/lib/pages/workspace_page.dart @@ -1319,6 +1319,7 @@ class _PrimaryFilePane extends ConsumerWidget { onCut: () => notifier.cutToClipboard([file]), onDownload: () => notifier.downloadFile(file), onShare: () => notifier.createShare(file), + onCopyFastTransfer: () => notifier.copyFastTransferJSON(file), isRecycleItem: state.section == WorkspaceSection.recycle, onDelete: () => state.section == WorkspaceSection.recycle ? notifier.restoreFiles([file]) @@ -1351,19 +1352,23 @@ class _PrimaryFilePane extends ConsumerWidget { : () => _openCloudFile(context, ref, file), ), ), - child: _FileGridCard( + child: _FastTransferContextMenu( file: file, - isSelected: selected, - onSelect: () { - if (file.isDirectory && !selected) { - notifier.navigateToFolder(file); - } else { - notifier.toggleSelection(file.id); - } - }, - onOpen: file.isDirectory - ? () => notifier.navigateToFolder(file) - : () => _openCloudFile(context, ref, file), + onCopyFastTransfer: () => notifier.copyFastTransferJSON(file), + child: _FileGridCard( + file: file, + isSelected: selected, + onSelect: () { + if (file.isDirectory && !selected) { + notifier.navigateToFolder(file); + } else { + notifier.toggleSelection(file.id); + } + }, + onOpen: file.isDirectory + ? () => notifier.navigateToFolder(file) + : () => _openCloudFile(context, ref, file), + ), ), ); }, @@ -1760,6 +1765,9 @@ class _ColumnFileBrowserState extends ConsumerState<_ColumnFileBrowser> { _moveFiles(files, parentID, index), onUploadLocalFiles: (files, parentID) => _uploadFiles(files, parentID, index), + onCopyFastTransfer: (file) => ref + .read(fileProvider.notifier) + .copyFastTransferJSON(file), ), ], ), @@ -1796,6 +1804,7 @@ class _FinderColumn extends StatefulWidget { final _PaneIdentity source; final ValueChanged onOpenFolder; final ValueChanged onOpenFile; + final ValueChanged onCopyFastTransfer; final Future Function(List files, String? parentID) onMoveCloudFiles; final Future Function(List files, String? parentID) @@ -1807,6 +1816,7 @@ class _FinderColumn extends StatefulWidget { required this.source, required this.onOpenFolder, required this.onOpenFile, + required this.onCopyFastTransfer, required this.onMoveCloudFiles, required this.onUploadLocalFiles, }); @@ -1903,21 +1913,26 @@ class _FinderColumnState extends State<_FinderColumn> { itemBuilder: (context, index) { final file = column.files[index]; final selected = column.selectedID == file.id; - final row = _FinderColumnItem( + final row = _FastTransferContextMenu( file: file, - selected: selected, - onTap: () { - if (file.isDirectory) { - widget.onOpenFolder(file); - } - }, - onOpen: () { - if (file.isDirectory) { - widget.onOpenFolder(file); - } else { - widget.onOpenFile(file); - } - }, + onCopyFastTransfer: () => + widget.onCopyFastTransfer(file), + child: _FinderColumnItem( + file: file, + selected: selected, + onTap: () { + if (file.isDirectory) { + widget.onOpenFolder(file); + } + }, + onOpen: () { + if (file.isDirectory) { + widget.onOpenFolder(file); + } else { + widget.onOpenFile(file); + } + }, + ), ); return Draggable<_DraggedCloudFiles>( data: _DraggedCloudFiles([file], widget.source), @@ -2085,6 +2100,32 @@ class _FileGridCard extends StatelessWidget { } } +class _FastTransferContextMenu extends StatelessWidget { + final CloudFile file; + final VoidCallback onCopyFastTransfer; + final Widget child; + + const _FastTransferContextMenu({ + required this.file, + required this.onCopyFastTransfer, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return ShadContextMenuRegion( + items: [ + ShadContextMenuItem.inset( + leading: const Icon(Icons.bolt_rounded, size: 16), + onPressed: onCopyFastTransfer, + child: Text(file.isDirectory ? '复制目录秒传' : '复制秒传'), + ), + ], + child: child, + ); + } +} + class _SecondaryFilePane extends ConsumerStatefulWidget { const _SecondaryFilePane(); @@ -2411,6 +2452,8 @@ class _SecondaryFilePaneState extends ConsumerState<_SecondaryFilePane> { onDownload: () => ref.read(fileProvider.notifier).downloadFile(file), onShare: () => ref.read(fileProvider.notifier).createShare(file), + onCopyFastTransfer: () => + ref.read(fileProvider.notifier).copyFastTransferJSON(file), onDelete: () => ref.read(fileProvider.notifier).deleteFiles([file]), ); @@ -2434,10 +2477,11 @@ class _SecondaryFilePaneState extends ConsumerState<_SecondaryFilePane> { } } - final card = _FileGridCard( + final card = _FastTransferContextMenu( file: file, - onSelect: open, - onOpen: open, + onCopyFastTransfer: () => + ref.read(fileProvider.notifier).copyFastTransferJSON(file), + child: _FileGridCard(file: file, onSelect: open, onOpen: open), ); return Draggable<_DraggedCloudFiles>( data: _DraggedCloudFiles([file], _PaneIdentity.secondary), diff --git a/lib/providers/file_provider.dart b/lib/providers/file_provider.dart index d45efac..4701225 100644 --- a/lib/providers/file_provider.dart +++ b/lib/providers/file_provider.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/legacy.dart'; @@ -7,6 +8,7 @@ import '../api/guangya_api.dart'; import '../core/storage/file_metadata_cache.dart'; import '../core/storage/storage_manager.dart'; import '../models/cloud_file.dart'; +import '../models/fast_transfer.dart'; enum FileSort { name, size, modifiedAt, createdAt, type } @@ -17,6 +19,13 @@ class ExternalPlayer { const ExternalPlayer(this.name, this.bundleID); } +class _FastTransferSource { + final CloudFile file; + final String path; + + const _FastTransferSource(this.file, this.path); +} + extension FileSortExt on FileSort { String get title { switch (this) { @@ -687,6 +696,135 @@ class FileNotifier extends StateNotifier { state = state.copyWith(clearClipboard: true); } + Future copyFastTransferJSON(CloudFile target) async { + if (_api == null) return; + state = state.copyWith(statusMessage: '正在生成秒传信息…', clearError: true); + try { + final sources = target.isDirectory + ? await _collectFastTransferFolder(target) + : [_FastTransferSource(target, target.name)]; + if (sources.isEmpty) { + throw Exception('该目录中没有可生成秒传信息的文件'); + } + final entries = []; + var skipped = 0; + var next = 0; + Future worker() async { + while (next < sources.length) { + final source = sources[next++]; + final file = await _resolveFastTransferFile(source.file); + final gcid = file.gcid?.trim(); + final size = file.size; + if (gcid == null || gcid.isEmpty || size == null || size < 0) { + skipped += 1; + continue; + } + entries.add( + FastTransferEntry(path: source.path, size: size, gcid: gcid), + ); + } + } + + await Future.wait(List.generate(6, (_) => worker())); + if (entries.isEmpty) { + throw Exception('没有找到包含 GCID 和大小的有效文件'); + } + entries.sort((a, b) => a.path.compareTo(b.path)); + await Clipboard.setData( + ClipboardData( + text: jsonEncode({ + 'files': entries.map((entry) => entry.toJson()).toList(), + }), + ), + ); + state = state.copyWith( + statusMessage: skipped == 0 + ? '已复制 ${entries.length} 个文件的秒传 JSON' + : '已复制 ${entries.length} 个文件的秒传 JSON,跳过 $skipped 个无 GCID 项目', + ); + } catch (error) { + state = state.copyWith(errorMessage: '复制秒传失败:$error'); + } + } + + Future> _collectFastTransferFolder( + CloudFile folder, + ) async { + final queue = <_FastTransferSource>[ + _FastTransferSource(folder, folder.name), + ]; + final result = <_FastTransferSource>[]; + final visited = {}; + while (queue.isNotEmpty) { + final current = queue.removeLast(); + if (!visited.add(current.file.id)) continue; + final children = await _folderChildrenForFastTransfer(current.file); + for (final child in children) { + final path = '${current.path}/${child.name}'; + if (child.isDirectory) { + queue.add(_FastTransferSource(child, path)); + } else { + result.add(_FastTransferSource(child, path)); + } + } + } + return result; + } + + Future> _folderChildrenForFastTransfer( + CloudFile folder, + ) async { + final cached = await FileMetadataCache.folderChildren(folder.id); + if (cached != null) return cached; + final children = []; + var page = 0; + while (true) { + final response = await _api!.fsFiles( + parentID: folder.id, + page: page, + pageSize: 200, + orderBy: 0, + sortType: 0, + ); + final values = _extractFiles(response); + children.addAll(values); + if (values.length < 200) break; + page += 1; + } + await FileMetadataCache.cacheFolderChildren(folder.id, children); + return children; + } + + Future _resolveFastTransferFile(CloudFile file) async { + final cached = await FileMetadataCache.file(file.id); + var resolved = cached == null + ? file + : file.copyWith(size: cached.size, gcid: cached.gcid); + if (resolved.gcid?.isNotEmpty == true && resolved.size != null) { + return resolved; + } + final detail = await _api!.fsDetail(file.id); + final detailFile = _extractFiles(detail).cast().firstWhere( + (candidate) => candidate?.id == file.id, + orElse: () => null, + ); + resolved = resolved.copyWith( + gcid: + detailFile?.gcid ?? + _findStringDeep(detail, const ['gcid', 'gcId', 'gcidValue', 'hash']), + size: + detailFile?.size ?? + _findIntDeep(detail, const [ + 'size', + 'fileSize', + 'resSize', + 'totalSize', + ]), + ); + await FileMetadataCache.cacheFiles([resolved]); + return resolved; + } + Future downloadFile(CloudFile file) async { await _openRemoteFile( file, diff --git a/lib/widgets/file_list_tile.dart b/lib/widgets/file_list_tile.dart index 412e40d..8af1e85 100644 --- a/lib/widgets/file_list_tile.dart +++ b/lib/widgets/file_list_tile.dart @@ -14,6 +14,7 @@ class FileListTile extends StatelessWidget { final VoidCallback? onCut; final VoidCallback? onDownload; final VoidCallback? onShare; + final VoidCallback? onCopyFastTransfer; final VoidCallback? onDelete; final bool isRecycleItem; @@ -28,6 +29,7 @@ class FileListTile extends StatelessWidget { this.onCut, this.onDownload, this.onShare, + this.onCopyFastTransfer, this.onDelete, this.isRecycleItem = false, }); @@ -75,6 +77,11 @@ class FileListTile extends StatelessWidget { onPressed: onShare, child: const Text('分享'), ), + ShadContextMenuItem.inset( + leading: const Icon(LucideIcons.zap, size: 16), + onPressed: onCopyFastTransfer, + child: const Text('复制秒传'), + ), const Divider(height: 8), ShadContextMenuItem.inset( leading: Icon(