Add play/pause/prev/next control to slideshow viewer

This commit is contained in:
Ming Ming 2024-07-13 13:44:04 +08:00
parent dd10f45f33
commit af4cf6497b
5 changed files with 287 additions and 34 deletions

View file

@ -278,6 +278,17 @@ class HorizontalPageViewerController {
curve: curve,
);
void animateToPage(
int page, {
required Duration duration,
required Curve curve,
}) =>
_pageController.animateToPage(
page,
duration: duration,
curve: curve,
);
void jumpToPage(int page) {
_pageController.jumpToPage(page);
}

View file

@ -18,7 +18,11 @@ abstract class $_StateCopyWithWorker {
int? page,
int? nextPage,
FileDescriptor? currentFile,
bool? isShowUi});
bool? isShowUi,
bool? isPlay,
bool? isVideoCompleted,
bool? hasPrev,
bool? hasNext});
}
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
@ -30,13 +34,21 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
dynamic page,
dynamic nextPage,
dynamic currentFile,
dynamic isShowUi}) {
dynamic isShowUi,
dynamic isPlay,
dynamic isVideoCompleted,
dynamic hasPrev,
dynamic hasNext}) {
return _State(
hasInit: hasInit as bool? ?? that.hasInit,
page: page as int? ?? that.page,
nextPage: nextPage as int? ?? that.nextPage,
currentFile: currentFile as FileDescriptor? ?? that.currentFile,
isShowUi: isShowUi as bool? ?? that.isShowUi);
isShowUi: isShowUi as bool? ?? that.isShowUi,
isPlay: isPlay as bool? ?? that.isPlay,
isVideoCompleted: isVideoCompleted as bool? ?? that.isVideoCompleted,
hasPrev: hasPrev as bool? ?? that.hasPrev,
hasNext: hasNext as bool? ?? that.hasNext);
}
final _State that;
@ -79,7 +91,7 @@ extension _$_PageViewNpLog on _PageView {
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {hasInit: $hasInit, page: $page, nextPage: $nextPage, currentFile: ${currentFile.fdPath}, isShowUi: $isShowUi}";
return "_State {hasInit: $hasInit, page: $page, nextPage: $nextPage, currentFile: ${currentFile.fdPath}, isShowUi: $isShowUi, isPlay: $isPlay, isVideoCompleted: $isVideoCompleted, hasPrev: $hasPrev, hasNext: $hasNext}";
}
}
@ -107,7 +119,35 @@ extension _$_PreloadSidePagesToString on _PreloadSidePages {
extension _$_VideoCompletedToString on _VideoCompleted {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_VideoCompleted {page: $page}";
return "_VideoCompleted {}";
}
}
extension _$_SetPauseToString on _SetPause {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetPause {}";
}
}
extension _$_SetPlayToString on _SetPlay {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetPlay {}";
}
}
extension _$_RequestPrevPageToString on _RequestPrevPage {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_RequestPrevPage {}";
}
}
extension _$_RequestNextPageToString on _RequestNextPage {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_RequestNextPage {}";
}
}

View file

@ -14,12 +14,17 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
on<_ToggleShowUi>(_onToggleShowUi);
on<_PreloadSidePages>(_onPreloadSidePages);
on<_VideoCompleted>(_onVideoCompleted);
on<_SetPause>(_onSetPause);
on<_SetPlay>(_onSetPlay);
on<_RequestPrevPage>(_onRequestPrevPage);
on<_RequestNextPage>(_onRequestNextPage);
on<_SetCurrentPage>(_onSetCurrentPage);
on<_NextPage>(_onNextPage);
}
@override
Future<void> close() {
_pageChangeTimer?.cancel();
_showUiTimer?.cancel();
for (final s in _subscriptions) {
s.cancel();
@ -48,6 +53,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
hasInit: true,
page: initialPage,
currentFile: _getFileByPageIndex(initialPage),
hasPrev: initialPage > 0,
hasNext: pageCount == null || initialPage < (pageCount! - 1),
));
_prepareNextPage();
}
@ -60,15 +67,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
_showUiTimer?.cancel();
_showUiTimer = Timer(
const Duration(seconds: 3),
() {
if (state.isShowUi) {
add(const _ToggleShowUi());
}
},
);
_restartUiCountdown();
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
@ -99,8 +98,47 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
}
void _onVideoCompleted(_VideoCompleted ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(isVideoCompleted: true));
if (state.isPlay) {
_gotoNextPage();
}
}
void _onSetPause(_SetPause ev, Emitter<_State> emit) {
_log.info(ev);
_pageChangeTimer?.cancel();
_pageChangeTimer = null;
emit(state.copyWith(isPlay: false));
_restartUiCountdown();
}
void _onSetPlay(_SetPlay ev, Emitter<_State> emit) {
_log.info(ev);
if (file_util.isSupportedVideoFormat(state.currentFile)) {
// only start the countdown if the video completed
if (state.isVideoCompleted) {
_pageChangeTimer?.cancel();
_pageChangeTimer = Timer(config.duration, _gotoNextPage);
}
} else {
_pageChangeTimer?.cancel();
_pageChangeTimer = Timer(config.duration, _gotoNextPage);
}
emit(state.copyWith(isPlay: true));
_restartUiCountdown();
}
void _onRequestPrevPage(_RequestPrevPage ev, Emitter<_State> emit) {
_log.info(ev);
_gotoPrevPage();
_restartUiCountdown();
}
void _onRequestNextPage(_RequestNextPage ev, Emitter<_State> emit) {
_log.info(ev);
_gotoNextPage();
_restartUiCountdown();
}
void _onSetCurrentPage(_SetCurrentPage ev, Emitter<_State> emit) {
@ -108,8 +146,13 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
emit(state.copyWith(
page: ev.value,
currentFile: _getFileByPageIndex(ev.value),
isVideoCompleted: false,
hasPrev: ev.value > 0,
hasNext: pageCount == null || ev.value < (pageCount! - 1),
));
_prepareNextPage();
if (state.isPlay) {
_prepareNextPage();
}
}
void _onNextPage(_NextPage ev, Emitter<_State> emit) {
@ -152,8 +195,23 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
return;
}
// for photos, we wait for a fixed amount of time defined in config
await Future.delayed(config.duration);
_gotoNextPage();
_pageChangeTimer?.cancel();
_pageChangeTimer = Timer(config.duration, _gotoNextPage);
}
void _gotoPrevPage() {
if (isClosed) {
return;
}
final nextPage = state.page - 1;
if (nextPage < 0) {
// end reached
_log.info("[_gotoPrevPage] Reached the end");
return;
}
_log.info("[_gotoPrevPage] To page: $nextPage");
_pageChangeTimer?.cancel();
add(_NextPage(nextPage));
}
void _gotoNextPage() {
@ -163,16 +221,31 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
final nextPage = state.page + 1;
if (pageCount != null && nextPage >= pageCount!) {
// end reached
_log.info("[_gotoNextSlide] Reached the end");
_log.info("[_gotoNextPage] Reached the end");
return;
}
_log.info("[_gotoNextSlide] Next page: $nextPage");
_log.info("[_gotoNextPage] To page: $nextPage");
_pageChangeTimer?.cancel();
add(_NextPage(nextPage));
}
FileDescriptor _getFileByPageIndex(int pageIndex) =>
files[convertPageToFileIndex(pageIndex)];
/// Restart the timer to hide the UI, mainly after user interacted with the
/// UI elements
void _restartUiCountdown() {
_showUiTimer?.cancel();
_showUiTimer = Timer(
const Duration(seconds: 3),
() {
if (state.isShowUi) {
add(const _ToggleShowUi());
}
},
);
}
final Account account;
final List<FileDescriptor> files;
final int startIndex;
@ -181,6 +254,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
late final List<int> _shuffledIndex;
late final int initialPage;
late final int? pageCount;
Timer? _pageChangeTimer;
final _subscriptions = <StreamSubscription>[];
Timer? _showUiTimer;

View file

@ -9,6 +9,10 @@ class _State {
required this.nextPage,
required this.currentFile,
required this.isShowUi,
required this.isPlay,
required this.isVideoCompleted,
required this.hasPrev,
required this.hasNext,
});
factory _State.init({
@ -20,6 +24,10 @@ class _State {
nextPage: 0,
currentFile: initialFile,
isShowUi: false,
isPlay: true,
isVideoCompleted: false,
hasPrev: false,
hasNext: false,
);
@override
@ -30,6 +38,10 @@ class _State {
final int nextPage;
final FileDescriptor currentFile;
final bool isShowUi;
final bool isPlay;
final bool isVideoCompleted;
final bool hasPrev;
final bool hasNext;
}
abstract class _Event {}
@ -62,12 +74,42 @@ class _PreloadSidePages implements _Event {
@toString
class _VideoCompleted implements _Event {
const _VideoCompleted(this.page);
const _VideoCompleted();
@override
String toString() => _$toString();
}
final int page;
@toString
class _SetPause implements _Event {
const _SetPause();
@override
String toString() => _$toString();
}
@toString
class _SetPlay implements _Event {
const _SetPlay();
@override
String toString() => _$toString();
}
@toString
class _RequestPrevPage implements _Event {
const _RequestPrevPage();
@override
String toString() => _$toString();
}
@toString
class _RequestNextPage implements _Event {
const _RequestNextPage();
@override
String toString() => _$toString();
}
@toString

View file

@ -37,6 +37,88 @@ class _AppBar extends StatelessWidget {
}
}
class _ControlBar extends StatelessWidget {
const _ControlBar();
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
height: kToolbarHeight,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromARGB(0, 0, 0, 0),
Color.fromARGB(192, 0, 0, 0),
],
),
),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
height: kToolbarHeight,
alignment: Alignment.center,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_BlocSelector<bool>(
selector: (state) => state.hasPrev,
builder: (context, hasPrev) => IconButton(
onPressed: hasPrev
? () {
context.addEvent(const _RequestPrevPage());
}
: null,
icon: const Icon(Icons.skip_previous_outlined),
),
),
_BlocSelector<bool>(
selector: (state) => state.isPlay,
builder: (context, isPlay) => isPlay
? IconButton(
onPressed: () {
context.addEvent(const _SetPause());
},
icon: const Icon(Icons.pause_outlined),
)
: IconButton(
onPressed: () {
context.addEvent(const _SetPlay());
},
icon: const Icon(Icons.play_arrow_outlined),
),
),
_BlocSelector<bool>(
selector: (state) => state.hasNext,
builder: (context, hasNext) => IconButton(
onPressed: hasNext
? () {
context.addEvent(const _RequestNextPage());
}
: null,
icon: const Icon(Icons.skip_next_outlined),
),
),
],
),
),
),
],
);
}
}
@npLog
class _Body extends StatelessWidget {
const _Body();
@ -61,10 +143,15 @@ class _Body extends StatelessWidget {
builder: (context, isShowUi) => AnimatedVisibility(
opacity: isShowUi ? 1 : 0,
duration: k.animationDurationNormal,
child: const Align(
alignment: Alignment.topCenter,
child: _AppBar(),
),
child: const _AppBar(),
),
),
_BlocSelector<bool>(
selector: (state) => state.isShowUi,
builder: (context, isShowUi) => AnimatedVisibility(
opacity: isShowUi ? 1 : 0,
duration: k.animationDurationNormal,
child: const _ControlBar(),
),
),
],
@ -87,8 +174,9 @@ class _PageViewerState extends State<_PageViewer> {
listeners: [
_BlocListenerT(
selector: (state) => state.nextPage,
listener: (context, state) {
_controller.nextPage(
listener: (context, nextPage) {
_controller.animateToPage(
nextPage,
duration: k.animationDurationLong,
curve: Curves.easeInOut,
);
@ -97,12 +185,10 @@ class _PageViewerState extends State<_PageViewer> {
],
child: HorizontalPageViewer(
pageCount: context.bloc.pageCount,
pageBuilder: (context, index) {
return FractionallySizedBox(
widthFactor: 1 / _viewportFraction,
child: _PageView.ofPage(context, index),
);
},
pageBuilder: (context, index) => FractionallySizedBox(
widthFactor: 1 / _viewportFraction,
child: _PageView.ofPage(context, index),
),
initialPage: context.bloc.initialPage,
controller: _controller,
viewportFraction: _viewportFraction,
@ -143,7 +229,7 @@ class _PageView extends StatelessWidget {
return _VideoPageView(
file: file,
onCompleted: () {
context.addEvent(_VideoCompleted(page));
context.addEvent(const _VideoCompleted());
},
);
} else {