From a85083429b0dbf5b2c8a28f42707a3adfb98de7c Mon Sep 17 00:00:00 2001 From: ngfchl Date: Sat, 18 Jul 2026 18:52:04 +0800 Subject: [PATCH] add desktop file selection and folder drop targets --- lib/pages/workspace_page.dart | 120 +++++++++++++++++++++++-------- lib/providers/file_provider.dart | 39 ++++++++++ 2 files changed, 128 insertions(+), 31 deletions(-) diff --git a/lib/pages/workspace_page.dart b/lib/pages/workspace_page.dart index 4e617a2..9e4a096 100644 --- a/lib/pages/workspace_page.dart +++ b/lib/pages/workspace_page.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:window_manager/window_manager.dart'; @@ -41,6 +42,57 @@ class _DraggedCloudFiles { const _DraggedCloudFiles(this.files, this.source); } +bool _hasPressedKey(LogicalKeyboardKey key) => + HardwareKeyboard.instance.logicalKeysPressed.contains(key); + +void _selectDesktopFile(FileNotifier notifier, CloudFile file) { + notifier.selectWithModifiers( + file.id, + command: + _hasPressedKey(LogicalKeyboardKey.metaLeft) || + _hasPressedKey(LogicalKeyboardKey.metaRight) || + _hasPressedKey(LogicalKeyboardKey.controlLeft) || + _hasPressedKey(LogicalKeyboardKey.controlRight), + shift: + _hasPressedKey(LogicalKeyboardKey.shiftLeft) || + _hasPressedKey(LogicalKeyboardKey.shiftRight), + ); +} + +class _FolderMoveTarget extends StatelessWidget { + final CloudFile file; + final Future Function(List files, String? parentID) onMove; + final Widget child; + + const _FolderMoveTarget({ + required this.file, + required this.onMove, + required this.child, + }); + + @override + Widget build(BuildContext context) { + if (!file.isDirectory) return child; + return DragTarget<_DraggedCloudFiles>( + onWillAcceptWithDetails: (details) => + !details.data.files.any((source) => source.id == file.id), + onAcceptWithDetails: (details) => onMove(details.data.files, file.id), + builder: (context, candidates, _) => DecoratedBox( + decoration: BoxDecoration( + border: candidates.isEmpty + ? null + : Border.all( + color: ShadTheme.of(context).colorScheme.primary, + width: 2, + ), + borderRadius: BorderRadius.circular(6), + ), + child: child, + ), + ); + } +} + void _openCloudFile(BuildContext context, WidgetRef ref, CloudFile file) { if (file.isVideo) { unawaited(showMediaPlayerDialog(context, file)); @@ -1308,13 +1360,7 @@ class _PrimaryFilePane extends ConsumerWidget { final tile = FileListTile( file: file, isSelected: selected, - onSelect: () { - if (file.isDirectory && !selected) { - notifier.navigateToFolder(file); - } else { - notifier.toggleSelection(file.id); - } - }, + onSelect: () => _selectDesktopFile(notifier, file), onOpen: file.isDirectory ? () => notifier.navigateToFolder(file) : () => _openCloudFile(context, ref, file), @@ -1330,48 +1376,60 @@ class _PrimaryFilePane extends ConsumerWidget { : notifier.deleteFiles([file]), ); final item = Draggable<_DraggedCloudFiles>( - data: _DraggedCloudFiles([file], _PaneIdentity.primary), + data: _DraggedCloudFiles( + selected + ? files + .where((item) => state.selectedIDs.contains(item.id)) + .toList() + : [file], + _PaneIdentity.primary, + ), feedback: _DragFeedback(label: file.name), childWhenDragging: Opacity(opacity: 0.35, child: tile), - child: tile, + child: _FolderMoveTarget( + file: file, + onMove: (sources, parentID) => + notifier.moveFilesTo(sources, parentID: parentID), + child: tile, + ), ); if (viewMode == _FileViewMode.list) return item; return Draggable<_DraggedCloudFiles>( - data: _DraggedCloudFiles([file], _PaneIdentity.primary), + data: _DraggedCloudFiles( + selected + ? files + .where((item) => state.selectedIDs.contains(item.id)) + .toList() + : [file], + _PaneIdentity.primary, + ), feedback: _DragFeedback(label: file.name), childWhenDragging: Opacity( opacity: 0.35, child: _FileGridCard( file: file, isSelected: selected, - onSelect: () { - if (file.isDirectory && !selected) { - notifier.navigateToFolder(file); - } else { - notifier.toggleSelection(file.id); - } - }, + onSelect: () => _selectDesktopFile(notifier, file), onOpen: file.isDirectory ? () => notifier.navigateToFolder(file) : () => _openCloudFile(context, ref, file), ), ), - child: _FastTransferContextMenu( + child: _FolderMoveTarget( file: file, - onCopyFastTransfer: () => notifier.copyFastTransferJSON(file), - child: _FileGridCard( + onMove: (sources, parentID) => + notifier.moveFilesTo(sources, parentID: parentID), + 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: () => _selectDesktopFile(notifier, file), + onOpen: file.isDirectory + ? () => notifier.navigateToFolder(file) + : () => _openCloudFile(context, ref, file), + ), ), ), ); diff --git a/lib/providers/file_provider.dart b/lib/providers/file_provider.dart index 4701225..19245b8 100644 --- a/lib/providers/file_provider.dart +++ b/lib/providers/file_provider.dart @@ -155,6 +155,7 @@ class FileState { class FileNotifier extends StateNotifier { GuangyaAPI? _api; var _detailGeneration = 0; + String? _selectionAnchorID; FileNotifier() : super(const FileState()); @@ -491,6 +492,44 @@ class FileNotifier extends StateNotifier { state = state.copyWith(selectedIDs: newSelected); } + void selectWithModifiers( + String id, { + required bool command, + required bool shift, + }) { + final index = state.files.indexWhere((file) => file.id == id); + if (index < 0) return; + final selected = Set.from(state.selectedIDs); + if (shift && _selectionAnchorID != null) { + final anchor = state.files.indexWhere( + (file) => file.id == _selectionAnchorID, + ); + if (anchor >= 0) { + final start = anchor < index ? anchor : index; + final end = anchor > index ? anchor : index; + final range = state.files + .sublist(start, end + 1) + .map((file) => file.id); + if (command) { + selected.addAll(range); + } else { + selected + ..clear() + ..addAll(range); + } + } + } else if (command) { + selected.contains(id) ? selected.remove(id) : selected.add(id); + _selectionAnchorID = id; + } else { + selected + ..clear() + ..add(id); + _selectionAnchorID = id; + } + state = state.copyWith(selectedIDs: selected); + } + void selectAll() { if (state.selectedIDs.length == state.files.length) { state = state.copyWith(selectedIDs: {});