feat: copy fast transfer json from file menus
This commit is contained in:
@@ -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]),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user