300 lines
9.2 KiB
Dart
300 lines
9.2 KiB
Dart
import 'package:filesize/filesize.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:nextcloud/nextcloud.dart';
|
|
|
|
import '../../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
|
|
import '../../../../api/marianumcloud/webdav/webdav_api.dart';
|
|
import '../../../../extensions/date_time.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/details_bottom_sheet.dart';
|
|
import '../../../../widget/info_dialog.dart';
|
|
import 'file_details_sheet.dart';
|
|
|
|
class FileElement extends StatefulWidget {
|
|
final CacheableFile file;
|
|
final List<String> path;
|
|
final void Function() refetch;
|
|
const FileElement(this.file, this.path, this.refetch, {super.key});
|
|
|
|
@override
|
|
State<FileElement> createState() => _FileElementState();
|
|
}
|
|
|
|
class _FileElementState extends State<FileElement> {
|
|
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(() {});
|
|
InfoDialog.show(context, message, title: 'Download', copyable: true);
|
|
} else if (status is DownloadCancelled) {
|
|
DownloadManager.instance.clear(widget.file.path);
|
|
_detachJob();
|
|
setState(() {});
|
|
} else {
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
Future<void> _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<void>(
|
|
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()}%'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
final modified = widget.file.modifiedAt ?? DateTime.now();
|
|
return widget.file.isDirectory
|
|
? Text('geändert ${modified.formatRelative()}')
|
|
: Text('${filesize(widget.file.size)}, ${modified.formatRelative()}');
|
|
}
|
|
|
|
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<void> _rename() async {
|
|
final controller = TextEditingController(text: widget.file.name);
|
|
try {
|
|
final newName = await showDialog<String>(
|
|
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');
|
|
} finally {
|
|
controller.dispose();
|
|
}
|
|
}
|
|
|
|
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<void> _delete() async {
|
|
await showDialog<void>(
|
|
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<void> _runWebdavOp(Future<void> Function() action, {required String errorTitle}) async {
|
|
try {
|
|
await action();
|
|
widget.refetch();
|
|
} on Object catch (e) {
|
|
if (!mounted) return;
|
|
InfoDialog.show(context, e.toString(), title: errorTitle, copyable: true);
|
|
}
|
|
}
|
|
|
|
void _showActionSheet() {
|
|
showDetailsBottomSheet(
|
|
context,
|
|
children: (sheetCtx) => [
|
|
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,
|
|
);
|
|
}
|