Add remote sync to the new HomePhotos

This commit is contained in:
Ming Ming 2024-01-13 18:57:44 +08:00
parent db8f93b052
commit 1a09dc37a0
6 changed files with 326 additions and 143 deletions

View file

@ -10,9 +10,11 @@ import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/progress_util.dart';
import 'package:nc_photos/rx_extension.dart'; import 'package:nc_photos/rx_extension.dart';
import 'package:nc_photos/use_case/file/list_file.dart'; import 'package:nc_photos/use_case/file/list_file.dart';
import 'package:nc_photos/use_case/remove.dart'; import 'package:nc_photos/use_case/remove.dart';
import 'package:nc_photos/use_case/sync_dir.dart';
import 'package:nc_photos/use_case/update_property.dart'; import 'package:nc_photos/use_case/update_property.dart';
import 'package:np_codegen/np_codegen.dart'; import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/lazy.dart'; import 'package:np_common/lazy.dart';
@ -54,27 +56,52 @@ class FilesController {
ValueStream<FilesStreamEvent> get stream { ValueStream<FilesStreamEvent> get stream {
if (!_isDataStreamInited) { if (!_isDataStreamInited) {
_isDataStreamInited = true; _isDataStreamInited = true;
unawaited(_load()); _load();
} }
return _dataStreamController.stream; return _dataStreamController.stream;
} }
Future<void> reload() async { Future<void> syncRemote({
var results = <FileDescriptor>[]; void Function(Progress progress)? onProgressUpdate,
final completer = Completer(); }) async {
ListFile(_c)( if (_isSyncing) {
account, _log.fine("[syncRemote] Skipped as another sync running");
file_util.unstripPath(account, accountPrefController.shareFolder.value), return;
).listen( }
(ev) { _isSyncing = true;
results = ev; try {
}, final shareDir = File(
onError: _dataStreamController.addError, path: file_util.unstripPath(
onDone: () => completer.complete(), account, accountPrefController.shareFolder.value),
); );
await completer.future; var isShareDirIncluded = false;
_dataStreamController
.add(_convertListResultsToEvent(results, hasNext: false)); _c.touchManager.clearTouchCache();
final progress = IntProgress(account.roots.length);
for (final r in account.roots) {
final dirPath = file_util.unstripPath(account, r);
await SyncDir(_c)(
account,
dirPath,
onProgressUpdate: (value) {
final merged = progress.progress + progress.step * value.progress;
onProgressUpdate?.call(Progress(merged, value.text));
},
);
isShareDirIncluded |=
file_util.isOrUnderDirPath(shareDir.path, dirPath);
progress.next();
}
if (!isShareDirIncluded) {
_log.info("[syncRemote] Explicitly scanning share folder");
await SyncDir(_c)(account, shareDir.path, isRecursive: false);
}
// load the synced content to stream
unawaited(_reload());
} finally {
_isSyncing = false;
}
} }
/// Update files property and return number of files updated /// Update files property and return number of files updated
@ -227,6 +254,24 @@ class FilesController {
_dataStreamController.add(lastData.copyWith(hasNext: false)); _dataStreamController.add(lastData.copyWith(hasNext: false));
} }
Future<void> _reload() async {
var results = <FileDescriptor>[];
final completer = Completer();
ListFile(_c)(
account,
file_util.unstripPath(account, accountPrefController.shareFolder.value),
).listen(
(ev) {
results = ev;
},
onError: _dataStreamController.addError,
onDone: () => completer.complete(),
);
await completer.future;
_dataStreamController
.add(_convertListResultsToEvent(results, hasNext: false));
}
_FilesStreamEvent _convertListResultsToEvent( _FilesStreamEvent _convertListResultsToEvent(
List<FileDescriptor> results, { List<FileDescriptor> results, {
required bool hasNext, required bool hasNext,
@ -252,6 +297,7 @@ class FilesController {
); );
final _mutex = Mutex(); final _mutex = Mutex();
var _isSyncing = false;
} }
@toString @toString

View file

@ -5,11 +5,11 @@ class _AppBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _BlocBuilder( return _BlocSelector<bool>(
buildWhen: (previous, current) => previous.isLoading != current.isLoading, selector: (state) => state.isLoading || state.syncProgress != null,
builder: (context, state) => HomeSliverAppBar( builder: (context, isProcessing) => HomeSliverAppBar(
account: context.bloc.account, account: context.bloc.account,
isShowProgressIcon: state.isLoading, isShowProgressIcon: isProcessing,
), ),
); );
} }

View file

@ -29,6 +29,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
on<_RemoveVisibleItem>(_onRemoveVisibleItem); on<_RemoveVisibleItem>(_onRemoveVisibleItem);
on<_SetContentListMaxExtent>(_onSetContentListMaxExtent); on<_SetContentListMaxExtent>(_onSetContentListMaxExtent);
on<_SetSyncProgress>(_onSetSyncProgress);
on<_StartScaling>(_onStartScaling); on<_StartScaling>(_onStartScaling);
on<_EndScaling>(_onEndScaling); on<_EndScaling>(_onEndScaling);
@ -60,7 +61,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
currentState = currentState as _State; currentState = currentState as _State;
nextState = nextState as _State; nextState = nextState as _State;
return currentState.scale == nextState.scale && return currentState.scale == nextState.scale &&
currentState.visibleItems == nextState.visibleItems; currentState.visibleItems == nextState.visibleItems &&
currentState.syncProgress == nextState.syncProgress;
}; };
@override @override
@ -80,10 +82,16 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
_log.info(ev); _log.info(ev);
return emit.forEach<FilesStreamEvent>( return emit.forEach<FilesStreamEvent>(
controller.stream, controller.stream,
onData: (data) => state.copyWith( onData: (data) {
files: data.data, if (_isInitialLoad && !data.hasNext) {
isLoading: data.hasNext || _itemTransformerQueue.isProcessing, _isInitialLoad = false;
), _syncRemote();
}
return state.copyWith(
files: data.data,
isLoading: data.hasNext || _itemTransformerQueue.isProcessing,
);
},
onError: (e, stackTrace) { onError: (e, stackTrace) {
_log.severe("[_onLoad] Uncaught exception", e, stackTrace); _log.severe("[_onLoad] Uncaught exception", e, stackTrace);
return state.copyWith( return state.copyWith(
@ -96,7 +104,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
void _onReload(_Reload ev, Emitter<_State> emit) { void _onReload(_Reload ev, Emitter<_State> emit) {
_log.info(ev); _log.info(ev);
unawaited(controller.reload()); _syncRemote();
} }
void _onTransformItems(_TransformItems ev, Emitter<_State> emit) { void _onTransformItems(_TransformItems ev, Emitter<_State> emit) {
@ -204,6 +212,11 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
emit(state.copyWith(contentListMaxExtent: ev.value)); emit(state.copyWith(contentListMaxExtent: ev.value));
} }
void _onSetSyncProgress(_SetSyncProgress ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(syncProgress: ev.progress));
}
void _onStartScaling(_StartScaling ev, Emitter<_State> emit) { void _onStartScaling(_StartScaling ev, Emitter<_State> emit) {
_log.info(ev); _log.info(ev);
} }
@ -264,6 +277,22 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
); );
} }
void _syncRemote() {
final stopwatch = Stopwatch()..start();
controller.syncRemote(
onProgressUpdate: (progress) {
if (!isClosed) {
add(_SetSyncProgress(progress));
}
},
).whenComplete(() {
if (!isClosed) {
add(const _SetSyncProgress(null));
}
_log.info("[_syncRemote] Elapsed time: ${stopwatch.elapsedMilliseconds}ms");
});
}
void _clearSelection(Emitter<_State> emit) { void _clearSelection(Emitter<_State> emit) {
emit(state.copyWith(selectedItems: const {})); emit(state.copyWith(selectedItems: const {}));
} }
@ -279,6 +308,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
ComputeQueue<_ItemTransformerArgument, _ItemTransformerResult>(); ComputeQueue<_ItemTransformerArgument, _ItemTransformerResult>();
final _subscriptions = <StreamSubscription>[]; final _subscriptions = <StreamSubscription>[];
var _isHandlingError = false; var _isHandlingError = false;
var _isInitialLoad = true;
} }
_ItemTransformerResult _buildItem(_ItemTransformerArgument arg) { _ItemTransformerResult _buildItem(_ItemTransformerArgument arg) {

View file

@ -12,6 +12,7 @@ class _State {
required this.isEnableMemoryCollection, required this.isEnableMemoryCollection,
required this.memoryCollections, required this.memoryCollections,
this.contentListMaxExtent, this.contentListMaxExtent,
this.syncProgress,
required this.zoom, required this.zoom,
this.scale, this.scale,
this.error, this.error,
@ -45,6 +46,7 @@ class _State {
final List<Collection> memoryCollections; final List<Collection> memoryCollections;
final double? contentListMaxExtent; final double? contentListMaxExtent;
final Progress? syncProgress;
final int zoom; final int zoom;
final double? scale; final double? scale;
@ -170,6 +172,16 @@ class _SetContentListMaxExtent implements _Event {
final double? value; final double? value;
} }
@toString
class _SetSyncProgress implements _Event {
const _SetSyncProgress(this.progress);
@override
String toString() => _$toString();
final Progress? progress;
}
@toString @toString
class _StartScaling implements _Event { class _StartScaling implements _Event {
const _StartScaling(); const _StartScaling();

View file

@ -29,6 +29,7 @@ import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/flutter_util.dart' as flutter_util; import 'package:nc_photos/flutter_util.dart' as flutter_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/language_util.dart' as language_util; import 'package:nc_photos/language_util.dart' as language_util;
import 'package:nc_photos/progress_util.dart';
import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart'; import 'package:nc_photos/theme.dart';
import 'package:nc_photos/theme/dimension.dart'; import 'package:nc_photos/theme/dimension.dart';
@ -139,123 +140,200 @@ class _WrappedHomePhotosState extends State<_WrappedHomePhotos> {
}, },
), ),
], ],
child: FingerListener( child: _BlocSelector<bool>(
onFingerChanged: (finger) { selector: (state) =>
setState(() { state.files.isEmpty &&
_finger = finger; state.syncProgress != null,
}); builder: (context, isInitialSyncing) {
if (isInitialSyncing) {
return const _InitialSyncBody();
} else {
return const _Body();
}
}, },
child: GestureDetector( ),
onScaleStart: (_) { ),
_bloc.add(const _StartScaling()); );
}, }
onScaleUpdate: (details) {
_bloc.add(_SetScale(details.scale)); late final _bloc = context.bloc;
},
onScaleEnd: (_) { final _key = GlobalKey();
_bloc.add(const _EndScaling()); bool? _isVisible;
}, }
child: LayoutBuilder(
builder: (context, constraints) => _BlocBuilder( class _InitialSyncBody extends StatelessWidget {
buildWhen: (previous, current) => const _InitialSyncBody();
previous.contentListMaxExtent !=
current.contentListMaxExtent || @override
(previous.isEnableMemoryCollection && Widget build(BuildContext context) {
previous.memoryCollections.isNotEmpty) != return CustomScrollView(
(current.isEnableMemoryCollection && slivers: [
current.memoryCollections.isNotEmpty), const _AppBar(),
builder: (context, state) { _BlocSelector<Progress?>(
final scrollExtent = _getScrollViewExtent( selector: (state) => state.syncProgress,
context: context, builder: (context, syncProgress) {
constraints: constraints, return SliverToBoxAdapter(
hasMemoryCollection: state.isEnableMemoryCollection && child: Padding(
state.memoryCollections.isNotEmpty, padding: const EdgeInsets.fromLTRB(16, 56, 16, 0),
contentListMaxExtent: state.contentListMaxExtent, child: Center(
); child: ConstrainedBox(
return Stack( constraints: BoxConstraints(
children: [ maxWidth: Theme.of(context).widthLimitedContentMaxWidth,
DraggableScrollbar.semicircle( ),
controller: _scrollController, child: Column(
overrideMaxScrollExtent: scrollExtent, mainAxisSize: MainAxisSize.min,
// status bar + app bar crossAxisAlignment: CrossAxisAlignment.start,
topOffset: _getAppBarExtent(context), children: [
bottomOffset: Text(
AppDimension.of(context).homeBottomAppBarHeight, L10n.global().initialSyncMessage,
labelTextBuilder: (_) => const _ScrollLabel(), style: Theme.of(context).textTheme.bodyLarge,
labelPadding:
const EdgeInsets.symmetric(horizontal: 40),
backgroundColor: Theme.of(context).elevate(
Theme.of(context).colorScheme.inverseSurface, 3),
heightScrollThumb: 60,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context)
.copyWith(scrollbars: false),
child: RefreshIndicator(
onRefresh: () async {
_bloc.add(const _Reload());
await _bloc.stream.first;
},
child: CustomScrollView(
controller: _scrollController,
physics: _finger >= 2
? const NeverScrollableScrollPhysics()
: null,
slivers: [
_BlocSelector<bool>(
selector: (state) =>
state.selectedItems.isEmpty,
builder: (context, isEmpty) => isEmpty
? const _AppBar()
: const _SelectionAppBar(),
),
_BlocBuilder(
buildWhen: (previous, current) =>
(previous.isEnableMemoryCollection &&
previous
.memoryCollections.isNotEmpty) !=
(current.isEnableMemoryCollection &&
current.memoryCollections.isNotEmpty),
builder: (context, state) {
if (state.isEnableMemoryCollection &&
state.memoryCollections.isNotEmpty) {
return const _MemoryCollectionList();
} else {
return const SliverToBoxAdapter();
}
},
),
_BlocSelector<double?>(
selector: (state) => state.scale,
builder: (context, scale) =>
SliverTransitionedScale(
scale: scale,
baseSliver: const _ContentList(),
overlaySliver: const _ScalingList(),
),
),
SliverToBoxAdapter(
child: SizedBox(
height: AppDimension.of(context)
.homeBottomAppBarHeight,
),
),
],
),
),
), ),
), const SizedBox(height: 8),
Align( LinearProgressIndicator(
alignment: Alignment.bottomCenter, value: (syncProgress?.progress ?? 0) == 0
child: NavigationBarBlurFilter( ? null
height: : syncProgress!.progress,
AppDimension.of(context).homeBottomAppBarHeight,
), ),
), const SizedBox(height: 8),
], Text(
); syncProgress?.text ?? "",
}, style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
),
), ),
), );
},
),
],
);
}
}
class _Body extends StatefulWidget {
const _Body();
@override
State<StatefulWidget> createState() => _BodyState();
}
@npLog
class _BodyState extends State<_Body> {
@override
Widget build(BuildContext context) {
return FingerListener(
onFingerChanged: (finger) {
setState(() {
_finger = finger;
});
},
child: GestureDetector(
onScaleStart: (_) {
_bloc.add(const _StartScaling());
},
onScaleUpdate: (details) {
_bloc.add(_SetScale(details.scale));
},
onScaleEnd: (_) {
_bloc.add(const _EndScaling());
},
child: LayoutBuilder(
builder: (context, constraints) => _BlocBuilder(
buildWhen: (previous, current) =>
previous.contentListMaxExtent != current.contentListMaxExtent ||
(previous.isEnableMemoryCollection &&
previous.memoryCollections.isNotEmpty) !=
(current.isEnableMemoryCollection &&
current.memoryCollections.isNotEmpty),
builder: (context, state) {
final scrollExtent = _getScrollViewExtent(
context: context,
constraints: constraints,
hasMemoryCollection: state.isEnableMemoryCollection &&
state.memoryCollections.isNotEmpty,
contentListMaxExtent: state.contentListMaxExtent,
);
return Stack(
children: [
DraggableScrollbar.semicircle(
controller: _scrollController,
overrideMaxScrollExtent: scrollExtent,
// status bar + app bar
topOffset: _getAppBarExtent(context),
bottomOffset:
AppDimension.of(context).homeBottomAppBarHeight,
labelTextBuilder: (_) => const _ScrollLabel(),
labelPadding: const EdgeInsets.symmetric(horizontal: 40),
backgroundColor: Theme.of(context).elevate(
Theme.of(context).colorScheme.inverseSurface, 3),
heightScrollThumb: 60,
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context)
.copyWith(scrollbars: false),
child: RefreshIndicator(
onRefresh: () async {
_bloc.add(const _Reload());
await _bloc.stream.first;
},
child: CustomScrollView(
controller: _scrollController,
physics: _finger >= 2
? const NeverScrollableScrollPhysics()
: null,
slivers: [
_BlocSelector<bool>(
selector: (state) => state.selectedItems.isEmpty,
builder: (context, isEmpty) => isEmpty
? const _AppBar()
: const _SelectionAppBar(),
),
_BlocBuilder(
buildWhen: (previous, current) =>
(previous.isEnableMemoryCollection &&
previous.memoryCollections.isNotEmpty) !=
(current.isEnableMemoryCollection &&
current.memoryCollections.isNotEmpty),
builder: (context, state) {
if (state.isEnableMemoryCollection &&
state.memoryCollections.isNotEmpty) {
return const _MemoryCollectionList();
} else {
return const SliverToBoxAdapter();
}
},
),
_BlocSelector<double?>(
selector: (state) => state.scale,
builder: (context, scale) =>
SliverTransitionedScale(
scale: scale,
baseSliver: const _ContentList(),
overlaySliver: const _ScalingList(),
),
),
SliverToBoxAdapter(
child: SizedBox(
height: AppDimension.of(context)
.homeBottomAppBarHeight,
),
),
],
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: NavigationBarBlurFilter(
height: AppDimension.of(context).homeBottomAppBarHeight,
),
),
],
);
},
), ),
), ),
), ),
@ -303,9 +381,7 @@ class _WrappedHomePhotosState extends State<_WrappedHomePhotos> {
late final _bloc = context.bloc; late final _bloc = context.bloc;
final _key = GlobalKey();
final _scrollController = ScrollController(); final _scrollController = ScrollController();
bool? _isVisible;
var _finger = 0; var _finger = 0;
} }

View file

@ -22,6 +22,7 @@ abstract class $_StateCopyWithWorker {
bool? isEnableMemoryCollection, bool? isEnableMemoryCollection,
List<Collection>? memoryCollections, List<Collection>? memoryCollections,
double? contentListMaxExtent, double? contentListMaxExtent,
Progress? syncProgress,
int? zoom, int? zoom,
double? scale, double? scale,
ExceptionEvent? error}); ExceptionEvent? error});
@ -40,6 +41,7 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
dynamic isEnableMemoryCollection, dynamic isEnableMemoryCollection,
dynamic memoryCollections, dynamic memoryCollections,
dynamic contentListMaxExtent = copyWithNull, dynamic contentListMaxExtent = copyWithNull,
dynamic syncProgress = copyWithNull,
dynamic zoom, dynamic zoom,
dynamic scale = copyWithNull, dynamic scale = copyWithNull,
dynamic error = copyWithNull}) { dynamic error = copyWithNull}) {
@ -57,6 +59,9 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
contentListMaxExtent: contentListMaxExtent == copyWithNull contentListMaxExtent: contentListMaxExtent == copyWithNull
? that.contentListMaxExtent ? that.contentListMaxExtent
: contentListMaxExtent as double?, : contentListMaxExtent as double?,
syncProgress: syncProgress == copyWithNull
? that.syncProgress
: syncProgress as Progress?,
zoom: zoom as int? ?? that.zoom, zoom: zoom as int? ?? that.zoom,
scale: scale == copyWithNull ? that.scale : scale as double?, scale: scale == copyWithNull ? that.scale : scale as double?,
error: error == copyWithNull ? that.error : error as ExceptionEvent?); error: error == copyWithNull ? that.error : error as ExceptionEvent?);
@ -81,6 +86,13 @@ extension _$_WrappedHomePhotosStateNpLog on _WrappedHomePhotosState {
static final log = Logger("widget.home_photos2._WrappedHomePhotosState"); static final log = Logger("widget.home_photos2._WrappedHomePhotosState");
} }
extension _$_BodyStateNpLog on _BodyState {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.home_photos2._BodyState");
}
extension _$__NpLog on __ { extension _$__NpLog on __ {
// ignore: unused_element // ignore: unused_element
Logger get _log => log; Logger get _log => log;
@ -123,7 +135,7 @@ extension _$_ContentListBodyNpLog on _ContentListBody {
extension _$_StateToString on _State { extension _$_StateToString on _State {
String _$toString() { String _$toString() {
// ignore: unnecessary_string_interpolations // ignore: unnecessary_string_interpolations
return "_State {files: [length: ${files.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, visibleItems: {length: ${visibleItems.length}}, isEnableMemoryCollection: $isEnableMemoryCollection, memoryCollections: [length: ${memoryCollections.length}], contentListMaxExtent: ${contentListMaxExtent == null ? null : "${contentListMaxExtent!.toStringAsFixed(3)}"}, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, error: $error}"; return "_State {files: [length: ${files.length}], isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, visibleItems: {length: ${visibleItems.length}}, isEnableMemoryCollection: $isEnableMemoryCollection, memoryCollections: [length: ${memoryCollections.length}], contentListMaxExtent: ${contentListMaxExtent == null ? null : "${contentListMaxExtent!.toStringAsFixed(3)}"}, syncProgress: $syncProgress, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, error: $error}";
} }
} }
@ -212,6 +224,13 @@ extension _$_SetContentListMaxExtentToString on _SetContentListMaxExtent {
} }
} }
extension _$_SetSyncProgressToString on _SetSyncProgress {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetSyncProgress {progress: $progress}";
}
}
extension _$_StartScalingToString on _StartScaling { extension _$_StartScalingToString on _StartScaling {
String _$toString() { String _$toString() {
// ignore: unnecessary_string_interpolations // ignore: unnecessary_string_interpolations