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_descriptor.dart';
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/use_case/file/list_file.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:np_codegen/np_codegen.dart';
import 'package:np_common/lazy.dart';
@ -54,27 +56,52 @@ class FilesController {
ValueStream<FilesStreamEvent> get stream {
if (!_isDataStreamInited) {
_isDataStreamInited = true;
unawaited(_load());
_load();
}
return _dataStreamController.stream;
}
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));
Future<void> syncRemote({
void Function(Progress progress)? onProgressUpdate,
}) async {
if (_isSyncing) {
_log.fine("[syncRemote] Skipped as another sync running");
return;
}
_isSyncing = true;
try {
final shareDir = File(
path: file_util.unstripPath(
account, accountPrefController.shareFolder.value),
);
var isShareDirIncluded = 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
@ -227,6 +254,24 @@ class FilesController {
_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(
List<FileDescriptor> results, {
required bool hasNext,
@ -252,6 +297,7 @@ class FilesController {
);
final _mutex = Mutex();
var _isSyncing = false;
}
@toString

View file

@ -5,11 +5,11 @@ class _AppBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) => previous.isLoading != current.isLoading,
builder: (context, state) => HomeSliverAppBar(
return _BlocSelector<bool>(
selector: (state) => state.isLoading || state.syncProgress != null,
builder: (context, isProcessing) => HomeSliverAppBar(
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<_SetContentListMaxExtent>(_onSetContentListMaxExtent);
on<_SetSyncProgress>(_onSetSyncProgress);
on<_StartScaling>(_onStartScaling);
on<_EndScaling>(_onEndScaling);
@ -60,7 +61,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
currentState = currentState as _State;
nextState = nextState as _State;
return currentState.scale == nextState.scale &&
currentState.visibleItems == nextState.visibleItems;
currentState.visibleItems == nextState.visibleItems &&
currentState.syncProgress == nextState.syncProgress;
};
@override
@ -80,10 +82,16 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
_log.info(ev);
return emit.forEach<FilesStreamEvent>(
controller.stream,
onData: (data) => state.copyWith(
files: data.data,
isLoading: data.hasNext || _itemTransformerQueue.isProcessing,
),
onData: (data) {
if (_isInitialLoad && !data.hasNext) {
_isInitialLoad = false;
_syncRemote();
}
return state.copyWith(
files: data.data,
isLoading: data.hasNext || _itemTransformerQueue.isProcessing,
);
},
onError: (e, stackTrace) {
_log.severe("[_onLoad] Uncaught exception", e, stackTrace);
return state.copyWith(
@ -96,7 +104,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
void _onReload(_Reload ev, Emitter<_State> emit) {
_log.info(ev);
unawaited(controller.reload());
_syncRemote();
}
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));
}
void _onSetSyncProgress(_SetSyncProgress ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(syncProgress: ev.progress));
}
void _onStartScaling(_StartScaling ev, Emitter<_State> emit) {
_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) {
emit(state.copyWith(selectedItems: const {}));
}
@ -279,6 +308,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
ComputeQueue<_ItemTransformerArgument, _ItemTransformerResult>();
final _subscriptions = <StreamSubscription>[];
var _isHandlingError = false;
var _isInitialLoad = true;
}
_ItemTransformerResult _buildItem(_ItemTransformerArgument arg) {

View file

@ -12,6 +12,7 @@ class _State {
required this.isEnableMemoryCollection,
required this.memoryCollections,
this.contentListMaxExtent,
this.syncProgress,
required this.zoom,
this.scale,
this.error,
@ -45,6 +46,7 @@ class _State {
final List<Collection> memoryCollections;
final double? contentListMaxExtent;
final Progress? syncProgress;
final int zoom;
final double? scale;
@ -170,6 +172,16 @@ class _SetContentListMaxExtent implements _Event {
final double? value;
}
@toString
class _SetSyncProgress implements _Event {
const _SetSyncProgress(this.progress);
@override
String toString() => _$toString();
final Progress? progress;
}
@toString
class _StartScaling implements _Event {
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/k.dart' as k;
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/theme.dart';
import 'package:nc_photos/theme/dimension.dart';
@ -139,123 +140,200 @@ class _WrappedHomePhotosState extends State<_WrappedHomePhotos> {
},
),
],
child: FingerListener(
onFingerChanged: (finger) {
setState(() {
_finger = finger;
});
child: _BlocSelector<bool>(
selector: (state) =>
state.files.isEmpty &&
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));
},
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,
),
),
],
),
),
),
),
);
}
late final _bloc = context.bloc;
final _key = GlobalKey();
bool? _isVisible;
}
class _InitialSyncBody extends StatelessWidget {
const _InitialSyncBody();
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
const _AppBar(),
_BlocSelector<Progress?>(
selector: (state) => state.syncProgress,
builder: (context, syncProgress) {
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,
),
),
Align(
alignment: Alignment.bottomCenter,
child: NavigationBarBlurFilter(
height:
AppDimension.of(context).homeBottomAppBarHeight,
const SizedBox(height: 8),
LinearProgressIndicator(
value: (syncProgress?.progress ?? 0) == 0
? null
: syncProgress!.progress,
),
),
],
);
},
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;
final _key = GlobalKey();
final _scrollController = ScrollController();
bool? _isVisible;
var _finger = 0;
}

View file

@ -22,6 +22,7 @@ abstract class $_StateCopyWithWorker {
bool? isEnableMemoryCollection,
List<Collection>? memoryCollections,
double? contentListMaxExtent,
Progress? syncProgress,
int? zoom,
double? scale,
ExceptionEvent? error});
@ -40,6 +41,7 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
dynamic isEnableMemoryCollection,
dynamic memoryCollections,
dynamic contentListMaxExtent = copyWithNull,
dynamic syncProgress = copyWithNull,
dynamic zoom,
dynamic scale = copyWithNull,
dynamic error = copyWithNull}) {
@ -57,6 +59,9 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
contentListMaxExtent: contentListMaxExtent == copyWithNull
? that.contentListMaxExtent
: contentListMaxExtent as double?,
syncProgress: syncProgress == copyWithNull
? that.syncProgress
: syncProgress as Progress?,
zoom: zoom as int? ?? that.zoom,
scale: scale == copyWithNull ? that.scale : scale as double?,
error: error == copyWithNull ? that.error : error as ExceptionEvent?);
@ -81,6 +86,13 @@ extension _$_WrappedHomePhotosStateNpLog on _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 __ {
// ignore: unused_element
Logger get _log => log;
@ -123,7 +135,7 @@ extension _$_ContentListBodyNpLog on _ContentListBody {
extension _$_StateToString on _State {
String _$toString() {
// 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 {
String _$toString() {
// ignore: unnecessary_string_interpolations