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 createState() => _ClipboardBannerState(); } class _ClipboardBannerState extends State { 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 _paste() async { final cb = FileClipboard.instance; if (_busy || !_canPaste) return; setState(() => _busy = true); final operation = cb.operation; final errors = []; final invalidatedSourceFolders = {}; 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, ), ], ), ), ); }, ); }