import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:nextcloud/nextcloud.dart'; import '../../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart'; import '../../../../api/marianumcloud/webdav/webdav_api.dart'; import '../../../../model/endpoint_data.dart'; import '../../../../routing/app_routes.dart'; import '../../../../utils/download_manager.dart'; import '../../../../utils/file_clipboard.dart'; import '../../../../widget/centered_leading.dart'; import '../../../../widget/confirm_dialog.dart'; import '../../../../widget/info_dialog.dart'; import 'file_details_sheet.dart'; class FileElement extends StatefulWidget { final CacheableFile file; final List path; final void Function() refetch; const FileElement(this.file, this.path, this.refetch, {super.key}); @override State createState() => _FileElementState(); } class _FileElementState extends State { DownloadJob? _job; @override void initState() { super.initState(); _attachJob(DownloadManager.instance.jobFor(widget.file.path)); } @override void didUpdateWidget(covariant FileElement oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.file.path != widget.file.path) { _detachJob(); _attachJob(DownloadManager.instance.jobFor(widget.file.path)); } } @override void dispose() { _detachJob(); super.dispose(); } void _attachJob(DownloadJob? job) { _job = job; if (job == null) return; job.status.addListener(_onStatusChange); if (job.isFinished) { WidgetsBinding.instance.addPostFrameCallback((_) => _onStatusChange()); } } void _detachJob() { _job?.status.removeListener(_onStatusChange); _job = null; } void _onStatusChange() { if (!mounted) return; final job = _job; if (job == null) return; final status = job.status.value; if (status is DownloadDone) { DownloadManager.instance.clear(widget.file.path); _detachJob(); AppRoutes.openFileViewer(context, status.localPath); setState(() {}); } else if (status is DownloadFailed) { final message = status.message; DownloadManager.instance.clear(widget.file.path); _detachJob(); setState(() {}); showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Download'), content: Text(message), ), ); } else if (status is DownloadCancelled) { DownloadManager.instance.clear(widget.file.path); _detachJob(); setState(() {}); } else { setState(() {}); } } Future _startDownload() async { final job = await DownloadManager.instance.start( remotePath: widget.file.path, name: widget.file.name, ); if (!mounted) return; if (_job == job) return; _detachJob(); _attachJob(job); setState(() {}); } void _confirmCancel() { showDialog( context: context, builder: (dialogContext) => ConfirmDialog( title: 'Download abbrechen?', content: 'Möchtest du den Download abbrechen?', cancelButton: 'Nein', confirmButton: 'Ja, Abbrechen', onConfirm: () => _job?.cancel(), ), ); } Widget _subtitle() { final status = _job?.status.value; if (status is DownloadInProgress) { return Row( children: [ Container( margin: const EdgeInsets.only(right: 10), child: const Text('Download:'), ), Expanded(child: LinearProgressIndicator(value: status.percent / 100)), Container( margin: const EdgeInsets.only(left: 10), child: Text('${status.percent.round()}%'), ), ], ); } return widget.file.isDirectory ? Text('geändert ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}') : Text('${filesize(widget.file.size)}, ${Jiffy.parseFromDateTime(widget.file.modifiedAt ?? DateTime.now()).fromNow()}'); } void _onTap() { if (widget.file.isDirectory) { AppRoutes.openFolder(context, widget.path.toList()..add(widget.file.name)); return; } if (EndpointData().getEndpointMode() == EndpointMode.stage) { InfoDialog.show(context, 'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!'); return; } final status = _job?.status.value; if (status is DownloadInProgress) { _confirmCancel(); return; } _startDownload(); } // All paths here are relative to the WebDAV root (matching CacheableFile.path). // Root parent is the empty string ''. Folders end with '/'. String _parentPathOf(String path) { final stripped = path.replaceAll(RegExp(r'^/+|/+$'), ''); if (!stripped.contains('/')) return ''; final parts = stripped.split('/')..removeLast(); return parts.isEmpty ? '' : '${parts.join('/')}/'; } String _joinPath(String folder, String name, {required bool isDirectory}) => isDirectory ? '$folder$name/' : '$folder$name'; Future _rename() async { final controller = TextEditingController(text: widget.file.name); final newName = await showDialog( context: context, builder: (dialogCtx) => AlertDialog( title: const Text('Umbenennen'), content: TextField( controller: controller, decoration: const InputDecoration(labelText: 'Neuer Name'), autofocus: true, ), actions: [ TextButton(onPressed: () => Navigator.of(dialogCtx).pop(), child: const Text('Abbrechen')), TextButton( onPressed: () => Navigator.of(dialogCtx).pop(controller.text.trim()), child: const Text('Umbenennen'), ), ], ), ); if (newName == null || newName.isEmpty || newName == widget.file.name) return; final parent = _parentPathOf(widget.file.path); final destination = _joinPath(parent, newName, isDirectory: widget.file.isDirectory); await _runWebdavOp(() async { final webdav = await WebdavApi.webdav; await webdav.move(PathUri.parse(widget.file.path), PathUri.parse(destination)); }, errorTitle: 'Umbenennen fehlgeschlagen'); } void _putOnClipboard({required bool copy}) { if (copy) { FileClipboard.instance.copy([widget.file]); } else { FileClipboard.instance.cut([widget.file]); } ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('"${widget.file.name}" zum ${copy ? "Kopieren" : "Verschieben"} bereitgelegt'), duration: const Duration(seconds: 2), )); } Future _delete() async { await showDialog( context: context, builder: (context) => ConfirmDialog( title: 'Element löschen?', content: 'Das Element wird unwiederruflich gelöscht.', confirmButton: 'Löschen', onConfirmAsync: () async { final webdav = await WebdavApi.webdav; await webdav.delete(PathUri.parse(widget.file.path)); widget.refetch(); }, ), ); } Future _runWebdavOp(Future Function() action, {required String errorTitle}) async { try { await action(); widget.refetch(); } on Object catch (e) { if (!mounted) return; await showDialog( context: context, builder: (dialogCtx) => AlertDialog( title: Text(errorTitle), content: Text(e.toString()), actions: [TextButton(onPressed: () => Navigator.of(dialogCtx).pop(), child: const Text('OK'))], ), ); } } void _showActionSheet() { showModalBottomSheet( context: context, showDragHandle: true, builder: (sheetCtx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const CenteredLeading(Icon(Icons.info_outline)), title: const Text('Info'), onTap: () { Navigator.of(sheetCtx).pop(); showFileDetailsSheet(context, widget.file); }, ), ListTile( leading: const CenteredLeading(Icon(Icons.drive_file_rename_outline)), title: const Text('Umbenennen'), onTap: () { Navigator.of(sheetCtx).pop(); _rename(); }, ), ListTile( leading: const CenteredLeading(Icon(Icons.drive_file_move_outline)), title: const Text('Verschieben'), onTap: () { Navigator.of(sheetCtx).pop(); _putOnClipboard(copy: false); }, ), ListTile( leading: const CenteredLeading(Icon(Icons.copy_outlined)), title: const Text('Kopieren'), onTap: () { Navigator.of(sheetCtx).pop(); _putOnClipboard(copy: true); }, ), ListTile( leading: const CenteredLeading(Icon(Icons.delete_outline)), title: const Text('Löschen'), onTap: () { Navigator.of(sheetCtx).pop(); _delete(); }, ), ], ), ), ); } @override Widget build(BuildContext context) => ListTile( leading: CenteredLeading( Icon(widget.file.isDirectory ? Icons.folder : Icons.description_outlined), ), title: Text(widget.file.name, maxLines: 2, overflow: TextOverflow.ellipsis), subtitle: _subtitle(), trailing: Icon(widget.file.isDirectory ? Icons.arrow_right : null), onTap: _onTap, onLongPress: _showActionSheet, ); }