add desktop file selection and folder drop targets

This commit is contained in:
ngfchl
2026-07-18 18:52:04 +08:00
parent 43a07ac49b
commit a85083429b
2 changed files with 128 additions and 31 deletions
+89 -31
View File
@@ -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<void> Function(List<CloudFile> 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),
),
),
),
);
+39
View File
@@ -155,6 +155,7 @@ class FileState {
class FileNotifier extends StateNotifier<FileState> {
GuangyaAPI? _api;
var _detailGeneration = 0;
String? _selectionAnchorID;
FileNotifier() : super(const FileState());
@@ -491,6 +492,44 @@ class FileNotifier extends StateNotifier<FileState> {
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<String>.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: {});