feat: copy fast transfer json from file menus

This commit is contained in:
ngfchl
2026-07-18 16:58:07 +08:00
parent e0a3cad9b2
commit 5541e704d8
4 changed files with 219 additions and 29 deletions
+1
View File
@@ -127,6 +127,7 @@ class _FileSearchResultsPageState extends ConsumerState<FileSearchResultsPage> {
onOpen: () => notifier.downloadFile(file), onOpen: () => notifier.downloadFile(file),
onCopy: () => notifier.copyToClipboard([file]), onCopy: () => notifier.copyToClipboard([file]),
onCut: () => notifier.cutToClipboard([file]), onCut: () => notifier.cutToClipboard([file]),
onCopyFastTransfer: () => notifier.copyFastTransferJSON(file),
onDownload: () => notifier.downloadFile(file), onDownload: () => notifier.downloadFile(file),
onDelete: () => notifier.deleteFiles([file]), onDelete: () => notifier.deleteFiles([file]),
); );
+73 -29
View File
@@ -1319,6 +1319,7 @@ class _PrimaryFilePane extends ConsumerWidget {
onCut: () => notifier.cutToClipboard([file]), onCut: () => notifier.cutToClipboard([file]),
onDownload: () => notifier.downloadFile(file), onDownload: () => notifier.downloadFile(file),
onShare: () => notifier.createShare(file), onShare: () => notifier.createShare(file),
onCopyFastTransfer: () => notifier.copyFastTransferJSON(file),
isRecycleItem: state.section == WorkspaceSection.recycle, isRecycleItem: state.section == WorkspaceSection.recycle,
onDelete: () => state.section == WorkspaceSection.recycle onDelete: () => state.section == WorkspaceSection.recycle
? notifier.restoreFiles([file]) ? notifier.restoreFiles([file])
@@ -1351,19 +1352,23 @@ class _PrimaryFilePane extends ConsumerWidget {
: () => _openCloudFile(context, ref, file), : () => _openCloudFile(context, ref, file),
), ),
), ),
child: _FileGridCard( child: _FastTransferContextMenu(
file: file, file: file,
isSelected: selected, onCopyFastTransfer: () => notifier.copyFastTransferJSON(file),
onSelect: () { child: _FileGridCard(
if (file.isDirectory && !selected) { file: file,
notifier.navigateToFolder(file); isSelected: selected,
} else { onSelect: () {
notifier.toggleSelection(file.id); if (file.isDirectory && !selected) {
} notifier.navigateToFolder(file);
}, } else {
onOpen: file.isDirectory notifier.toggleSelection(file.id);
? () => notifier.navigateToFolder(file) }
: () => _openCloudFile(context, ref, file), },
onOpen: file.isDirectory
? () => notifier.navigateToFolder(file)
: () => _openCloudFile(context, ref, file),
),
), ),
); );
}, },
@@ -1760,6 +1765,9 @@ class _ColumnFileBrowserState extends ConsumerState<_ColumnFileBrowser> {
_moveFiles(files, parentID, index), _moveFiles(files, parentID, index),
onUploadLocalFiles: (files, parentID) => onUploadLocalFiles: (files, parentID) =>
_uploadFiles(files, parentID, index), _uploadFiles(files, parentID, index),
onCopyFastTransfer: (file) => ref
.read(fileProvider.notifier)
.copyFastTransferJSON(file),
), ),
], ],
), ),
@@ -1796,6 +1804,7 @@ class _FinderColumn extends StatefulWidget {
final _PaneIdentity source; final _PaneIdentity source;
final ValueChanged<CloudFile> onOpenFolder; final ValueChanged<CloudFile> onOpenFolder;
final ValueChanged<CloudFile> onOpenFile; final ValueChanged<CloudFile> onOpenFile;
final ValueChanged<CloudFile> onCopyFastTransfer;
final Future<void> Function(List<CloudFile> files, String? parentID) final Future<void> Function(List<CloudFile> files, String? parentID)
onMoveCloudFiles; onMoveCloudFiles;
final Future<void> Function(List<File> files, String? parentID) final Future<void> Function(List<File> files, String? parentID)
@@ -1807,6 +1816,7 @@ class _FinderColumn extends StatefulWidget {
required this.source, required this.source,
required this.onOpenFolder, required this.onOpenFolder,
required this.onOpenFile, required this.onOpenFile,
required this.onCopyFastTransfer,
required this.onMoveCloudFiles, required this.onMoveCloudFiles,
required this.onUploadLocalFiles, required this.onUploadLocalFiles,
}); });
@@ -1903,21 +1913,26 @@ class _FinderColumnState extends State<_FinderColumn> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final file = column.files[index]; final file = column.files[index];
final selected = column.selectedID == file.id; final selected = column.selectedID == file.id;
final row = _FinderColumnItem( final row = _FastTransferContextMenu(
file: file, file: file,
selected: selected, onCopyFastTransfer: () =>
onTap: () { widget.onCopyFastTransfer(file),
if (file.isDirectory) { child: _FinderColumnItem(
widget.onOpenFolder(file); file: file,
} selected: selected,
}, onTap: () {
onOpen: () { if (file.isDirectory) {
if (file.isDirectory) { widget.onOpenFolder(file);
widget.onOpenFolder(file); }
} else { },
widget.onOpenFile(file); onOpen: () {
} if (file.isDirectory) {
}, widget.onOpenFolder(file);
} else {
widget.onOpenFile(file);
}
},
),
); );
return Draggable<_DraggedCloudFiles>( return Draggable<_DraggedCloudFiles>(
data: _DraggedCloudFiles([file], widget.source), 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 { class _SecondaryFilePane extends ConsumerStatefulWidget {
const _SecondaryFilePane(); const _SecondaryFilePane();
@@ -2411,6 +2452,8 @@ class _SecondaryFilePaneState extends ConsumerState<_SecondaryFilePane> {
onDownload: () => onDownload: () =>
ref.read(fileProvider.notifier).downloadFile(file), ref.read(fileProvider.notifier).downloadFile(file),
onShare: () => ref.read(fileProvider.notifier).createShare(file), onShare: () => ref.read(fileProvider.notifier).createShare(file),
onCopyFastTransfer: () =>
ref.read(fileProvider.notifier).copyFastTransferJSON(file),
onDelete: () => onDelete: () =>
ref.read(fileProvider.notifier).deleteFiles([file]), ref.read(fileProvider.notifier).deleteFiles([file]),
); );
@@ -2434,10 +2477,11 @@ class _SecondaryFilePaneState extends ConsumerState<_SecondaryFilePane> {
} }
} }
final card = _FileGridCard( final card = _FastTransferContextMenu(
file: file, file: file,
onSelect: open, onCopyFastTransfer: () =>
onOpen: open, ref.read(fileProvider.notifier).copyFastTransferJSON(file),
child: _FileGridCard(file: file, onSelect: open, onOpen: open),
); );
return Draggable<_DraggedCloudFiles>( return Draggable<_DraggedCloudFiles>(
data: _DraggedCloudFiles([file], _PaneIdentity.secondary), data: _DraggedCloudFiles([file], _PaneIdentity.secondary),
+138
View File
@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/legacy.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/file_metadata_cache.dart';
import '../core/storage/storage_manager.dart'; import '../core/storage/storage_manager.dart';
import '../models/cloud_file.dart'; import '../models/cloud_file.dart';
import '../models/fast_transfer.dart';
enum FileSort { name, size, modifiedAt, createdAt, type } enum FileSort { name, size, modifiedAt, createdAt, type }
@@ -17,6 +19,13 @@ class ExternalPlayer {
const ExternalPlayer(this.name, this.bundleID); const ExternalPlayer(this.name, this.bundleID);
} }
class _FastTransferSource {
final CloudFile file;
final String path;
const _FastTransferSource(this.file, this.path);
}
extension FileSortExt on FileSort { extension FileSortExt on FileSort {
String get title { String get title {
switch (this) { switch (this) {
@@ -687,6 +696,135 @@ class FileNotifier extends StateNotifier<FileState> {
state = state.copyWith(clearClipboard: true); state = state.copyWith(clearClipboard: true);
} }
Future<void> 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 = <FastTransferEntry>[];
var skipped = 0;
var next = 0;
Future<void> 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<List<_FastTransferSource>> _collectFastTransferFolder(
CloudFile folder,
) async {
final queue = <_FastTransferSource>[
_FastTransferSource(folder, folder.name),
];
final result = <_FastTransferSource>[];
final visited = <String>{};
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<List<CloudFile>> _folderChildrenForFastTransfer(
CloudFile folder,
) async {
final cached = await FileMetadataCache.folderChildren(folder.id);
if (cached != null) return cached;
final children = <CloudFile>[];
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<CloudFile> _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<CloudFile?>().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<void> downloadFile(CloudFile file) async { Future<void> downloadFile(CloudFile file) async {
await _openRemoteFile( await _openRemoteFile(
file, file,
+7
View File
@@ -14,6 +14,7 @@ class FileListTile extends StatelessWidget {
final VoidCallback? onCut; final VoidCallback? onCut;
final VoidCallback? onDownload; final VoidCallback? onDownload;
final VoidCallback? onShare; final VoidCallback? onShare;
final VoidCallback? onCopyFastTransfer;
final VoidCallback? onDelete; final VoidCallback? onDelete;
final bool isRecycleItem; final bool isRecycleItem;
@@ -28,6 +29,7 @@ class FileListTile extends StatelessWidget {
this.onCut, this.onCut,
this.onDownload, this.onDownload,
this.onShare, this.onShare,
this.onCopyFastTransfer,
this.onDelete, this.onDelete,
this.isRecycleItem = false, this.isRecycleItem = false,
}); });
@@ -75,6 +77,11 @@ class FileListTile extends StatelessWidget {
onPressed: onShare, onPressed: onShare,
child: const Text('分享'), child: const Text('分享'),
), ),
ShadContextMenuItem.inset(
leading: const Icon(LucideIcons.zap, size: 16),
onPressed: onCopyFastTransfer,
child: const Text('复制秒传'),
),
const Divider(height: 8), const Divider(height: 8),
ShadContextMenuItem.inset( ShadContextMenuItem.inset(
leading: Icon( leading: Icon(