313 lines
9.4 KiB
Dart
313 lines
9.4 KiB
Dart
import 'package:filesize/filesize.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.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 '../../../../share_intent/remote_file_ref.dart';
|
|
import '../../../../state/app/modules/nextcloud_capabilities/bloc/nextcloud_capabilities_cubit.dart';
|
|
import '../../../../utils/downloads/download_job.dart';
|
|
import '../../../../utils/file_clipboard.dart';
|
|
import '../../../../utils/haptics.dart';
|
|
import '../../../../widget/centered_leading.dart';
|
|
import '../../../../widget/confirm_dialog.dart';
|
|
import '../../../../widget/demo_restricted.dart';
|
|
import '../../../../widget/details_bottom_sheet.dart';
|
|
import '../../../../widget/downloads/download_trigger.dart';
|
|
import '../../../../widget/info_dialog.dart';
|
|
import '../../../../widget/prompt_dialog.dart';
|
|
import '../../talk/widgets/highlighted_linkify.dart';
|
|
import '../sharing/share_sheet.dart';
|
|
import 'file_details_sheet.dart';
|
|
import 'file_leading.dart';
|
|
|
|
class FileElement extends StatefulWidget {
|
|
final CacheableFile file;
|
|
final List<String> path;
|
|
final void Function() refetch;
|
|
|
|
/// When non-null, occurrences of this string in the file name are visually
|
|
/// highlighted in the tile title. Used by the Files search delegate.
|
|
final String? highlight;
|
|
|
|
const FileElement(
|
|
this.file,
|
|
this.path,
|
|
this.refetch, {
|
|
this.highlight,
|
|
super.key,
|
|
});
|
|
|
|
@override
|
|
State<FileElement> createState() => _FileElementState();
|
|
}
|
|
|
|
class _FileElementState extends State<FileElement>
|
|
with DownloadTrigger<FileElement> {
|
|
@override
|
|
String? get downloadRemotePath =>
|
|
widget.file.isDirectory ? null : widget.file.path;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
initDownloadTrigger();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant FileElement oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.file.path != widget.file.path) refreshDownloadTrigger();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
disposeDownloadTrigger();
|
|
super.dispose();
|
|
}
|
|
|
|
Widget? _subtitle() {
|
|
final status = downloadJob?.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;
|
|
final size = widget.file.size;
|
|
if (widget.file.isDirectory) {
|
|
if (modified == null) return null;
|
|
return Text('geändert ${modified.formatRelative()}');
|
|
}
|
|
if (size == null && modified == null) return null;
|
|
if (size == null) return Text(modified!.formatRelative());
|
|
if (modified == null) return Text(filesize(size));
|
|
return Text('${filesize(size)}, ${modified.formatRelative()}');
|
|
}
|
|
|
|
void _onTap() {
|
|
if (widget.file.isDirectory) {
|
|
AppRoutes.openFolder(
|
|
context,
|
|
widget.path.toList()..add(widget.file.name),
|
|
);
|
|
return;
|
|
}
|
|
if (guardDemoAction(context)) return;
|
|
if (EndpointData().getEndpointMode() == EndpointMode.stage) {
|
|
InfoDialog.show(
|
|
context,
|
|
'Virtuelle Dateien im Staging Prozess können nicht heruntergeladen werden!',
|
|
);
|
|
return;
|
|
}
|
|
if (isDownloading) {
|
|
confirmCancelDownload();
|
|
return;
|
|
}
|
|
startDownload(
|
|
name: widget.file.name,
|
|
remoteFile: RemoteFileRef.fromCacheable(widget.file),
|
|
);
|
|
}
|
|
|
|
// 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';
|
|
|
|
void _rename() {
|
|
if (guardDemoAction(context)) return;
|
|
showPromptDialog(
|
|
context,
|
|
title: 'Umbenennen',
|
|
label: 'Neuer Name',
|
|
confirmButton: 'Umbenennen',
|
|
initialValue: widget.file.name,
|
|
onConfirm: (newName) async {
|
|
if (newName.isEmpty || newName == widget.file.name) return;
|
|
final parent = _parentPathOf(widget.file.path);
|
|
final destination = _joinPath(
|
|
parent,
|
|
newName,
|
|
isDirectory: widget.file.isDirectory,
|
|
);
|
|
final webdav = await WebdavApi.webdav;
|
|
await webdav.move(
|
|
PathUri.parse(widget.file.path),
|
|
PathUri.parse(destination),
|
|
);
|
|
widget.refetch();
|
|
},
|
|
);
|
|
}
|
|
|
|
void _putOnClipboard({required bool copy}) {
|
|
if (guardDemoAction(context)) return;
|
|
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 {
|
|
if (guardDemoAction(context)) return;
|
|
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();
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showActionSheet() {
|
|
Haptics.longPress();
|
|
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);
|
|
},
|
|
),
|
|
if (!widget.file.isDirectory)
|
|
ListTile(
|
|
leading: const CenteredLeading(Icon(Icons.chat_bubble_outline)),
|
|
title: const Text('Im Talk-Chat versenden'),
|
|
onTap: () {
|
|
Navigator.of(sheetCtx).pop();
|
|
if (guardDemoAction(context)) return;
|
|
AppRoutes.openInternalShareToChat(
|
|
context,
|
|
RemoteFileRef.fromCacheable(widget.file),
|
|
);
|
|
},
|
|
),
|
|
if (context.read<NextcloudCapabilitiesCubit>().canShareAtAll)
|
|
ListTile(
|
|
leading: const CenteredLeading(Icon(Icons.person_add_outlined)),
|
|
title: const Text('Freigeben'),
|
|
onTap: () {
|
|
Navigator.of(sheetCtx).pop();
|
|
showShareSheet(context, widget.file);
|
|
},
|
|
),
|
|
ListTile(
|
|
leading: CenteredLeading(
|
|
Icon(
|
|
Icons.delete_outline,
|
|
color: Theme.of(sheetCtx).colorScheme.error,
|
|
),
|
|
),
|
|
title: Text(
|
|
'Löschen',
|
|
style: TextStyle(color: Theme.of(sheetCtx).colorScheme.error),
|
|
),
|
|
onTap: () {
|
|
Navigator.of(sheetCtx).pop();
|
|
_delete();
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _title(BuildContext context) {
|
|
final base =
|
|
Theme.of(context).textTheme.bodyLarge ??
|
|
DefaultTextStyle.of(context).style;
|
|
if (widget.highlight == null || widget.highlight!.trim().isEmpty) {
|
|
return Text(
|
|
widget.file.name,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
);
|
|
}
|
|
return Text.rich(
|
|
TextSpan(
|
|
children: buildHighlightedSpans(
|
|
text: widget.file.name,
|
|
query: widget.highlight,
|
|
baseStyle: base,
|
|
),
|
|
),
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => ListTile(
|
|
leading: CenteredLeading(FileLeading(file: widget.file)),
|
|
title: _title(context),
|
|
subtitle: _subtitle(),
|
|
trailing: Icon(widget.file.isDirectory ? Icons.arrow_right : null),
|
|
onTap: _onTap,
|
|
onLongPress: _showActionSheet,
|
|
);
|
|
}
|