Files
Client/lib/state/app/modules/files/bloc/files_bloc.dart
T

114 lines
3.7 KiB
Dart

import 'package:collection/collection.dart';
import '../../../../../api/errors/error_mapper.dart';
import '../../../../../api/marianumcloud/webdav/queries/list_files/list_files_response.dart';
import '../../../infrastructure/loadable_state/loading_error.dart';
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
import '../repository/files_repository.dart';
import 'files_event.dart';
import 'files_state.dart';
class FilesBloc
extends LoadableHydratedBloc<FilesEvent, FilesState, FilesRepository> {
final List<String> initialPath;
FilesBloc({this.initialPath = const []});
@override
FilesRepository repository() => FilesRepository();
@override
FilesState fromNothing() => FilesState(currentPath: initialPath);
@override
FilesState fromStorage(Map<String, dynamic> json) =>
FilesState.fromJson(json);
@override
Map<String, dynamic>? toStorage(FilesState state) => null;
@override
Future<void> gatherData() async {
final path = innerState?.currentPath ?? initialPath;
await _query(path);
}
Future<void> refresh() async {
add(RefetchStarted<FilesState>());
final path = innerState?.currentPath ?? initialPath;
// Explicit user action — bypass the cache TTL so the root listing also
// refetches even though it is otherwise cached for a day.
await _query(path, renew: true);
}
Future<void> setPath(List<String> path) async {
add(Emit((s) => s.copyWith(currentPath: path, listing: null)));
add(RefetchStarted<FilesState>());
await _query(path);
}
Future<void> createFolder(String name) async {
final path = innerState?.currentPath ?? initialPath;
await repo.data.createFolder('${path.join('/')}/$name');
await refresh();
}
Future<void> _query(List<String> path, {bool renew = false}) async {
final pathString = path.isEmpty ? '/' : path.join('/');
// Drop late results when [setPath] has navigated elsewhere or when the
// bloc has been disposed (e.g. share-flow picker closed mid-fetch). Both
// would otherwise corrupt state or hit "add after close" on the stream.
const pathEquality = ListEquality<String>();
bool isStale() {
if (isClosed) return true;
final inner = innerState;
if (inner == null) return false;
return !pathEquality.equals(inner.currentPath, path);
}
Object? capturedError;
ListFilesResponse? listing;
try {
listing = await repo.data.listFiles(
pathString,
renew: renew,
onCacheData: (cached) {
if (isStale()) return;
// 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 (isStale()) return;
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: errorToUserMessage(capturedError),
technicalDetails: errorToTechnicalDetails(capturedError),
allowRetry: errorAllowsRetry(capturedError),
),
),
);
}
}
}