diff --git a/app/lib/widget/download_progress_dialog.dart b/app/lib/widget/download_progress_dialog.dart index 02379551..eaf60361 100644 --- a/app/lib/widget/download_progress_dialog.dart +++ b/app/lib/widget/download_progress_dialog.dart @@ -27,7 +27,7 @@ class DownloadProgressDialog extends StatelessWidget { Align( alignment: AlignmentDirectional.centerEnd, child: Text( - "$current/$max", + "${current + 1}/$max", style: Theme.of(context).textTheme.labelMedium, ), ), diff --git a/app/lib/widget/file_sharer_dialog.dart b/app/lib/widget/file_sharer_dialog.dart index 5b635751..0f2090c1 100644 --- a/app/lib/widget/file_sharer_dialog.dart +++ b/app/lib/widget/file_sharer_dialog.dart @@ -15,10 +15,12 @@ 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/exception.dart'; import 'package:nc_photos/exception_event.dart'; import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/mobile/share.dart'; +import 'package:nc_photos/platform/download.dart'; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/toast.dart'; import 'package:nc_photos/use_case/copy.dart'; @@ -27,6 +29,7 @@ import 'package:nc_photos/use_case/create_share.dart'; import 'package:nc_photos/use_case/download_file.dart'; import 'package:nc_photos/use_case/download_preview.dart'; import 'package:nc_photos/use_case/inflate_file_descriptor.dart'; +import 'package:nc_photos/widget/download_progress_dialog.dart'; import 'package:nc_photos/widget/processing_dialog.dart'; import 'package:nc_photos/widget/share_link_multiple_files_dialog.dart'; import 'package:nc_photos/widget/simple_input_dialog.dart'; @@ -42,8 +45,6 @@ part 'file_sharer_dialog/bloc.dart'; part 'file_sharer_dialog/state_event.dart'; part 'file_sharer_dialog/type.dart'; -typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; - /// Dialog to let user share files with different options /// /// Return true if the files are actually shared, false if user cancelled or @@ -78,7 +79,7 @@ class _WrappedFileSharerDialog extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocListener( listeners: [ - BlocListener<_Bloc, _State>( + _BlocListener( listenWhen: (previous, current) => previous.error != current.error, listener: (context, state) { if (state.error != null) { @@ -98,7 +99,7 @@ class _WrappedFileSharerDialog extends StatelessWidget { } }, ), - BlocListener<_Bloc, _State>( + _BlocListener( listenWhen: (previous, current) => previous.message != current.message, listener: (context, state) { @@ -111,7 +112,7 @@ class _WrappedFileSharerDialog extends StatelessWidget { } }, ), - BlocListener<_Bloc, _State>( + _BlocListener( listenWhen: (previous, current) => previous.result != current.result, listener: (context, state) { if (state.result != null) { @@ -146,10 +147,8 @@ class _ShareMethodDialog extends StatelessWidget { @override Widget build(BuildContext context) { - final isSupportPerview = context - .read<_Bloc>() - .files - .any((f) => file_util.isSupportedImageFormat(f)); + final isSupportPerview = + context.bloc.files.any((f) => file_util.isSupportedImageFormat(f)); return SimpleDialog( title: Text(L10n.global().shareMethodDialogTitle), children: [ @@ -161,9 +160,7 @@ class _ShareMethodDialog extends StatelessWidget { subtitle: Text(L10n.global().shareMethodPreviewDescription), ), onPressed: () { - context - .read<_Bloc>() - .add(const _SetMethod(ShareMethod.preview)); + context.addEvent(const _SetMethod(ShareMethod.preview)); }, ), SimpleDialogOption( @@ -172,7 +169,7 @@ class _ShareMethodDialog extends StatelessWidget { subtitle: Text(L10n.global().shareMethodOriginalFileDescription), ), onPressed: () { - context.read<_Bloc>().add(const _SetMethod(ShareMethod.file)); + context.addEvent(const _SetMethod(ShareMethod.file)); }, ), ], @@ -182,7 +179,7 @@ class _ShareMethodDialog extends StatelessWidget { subtitle: Text(L10n.global().shareMethodPublicLinkDescription), ), onPressed: () { - context.read<_Bloc>().add(const _SetMethod(ShareMethod.publicLink)); + context.addEvent(const _SetMethod(ShareMethod.publicLink)); }, ), SimpleDialogOption( @@ -191,9 +188,7 @@ class _ShareMethodDialog extends StatelessWidget { subtitle: Text(L10n.global().shareMethodPasswordLinkDescription), ), onPressed: () { - context - .read<_Bloc>() - .add(const _SetMethod(ShareMethod.passwordLink)); + context.addEvent(const _SetMethod(ShareMethod.passwordLink)); }, ), ], @@ -206,18 +201,24 @@ class _ShareFileDialog extends StatelessWidget { @override Widget build(BuildContext context) { - return _BlocBuilder( - buildWhen: (previous, current) => - previous.previewState?.index != current.previewState?.index || - previous.previewState?.count != current.previewState?.count, - builder: (context, state) { - final text = state.previewState?.index != null && - state.previewState?.count != null - ? " (${state.previewState!.index}/${state.previewState!.count})" - : ""; - return ProcessingDialog( - text: L10n.global().shareDownloadingDialogContent + text, - ); + return _BlocSelector<_FileState?>( + selector: (state) => state.fileState, + builder: (context, fileState) { + if (fileState != null) { + return DownloadProgressDialog( + max: fileState.count, + current: fileState.index, + progress: fileState.progress, + label: context.bloc.files[fileState.index].filename, + onCancel: () { + context.addEvent(const _CancelFileDownload()); + }, + ); + } else { + return ProcessingDialog( + text: L10n.global().genericProcessingDialogContent, + ); + } }, ); } @@ -288,7 +289,7 @@ class _SharePublicLinkDialogState extends State<_SharePublicLinkDialog> { } } - late final _bloc = context.read<_Bloc>(); + late final _bloc = context.bloc; } class _SharePasswordLinkDialog extends StatefulWidget { @@ -368,5 +369,15 @@ class _SharePasswordLinkDialogState extends State<_SharePasswordLinkDialog> { } } - late final _bloc = context.read<_Bloc>(); + late final _bloc = context.bloc; +} + +typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; +typedef _BlocListener = BlocListener<_Bloc, _State>; +typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; + +extension on BuildContext { + _Bloc get bloc => read<_Bloc>(); + // _State get state => bloc.state; + void addEvent(_Event event) => bloc.add(event); } diff --git a/app/lib/widget/file_sharer_dialog.g.dart b/app/lib/widget/file_sharer_dialog.g.dart index b7b7ee94..bda66b75 100644 --- a/app/lib/widget/file_sharer_dialog.g.dart +++ b/app/lib/widget/file_sharer_dialog.g.dart @@ -88,16 +88,32 @@ extension $_PreviewStateCopyWith on _PreviewState { } abstract class $_FileStateCopyWithWorker { - _FileState call({int? index, int? count}); + _FileState call( + {int? index, + double? progress, + int? count, + Download? download, + bool? shouldRun}); } class _$_FileStateCopyWithWorkerImpl implements $_FileStateCopyWithWorker { _$_FileStateCopyWithWorkerImpl(this.that); @override - _FileState call({dynamic index, dynamic count}) { + _FileState call( + {dynamic index, + dynamic progress = copyWithNull, + dynamic count, + dynamic download = copyWithNull, + dynamic shouldRun}) { return _FileState( - index: index as int? ?? that.index, count: count as int? ?? that.count); + index: index as int? ?? that.index, + progress: + progress == copyWithNull ? that.progress : progress as double?, + count: count as int? ?? that.count, + download: + download == copyWithNull ? that.download : download as Download?, + shouldRun: shouldRun as bool? ?? that.shouldRun); } final _FileState that; @@ -165,7 +181,7 @@ extension _$_PreviewStateToString on _PreviewState { extension _$_FileStateToString on _FileState { String _$toString() { // ignore: unnecessary_string_interpolations - return "_FileState {index: $index, count: $count}"; + return "_FileState {index: $index, progress: ${progress == null ? null : "${progress!.toStringAsFixed(3)}"}, count: $count, download: $download, shouldRun: $shouldRun}"; } } @@ -197,6 +213,13 @@ extension _$_SetResultToString on _SetResult { } } +extension _$_CancelFileDownloadToString on _CancelFileDownload { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_CancelFileDownload {}"; + } +} + extension _$_SetPublicLinkDetailsToString on _SetPublicLinkDetails { String _$toString() { // ignore: unnecessary_string_interpolations diff --git a/app/lib/widget/file_sharer_dialog/bloc.dart b/app/lib/widget/file_sharer_dialog/bloc.dart index 93593267..212ad985 100644 --- a/app/lib/widget/file_sharer_dialog/bloc.dart +++ b/app/lib/widget/file_sharer_dialog/bloc.dart @@ -12,6 +12,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_SetResult>(_onSetResult); on<_SetPublicLinkDetails>(_onSetPublicLinkDetails); on<_SetPasswordLinkDetails>(_onSetPasswordLinkDetails); + on<_CancelFileDownload>(_onCancelFileDownload); on<_SetError>(_onSetError); } @@ -19,6 +20,20 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { @override String get tag => _log.fullName; + @override + bool Function(dynamic, dynamic)? get shouldLog => (currentState, nextState) { + currentState = currentState as _State; + nextState = nextState as _State; + if (identical(currentState.fileState, nextState.fileState)) { + return true; + } + // don't log download progress + if (currentState.fileState?.progress != nextState.fileState?.progress) { + return false; + } + return true; + }; + @override void onError(Object error, StackTrace stackTrace) { // we need this to prevent onError being triggered recursively @@ -33,7 +48,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { } Future _onSetMethod(_SetMethod ev, Emitter<_State> emit) async { - _log.info("$ev"); + _log.info(ev); emit(state.copyWith(method: ev.method)); switch (ev.method) { case ShareMethod.file: @@ -48,22 +63,30 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { } void _onSetResult(_SetResult ev, Emitter<_State> emit) { - _log.info("$ev"); + _log.info(ev); emit(state.copyWith(result: ev.result)); } Future _onSetPublicLinkDetails( _SetPublicLinkDetails ev, Emitter<_State> emit) { - _log.info("$ev"); + _log.info(ev); return _doShareLink(emit, albumName: ev.albumName, password: null); } Future _onSetPasswordLinkDetails( _SetPasswordLinkDetails ev, Emitter<_State> emit) { - _log.info("$ev"); + _log.info(ev); return _doShareLink(emit, albumName: ev.albumName, password: ev.password); } + void _onCancelFileDownload(_CancelFileDownload ev, Emitter<_State> emit) { + _log.info(ev); + state.fileState?.download?.cancel(); + emit(state.copyWith( + fileState: state.fileState?.copyWith(shouldRun: false), + )); + } + void _onSetError(_SetError ev, Emitter<_State> emit) { _log.info(ev); emit(state.copyWith(error: ExceptionEvent(ev.error, ev.stackTrace))); @@ -72,22 +95,45 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { Future _doShareFile(Emitter<_State> emit) async { assert(getRawPlatform() == NpPlatform.android); emit(state.copyWith( - previewState: _PreviewState(index: 0, count: files.length), + fileState: _FileState.init(count: files.length), )); final results = >[]; for (final pair in files.withIndex()) { final i = pair.item1, f = pair.item2; emit(state.copyWith( - previewState: state.previewState?.copyWith(index: i), + fileState: state.fileState?.copyWith( + index: i, + progress: null, + ), )); try { - final uri = await DownloadFile()(account, f, shouldNotify: false); - results.add(Tuple2(f, uri)); + final download = DownloadFile().build( + account, + f, + shouldNotify: false, + onProgress: (progress) { + emit(state.copyWith( + fileState: state.fileState?.copyWith(progress: progress), + )); + }, + ); + emit(state.copyWith( + fileState: state.fileState?.copyWith(download: download), + )); + final result = await download(); + if (state.fileState?.shouldRun == false) { + throw const JobCanceledException(); + } + results.add(Tuple2(f, result)); } on PermissionException catch (e, stackTrace) { _log.warning("[_doShareFile] Permission not granted"); emit(state.copyWith(error: ExceptionEvent(e, stackTrace))); emit(state.copyWith(result: false)); return; + } on JobCanceledException catch (_) { + _log.info("[_doShareFile] Job canceled"); + emit(state.copyWith(result: false)); + return; } catch (e, stackTrace) { _log.shout("[_doShareFile] Failed while DownloadFile", e, stackTrace); emit(state.copyWith(error: ExceptionEvent(e, stackTrace))); diff --git a/app/lib/widget/file_sharer_dialog/state_event.dart b/app/lib/widget/file_sharer_dialog/state_event.dart index c7c8258a..d906433e 100644 --- a/app/lib/widget/file_sharer_dialog/state_event.dart +++ b/app/lib/widget/file_sharer_dialog/state_event.dart @@ -51,14 +51,31 @@ class _PreviewState { class _FileState { const _FileState({ required this.index, + required this.progress, required this.count, + required this.download, + required this.shouldRun, }); + factory _FileState.init({ + required int count, + }) => + _FileState( + index: 0, + progress: null, + count: count, + download: null, + shouldRun: true, + ); + @override String toString() => _$toString(); final int index; + final double? progress; final int count; + final Download? download; + final bool shouldRun; } @toString @@ -108,6 +125,14 @@ class _SetResult implements _Event { final bool result; } +@toString +class _CancelFileDownload implements _Event { + const _CancelFileDownload(); + + @override + String toString() => _$toString(); +} + /// Set the details needed to share files as public link @toString class _SetPublicLinkDetails implements _Event {