Show proper progress during initial sync

This commit is contained in:
Ming Ming 2022-12-04 00:26:44 +08:00
parent 94a34f3124
commit 11279b4119
11 changed files with 346 additions and 47 deletions

View file

@ -0,0 +1,57 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:logging/logging.dart';
import 'package:to_string/to_string.dart';
part 'progress.g.dart';
abstract class ProgressBlocEvent {
const ProgressBlocEvent();
}
@toString
class ProgressBlocUpdate extends ProgressBlocEvent {
const ProgressBlocUpdate(this.progress, [this.text]);
@override
String toString() => _$toString();
final double progress;
final String? text;
}
@toString
class ProgressBlocState with EquatableMixin {
const ProgressBlocState(this.progress, this.text);
@override
String toString() => _$toString();
@override
List<Object?> get props => [progress, text];
final double progress;
final String? text;
}
/// A generic bloc to bubble progress update for some events
class ProgressBloc extends Bloc<ProgressBlocEvent, ProgressBlocState> {
ProgressBloc() : super(const ProgressBlocState(0, null)) {
on<ProgressBlocEvent>(_onEvent);
}
Future<void> _onEvent(
ProgressBlocEvent ev, Emitter<ProgressBlocState> emit) async {
_log.info("[_onEvent] $ev");
if (ev is ProgressBlocUpdate) {
await _onEventUpdate(ev, emit);
}
}
Future<void> _onEventUpdate(
ProgressBlocUpdate ev, Emitter<ProgressBlocState> emit) async {
emit(ProgressBlocState(ev.progress, ev.text));
}
static final _log = Logger("bloc.progress.ProgressBloc");
}

View file

@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'progress.dart';
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$ProgressBlocUpdateToString on ProgressBlocUpdate {
String _$toString() {
return "ProgressBlocUpdate {progress: $progress, text: $text}";
}
}
extension _$ProgressBlocStateToString on ProgressBlocState {
String _$toString() {
return "ProgressBlocState {progress: $progress, text: $text}";
}
}

View file

@ -1,10 +1,12 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
import 'package:nc_photos/bloc/progress.dart';
import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart';
@ -15,6 +17,7 @@ import 'package:nc_photos/event/native_event.dart';
import 'package:nc_photos/exception.dart';
import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:nc_photos/pref.dart';
import 'package:nc_photos/progress_util.dart';
import 'package:nc_photos/throttler.dart';
import 'package:nc_photos/use_case/ls.dart';
import 'package:nc_photos/use_case/scan_dir.dart';
@ -25,22 +28,31 @@ abstract class ScanAccountDirBlocEvent {
const ScanAccountDirBlocEvent();
}
class ScanAccountDirBlocQueryBase extends ScanAccountDirBlocEvent {
const ScanAccountDirBlocQueryBase();
abstract class ScanAccountDirBlocQueryBase extends ScanAccountDirBlocEvent {
const ScanAccountDirBlocQueryBase({
this.progressBloc,
});
@override
toString() {
return "$runtimeType {"
"}";
}
/// Get notified about the query progress
final ProgressBloc? progressBloc;
}
class ScanAccountDirBlocQuery extends ScanAccountDirBlocQueryBase {
const ScanAccountDirBlocQuery();
const ScanAccountDirBlocQuery({
super.progressBloc,
});
}
class ScanAccountDirBlocRefresh extends ScanAccountDirBlocQueryBase {
const ScanAccountDirBlocRefresh();
const ScanAccountDirBlocRefresh({
super.progressBloc,
});
}
/// An external event has happened and may affect the state of this bloc
@ -72,7 +84,12 @@ class ScanAccountDirBlocInit extends ScanAccountDirBlocState {
}
class ScanAccountDirBlocLoading extends ScanAccountDirBlocState {
const ScanAccountDirBlocLoading(List<FileDescriptor> files) : super(files);
const ScanAccountDirBlocLoading(
List<FileDescriptor> files, {
this.isInitialLoad = false,
}) : super(files);
final bool isInitialLoad;
}
class ScanAccountDirBlocSuccess extends ScanAccountDirBlocState {
@ -207,10 +224,22 @@ class ScanAccountDirBloc
emit(ScanAccountDirBlocLoading(cacheFiles));
}
if (!hasContent && cacheFiles.isEmpty) {
emit(const ScanAccountDirBlocLoading([], isInitialLoad: true));
}
stopwatch.reset();
final bool hasUpdate;
try {
hasUpdate = await _syncOnline(ev);
hasUpdate = await _syncOnline(
ev,
onProgressUpdate: (value) {
if (ev.progressBloc?.isClosed == false) {
ev.progressBloc!
.add(ProgressBlocUpdate(value.progress, value.text));
}
},
);
} catch (e, stackTrace) {
_log.shout("[_onEventQuery] Exception while request", e, stackTrace);
emit(ScanAccountDirBlocFailure(cacheFiles, e));
@ -230,7 +259,10 @@ class ScanAccountDirBloc
}
}
Future<bool> _syncOnline(ScanAccountDirBlocQueryBase ev) async {
Future<bool> _syncOnline(
ScanAccountDirBlocQueryBase ev, {
ValueChanged<Progress>? onProgressUpdate,
}) async {
final settings = AccountPref.of(account);
final shareDir =
File(path: file_util.unstripPath(account, settings.getShareFolderOr()));
@ -238,11 +270,20 @@ class ScanAccountDirBloc
bool hasUpdate = false;
_c.touchManager.clearTouchCache();
final progress = IntProgress(account.roots.length);
for (final r in account.roots) {
final dirPath = file_util.unstripPath(account, r);
hasUpdate |= await SyncDir(_c)(account, dirPath);
hasUpdate |= await SyncDir(_c)(
account,
dirPath,
onProgressUpdate: (value) {
final merged = progress.progress + progress.step * value.progress;
onProgressUpdate?.call(Progress(merged, value.text));
},
);
isShareDirIncluded |=
file_util.isOrUnderDir(shareDir, File(path: dirPath));
progress.next();
}
if (!isShareDirIncluded) {

View file

@ -1489,6 +1489,11 @@
"@imageSaveOptionDialogServerButtonLabel": {
"description": "Save the image on your Nextcloud server"
},
"initialSyncMessage": "Syncing with your server for the first time",
"@initialSyncMessage": {
"description": "After adding a new account, the app need to sync with the server before showing anything. This message will be shown on screen instead with a proper progress bar and the folder being synced."
},
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
"@errorUnauthenticated": {
"description": "Error message when server responds with HTTP401"

View file

@ -179,6 +179,7 @@
"imageSaveOptionDialogContent",
"imageSaveOptionDialogDeviceButtonLabel",
"imageSaveOptionDialogServerButtonLabel",
"initialSyncMessage",
"errorAlbumDowngrade"
],
@ -374,6 +375,7 @@
"imageSaveOptionDialogContent",
"imageSaveOptionDialogDeviceButtonLabel",
"imageSaveOptionDialogServerButtonLabel",
"initialSyncMessage",
"errorAlbumDowngrade"
],
@ -459,7 +461,8 @@
"imageSaveOptionDialogTitle",
"imageSaveOptionDialogContent",
"imageSaveOptionDialogDeviceButtonLabel",
"imageSaveOptionDialogServerButtonLabel"
"imageSaveOptionDialogServerButtonLabel",
"initialSyncMessage"
],
"es": [
@ -470,7 +473,8 @@
"settingsSeedColorDescription",
"settingsSeedColorPickerTitle",
"rootPickerSkipConfirmationDialogContent2",
"slideshowSetupDialogReverseTitle"
"slideshowSetupDialogReverseTitle",
"initialSyncMessage"
],
"fi": [
@ -478,7 +482,8 @@
"signInHeaderText2",
"settingsSeedColorTitle",
"settingsSeedColorDescription",
"settingsSeedColorPickerTitle"
"settingsSeedColorPickerTitle",
"initialSyncMessage"
],
"fr": [
@ -583,7 +588,8 @@
"imageSaveOptionDialogTitle",
"imageSaveOptionDialogContent",
"imageSaveOptionDialogDeviceButtonLabel",
"imageSaveOptionDialogServerButtonLabel"
"imageSaveOptionDialogServerButtonLabel",
"initialSyncMessage"
],
"pl": [
@ -705,7 +711,8 @@
"imageSaveOptionDialogTitle",
"imageSaveOptionDialogContent",
"imageSaveOptionDialogDeviceButtonLabel",
"imageSaveOptionDialogServerButtonLabel"
"imageSaveOptionDialogServerButtonLabel",
"initialSyncMessage"
],
"pt": [
@ -806,7 +813,8 @@
"imageSaveOptionDialogTitle",
"imageSaveOptionDialogContent",
"imageSaveOptionDialogDeviceButtonLabel",
"imageSaveOptionDialogServerButtonLabel"
"imageSaveOptionDialogServerButtonLabel",
"initialSyncMessage"
],
"ru": [
@ -907,7 +915,8 @@
"imageSaveOptionDialogTitle",
"imageSaveOptionDialogContent",
"imageSaveOptionDialogDeviceButtonLabel",
"imageSaveOptionDialogServerButtonLabel"
"imageSaveOptionDialogServerButtonLabel",
"initialSyncMessage"
],
"zh": [
@ -1008,7 +1017,8 @@
"imageSaveOptionDialogTitle",
"imageSaveOptionDialogContent",
"imageSaveOptionDialogDeviceButtonLabel",
"imageSaveOptionDialogServerButtonLabel"
"imageSaveOptionDialogServerButtonLabel",
"initialSyncMessage"
],
"zh_Hant": [
@ -1109,6 +1119,7 @@
"imageSaveOptionDialogTitle",
"imageSaveOptionDialogContent",
"imageSaveOptionDialogDeviceButtonLabel",
"imageSaveOptionDialogServerButtonLabel"
"imageSaveOptionDialogServerButtonLabel",
"initialSyncMessage"
]
}

View file

@ -0,0 +1,34 @@
import 'dart:math' as math;
import 'package:to_string/to_string.dart';
part 'progress_util.g.dart';
@toString
class IntProgress {
IntProgress(this.max) : step = max <= 0 ? 1 : 1 / max;
void next() {
_current = math.min(_current + 1, max);
}
double get progress => max <= 0 ? 1 : _current / max;
@override
String toString() => _$toString();
final int max;
final double step;
var _current = 0;
}
@ToString(ignoreNull: true)
class Progress {
const Progress(this.progress, [this.text]);
@override
String toString() => _$toString();
final double progress;
final String? text;
}

View file

@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'progress_util.dart';
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$IntProgressToString on IntProgress {
String _$toString() {
return "IntProgress {max: $max, step: $step, _current: $_current}";
}
}
extension _$ProgressToString on Progress {
String _$toString() {
return "Progress {progress: $progress, ${text == null ? "" : "text: $text, "}}";
}
}

View file

@ -1,3 +1,4 @@
import 'package:flutter/rendering.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/debug_util.dart';
@ -7,6 +8,7 @@ import 'package:nc_photos/entity/file/data_source.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/sqlite_table_extension.dart' as sql;
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/progress_util.dart';
import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util;
import 'package:nc_photos/use_case/ls_single_file.dart';
import 'package:tuple/tuple.dart';
@ -26,12 +28,18 @@ class SyncDir {
Account account,
String dirPath, {
bool isRecursive = true,
ValueChanged<Progress>? onProgressUpdate,
}) async {
final dirCache = await _queryAllDirEtags(account, dirPath);
final remoteRoot =
await LsSingleFile(_c.withRemoteFileRepo())(account, dirPath);
return await _syncDir(account, remoteRoot, dirCache,
isRecursive: isRecursive);
return await _syncDir(
account,
remoteRoot,
dirCache,
isRecursive: isRecursive,
onProgressUpdate: onProgressUpdate,
);
}
Future<bool> _syncDir(
@ -39,6 +47,7 @@ class SyncDir {
File remoteDir,
Map<int, String> dirCache, {
required bool isRecursive,
ValueChanged<Progress>? onProgressUpdate,
}) async {
final status = await _checkContentUpdated(account, remoteDir, dirCache);
if (!status.item1) {
@ -52,16 +61,32 @@ class SyncDir {
if (!isRecursive) {
return true;
}
for (final d in children.where((c) =>
c.isCollection == true &&
!remoteDir.compareServerIdentity(c) &&
!c.path.endsWith(remote_storage_util.getRemoteStorageDir(account)))) {
final subDirs = children
.where((f) =>
f.isCollection == true &&
!remoteDir.compareServerIdentity(f) &&
!f.path.endsWith(remote_storage_util.getRemoteStorageDir(account)))
.toList();
final progress = IntProgress(subDirs.length);
for (final d in subDirs) {
onProgressUpdate
?.call(Progress(progress.progress, d.strippedPathWithEmpty));
try {
await _syncDir(account, d, dirCache, isRecursive: isRecursive);
await _syncDir(
account,
d,
dirCache,
isRecursive: isRecursive,
onProgressUpdate: (value) {
final merged = progress.progress + progress.step * value.progress;
onProgressUpdate?.call(Progress(merged, value.text));
},
);
} catch (e, stackTrace) {
_log.severe("[_syncDir] Failed while _syncDir: ${logFilename(d.path)}",
e, stackTrace);
}
progress.next();
}
return true;
}

View file

@ -14,6 +14,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util;
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/bloc_util.dart' as bloc_util;
import 'package:nc_photos/bloc/progress.dart';
import 'package:nc_photos/bloc/scan_account_dir.dart';
import 'package:nc_photos/compute_queue.dart';
import 'package:nc_photos/di_container.dart';
@ -95,10 +96,7 @@ class _HomePhotosState extends State<HomePhotos>
return BlocListener<ScanAccountDirBloc, ScanAccountDirBlocState>(
bloc: _bloc,
listener: (context, state) => _onStateChange(context, state),
child: BlocBuilder<ScanAccountDirBloc, ScanAccountDirBlocState>(
bloc: _bloc,
builder: (context, state) => _buildContent(context, state),
),
child: _buildContent(context),
);
}
@ -148,7 +146,7 @@ class _HomePhotosState extends State<HomePhotos>
}
}
Widget _buildContent(BuildContext context, ScanAccountDirBlocState state) {
Widget _buildContent(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final scrollExtent = _getScrollViewExtent(context, constraints);
return Stack(
@ -183,7 +181,15 @@ class _HomePhotosState extends State<HomePhotos>
.isEnableMemoryAlbumOr(true) &&
_smartAlbums.isNotEmpty)
_buildSmartAlbumList(context),
buildItemStreamList(
BlocBuilder<ScanAccountDirBloc, ScanAccountDirBlocState>(
bloc: _bloc,
builder: (context, state) {
if (_isInitialSync(state)) {
return _InitialLoadingProgress(
progressBloc: _queryProgressBloc,
);
} else {
return buildItemStreamList(
maxCrossAxisExtent: _thumbSize.toDouble(),
onMaxExtentChanged: (value) {
setState(() {
@ -191,13 +197,16 @@ class _HomePhotosState extends State<HomePhotos>
});
},
isEnableVisibilityCallback: true,
);
}
},
),
SliverToBoxAdapter(
child: SizedBox(
height: _calcBottomAppBarExtent(context),
),
),
].whereType<Widget>().toList(),
].whereNotNull().toList(),
),
),
),
@ -279,15 +288,24 @@ class _HomePhotosState extends State<HomePhotos>
}
Widget _buildNormalAppBar(BuildContext context) {
return BlocBuilder(
return BlocBuilder<ScanAccountDirBloc, ScanAccountDirBlocState>(
bloc: _bloc,
buildWhen: (previous, current) =>
previous is ScanAccountDirBlocLoading !=
current is ScanAccountDirBlocLoading,
buildWhen: (previous, current) {
if (previous is ScanAccountDirBlocLoading &&
current is ScanAccountDirBlocLoading) {
// both loading, check if initial flag changed
return previous.isInitialLoad != current.isInitialLoad;
} else {
// check if any one is loading == state changed from/to loading
return previous is ScanAccountDirBlocLoading ||
current is ScanAccountDirBlocLoading;
}
},
builder: (context, state) {
return HomeSliverAppBar(
account: widget.account,
isShowProgressIcon: (state is ScanAccountDirBlocLoading ||
isShowProgressIcon: !_isInitialSync(state) &&
(state is ScanAccountDirBlocLoading ||
_buildItemQueue.isProcessing) &&
!_isRefreshIndicatorActive,
actions: [
@ -620,7 +638,9 @@ class _HomePhotosState extends State<HomePhotos>
}
void _reqQuery() {
_bloc.add(const ScanAccountDirBlocQuery());
_bloc.add(ScanAccountDirBlocQuery(
progressBloc: _queryProgressBloc,
));
}
void _reqRefresh() {
@ -691,6 +711,11 @@ class _HomePhotosState extends State<HomePhotos>
}
}
bool _isInitialSync(ScanAccountDirBlocState state) =>
state is ScanAccountDirBlocLoading &&
state.files.isEmpty &&
state.isInitialLoad;
double _calcAppBarExtent(BuildContext context) =>
MediaQuery.of(context).padding.top + kToolbarHeight;
@ -728,6 +753,7 @@ class _HomePhotosState extends State<HomePhotos>
}
late final _bloc = ScanAccountDirBloc.of(widget.account);
late final _queryProgressBloc = ProgressBloc();
var _backingFiles = <FileDescriptor>[];
var _smartAlbums = <Album>[];
@ -1087,3 +1113,52 @@ class _VisibleItem implements Comparable<_VisibleItem> {
final int index;
final SelectableItem item;
}
class _InitialLoadingProgress extends StatelessWidget {
const _InitialLoadingProgress({
required this.progressBloc,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<ProgressBloc, ProgressBlocState>(
bloc: progressBloc,
buildWhen: (previous, current) => previous != current,
builder: (context, state) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 56, 16, 0),
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: Theme.of(context).widthLimitedContentMaxWidth,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
L10n.global().initialSyncMessage,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: state.progress == 0 ? null : state.progress,
),
const SizedBox(height: 8),
Text(
state.text ?? "",
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
),
);
},
);
}
final ProgressBloc progressBloc;
}

View file

@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "41.0.0"
version: "47.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.0"
version: "4.7.0"
analyzer_plugin:
dependency: transitive
description:
@ -121,7 +121,7 @@ packages:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "2.3.0"
version: "2.3.1"
build_config:
dependency: transitive
description:
@ -1154,7 +1154,7 @@ packages:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.2"
version: "1.2.6"
source_map_stack_trace:
dependency: transitive
description:
@ -1281,6 +1281,15 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
to_string:
dependency: "direct dev"
description:
path: "."
ref: "2432bd99c5c8c90b11d7e691992514e312cee0fe"
resolved-ref: "2432bd99c5c8c90b11d7e691992514e312cee0fe"
url: "https://gitlab.com/nkming2/dart-to-string"
source: git
version: "1.0.0"
tuple:
dependency: "direct main"
description:

View file

@ -126,6 +126,10 @@ dev_dependencies:
sdk: flutter
# integration_test:
# sdk: flutter
to_string:
git:
url: https://gitlab.com/nkming2/dart-to-string
ref: 2432bd99c5c8c90b11d7e691992514e312cee0fe
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec