Refactor slideshow to work with file ids

This also fixed files missing when starting slideshow from search result
This commit is contained in:
Ming Ming 2024-11-27 00:44:57 +08:00
parent 94cb4e7d92
commit 58e6b6c238
11 changed files with 199 additions and 93 deletions

View file

@ -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": {

View file

@ -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"
]
}

View file

@ -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<FileDescriptor> files;
final List<int> 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<AccountController>().filesController,
account: context.read<AccountController>().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<FileDescriptor> files;
final List<int> 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<bool>(
selector: (state) => state.hasInit,
builder: (context, hasInit) =>
hasInit ? const _Body() : const _InitBody(),
),
),
),
),

View file

@ -18,6 +18,7 @@ abstract class $_StateCopyWithWorker {
int? page,
int? nextPage,
bool? shouldAnimateNextPage,
List<FileDescriptor?>? 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<FileDescriptor?>? ?? 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

View file

@ -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<void> _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<int> shuffled, int initial, int? count}) _parseConfig({
required List<FileDescriptor> files,
required List<int> 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<void> _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<FileDescriptor> files;
final List<int> fileIds;
final int startIndex;
final SlideshowConfig config;

View file

@ -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<FileDescriptor?> 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<int, FileDescriptor> dataMap;
}
@toString
class _ToggleShowUi implements _Event {
const _ToggleShowUi();

View file

@ -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;
}

View file

@ -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<FileDescriptor?>(
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;

View file

@ -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,
),

View file

@ -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)));
}

View file

@ -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<FileDescriptor> files;
final List<int> fileIds;
final int startIndex;
}