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
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
import '../../../../api/marianumcloud/search/search_files.dart';
import '../../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
import '../../../../utils/debouncer.dart';
import '../data/webdav_path.dart';
import 'local_cache_search.dart';
/// Holds the live state of a Files-search session: current query, the latest
@@ -28,12 +29,11 @@ class FilesSearchController extends ChangeNotifier {
bool _disposed = false;
Future<List<CacheableFile>>? _localIndex;
Future<List<CacheableFile>> _searchLocal(List<String> pathScope) async =>
searchLocalCacheIndex(
await (_localIndex ??= loadLocalCacheIndex()),
_query,
pathScope: pathScope,
);
Future<List<CacheableFile>> _searchLocal() async => searchLocalCacheIndex(
await (_localIndex ??= loadLocalCacheIndex()),
_query,
pathScope: _pathScope,
);
/// Guards against the race where the search delegate is closed (and the
/// controller disposed) while a debounced cache scan or server call is
@@ -45,10 +45,7 @@ class FilesSearchController extends ChangeNotifier {
}
String get query => _query;
List<String> get pathScope => List.unmodifiable(_pathScope);
bool get isScoped => _pathScope.isNotEmpty;
List<CacheableFile> get cacheResults => _cacheResults;
List<CacheableFile> get serverResults => _serverResults;
bool get serverLoading => _serverLoading;
Object? get serverError => _serverError;
@@ -81,18 +78,7 @@ class FilesSearchController extends ChangeNotifier {
_safeNotify();
return;
}
// Show loading immediately — even before the (typically fast) cache
// scan resolves — so the indicator is visible the moment the user
// starts typing rather than after the first await hop.
_serverLoading = true;
_serverError = null;
_safeNotify();
final cacheHits = await _searchLocal(_pathScope);
if (epoch != _serverEpoch) return;
_cacheResults = cacheHits;
_safeNotify();
_scheduleServerCall();
await _searchLocalThenServer(epoch);
}
/// Drops the path filter and re-runs the current search globally. Used by
@@ -105,11 +91,18 @@ class FilesSearchController extends ChangeNotifier {
_safeNotify();
return;
}
await _searchLocalThenServer(epoch);
}
Future<void> _searchLocalThenServer(int epoch) async {
// Show loading immediately — even before the (typically fast) cache
// scan resolves — so the indicator is visible the moment the user
// starts typing rather than after the first await hop.
_serverLoading = true;
_serverError = null;
_safeNotify();
final cacheHits = await _searchLocal(const []);
final cacheHits = await _searchLocal();
if (epoch != _serverEpoch) return;
_cacheResults = cacheHits;
_safeNotify();
@@ -135,7 +128,7 @@ class FilesSearchController extends ChangeNotifier {
Future<void> _runServerCall() async {
final epoch = _serverEpoch;
final term = _query;
final scopePrefix = _pathScope.isEmpty ? '' : '${_pathScope.join('/')}/';
final scopePrefix = webdavFolderPath(_pathScope);
try {
final response = await _api.run(term: term);
if (epoch != _serverEpoch) return;
@@ -38,13 +38,7 @@ class FilesSearchDelegate extends SearchDelegate<void> {
);
@override
Widget buildResults(BuildContext context) {
_controller.setQuery(query);
return FilesSearchResults(
controller: _controller,
onResultTap: () => close(context, null),
);
}
Widget buildResults(BuildContext context) => buildSuggestions(context);
@override
Widget buildSuggestions(BuildContext context) {
@@ -11,6 +11,7 @@ import '../../../../state/app/infrastructure/loadable_state/view/loadable_state_
import '../../../../state/app/infrastructure/loadable_state/view/loadable_state_primary_loading.dart';
import '../../../../state/app/infrastructure/utility_widgets/bloc_module.dart';
import '../../../../widget/placeholder_view.dart';
import '../data/webdav_path.dart';
import '../widgets/file_element.dart';
import 'files_search_controller.dart';
@@ -80,8 +81,8 @@ class FilesSearchResults extends StatelessWidget {
visible: showErrorScreen,
message: errorMessage,
),
if (showEmpty) _emptyState(context),
if (hasContent) _resultList(context, combined),
if (showEmpty) _emptyState(),
if (hasContent) _resultList(combined),
],
),
),
@@ -89,7 +90,7 @@ class FilesSearchResults extends StatelessWidget {
);
}
Widget _emptyState(BuildContext context) => PlaceholderView(
Widget _emptyState() => PlaceholderView(
icon: Icons.search_off_outlined,
text: 'Keine Treffer gefunden.',
button: controller.isScoped
@@ -101,7 +102,7 @@ class FilesSearchResults extends StatelessWidget {
: null,
);
Widget _resultList(BuildContext context, List<CacheableFile> combined) {
Widget _resultList(List<CacheableFile> combined) {
final groups = _groupByParent(combined);
final orderedKeys = groups.keys.toList()..sort();
// Flat (folder header | file) rows built lazily: results can run into
@@ -117,7 +118,7 @@ class FilesSearchResults extends StatelessWidget {
itemCount: rows.length,
itemBuilder: (context, index) {
final (folder, file) = rows[index];
final segments = _segmentsOf(folder);
final segments = webdavSegmentsOf(folder);
if (file == null) {
return _FolderHeader(
key: ValueKey('folder:$folder'),
@@ -139,29 +140,14 @@ class FilesSearchResults extends StatelessWidget {
);
}
static final RegExp _edgeSlashes = RegExp(r'^/+|/+$');
Map<String, List<CacheableFile>> _groupByParent(List<CacheableFile> files) {
final map = <String, List<CacheableFile>>{};
for (final file in files) {
map.putIfAbsent(_parentOf(file), () => []).add(file);
final parent = '/${webdavParentSegmentsOf(file.path).join('/')}';
map.putIfAbsent(parent, () => []).add(file);
}
return map;
}
String _parentOf(CacheableFile file) {
final stripped = file.path.replaceAll(_edgeSlashes, '');
final segments = stripped.split('/');
if (segments.length <= 1) return '/';
segments.removeLast();
return '/${segments.join('/')}';
}
List<String> _segmentsOf(String folder) {
final stripped = folder.replaceAll(_edgeSlashes, '');
if (stripped.isEmpty) return const [];
return stripped.split('/');
}
}
class _FolderHeader extends StatelessWidget {
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart';
import '../../../../api/cache_store.dart';
import '../../../../api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
import '../../../../api/marianumcloud/webdav/queries/list_files/list_files_response.dart';
import '../data/webdav_path.dart';
/// Document key prefix used by `ListFilesCache._documentId`.
const String _folderCachePrefix = 'wd-folder-';
@@ -42,18 +43,16 @@ List<CacheableFile> buildLocalCacheIndex(List<String> payloads) {
/// Files in [index] whose name contains [query] (case-insensitive).
///
/// [pathScope] restricts results to entries whose WebDAV path starts with
/// the given folder. Pass an empty list (or null) to search globally.
/// the given folder. Pass an empty list to search globally.
List<CacheableFile> searchLocalCacheIndex(
List<CacheableFile> index,
String query, {
List<String>? pathScope,
List<String> pathScope = const [],
}) {
final trimmed = query.trim();
if (trimmed.isEmpty) return const [];
final needle = trimmed.toLowerCase();
final scopePrefix = pathScope == null || pathScope.isEmpty
? ''
: '${pathScope.join('/')}/';
final scopePrefix = webdavFolderPath(pathScope);
return [
for (final file in index)
if (file.lowerName.contains(needle) &&