simplified code across the app and consolidated duplicated helpers

This commit is contained in:
2026-09-27 17:29:54 +02:00
parent 3e856d0b1c
commit ee1240f71a
155 changed files with 1772 additions and 2546 deletions
@@ -5,6 +5,7 @@ import '../../../../api/marianumcloud/webdav/queries/list_files/list_files_cache
import '../../../../api/marianumcloud/webdav/webdav_api.dart';
import '../../../../utils/file_clipboard.dart';
import '../../../../widget/info_dialog.dart';
import '../data/webdav_path.dart';
/// Banner that appears at the top of a Files folder while there is something
/// in the file clipboard. Shows the cut/copy state and offers a "Hier
@@ -26,16 +27,6 @@ class ClipboardBanner extends StatefulWidget {
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
@@ -43,14 +34,14 @@ class _ClipboardBannerState extends State<ClipboardBanner> {
bool get _canPaste {
final cb = FileClipboard.instance;
if (cb.isEmpty) return false;
final dst = _normalised(widget.currentFolder);
final dst = webdavFolderPath(webdavSegmentsOf(widget.currentFolder));
var atLeastOneActionable = false;
for (final f in cb.files) {
if (f.isDirectory) {
final src = _normalised(f.path);
final src = webdavFolderPath(webdavSegmentsOf(f.path));
if (dst == src || dst.startsWith(src)) return false;
}
final destination = _joinPath(
final destination = webdavJoin(
widget.currentFolder,
f.name,
isDirectory: f.isDirectory,
@@ -60,15 +51,6 @@ class _ClipboardBannerState extends State<ClipboardBanner> {
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;
@@ -79,7 +61,7 @@ class _ClipboardBannerState extends State<ClipboardBanner> {
try {
final webdav = await WebdavApi.webdav;
for (final file in cb.files) {
final destination = _joinPath(
final destination = webdavJoin(
widget.currentFolder,
file.name,
isDirectory: file.isDirectory,
@@ -91,7 +73,9 @@ class _ClipboardBannerState extends State<ClipboardBanner> {
PathUri.parse(file.path),
PathUri.parse(destination),
);
invalidatedSourceFolders.add(_parentCacheKey(file.path));
invalidatedSourceFolders.add(
listFilesCacheKey(webdavParentSegmentsOf(file.path)),
);
} else {
await webdav.copy(
PathUri.parse(file.path),
@@ -105,9 +89,9 @@ class _ClipboardBannerState extends State<ClipboardBanner> {
// 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);
}
await Future.wait(
invalidatedSourceFolders.map(ListFilesCache.invalidate),
);
if (operation == FileClipboardOperation.cut) cb.clear();
widget.onPasteDone();
} finally {
+5 -17
View File
@@ -19,6 +19,7 @@ import '../../../../widget/details_bottom_sheet.dart';
import '../../../../widget/downloads/download_trigger.dart';
import '../../../../widget/prompt_dialog.dart';
import '../../talk/widgets/highlighted_linkify.dart';
import '../data/webdav_path.dart';
import '../sharing/share_sheet.dart';
import 'file_details_sheet.dart';
import 'file_leading.dart';
@@ -26,7 +27,7 @@ import 'file_leading.dart';
class FileElement extends StatefulWidget {
final CacheableFile file;
final List<String> path;
final void Function() refetch;
final VoidCallback 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.
@@ -101,7 +102,7 @@ class _FileElementState extends State<FileElement>
if (widget.file.isDirectory) {
AppRoutes.openFolder(
context,
widget.path.toList()..add(widget.file.name),
[...widget.path, widget.file.name],
);
return;
}
@@ -116,18 +117,6 @@ class _FileElementState extends State<FileElement>
);
}
// 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(
@@ -138,9 +127,8 @@ class _FileElementState extends State<FileElement>
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,
final destination = webdavJoin(
webdavFolderPath(webdavParentSegmentsOf(widget.file.path)),
newName,
isDirectory: widget.file.isDirectory,
);
@@ -1,24 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../state/app/modules/settings/bloc/settings_cubit.dart';
import '../data/sort_options.dart';
/// Sort direction and sort key pickers, read from and persisted to the file
/// settings.
class FilesSortActions extends StatelessWidget {
final SortOption currentSort;
final bool ascending;
final ValueChanged<bool> onDirectionChanged;
final ValueChanged<SortOption> onSortChanged;
const FilesSortActions({
required this.currentSort,
required this.ascending,
required this.onDirectionChanged,
required this.onSortChanged,
super.key,
});
const FilesSortActions({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final settings = context.read<SettingsCubit>();
final (currentSort, ascending) = context.select(
(SettingsCubit c) =>
(c.state.fileSettings.sortBy, c.state.fileSettings.ascending),
);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -46,7 +44,7 @@ class FilesSortActions extends StatelessWidget {
),
)
.toList(),
onSelected: onDirectionChanged,
onSelected: (e) => settings.val(write: true).fileSettings.ascending = e,
),
PopupMenuButton<SortOption>(
tooltip:
@@ -70,7 +68,7 @@ class FilesSortActions extends StatelessWidget {
),
)
.toList(),
onSelected: onSortChanged,
onSelected: (e) => settings.val(write: true).fileSettings.sortBy = e,
),
],
);