added support for simultan downloads in files and talk, support for background downloads, enhanced loading in files with retry
This commit is contained in:
@@ -14,6 +14,9 @@ abstract class LoadableState<TState> with _$LoadableState<TState> {
|
||||
required int? lastFetch,
|
||||
required void Function()? reFetch,
|
||||
required LoadingError? error,
|
||||
// Transient progress note under the primary loading spinner (e.g. retry
|
||||
// progress). Lives only within one fetch cycle — never persisted.
|
||||
String? statusText,
|
||||
}) = _LoadableState<TState>;
|
||||
|
||||
bool _hasError() => error != null;
|
||||
|
||||
@@ -14,7 +14,9 @@ T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$LoadableState<TState> {
|
||||
|
||||
bool get isLoading; TState? get data; int? get lastFetch; void Function()? get reFetch; LoadingError? get error;
|
||||
bool get isLoading; TState? get data; int? get lastFetch; void Function()? get reFetch; LoadingError? get error;// Transient progress note under the primary loading spinner (e.g. retry
|
||||
// progress). Lives only within one fetch cycle — never persisted.
|
||||
String? get statusText;
|
||||
/// Create a copy of LoadableState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -25,16 +27,16 @@ $LoadableStateCopyWith<TState, LoadableState<TState>> get copyWith => _$Loadable
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadableState<TState>&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.lastFetch, lastFetch) || other.lastFetch == lastFetch)&&(identical(other.reFetch, reFetch) || other.reFetch == reFetch)&&(identical(other.error, error) || other.error == error));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is LoadableState<TState>&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.lastFetch, lastFetch) || other.lastFetch == lastFetch)&&(identical(other.reFetch, reFetch) || other.reFetch == reFetch)&&(identical(other.error, error) || other.error == error)&&(identical(other.statusText, statusText) || other.statusText == statusText));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,isLoading,const DeepCollectionEquality().hash(data),lastFetch,reFetch,error);
|
||||
int get hashCode => Object.hash(runtimeType,isLoading,const DeepCollectionEquality().hash(data),lastFetch,reFetch,error,statusText);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error)';
|
||||
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error, statusText: $statusText)';
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +47,7 @@ abstract mixin class $LoadableStateCopyWith<TState,$Res> {
|
||||
factory $LoadableStateCopyWith(LoadableState<TState> value, $Res Function(LoadableState<TState>) _then) = _$LoadableStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error
|
||||
bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error, String? statusText
|
||||
});
|
||||
|
||||
|
||||
@@ -62,14 +64,15 @@ class _$LoadableStateCopyWithImpl<TState,$Res>
|
||||
|
||||
/// Create a copy of LoadableState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? data = freezed,Object? lastFetch = freezed,Object? reFetch = freezed,Object? error = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? data = freezed,Object? lastFetch = freezed,Object? reFetch = freezed,Object? error = freezed,Object? statusText = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as TState?,lastFetch: freezed == lastFetch ? _self.lastFetch : lastFetch // ignore: cast_nullable_to_non_nullable
|
||||
as int?,reFetch: freezed == reFetch ? _self.reFetch : reFetch // ignore: cast_nullable_to_non_nullable
|
||||
as void Function()?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as LoadingError?,
|
||||
as LoadingError?,statusText: freezed == statusText ? _self.statusText : statusText // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of LoadableState
|
||||
@@ -166,10 +169,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error, String? statusText)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LoadableState() when $default != null:
|
||||
return $default(_that.isLoading,_that.data,_that.lastFetch,_that.reFetch,_that.error);case _:
|
||||
return $default(_that.isLoading,_that.data,_that.lastFetch,_that.reFetch,_that.error,_that.statusText);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -187,10 +190,10 @@ return $default(_that.isLoading,_that.data,_that.lastFetch,_that.reFetch,_that.e
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error, String? statusText) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LoadableState():
|
||||
return $default(_that.isLoading,_that.data,_that.lastFetch,_that.reFetch,_that.error);case _:
|
||||
return $default(_that.isLoading,_that.data,_that.lastFetch,_that.reFetch,_that.error,_that.statusText);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -207,10 +210,10 @@ return $default(_that.isLoading,_that.data,_that.lastFetch,_that.reFetch,_that.e
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error, String? statusText)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _LoadableState() when $default != null:
|
||||
return $default(_that.isLoading,_that.data,_that.lastFetch,_that.reFetch,_that.error);case _:
|
||||
return $default(_that.isLoading,_that.data,_that.lastFetch,_that.reFetch,_that.error,_that.statusText);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -222,7 +225,7 @@ return $default(_that.isLoading,_that.data,_that.lastFetch,_that.reFetch,_that.e
|
||||
|
||||
|
||||
class _LoadableState<TState> extends LoadableState<TState> {
|
||||
const _LoadableState({required this.isLoading, required this.data, required this.lastFetch, required this.reFetch, required this.error}): super._();
|
||||
const _LoadableState({required this.isLoading, required this.data, required this.lastFetch, required this.reFetch, required this.error, this.statusText}): super._();
|
||||
|
||||
|
||||
@override final bool isLoading;
|
||||
@@ -230,6 +233,9 @@ class _LoadableState<TState> extends LoadableState<TState> {
|
||||
@override final int? lastFetch;
|
||||
@override final void Function()? reFetch;
|
||||
@override final LoadingError? error;
|
||||
// Transient progress note under the primary loading spinner (e.g. retry
|
||||
// progress). Lives only within one fetch cycle — never persisted.
|
||||
@override final String? statusText;
|
||||
|
||||
/// Create a copy of LoadableState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -241,16 +247,16 @@ _$LoadableStateCopyWith<TState, _LoadableState<TState>> get copyWith => __$Loada
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadableState<TState>&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.lastFetch, lastFetch) || other.lastFetch == lastFetch)&&(identical(other.reFetch, reFetch) || other.reFetch == reFetch)&&(identical(other.error, error) || other.error == error));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LoadableState<TState>&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.lastFetch, lastFetch) || other.lastFetch == lastFetch)&&(identical(other.reFetch, reFetch) || other.reFetch == reFetch)&&(identical(other.error, error) || other.error == error)&&(identical(other.statusText, statusText) || other.statusText == statusText));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,isLoading,const DeepCollectionEquality().hash(data),lastFetch,reFetch,error);
|
||||
int get hashCode => Object.hash(runtimeType,isLoading,const DeepCollectionEquality().hash(data),lastFetch,reFetch,error,statusText);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error)';
|
||||
return 'LoadableState<$TState>(isLoading: $isLoading, data: $data, lastFetch: $lastFetch, reFetch: $reFetch, error: $error, statusText: $statusText)';
|
||||
}
|
||||
|
||||
|
||||
@@ -261,7 +267,7 @@ abstract mixin class _$LoadableStateCopyWith<TState,$Res> implements $LoadableSt
|
||||
factory _$LoadableStateCopyWith(_LoadableState<TState> value, $Res Function(_LoadableState<TState>) _then) = __$LoadableStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error
|
||||
bool isLoading, TState? data, int? lastFetch, void Function()? reFetch, LoadingError? error, String? statusText
|
||||
});
|
||||
|
||||
|
||||
@@ -278,14 +284,15 @@ class __$LoadableStateCopyWithImpl<TState,$Res>
|
||||
|
||||
/// Create a copy of LoadableState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? data = freezed,Object? lastFetch = freezed,Object? reFetch = freezed,Object? error = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? data = freezed,Object? lastFetch = freezed,Object? reFetch = freezed,Object? error = freezed,Object? statusText = freezed,}) {
|
||||
return _then(_LoadableState<TState>(
|
||||
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
|
||||
as bool,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as TState?,lastFetch: freezed == lastFetch ? _self.lastFetch : lastFetch // ignore: cast_nullable_to_non_nullable
|
||||
as int?,reFetch: freezed == reFetch ? _self.reFetch : reFetch // ignore: cast_nullable_to_non_nullable
|
||||
as void Function()?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as LoadingError?,
|
||||
as LoadingError?,statusText: freezed == statusText ? _self.statusText : statusText // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -111,7 +111,10 @@ class LoadableStateConsumer<
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
LoadableStatePrimaryLoading(visible: showPrimaryLoading),
|
||||
LoadableStatePrimaryLoading(
|
||||
visible: showPrimaryLoading,
|
||||
statusText: loadableState.statusText,
|
||||
),
|
||||
LoadableStateBackgroundLoading(
|
||||
visible: showBackgroundLoading,
|
||||
),
|
||||
|
||||
+96
-8
@@ -1,17 +1,105 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../../widget/app_progress_indicator.dart';
|
||||
import 'loadable_state_consumer.dart';
|
||||
|
||||
class LoadableStatePrimaryLoading extends StatelessWidget {
|
||||
/// Full-screen spinner for the initial load. A minutes-long spinner without
|
||||
/// feedback looks stuck (the Nextcloud root listing can take that long), so
|
||||
/// after [slowHintAfter] a generic slow-server note fades in below it. An
|
||||
/// explicit [statusText] (e.g. retry progress from the bloc) always takes
|
||||
/// precedence over the timer-based hint.
|
||||
class LoadableStatePrimaryLoading extends StatefulWidget {
|
||||
final bool visible;
|
||||
const LoadableStatePrimaryLoading({required this.visible, super.key});
|
||||
final String? statusText;
|
||||
final Duration slowHintAfter;
|
||||
|
||||
const LoadableStatePrimaryLoading({
|
||||
required this.visible,
|
||||
this.statusText,
|
||||
this.slowHintAfter = const Duration(seconds: 8),
|
||||
super.key,
|
||||
});
|
||||
|
||||
static const String slowHintText = 'Der Server antwortet verzögert …';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AnimatedOpacity(
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: const Center(child: AppProgressIndicator.large()),
|
||||
);
|
||||
State<LoadableStatePrimaryLoading> createState() =>
|
||||
_LoadableStatePrimaryLoadingState();
|
||||
}
|
||||
|
||||
class _LoadableStatePrimaryLoadingState
|
||||
extends State<LoadableStatePrimaryLoading> {
|
||||
Timer? _slowHintTimer;
|
||||
bool _showSlowHint = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_restartSlowHintTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant LoadableStatePrimaryLoading oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.visible != oldWidget.visible) _restartSlowHintTimer();
|
||||
}
|
||||
|
||||
void _restartSlowHintTimer() {
|
||||
_slowHintTimer?.cancel();
|
||||
_showSlowHint = false;
|
||||
if (!widget.visible) return;
|
||||
_slowHintTimer = Timer(widget.slowHintAfter, () {
|
||||
if (mounted) setState(() => _showSlowHint = true);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_slowHintTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final status =
|
||||
widget.statusText ??
|
||||
(_showSlowHint ? LoadableStatePrimaryLoading.slowHintText : null);
|
||||
|
||||
return AnimatedOpacity(
|
||||
opacity: widget.visible ? 1.0 : 0.0,
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
curve: Curves.easeInOut,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const AppProgressIndicator.large(),
|
||||
AnimatedSwitcher(
|
||||
duration: LoadableStateConsumer.animationDuration,
|
||||
child: status == null
|
||||
? const SizedBox.shrink()
|
||||
: Padding(
|
||||
key: ValueKey(status),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 24,
|
||||
right: 24,
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -35,10 +35,24 @@ abstract class LoadableHydratedBloc<
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: retry,
|
||||
error: state.error,
|
||||
statusText: state.statusText,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
on<LoadingStatus<TState>>(
|
||||
(event, emit) => emit(
|
||||
LoadableState(
|
||||
isLoading: state.isLoading,
|
||||
data: innerState,
|
||||
lastFetch: state.lastFetch,
|
||||
reFetch: state.reFetch,
|
||||
error: state.error,
|
||||
statusText: event.statusText,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
on<DataGathered<TState>>(
|
||||
(event, emit) => emit(
|
||||
LoadableState(
|
||||
|
||||
+8
@@ -19,4 +19,12 @@ class Error<TState> extends LoadableHydratedBlocEvent<TState> {
|
||||
|
||||
class RefetchStarted<TState> extends LoadableHydratedBlocEvent<TState> {}
|
||||
|
||||
/// Sets the state's `statusText` — a transient progress note under the
|
||||
/// primary loading spinner (e.g. "Erneuter Versuch (2 von 3) …"). Cleared
|
||||
/// automatically by [DataGathered], [Error], [RefetchStarted] and [Reset].
|
||||
class LoadingStatus<TState> extends LoadableHydratedBlocEvent<TState> {
|
||||
final String? statusText;
|
||||
LoadingStatus(this.statusText);
|
||||
}
|
||||
|
||||
class Reset<TState> extends LoadableHydratedBlocEvent<TState> {}
|
||||
|
||||
@@ -96,6 +96,10 @@ class FilesBloc
|
||||
);
|
||||
add(Emit((s) => s.copyWith(listing: cached)));
|
||||
},
|
||||
onRetry: (next, max) {
|
||||
if (isStale()) return;
|
||||
add(LoadingStatus('Erneuter Versuch ($next von $max) …'));
|
||||
},
|
||||
onError: (e) => capturedError = e,
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@@ -21,6 +21,7 @@ class FilesDataProvider {
|
||||
void Function(ListFilesResponse)? onCacheData,
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
void Function(int nextAttempt, int maxAttempts)? onRetry,
|
||||
}) => resolveFromCache<ListFilesResponse>(
|
||||
(onUpdate, onError) => ListFilesCache(
|
||||
path: path,
|
||||
@@ -28,6 +29,7 @@ class FilesDataProvider {
|
||||
onCacheData: onCacheData,
|
||||
onError: onError,
|
||||
renew: renew,
|
||||
onRetry: onRetry,
|
||||
),
|
||||
onError: onError,
|
||||
operationName: 'listFiles',
|
||||
|
||||
Reference in New Issue
Block a user