claude refactorings, flutter best practices, platform dependent changes, general cleanup

This commit is contained in:
2026-05-06 11:58:50 +02:00
parent 4b1d4379a0
commit 4e1272aba9
281 changed files with 1948 additions and 1041 deletions
+218 -38
View File
@@ -1,21 +1,26 @@
import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
import '../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
import '../../../state/app/infrastructure/loadableState/loadable_state.dart';
import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/utilityWidgets/bloc_module.dart';
import '../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
import '../../../api/marianumcloud/webdav/queries/list_files/list_files_cache.dart';
import '../../../api/marianumcloud/webdav/webdav_api.dart';
import '../../../state/app/infrastructure/loadable_state/loadable_state.dart';
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
import '../../../state/app/infrastructure/utility_widgets/bloc_module.dart';
import '../../../state/app/modules/files/bloc/files_bloc.dart';
import '../../../state/app/modules/files/bloc/files_state.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../../../utils/cache_invalidation_bus.dart';
import '../../../utils/file_clipboard.dart';
import '../../../widget/async_action_button.dart';
import '../../../widget/file_pick.dart';
import '../../../widget/placeholder_view.dart';
import 'widgets/file_element.dart';
import 'files_upload_dialog.dart';
import 'widgets/file_element.dart';
class BetterSortOption {
String displayName;
@@ -78,6 +83,11 @@ class _FilesViewState extends State<_FilesView> {
late final SettingsCubit settings;
late SortOption currentSort;
late bool currentSortDirection;
late final StreamSubscription<String> _invalidationSub;
// Cache key in FilesBloc's pathString format: '/' for root, otherwise
// segments joined without leading/trailing slash.
String get _myPathString => widget.path.isEmpty ? '/' : widget.path.join('/');
@override
void initState() {
@@ -85,12 +95,25 @@ class _FilesViewState extends State<_FilesView> {
settings = context.read<SettingsCubit>();
currentSort = settings.val().fileSettings.sortBy;
currentSortDirection = settings.val().fileSettings.ascending;
_invalidationSub = CacheInvalidationBus.listFilesStream.listen(_onInvalidation);
}
void _onInvalidation(String invalidatedPath) {
if (!mounted) return;
if (invalidatedPath != _myPathString) return;
context.read<FilesBloc>().refresh();
}
@override
void dispose() {
_invalidationSub.cancel();
super.dispose();
}
Future<void> mediaUpload(List<String>? paths) async {
if (paths == null) return;
final bloc = context.read<FilesBloc>();
pushScreen(
unawaited(pushScreen(
context,
withNavBar: false,
screen: FilesUploadDialog(
@@ -98,7 +121,7 @@ class _FilesViewState extends State<_FilesView> {
remotePath: widget.path.join('/'),
onUploadFinished: (_) => bloc.refresh(),
),
);
));
}
@override
@@ -163,28 +186,39 @@ class _FilesViewState extends State<_FilesView> {
onPressed: () => _showAddDialog(context, bloc),
child: const Icon(Icons.add),
),
body: LoadableStateConsumer<FilesBloc, FilesState>(
isReady: (state) => state.listing != null,
child: (state, _) {
final listing = state.listing!;
if (listing.files.isEmpty) {
return const PlaceholderView(icon: Icons.folder_off_rounded, text: 'Der Ordner ist leer');
}
final files = listing.sortBy(
sortOption: currentSort,
foldersToTop: context.watch<SettingsCubit>().val().fileSettings.sortFoldersToTop,
reversed: currentSortDirection,
);
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: files.length,
itemBuilder: (context, index) => FileElement(files[index], widget.path, bloc.refresh),
);
},
body: Column(
children: [
_ClipboardBanner(currentFolder: _currentFolderPath, onPasteDone: bloc.refresh),
Expanded(
child: LoadableStateConsumer<FilesBloc, FilesState>(
isReady: (state) => state.listing != null,
child: (state, _) {
final listing = state.listing!;
if (listing.files.isEmpty) {
return const PlaceholderView(icon: Icons.folder_off_rounded, text: 'Der Ordner ist leer');
}
final files = listing.sortBy(
sortOption: currentSort,
foldersToTop: context.watch<SettingsCubit>().val().fileSettings.sortFoldersToTop,
reversed: currentSortDirection,
);
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: files.length,
itemBuilder: (context, index) => FileElement(files[index], widget.path, bloc.refresh),
);
},
),
),
],
),
);
}
// Relative folder path matching the WebDAV format used by `CacheableFile.path`
// (no leading slash; trailing slash for non-root). Empty string means root.
String get _currentFolderPath => widget.path.isEmpty ? '' : '${widget.path.join('/')}/';
void _showAddDialog(BuildContext context, FilesBloc bloc) {
showDialog(
context: context,
@@ -205,18 +239,25 @@ class _FilesViewState extends State<_FilesView> {
Navigator.of(dialogCtx).pop();
},
),
Visibility(
visible: !Platform.isIOS,
child: ListTile(
leading: const Icon(Icons.add_a_photo_outlined),
title: const Text('Aus Gallerie hochladen'),
onTap: () {
FilePick.multipleGalleryPick().then((value) {
if (value != null) mediaUpload(value.map((e) => e.path).toList());
});
Navigator.of(dialogCtx).pop();
},
),
ListTile(
leading: const Icon(Icons.add_a_photo_outlined),
title: const Text('Aus Galerie hochladen'),
onTap: () {
FilePick.multipleGalleryPick().then((value) {
if (value != null) mediaUpload(value.map((e) => e.path).toList());
});
Navigator.of(dialogCtx).pop();
},
),
ListTile(
leading: const Icon(Icons.camera_alt_outlined),
title: const Text('Foto aufnehmen'),
onTap: () {
FilePick.cameraPick().then((image) {
if (image != null) mediaUpload([image.path]);
});
Navigator.of(dialogCtx).pop();
},
),
]),
);
@@ -248,3 +289,142 @@ class _FilesViewState extends State<_FilesView> {
);
}
}
class _ClipboardBanner extends StatefulWidget {
const _ClipboardBanner({required this.currentFolder, required this.onPasteDone});
final String currentFolder;
final void Function() onPasteDone;
@override
State<_ClipboardBanner> createState() => _ClipboardBannerState();
}
class _ClipboardBannerState extends State<_ClipboardBanner> {
bool _busy = false;
// All paths here are relative to the WebDAV root (matching `CacheableFile.path`).
// Root is the empty string ''. Folders end with '/'.
String _normalised(String path) {
final stripped = path.replaceAll(RegExp(r'^/+|/+$'), '');
return stripped.isEmpty ? '' : '$stripped/';
}
String _joinPath(String folder, String name, {required bool isDirectory}) =>
isDirectory ? '$folder$name/' : '$folder$name';
// 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 = _normalised(widget.currentFolder);
var atLeastOneActionable = false;
for (final f in cb.files) {
if (f.isDirectory) {
final src = _normalised(f.path);
if (dst == src || dst.startsWith(src)) return false;
}
final destination = _joinPath(widget.currentFolder, f.name, isDirectory: f.isDirectory);
if (destination != f.path) atLeastOneActionable = true;
}
return atLeastOneActionable;
}
// Cache key format used by ListFilesCache (matches FilesBloc's pathString:
// relative, no leading or trailing slash; root is '/').
String _parentCacheKey(String relativePath) {
final stripped = relativePath.replaceAll(RegExp(r'^/+|/+$'), '');
if (!stripped.contains('/')) return '/';
final parts = stripped.split('/')..removeLast();
return parts.isEmpty ? '/' : parts.join('/');
}
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 = _joinPath(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(_parentCacheKey(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.
for (final folder in invalidatedSourceFolders) {
await ListFilesCache.invalidate(folder);
}
if (operation == FileClipboardOperation.cut) cb.clear();
widget.onPasteDone();
} finally {
if (mounted) setState(() => _busy = false);
}
if (errors.isNotEmpty && mounted) {
await showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Einfügen teilweise fehlgeschlagen'),
content: SingleChildScrollView(child: Text(errors.join('\n\n'))),
actions: [TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('OK'))],
),
);
}
}
@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,
),
],
),
),
);
},
);
}
+12 -10
View File
@@ -3,9 +3,8 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:loader_overlay/loader_overlay.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:uuid/uuid.dart';
import '../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../api/marianumcloud/webdav/webdav_api.dart';
import '../../../widget/confirm_dialog.dart';
import '../../../widget/focus_behaviour.dart';
@@ -107,14 +106,14 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
_showUploadError('Verbindung fehlgeschlagen: $e');
return;
}
var conflictingFiles = _uploadableFiles.where((file) {
var fileName = file.fileName;
return result.any((element) => Uri.decodeComponent(element.href!).endsWith('/$fileName'));
final conflictingFiles = _uploadableFiles.where((file) {
final fileName = file.fileName;
return result.any((element) => Uri.decodeComponent((element as WebDavResponse).href!).endsWith('/$fileName'));
}).toList();
if(conflictingFiles.isNotEmpty) {
if (conflictingFiles.isNotEmpty) {
if (!mounted) return;
bool replaceFiles = await showDialog(
final replaceFiles = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
@@ -160,7 +159,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
)
);
if(!replaceFiles) {
if (replaceFiles != true) {
setState(() {
_isUploading = false;
_overallProgressValue = 0.0;
@@ -179,7 +178,10 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
var fileName = file.fileName;
var filePath = file.filePath;
if(widget.uniqueNames) fileName = '${fileName.split('.').first}-${const Uuid().v4()}.${fileName.split('.').last}';
if (widget.uniqueNames) {
final unique = DateTime.now().microsecondsSinceEpoch.toRadixString(36);
fileName = '${fileName.split('.').first}-$unique.${fileName.split('.').last}';
}
var fullRemotePath = '${widget.remotePath}/$fileName';
@@ -187,7 +189,7 @@ class _FilesUploadDialogState extends State<FilesUploadDialog> {
_infoText = '${_uploadableFiles.indexOf(file) + 1}/${_uploadableFiles.length}';
});
final dynamic uploadTask;
final HttpClientResponse uploadTask;
try {
uploadTask = await webdavClient.putFile(
File(filePath),
@@ -0,0 +1,80 @@
import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:jiffy/jiffy.dart';
import '../../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
/// Shows a modal bottom sheet with technical metadata about a single file or
/// folder: full path, MIME type, size, timestamps, ETag.
Future<void> showFileDetailsSheet(BuildContext context, CacheableFile file) {
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
showDragHandle: true,
builder: (context) => SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: Icon(file.isDirectory ? Icons.folder : Icons.description_outlined, size: 32),
title: Text(file.name, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(file.isDirectory ? 'Ordner' : (file.mimeType ?? '')),
),
const Divider(),
_DetailRow(label: 'Pfad', value: file.path, copyable: true),
if (!file.isDirectory) _DetailRow(label: 'Größe', value: filesize(file.size)),
if (file.modifiedAt != null)
_DetailRow(
label: 'Geändert',
value: '${Jiffy.parseFromDateTime(file.modifiedAt!).format(pattern: 'dd.MM.yyyy HH:mm')} '
'(${Jiffy.parseFromDateTime(file.modifiedAt!).fromNow()})',
),
if (file.createdAt != null)
_DetailRow(
label: 'Erstellt',
value: Jiffy.parseFromDateTime(file.createdAt!).format(pattern: 'dd.MM.yyyy HH:mm'),
),
if (file.eTag != null) _DetailRow(label: 'ETag', value: file.eTag!, copyable: true),
],
),
),
),
);
}
class _DetailRow extends StatelessWidget {
const _DetailRow({required this.label, required this.value, this.copyable = false});
final String label;
final String value;
final bool copyable;
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 90,
child: Text(label, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
),
Expanded(child: SelectableText(value)),
if (copyable)
IconButton(
tooltip: 'Kopieren',
icon: const Icon(Icons.copy, size: 18),
onPressed: () {
Clipboard.setData(ClipboardData(text: value));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('In Zwischenablage kopiert')),
);
},
),
],
),
);
}
+270 -144
View File
@@ -1,24 +1,18 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:filesize/filesize.dart';
import 'package:flowder/flowder.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:open_filex/open_filex.dart';
import '../../../../widget/info_dialog.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:path_provider/path_provider.dart';
import '../../../../api/marianumcloud/webdav/queries/listFiles/cacheableFile.dart';
import '../../../../api/marianumcloud/webdav/webdavApi.dart';
import '../../../../model/account_data.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/unimplemented_dialog.dart';
import '../../../../widget/info_dialog.dart';
import 'file_details_sheet.dart';
class FileElement extends StatefulWidget {
final CacheableFile file;
@@ -26,57 +20,117 @@ class FileElement extends StatefulWidget {
final void Function() refetch;
const FileElement(this.file, this.path, this.refetch, {super.key});
static Future<DownloaderCore> download(BuildContext context, String remotePath, String name, Function(double) onProgress, Function(OpenResult) onDone) async {
var paths = await getTemporaryDirectory();
var encodedPath = Uri.encodeComponent(remotePath);
encodedPath = encodedPath.replaceAll('%2F', '/');
var local = paths.path + Platform.pathSeparator + name;
var options = DownloaderUtils(
progressCallback: (current, total) {
final progress = (current / total) * 100;
onProgress(progress);
},
file: File(local),
progress: ProgressImplementation(),
deleteOnCancel: true,
client: Dio(BaseOptions(headers: AccountData().authHeaders())),
onDone: () {
AppRoutes.openFileViewer(context, local);
onDone(OpenResult(message: 'File viewer opened', type: ResultType.done));
},
);
return await Flowder.download(
'${WebdavApi.buildWebdavUrl()}$encodedPath',
options,
);
}
@override
State<FileElement> createState() => _FileElementState();
}
class _FileElementState extends State<FileElement> {
double percent = 0;
Future<DownloaderCore>? downloadCore;
DownloadJob? _job;
Widget getSubtitle() {
if(widget.file.currentlyDownloading) {
@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<void>(
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<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: percent/100),
),
Expanded(child: LinearProgressIndicator(value: status.percent / 100)),
Container(
margin: const EdgeInsets.only(left: 10),
child: Text('${percent.round()}%'),
child: Text('${status.percent.round()}%'),
),
],
);
@@ -86,101 +140,173 @@ class _FileElementState extends State<FileElement> {
: 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<void> _rename() async {
final controller = TextEditingController(text: widget.file.name);
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');
}
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;
await showDialog<void>(
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<void>(
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: getSubtitle(),
trailing: Icon(widget.file.isDirectory ? Icons.arrow_right : null),
onTap: () {
if(widget.file.isDirectory) {
AppRoutes.openFolder(context, widget.path.toList()..add(widget.file.name));
} else {
if(EndpointData().getEndpointMode() == EndpointMode.stage) {
InfoDialog.show(context, 'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!');
return;
}
if(widget.file.currentlyDownloading) {
showDialog(
context: context,
builder: (context) => ConfirmDialog(
title: 'Download abbrechen?',
content: 'Möchtest du den Download abbrechen?',
cancelButton: 'Nein',
confirmButton: 'Ja, Abbrechen',
onConfirm: () {
downloadCore?.then((value) {
if(!value.isCancelled) value.cancel();
});
setState(() {
widget.file.currentlyDownloading = false;
percent = 0;
downloadCore = null;
});
},
),
);
return;
}
setState(() {
widget.file.currentlyDownloading = true;
});
downloadCore = FileElement.download(context, widget.file.path, widget.file.name, (progress) {
setState(() => percent = progress);
}, (result) {
if(result.type != ResultType.done) {
showDialog(context: context, builder: (context) => AlertDialog(
title: const Text('Download'),
content: Text(result.message),
));
}
setState(() {
widget.file.currentlyDownloading = false;
percent = 0;
});
});
}
},
onLongPress: () {
showDialog(context: context, builder: (context) => SimpleDialog(
children: [
ListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('Löschen'),
onTap: () {
Navigator.of(context).pop();
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();
},
));
},
),
Visibility(
visible: !kReleaseMode,
child: ListTile(
leading: const Icon(Icons.share_outlined),
title: const Text('Teilen'),
onTap: () {
Navigator.of(context).pop();
UnimplementedDialog.show(context);
},
),
),
],
));
},
);
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,
);
}