mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +01:00
Add play/pause/prev/next control to slideshow viewer
This commit is contained in:
parent
dd10f45f33
commit
af4cf6497b
5 changed files with 287 additions and 34 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {}";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue