better loading indicators for timetables, talk and files
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import '../../../../../api/marianumcloud/talk/chat/getChatResponse.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/chat_repository.dart';
|
||||
@@ -22,8 +24,11 @@ class ChatBloc extends LoadableHydratedBloc<ChatEvent, ChatState, ChatRepository
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
final token = innerState?.currentToken ?? '';
|
||||
if (token.isEmpty) return;
|
||||
_loadChat(token);
|
||||
if (token.isEmpty) {
|
||||
add(DataGathered((s) => s));
|
||||
return;
|
||||
}
|
||||
await _loadChat(token);
|
||||
}
|
||||
|
||||
void setToken(String token) {
|
||||
@@ -32,6 +37,7 @@ class ChatBloc extends LoadableHydratedBloc<ChatEvent, ChatState, ChatRepository
|
||||
return;
|
||||
}
|
||||
add(Emit((s) => s.copyWith(currentToken: token, chatResponse: null)));
|
||||
add(RefetchStarted<ChatState>());
|
||||
_loadChat(token);
|
||||
}
|
||||
|
||||
@@ -41,19 +47,37 @@ class ChatBloc extends LoadableHydratedBloc<ChatEvent, ChatState, ChatRepository
|
||||
|
||||
void refresh() {
|
||||
final token = innerState?.currentToken ?? '';
|
||||
if (token.isNotEmpty) _loadChat(token);
|
||||
if (token.isEmpty) return;
|
||||
add(RefetchStarted<ChatState>());
|
||||
_loadChat(token);
|
||||
}
|
||||
|
||||
void _loadChat(String token) {
|
||||
Future<void> _loadChat(String token) async {
|
||||
final requestStart = DateTime.now();
|
||||
_lastTokenSet = requestStart;
|
||||
repo.data.getChat(
|
||||
token: token,
|
||||
onUpdate: (data) {
|
||||
if (_lastTokenSet.isAfter(requestStart)) return;
|
||||
if ((innerState?.currentToken ?? '') != token) return;
|
||||
add(DataGathered((s) => s.copyWith(chatResponse: data)));
|
||||
},
|
||||
);
|
||||
|
||||
Object? capturedError;
|
||||
GetChatResponse? response;
|
||||
try {
|
||||
response = await repo.data.getChat(
|
||||
token: token,
|
||||
onError: (e) => capturedError = e,
|
||||
);
|
||||
} catch (e) {
|
||||
capturedError = e;
|
||||
}
|
||||
|
||||
if (_lastTokenSet.isAfter(requestStart)) return;
|
||||
if ((innerState?.currentToken ?? '') != token) return;
|
||||
|
||||
if (response != null) {
|
||||
add(DataGathered((s) => s.copyWith(chatResponse: response)));
|
||||
}
|
||||
if (capturedError != null) {
|
||||
add(Error(LoadingError(
|
||||
message: capturedError.toString(),
|
||||
allowRetry: true,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,26 @@ import '../../../../../api/marianumcloud/talk/chat/getChatCache.dart';
|
||||
import '../../../../../api/marianumcloud/talk/chat/getChatResponse.dart';
|
||||
|
||||
class ChatDataProvider {
|
||||
void getChat({
|
||||
Future<GetChatResponse> getChat({
|
||||
required String token,
|
||||
required void Function(GetChatResponse data) onUpdate,
|
||||
}) {
|
||||
GetChatCache(chatToken: token, onUpdate: onUpdate);
|
||||
void Function(GetChatResponse data)? onUpdate,
|
||||
void Function(Object)? onError,
|
||||
}) async {
|
||||
GetChatResponse? latest;
|
||||
Object? capturedError;
|
||||
final cache = GetChatCache(
|
||||
chatToken: token,
|
||||
onUpdate: (data) {
|
||||
latest = data;
|
||||
onUpdate?.call(data);
|
||||
},
|
||||
onError: (e) {
|
||||
capturedError = e;
|
||||
onError?.call(e);
|
||||
},
|
||||
);
|
||||
await cache.ready;
|
||||
if (latest != null) return latest!;
|
||||
throw capturedError ?? Exception('No data and no error from getChat');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_app_badge/flutter_app_badge.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/chat_list_repository.dart';
|
||||
@@ -7,6 +8,14 @@ import 'chat_list_event.dart';
|
||||
import 'chat_list_state.dart';
|
||||
|
||||
class ChatListBloc extends LoadableHydratedBloc<ChatListEvent, ChatListState, ChatListRepository> {
|
||||
bool _forceRenew = false;
|
||||
|
||||
@override
|
||||
void retry() {
|
||||
_forceRenew = true;
|
||||
super.retry();
|
||||
}
|
||||
|
||||
@override
|
||||
ChatListRepository repository() => ChatListRepository();
|
||||
|
||||
@@ -21,15 +30,39 @@ class ChatListBloc extends LoadableHydratedBloc<ChatListEvent, ChatListState, Ch
|
||||
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
final rooms = await repo.data.getRooms();
|
||||
final renew = _forceRenew;
|
||||
_forceRenew = false;
|
||||
|
||||
Object? capturedError;
|
||||
final rooms = await repo.data.getRooms(
|
||||
renew: renew,
|
||||
onError: (e) => capturedError = e,
|
||||
);
|
||||
add(DataGathered((s) => s.copyWith(rooms: rooms)));
|
||||
_updateAppBadge(rooms);
|
||||
|
||||
if (capturedError != null) throw capturedError!;
|
||||
}
|
||||
|
||||
Future<void> refresh({bool renew = true}) async {
|
||||
final rooms = await repo.data.getRooms(renew: renew);
|
||||
add(DataGathered((s) => s.copyWith(rooms: rooms)));
|
||||
_updateAppBadge(rooms);
|
||||
add(RefetchStarted<ChatListState>());
|
||||
Object? capturedError;
|
||||
try {
|
||||
final rooms = await repo.data.getRooms(
|
||||
renew: renew,
|
||||
onError: (e) => capturedError = e,
|
||||
);
|
||||
add(DataGathered((s) => s.copyWith(rooms: rooms)));
|
||||
_updateAppBadge(rooms);
|
||||
} catch (e) {
|
||||
capturedError = e;
|
||||
}
|
||||
if (capturedError != null) {
|
||||
add(Error(LoadingError(
|
||||
message: capturedError.toString(),
|
||||
allowRetry: true,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createDirectChat(String invite) async {
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import 'dart:async';
|
||||
|
||||
import '../../../../../api/marianumcloud/talk/room/getRoomCache.dart';
|
||||
import '../../../../../api/marianumcloud/talk/room/getRoomResponse.dart';
|
||||
import '../../../../../api/marianumcloud/talk/createRoom/createRoom.dart';
|
||||
import '../../../../../api/marianumcloud/talk/createRoom/createRoomParams.dart';
|
||||
|
||||
class ChatListDataProvider {
|
||||
Future<GetRoomResponse> getRooms({bool renew = false}) {
|
||||
final completer = Completer<GetRoomResponse>();
|
||||
GetRoomCache(
|
||||
Future<GetRoomResponse> getRooms({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) async {
|
||||
GetRoomResponse? latest;
|
||||
Object? capturedError;
|
||||
final cache = GetRoomCache(
|
||||
renew: renew,
|
||||
onUpdate: (data) {
|
||||
if (!completer.isCompleted) completer.complete(data);
|
||||
onUpdate: (data) => latest = data,
|
||||
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 getRooms');
|
||||
}
|
||||
|
||||
Future<void> createDirectRoom(String invite) =>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user