Files
Client/lib/view/pages/files/widgets/clipboard_banner.dart
T

158 lines
5.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:nextcloud/nextcloud.dart';
import '../../../../api/marianumcloud/webdav/queries/list_files/list_files_cache.dart';
import '../../../../api/marianumcloud/webdav/webdav_api.dart';
import '../../../../utils/file_clipboard.dart';
import '../../../../widget/info_dialog.dart';
import '../data/webdav_path.dart';
/// Banner that appears at the top of a Files folder while there is something
/// in the file clipboard. Shows the cut/copy state and offers a "Hier
/// einfügen" button.
class ClipboardBanner extends StatefulWidget {
final String currentFolder;
final VoidCallback onPasteDone;
const ClipboardBanner({
required this.currentFolder,
required this.onPasteDone,
super.key,
});
@override
State<ClipboardBanner> createState() => _ClipboardBannerState();
}
class _ClipboardBannerState extends State<ClipboardBanner> {
bool _busy = false;
// Disabled when:
// - clipboard is empty
// - we'd be pasting a folder into itself or one of its descendants
// - every entry already lives in the current folder (paste would be a no-op)
bool get _canPaste {
final cb = FileClipboard.instance;
if (cb.isEmpty) return false;
final dst = webdavFolderPath(webdavSegmentsOf(widget.currentFolder));
var atLeastOneActionable = false;
for (final f in cb.files) {
if (f.isDirectory) {
final src = webdavFolderPath(webdavSegmentsOf(f.path));
if (dst == src || dst.startsWith(src)) return false;
}
final destination = webdavJoin(
widget.currentFolder,
f.name,
isDirectory: f.isDirectory,
);
if (destination != f.path) atLeastOneActionable = true;
}
return atLeastOneActionable;
}
Future<void> _paste() async {
final cb = FileClipboard.instance;
if (_busy || !_canPaste) return;
setState(() => _busy = true);
final operation = cb.operation;
final errors = <String>[];
final invalidatedSourceFolders = <String>{};
try {
final webdav = await WebdavApi.webdav;
for (final file in cb.files) {
final destination = webdavJoin(
widget.currentFolder,
file.name,
isDirectory: file.isDirectory,
);
if (destination == file.path) continue;
try {
if (operation == FileClipboardOperation.cut) {
await webdav.move(
PathUri.parse(file.path),
PathUri.parse(destination),
);
invalidatedSourceFolders.add(
listFilesCacheKey(webdavParentSegmentsOf(file.path)),
);
} else {
await webdav.copy(
PathUri.parse(file.path),
PathUri.parse(destination),
);
}
} on Object catch (e) {
errors.add('${file.name}: $e');
}
}
// After cut, the source folders no longer contain the moved files. Drop
// their cached listings so the next visit fetches fresh data instead of
// briefly showing the moved file as still present.
await Future.wait(
invalidatedSourceFolders.map(ListFilesCache.invalidate),
);
if (operation == FileClipboardOperation.cut) cb.clear();
widget.onPasteDone();
} finally {
if (mounted) setState(() => _busy = false);
}
if (errors.isNotEmpty && mounted) {
InfoDialog.show(
context,
errors.join('\n\n'),
copyable: true,
title: 'Einfügen teilweise fehlgeschlagen',
);
}
}
@override
Widget build(BuildContext context) => ListenableBuilder(
listenable: FileClipboard.instance,
builder: (context, _) {
final cb = FileClipboard.instance;
if (cb.isEmpty) return const SizedBox.shrink();
final cut = cb.operation == FileClipboardOperation.cut;
final count = cb.files.length;
final label = count == 1 ? '"${cb.files.first.name}"' : '$count Elemente';
return Material(
color: Theme.of(context).colorScheme.secondaryContainer,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Icon(
cut ? Icons.drive_file_move_outline : Icons.copy_outlined,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
cut ? '$label verschieben' : '$label kopieren',
overflow: TextOverflow.ellipsis,
),
),
TextButton(
onPressed: _busy || !_canPaste ? null : _paste,
child: _busy
? const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Hier einfügen'),
),
IconButton(
tooltip: 'Verwerfen',
icon: const Icon(Icons.close, size: 20),
onPressed: _busy ? null : cb.clear,
),
],
),
),
);
},
);
}