Files
Client/lib/view/pages/files/widgets/file_element.dart
T

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,
);
}