better loading indicators for timetables, talk and files

This commit is contained in:
2026-05-05 21:07:48 +02:00
parent bee5c02a4f
commit db9c3386f1
25 changed files with 439 additions and 203 deletions
@@ -1,3 +1,5 @@
import '../../../../../api/marianumcloud/webdav/queries/listFiles/listFilesResponse.dart';
import '../../../infrastructure/loadableState/loading_error.dart';
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utilityWidgets/loadableHydratedBloc/loadable_hydrated_bloc_event.dart';
import '../repository/files_repository.dart';
@@ -28,12 +30,14 @@ class FilesBloc extends LoadableHydratedBloc<FilesEvent, FilesState, FilesReposi
}
Future<void> refresh() async {
add(RefetchStarted<FilesState>());
final path = innerState?.currentPath ?? initialPath;
await _query(path);
}
Future<void> setPath(List<String> path) async {
add(Emit((s) => s.copyWith(currentPath: path, listing: null)));
add(RefetchStarted<FilesState>());
await _query(path);
}
@@ -45,8 +49,34 @@ class FilesBloc extends LoadableHydratedBloc<FilesEvent, FilesState, FilesReposi
Future<void> _query(List<String> path) async {
final pathString = path.isEmpty ? '/' : path.join('/');
final listing = await repo.data.listFiles(pathString);
listing.files.removeWhere((file) => file.name.isEmpty || file.name == path.lastOrNull);
add(DataGathered((s) => s.copyWith(listing: listing)));
Object? capturedError;
ListFilesResponse? listing;
try {
listing = await repo.data.listFiles(
pathString,
onCacheData: (cached) {
// Cached payload arrives before the network call settles. Surface it
// immediately via Emit so the listing is visible while isLoading
// stays true and the top loading bar keeps spinning.
cached.files.removeWhere((file) => file.name.isEmpty || file.name == path.lastOrNull);
add(Emit((s) => s.copyWith(listing: cached)));
},
onError: (e) => capturedError = e,
);
} catch (e) {
capturedError = e;
}
if (listing != null) {
listing.files.removeWhere((file) => file.name.isEmpty || file.name == path.lastOrNull);
add(DataGathered((s) => s.copyWith(listing: listing)));
}
if (capturedError != null) {
add(Error(LoadingError(
message: capturedError.toString(),
allowRetry: true,
)));
}
}
}
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:nextcloud/nextcloud.dart';
import '../../../../../api/marianumcloud/webdav/queries/listFiles/listFilesCache.dart';
@@ -7,15 +5,30 @@ import '../../../../../api/marianumcloud/webdav/queries/listFiles/listFilesRespo
import '../../../../../api/marianumcloud/webdav/webdavApi.dart';
class FilesDataProvider {
Future<ListFilesResponse> listFiles(String path) {
final completer = Completer<ListFilesResponse>();
ListFilesCache(
/// Lists files at [path]. Cached payload is delivered via [onCacheData] as
/// soon as it is read from disk, so callers can render stale data while the
/// network call is still pending. The Future itself resolves once both the
/// cache lookup and the network attempt have settled, throwing if no payload
/// could be obtained at all.
Future<ListFilesResponse> listFiles(
String path, {
void Function(ListFilesResponse)? onCacheData,
void Function(Object)? onError,
}) async {
ListFilesResponse? latest;
Object? capturedError;
final cache = ListFilesCache(
path: path,
onUpdate: (data) {
if (!completer.isCompleted) completer.complete(data);
onUpdate: (data) => latest = data,
onCacheData: onCacheData,
onError: (e) {
capturedError = e;
onError?.call(e);
},
);
return completer.future;
await cache.ready;
if (latest != null) return latest!;
throw capturedError ?? Exception('No data and no error from listFiles');
}
Future<void> createFolder(String fullPath) async {