simplified code across the app and consolidated duplicated helpers
This commit is contained in:
@@ -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) &&
|
||||
|
||||
Reference in New Issue
Block a user