diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index b73ad4ee..fb204832 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1530,6 +1530,7 @@ "@albumAddMapTooltip": { "description": "Add a map that display between photos to an album" }, + "fileNotFound": "File not found", "errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues", "@errorUnauthenticated": { diff --git a/app/lib/l10n/untranslated-messages.txt b/app/lib/l10n/untranslated-messages.txt index 09465826..d8e1d435 100644 --- a/app/lib/l10n/untranslated-messages.txt +++ b/app/lib/l10n/untranslated-messages.txt @@ -272,6 +272,7 @@ "customizeButtonsUnsupportedWarning", "placePickerTitle", "albumAddMapTooltip", + "fileNotFound", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -295,7 +296,8 @@ "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", "placePickerTitle", - "albumAddMapTooltip" + "albumAddMapTooltip", + "fileNotFound" ], "de": [ @@ -311,7 +313,8 @@ "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", "placePickerTitle", - "albumAddMapTooltip" + "albumAddMapTooltip", + "fileNotFound" ], "el": [ @@ -472,7 +475,8 @@ "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", "placePickerTitle", - "albumAddMapTooltip" + "albumAddMapTooltip", + "fileNotFound" ], "es": [ @@ -488,7 +492,8 @@ "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", "placePickerTitle", - "albumAddMapTooltip" + "albumAddMapTooltip", + "fileNotFound" ], "fi": [ @@ -540,7 +545,8 @@ "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", "placePickerTitle", - "albumAddMapTooltip" + "albumAddMapTooltip", + "fileNotFound" ], "fr": [ @@ -592,7 +598,8 @@ "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", "placePickerTitle", - "albumAddMapTooltip" + "albumAddMapTooltip", + "fileNotFound" ], "it": [ @@ -649,7 +656,8 @@ "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", "placePickerTitle", - "albumAddMapTooltip" + "albumAddMapTooltip", + "fileNotFound" ], "nl": [ @@ -1041,6 +1049,7 @@ "customizeButtonsUnsupportedWarning", "placePickerTitle", "albumAddMapTooltip", + "fileNotFound", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -1104,7 +1113,8 @@ "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", "placePickerTitle", - "albumAddMapTooltip" + "albumAddMapTooltip", + "fileNotFound" ], "pt": [ @@ -1176,7 +1186,8 @@ "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", "placePickerTitle", - "albumAddMapTooltip" + "albumAddMapTooltip", + "fileNotFound" ], "ru": [ @@ -1228,7 +1239,8 @@ "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", "placePickerTitle", - "albumAddMapTooltip" + "albumAddMapTooltip", + "fileNotFound" ], "tr": [ @@ -1244,7 +1256,8 @@ "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", "placePickerTitle", - "albumAddMapTooltip" + "albumAddMapTooltip", + "fileNotFound" ], "zh": [ @@ -1327,7 +1340,8 @@ "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", "placePickerTitle", - "albumAddMapTooltip" + "albumAddMapTooltip", + "fileNotFound" ], "zh_Hant": [ @@ -1504,6 +1518,7 @@ "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", "placePickerTitle", - "albumAddMapTooltip" + "albumAddMapTooltip", + "fileNotFound" ] } diff --git a/app/lib/widget/slideshow_viewer.dart b/app/lib/widget/slideshow_viewer.dart index 65d28b6e..e5067061 100644 --- a/app/lib/widget/slideshow_viewer.dart +++ b/app/lib/widget/slideshow_viewer.dart @@ -7,8 +7,10 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; +import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/bloc_util.dart'; import 'package:nc_photos/controller/account_controller.dart'; +import 'package:nc_photos/controller/files_controller.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/k.dart' as k; @@ -23,6 +25,7 @@ import 'package:nc_photos/widget/video_viewer.dart'; import 'package:nc_photos/widget/viewer_mixin.dart'; import 'package:nc_photos/widget/wakelock_util.dart'; import 'package:np_codegen/np_codegen.dart'; +import 'package:np_common/object_util.dart'; import 'package:np_ui/np_ui.dart'; import 'package:to_string/to_string.dart'; @@ -35,13 +38,13 @@ part 'slideshow_viewer/view.dart'; class SlideshowViewerArguments { const SlideshowViewerArguments( this.account, - this.files, + this.fileIds, this.startIndex, this.config, ); final Account account; - final List files; + final List fileIds; final int startIndex; final SlideshowConfig config; } @@ -59,7 +62,7 @@ class SlideshowViewer extends StatelessWidget { const SlideshowViewer({ super.key, required this.account, - required this.files, + required this.fileIds, required this.startIndex, required this.config, }); @@ -68,7 +71,7 @@ class SlideshowViewer extends StatelessWidget { : this( key: key, account: args.account, - files: args.files, + fileIds: args.fileIds, startIndex: args.startIndex, config: args.config, ); @@ -77,8 +80,9 @@ class SlideshowViewer extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => _Bloc( + filesController: context.read().filesController, account: context.read().account, - files: files, + fileIds: fileIds, startIndex: startIndex, config: config, )..add(const _Init()), @@ -87,7 +91,7 @@ class SlideshowViewer extends StatelessWidget { } final Account account; - final List files; + final List fileIds; final int startIndex; final SlideshowConfig config; } @@ -145,7 +149,11 @@ class _WrappedSlideshowViewerState extends State<_WrappedSlideshowViewer> onPopInvoked: (_) { context.addEvent(const _RequestExit()); }, - child: const _Body(), + child: _BlocSelector( + selector: (state) => state.hasInit, + builder: (context, hasInit) => + hasInit ? const _Body() : const _InitBody(), + ), ), ), ), diff --git a/app/lib/widget/slideshow_viewer.g.dart b/app/lib/widget/slideshow_viewer.g.dart index 88735d34..51d8eccd 100644 --- a/app/lib/widget/slideshow_viewer.g.dart +++ b/app/lib/widget/slideshow_viewer.g.dart @@ -18,6 +18,7 @@ abstract class $_StateCopyWithWorker { int? page, int? nextPage, bool? shouldAnimateNextPage, + List? files, FileDescriptor? currentFile, bool? isShowUi, bool? isPlay, @@ -38,7 +39,8 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { dynamic page, dynamic nextPage, dynamic shouldAnimateNextPage, - dynamic currentFile, + dynamic files, + dynamic currentFile = copyWithNull, dynamic isShowUi, dynamic isPlay, dynamic isVideoCompleted, @@ -53,7 +55,10 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { nextPage: nextPage as int? ?? that.nextPage, shouldAnimateNextPage: shouldAnimateNextPage as bool? ?? that.shouldAnimateNextPage, - currentFile: currentFile as FileDescriptor? ?? that.currentFile, + files: files as List? ?? that.files, + currentFile: currentFile == copyWithNull + ? that.currentFile + : currentFile as FileDescriptor?, isShowUi: isShowUi as bool? ?? that.isShowUi, isPlay: isPlay as bool? ?? that.isPlay, isVideoCompleted: isVideoCompleted as bool? ?? that.isVideoCompleted, @@ -104,7 +109,7 @@ extension _$_PageViewNpLog on _PageView { extension _$_StateToString on _State { String _$toString() { // ignore: unnecessary_string_interpolations - return "_State {hasInit: $hasInit, page: $page, nextPage: $nextPage, shouldAnimateNextPage: $shouldAnimateNextPage, currentFile: ${currentFile.fdPath}, isShowUi: $isShowUi, isPlay: $isPlay, isVideoCompleted: $isVideoCompleted, hasPrev: $hasPrev, hasNext: $hasNext, isShowTimeline: $isShowTimeline, hasShownTimeline: $hasShownTimeline, hasRequestExit: $hasRequestExit}"; + return "_State {hasInit: $hasInit, page: $page, nextPage: $nextPage, shouldAnimateNextPage: $shouldAnimateNextPage, files: [length: ${files.length}], currentFile: ${currentFile == null ? null : "${currentFile!.fdPath}"}, isShowUi: $isShowUi, isPlay: $isPlay, isVideoCompleted: $isVideoCompleted, hasPrev: $hasPrev, hasNext: $hasNext, isShowTimeline: $isShowTimeline, hasShownTimeline: $hasShownTimeline, hasRequestExit: $hasRequestExit}"; } } @@ -115,6 +120,13 @@ extension _$_InitToString on _Init { } } +extension _$_SetFilesToString on _SetFiles { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetFiles {dataMap: {length: ${dataMap.length}}}"; + } +} + extension _$_ToggleShowUiToString on _ToggleShowUi { String _$toString() { // ignore: unnecessary_string_interpolations diff --git a/app/lib/widget/slideshow_viewer/bloc.dart b/app/lib/widget/slideshow_viewer/bloc.dart index d9fc12f0..67603833 100644 --- a/app/lib/widget/slideshow_viewer/bloc.dart +++ b/app/lib/widget/slideshow_viewer/bloc.dart @@ -3,14 +3,14 @@ part of '../slideshow_viewer.dart'; @npLog class _Bloc extends Bloc<_Event, _State> with BlocLogger { _Bloc({ + required this.filesController, required this.account, - required this.files, + required this.fileIds, required this.startIndex, required this.config, - }) : super(_State.init( - initialFile: files[startIndex], - )) { + }) : super(_State.init()) { on<_Init>(_onInit); + on<_SetFiles>(_onSetFiles); on<_ToggleShowUi>(_onToggleShowUi); on<_PreloadSidePages>(_onPreloadSidePages); on<_VideoCompleted>(_onVideoCompleted); @@ -23,6 +23,10 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { on<_ToggleTimeline>(_onToggleTimeline); on<_RequestPage>(_onRequestPage); on<_RequestExit>(_onRequestExit); + + _subscriptions.add(filesController.stream.listen((event) { + add(_SetFiles(event.dataMap)); + })); } @override @@ -40,24 +44,27 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { /// Convert the page index to the corresponding item index int convertPageToFileIndex(int pageIndex) { if (config.isShuffle) { - final i = pageIndex ~/ files.length; + final i = pageIndex ~/ fileIds.length; if (!_shuffledIndex.containsKey(i)) { - final index = [for (var i = 0; i < files.length; ++i) i]; + final index = [for (var i = 0; i < fileIds.length; ++i) i]; _shuffledIndex[i] = index..shuffle(); } - return _shuffledIndex[i]![pageIndex % files.length]; + return _shuffledIndex[i]![pageIndex % fileIds.length]; } else { - return _shuffledIndex[0]![pageIndex % files.length]; + return _shuffledIndex[0]![pageIndex % fileIds.length]; } } - FileDescriptor getFileByPageIndex(int pageIndex) => - files[convertPageToFileIndex(pageIndex)]; + FileDescriptor? getFileByPageIndex(int pageIndex) => + state.files[convertPageToFileIndex(pageIndex)]; - void _onInit(_Init ev, Emitter<_State> emit) { + Future _onInit(_Init ev, Emitter<_State> emit) async { _log.info(ev); + // needed for now because some pages (e.g., search) haven't yet migrated + await filesController.queryByFileId(fileIds); + final parsedConfig = _parseConfig( - files: files, + fileIds: fileIds, startIndex: startIndex, config: config, ); @@ -71,7 +78,16 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { hasPrev: initialPage > 0, hasNext: pageCount == null || initialPage < (pageCount! - 1), )); - _prepareNextPage(); + unawaited(_prepareNextPage()); + } + + void _onSetFiles(_SetFiles ev, Emitter<_State> emit) { + _log.info(ev); + final files = fileIds.map((e) => ev.dataMap[e]).toList(); + emit(state.copyWith( + files: files, + currentFile: files[convertPageToFileIndex(state.page)], + )); } void _onToggleShowUi(_ToggleShowUi ev, Emitter<_State> emit) { @@ -96,16 +112,14 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { } _log.info("[_onPreloadSidePages] Pre-loading nearby images"); if (ev.center > 0) { - final fileIndex = convertPageToFileIndex(ev.center - 1); - final prevFile = files[fileIndex]; - if (file_util.isSupportedImageFormat(prevFile)) { + final prevFile = getFileByPageIndex(ev.center - 1); + if (prevFile != null && file_util.isSupportedImageFormat(prevFile)) { RemoteImageViewer.preloadImage(account, prevFile); } } if (pageCount == null || ev.center + 1 < pageCount!) { - final fileIndex = convertPageToFileIndex(ev.center + 1); - final nextFile = files[fileIndex]; - if (file_util.isSupportedImageFormat(nextFile)) { + final nextFile = getFileByPageIndex(ev.center + 1); + if (nextFile != null && file_util.isSupportedImageFormat(nextFile)) { RemoteImageViewer.preloadImage(account, nextFile); } } @@ -128,7 +142,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { void _onSetPlay(_SetPlay ev, Emitter<_State> emit) { _log.info(ev); - if (file_util.isSupportedVideoFormat(state.currentFile)) { + if (state.currentFile?.let(file_util.isSupportedVideoFormat) == true) { // only start the countdown if the video completed if (state.isVideoCompleted) { _pageChangeTimer?.cancel(); @@ -196,12 +210,12 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { } static ({List shuffled, int initial, int? count}) _parseConfig({ - required List files, + required List fileIds, required int startIndex, required SlideshowConfig config, }) { - final index = [for (var i = 0; i < files.length; ++i) i]; - final count = config.isRepeat ? null : files.length; + final index = [for (var i = 0; i < fileIds.length; ++i) i]; + final count = config.isRepeat ? null : fileIds.length; if (config.isShuffle) { return ( shuffled: index..shuffle(), @@ -211,7 +225,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { } else if (config.isReverse) { return ( shuffled: index.reversed.toList(), - initial: files.length - 1 - startIndex, + initial: fileIds.length - 1 - startIndex, count: count, ); } else { @@ -224,8 +238,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { } Future _prepareNextPage() async { - final file = state.currentFile; - if (file_util.isSupportedVideoFormat(file)) { + if (state.currentFile?.let(file_util.isSupportedVideoFormat) == true) { // for videos, we need to wait until it's ended return; } @@ -264,8 +277,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { add(_NextPage(nextPage)); } + final FilesController filesController; final Account account; - final List files; + final List fileIds; final int startIndex; final SlideshowConfig config; diff --git a/app/lib/widget/slideshow_viewer/state_event.dart b/app/lib/widget/slideshow_viewer/state_event.dart index 44915f7e..c85135f1 100644 --- a/app/lib/widget/slideshow_viewer/state_event.dart +++ b/app/lib/widget/slideshow_viewer/state_event.dart @@ -8,7 +8,8 @@ class _State { required this.page, required this.nextPage, required this.shouldAnimateNextPage, - required this.currentFile, + required this.files, + this.currentFile, required this.isShowUi, required this.isPlay, required this.isVideoCompleted, @@ -19,15 +20,12 @@ class _State { required this.hasRequestExit, }); - factory _State.init({ - required FileDescriptor initialFile, - }) => - _State( + factory _State.init() => const _State( hasInit: false, page: 0, nextPage: 0, shouldAnimateNextPage: true, - currentFile: initialFile, + files: [], isShowUi: false, isPlay: true, isVideoCompleted: false, @@ -45,7 +43,8 @@ class _State { final int page; final int nextPage; final bool shouldAnimateNextPage; - final FileDescriptor currentFile; + final List files; + final FileDescriptor? currentFile; final bool isShowUi; final bool isPlay; final bool isVideoCompleted; @@ -66,6 +65,16 @@ class _Init implements _Event { String toString() => _$toString(); } +@toString +class _SetFiles implements _Event { + const _SetFiles(this.dataMap); + + @override + String toString() => _$toString(); + + final Map dataMap; +} + @toString class _ToggleShowUi implements _Event { const _ToggleShowUi(); diff --git a/app/lib/widget/slideshow_viewer/timeline.dart b/app/lib/widget/slideshow_viewer/timeline.dart index b37ea991..b44755e5 100644 --- a/app/lib/widget/slideshow_viewer/timeline.dart +++ b/app/lib/widget/slideshow_viewer/timeline.dart @@ -87,13 +87,26 @@ class _TimelineItem extends StatelessWidget { color: isSelected ? Theme.of(context).colorScheme.secondaryContainer : Colors.transparent, - child: PhotoListImage( - account: context.bloc.account, - previewUrl: NetworkRectThumbnail.imageUrlForFile( - context.bloc.account, - file, - ), - ), + child: file != null + ? PhotoListImage( + account: context.bloc.account, + previewUrl: NetworkRectThumbnail.imageUrlForFile( + context.bloc.account, + file!, + ), + ) + : AspectRatio( + aspectRatio: 1, + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(4), + child: Text( + L10n.global().fileNotFound, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ), + ), ), if (!isSelected) Positioned.fill( @@ -111,6 +124,6 @@ class _TimelineItem extends StatelessWidget { } final int index; - final FileDescriptor file; + final FileDescriptor? file; final bool isSelected; } diff --git a/app/lib/widget/slideshow_viewer/view.dart b/app/lib/widget/slideshow_viewer/view.dart index df0a30a3..f2aed121 100644 --- a/app/lib/widget/slideshow_viewer/view.dart +++ b/app/lib/widget/slideshow_viewer/view.dart @@ -129,6 +129,31 @@ class _ControlBar extends StatelessWidget { } } +class _InitBody extends StatelessWidget { + const _InitBody(); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + const Center(child: CircularProgressIndicator()), + Positioned.directional( + textDirection: Directionality.of(context), + top: 0, + start: 0, + child: IconButton( + icon: const Icon(Icons.close), + tooltip: MaterialLocalizations.of(context).closeButtonTooltip, + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ); + } +} + @npLog class _Body extends StatelessWidget { const _Body(); @@ -259,25 +284,37 @@ class _PageView extends StatelessWidget { @override Widget build(BuildContext context) { - final file = context.bloc.files[itemIndex]; - if (file_util.isSupportedImageFormat(file)) { - return _ImagePageView( - file: file, - onLoaded: () { - context.addEvent(_PreloadSidePages(page)); - }, - ); - } else if (file_util.isSupportedVideoFormat(file)) { - return _VideoPageView( - file: file, - onCompleted: () { - context.addEvent(const _VideoCompleted()); - }, - ); - } else { - _log.shout("[build] Unknown file format: ${file.fdMime}"); - return const SizedBox.shrink(); - } + return _BlocSelector( + selector: (state) => state.files[itemIndex], + builder: (context, file) { + if (file == null) { + return Center( + child: Text( + L10n.global().fileNotFound, + style: Theme.of(context).textTheme.bodyLarge, + ), + ); + } + if (file_util.isSupportedImageFormat(file)) { + return _ImagePageView( + file: file, + onLoaded: () { + context.addEvent(_PreloadSidePages(page)); + }, + ); + } else if (file_util.isSupportedVideoFormat(file)) { + return _VideoPageView( + file: file, + onCompleted: () { + context.addEvent(const _VideoCompleted()); + }, + ); + } else { + _log.shout("[build] Unknown file format: ${file.fdMime}"); + return const SizedBox.shrink(); + } + }, + ); } final int page; diff --git a/app/lib/widget/viewer.dart b/app/lib/widget/viewer.dart index 08b1addd..0b421aca 100644 --- a/app/lib/widget/viewer.dart +++ b/app/lib/widget/viewer.dart @@ -254,7 +254,7 @@ class _WrappedViewerState extends State<_WrappedViewer> SlideshowViewer.routeName, arguments: SlideshowViewerArguments( slideshowRequest.value!.account, - slideshowRequest.value!.files, + slideshowRequest.value!.fileIds, slideshowRequest.value!.startIndex, result, ), diff --git a/app/lib/widget/viewer/bloc.dart b/app/lib/widget/viewer/bloc.dart index 31aa4386..42ccd739 100644 --- a/app/lib/widget/viewer/bloc.dart +++ b/app/lib/widget/viewer/bloc.dart @@ -343,14 +343,11 @@ class _Bloc extends Bloc<_Event, _State> void _onStartSlideshow(_StartSlideshow ev, _Emitter emit) { _log.info(ev); - final files = - state.fileIdOrders.map((id) => state.files[id]).nonNulls.toList(); final req = _SlideshowRequest( account: account, - files: files, - startIndex: files - .indexWhere((e) => e.fdId == ev.fileId) - .let((i) => i == -1 ? 0 : i), + fileIds: state.fileIdOrders, + startIndex: + state.fileIdOrders.indexOf(ev.fileId).let((i) => i == -1 ? 0 : i), ); emit(state.copyWith(slideshowRequest: Unique(req))); } diff --git a/app/lib/widget/viewer/type.dart b/app/lib/widget/viewer/type.dart index 014de030..8d7af11e 100644 --- a/app/lib/widget/viewer/type.dart +++ b/app/lib/widget/viewer/type.dart @@ -15,12 +15,12 @@ class _ShareRequest { class _SlideshowRequest { const _SlideshowRequest({ required this.account, - required this.files, + required this.fileIds, required this.startIndex, }); final Account account; - final List files; + final List fileIds; final int startIndex; }