mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-22 23:19:21 +01:00
Merge branch 'slideshow-control'
This commit is contained in:
commit
06b2c2189b
7 changed files with 544 additions and 71 deletions
|
@ -278,6 +278,17 @@ class HorizontalPageViewerController {
|
||||||
curve: curve,
|
curve: curve,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
void animateToPage(
|
||||||
|
int page, {
|
||||||
|
required Duration duration,
|
||||||
|
required Curve curve,
|
||||||
|
}) =>
|
||||||
|
_pageController.animateToPage(
|
||||||
|
page,
|
||||||
|
duration: duration,
|
||||||
|
curve: curve,
|
||||||
|
);
|
||||||
|
|
||||||
void jumpToPage(int page) {
|
void jumpToPage(int page) {
|
||||||
_pageController.jumpToPage(page);
|
_pageController.jumpToPage(page);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:clock/clock.dart';
|
||||||
import 'package:copy_with/copy_with.dart';
|
import 'package:copy_with/copy_with.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
@ -15,6 +16,8 @@ import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/widget/disposable.dart';
|
import 'package:nc_photos/widget/disposable.dart';
|
||||||
import 'package:nc_photos/widget/horizontal_page_viewer.dart';
|
import 'package:nc_photos/widget/horizontal_page_viewer.dart';
|
||||||
import 'package:nc_photos/widget/image_viewer.dart';
|
import 'package:nc_photos/widget/image_viewer.dart';
|
||||||
|
import 'package:nc_photos/widget/network_thumbnail.dart';
|
||||||
|
import 'package:nc_photos/widget/photo_list_item.dart';
|
||||||
import 'package:nc_photos/widget/slideshow_dialog.dart';
|
import 'package:nc_photos/widget/slideshow_dialog.dart';
|
||||||
import 'package:nc_photos/widget/video_viewer.dart';
|
import 'package:nc_photos/widget/video_viewer.dart';
|
||||||
import 'package:nc_photos/widget/viewer_mixin.dart';
|
import 'package:nc_photos/widget/viewer_mixin.dart';
|
||||||
|
@ -26,6 +29,7 @@ import 'package:to_string/to_string.dart';
|
||||||
part 'slideshow_viewer.g.dart';
|
part 'slideshow_viewer.g.dart';
|
||||||
part 'slideshow_viewer/bloc.dart';
|
part 'slideshow_viewer/bloc.dart';
|
||||||
part 'slideshow_viewer/state_event.dart';
|
part 'slideshow_viewer/state_event.dart';
|
||||||
|
part 'slideshow_viewer/timeline.dart';
|
||||||
part 'slideshow_viewer/view.dart';
|
part 'slideshow_viewer/view.dart';
|
||||||
|
|
||||||
class SlideshowViewerArguments {
|
class SlideshowViewerArguments {
|
||||||
|
@ -114,20 +118,26 @@ class _WrappedSlideshowViewerState extends State<_WrappedSlideshowViewer>
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Theme(
|
return Theme(
|
||||||
data: buildDarkTheme(context),
|
data: buildDarkTheme(context),
|
||||||
child: const Scaffold(
|
child: const AnnotatedRegion<SystemUiOverlayStyle>(
|
||||||
|
value: SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor: Colors.black,
|
||||||
|
systemNavigationBarIconBrightness: Brightness.dark,
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
body: _Body(),
|
body: _Body(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
|
// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
|
||||||
// typedef _BlocListener = BlocListener<_Bloc, _State>;
|
typedef _BlocListener = BlocListener<_Bloc, _State>;
|
||||||
typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
|
typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
|
||||||
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
|
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
|
||||||
|
|
||||||
extension on BuildContext {
|
extension on BuildContext {
|
||||||
_Bloc get bloc => read<_Bloc>();
|
_Bloc get bloc => read<_Bloc>();
|
||||||
// _State get state => bloc.state;
|
_State get state => bloc.state;
|
||||||
void addEvent(_Event event) => bloc.add(event);
|
void addEvent(_Event event) => bloc.add(event);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,15 @@ abstract class $_StateCopyWithWorker {
|
||||||
{bool? hasInit,
|
{bool? hasInit,
|
||||||
int? page,
|
int? page,
|
||||||
int? nextPage,
|
int? nextPage,
|
||||||
|
bool? shouldAnimateNextPage,
|
||||||
FileDescriptor? currentFile,
|
FileDescriptor? currentFile,
|
||||||
bool? isShowUi});
|
bool? isShowUi,
|
||||||
|
bool? isPlay,
|
||||||
|
bool? isVideoCompleted,
|
||||||
|
bool? hasPrev,
|
||||||
|
bool? hasNext,
|
||||||
|
bool? isShowTimeline,
|
||||||
|
bool? hasShownTimeline});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||||
|
@ -29,14 +36,29 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
|
||||||
{dynamic hasInit,
|
{dynamic hasInit,
|
||||||
dynamic page,
|
dynamic page,
|
||||||
dynamic nextPage,
|
dynamic nextPage,
|
||||||
|
dynamic shouldAnimateNextPage,
|
||||||
dynamic currentFile,
|
dynamic currentFile,
|
||||||
dynamic isShowUi}) {
|
dynamic isShowUi,
|
||||||
|
dynamic isPlay,
|
||||||
|
dynamic isVideoCompleted,
|
||||||
|
dynamic hasPrev,
|
||||||
|
dynamic hasNext,
|
||||||
|
dynamic isShowTimeline,
|
||||||
|
dynamic hasShownTimeline}) {
|
||||||
return _State(
|
return _State(
|
||||||
hasInit: hasInit as bool? ?? that.hasInit,
|
hasInit: hasInit as bool? ?? that.hasInit,
|
||||||
page: page as int? ?? that.page,
|
page: page as int? ?? that.page,
|
||||||
nextPage: nextPage as int? ?? that.nextPage,
|
nextPage: nextPage as int? ?? that.nextPage,
|
||||||
|
shouldAnimateNextPage:
|
||||||
|
shouldAnimateNextPage as bool? ?? that.shouldAnimateNextPage,
|
||||||
currentFile: currentFile as FileDescriptor? ?? that.currentFile,
|
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,
|
||||||
|
isShowTimeline: isShowTimeline as bool? ?? that.isShowTimeline,
|
||||||
|
hasShownTimeline: hasShownTimeline as bool? ?? that.hasShownTimeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
final _State that;
|
final _State that;
|
||||||
|
@ -79,7 +101,7 @@ extension _$_PageViewNpLog on _PageView {
|
||||||
extension _$_StateToString on _State {
|
extension _$_StateToString on _State {
|
||||||
String _$toString() {
|
String _$toString() {
|
||||||
// ignore: unnecessary_string_interpolations
|
// 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, shouldAnimateNextPage: $shouldAnimateNextPage, currentFile: ${currentFile.fdPath}, isShowUi: $isShowUi, isPlay: $isPlay, isVideoCompleted: $isVideoCompleted, hasPrev: $hasPrev, hasNext: $hasNext, isShowTimeline: $isShowTimeline, hasShownTimeline: $hasShownTimeline}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +129,35 @@ extension _$_PreloadSidePagesToString on _PreloadSidePages {
|
||||||
extension _$_VideoCompletedToString on _VideoCompleted {
|
extension _$_VideoCompletedToString on _VideoCompleted {
|
||||||
String _$toString() {
|
String _$toString() {
|
||||||
// ignore: unnecessary_string_interpolations
|
// 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 {}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,3 +174,17 @@ extension _$_NextPageToString on _NextPage {
|
||||||
return "_NextPage {value: $value}";
|
return "_NextPage {value: $value}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension _$_ToggleTimelineToString on _ToggleTimeline {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_ToggleTimeline {}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$_RequestPageToString on _RequestPage {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "_RequestPage {value: $value}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,13 +14,19 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||||
on<_ToggleShowUi>(_onToggleShowUi);
|
on<_ToggleShowUi>(_onToggleShowUi);
|
||||||
on<_PreloadSidePages>(_onPreloadSidePages);
|
on<_PreloadSidePages>(_onPreloadSidePages);
|
||||||
on<_VideoCompleted>(_onVideoCompleted);
|
on<_VideoCompleted>(_onVideoCompleted);
|
||||||
|
on<_SetPause>(_onSetPause);
|
||||||
|
on<_SetPlay>(_onSetPlay);
|
||||||
|
on<_RequestPrevPage>(_onRequestPrevPage);
|
||||||
|
on<_RequestNextPage>(_onRequestNextPage);
|
||||||
on<_SetCurrentPage>(_onSetCurrentPage);
|
on<_SetCurrentPage>(_onSetCurrentPage);
|
||||||
on<_NextPage>(_onNextPage);
|
on<_NextPage>(_onNextPage);
|
||||||
|
on<_ToggleTimeline>(_onToggleTimeline);
|
||||||
|
on<_RequestPage>(_onRequestPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
_showUiTimer?.cancel();
|
_pageChangeTimer?.cancel();
|
||||||
for (final s in _subscriptions) {
|
for (final s in _subscriptions) {
|
||||||
s.cancel();
|
s.cancel();
|
||||||
}
|
}
|
||||||
|
@ -34,6 +40,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||||
int convertPageToFileIndex(int pageIndex) =>
|
int convertPageToFileIndex(int pageIndex) =>
|
||||||
_shuffledIndex[pageIndex % files.length];
|
_shuffledIndex[pageIndex % files.length];
|
||||||
|
|
||||||
|
FileDescriptor getFileByPageIndex(int pageIndex) =>
|
||||||
|
files[convertPageToFileIndex(pageIndex)];
|
||||||
|
|
||||||
void _onInit(_Init ev, Emitter<_State> emit) {
|
void _onInit(_Init ev, Emitter<_State> emit) {
|
||||||
_log.info(ev);
|
_log.info(ev);
|
||||||
final parsedConfig = _parseConfig(
|
final parsedConfig = _parseConfig(
|
||||||
|
@ -47,7 +56,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
hasInit: true,
|
hasInit: true,
|
||||||
page: initialPage,
|
page: initialPage,
|
||||||
currentFile: _getFileByPageIndex(initialPage),
|
currentFile: getFileByPageIndex(initialPage),
|
||||||
|
hasPrev: initialPage > 0,
|
||||||
|
hasNext: pageCount == null || initialPage < (pageCount! - 1),
|
||||||
));
|
));
|
||||||
_prepareNextPage();
|
_prepareNextPage();
|
||||||
}
|
}
|
||||||
|
@ -60,15 +71,6 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||||
SystemUiMode.manual,
|
SystemUiMode.manual,
|
||||||
overlays: SystemUiOverlay.values,
|
overlays: SystemUiOverlay.values,
|
||||||
);
|
);
|
||||||
_showUiTimer?.cancel();
|
|
||||||
_showUiTimer = Timer(
|
|
||||||
const Duration(seconds: 3),
|
|
||||||
() {
|
|
||||||
if (state.isShowUi) {
|
|
||||||
add(const _ToggleShowUi());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
}
|
}
|
||||||
|
@ -99,6 +101,41 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onVideoCompleted(_VideoCompleted ev, Emitter<_State> emit) {
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRequestPrevPage(_RequestPrevPage ev, Emitter<_State> emit) {
|
||||||
|
_log.info(ev);
|
||||||
|
_gotoPrevPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRequestNextPage(_RequestNextPage ev, Emitter<_State> emit) {
|
||||||
_log.info(ev);
|
_log.info(ev);
|
||||||
_gotoNextPage();
|
_gotoNextPage();
|
||||||
}
|
}
|
||||||
|
@ -107,14 +144,39 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||||
_log.info(ev);
|
_log.info(ev);
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
page: ev.value,
|
page: ev.value,
|
||||||
currentFile: _getFileByPageIndex(ev.value),
|
currentFile: getFileByPageIndex(ev.value),
|
||||||
|
isVideoCompleted: false,
|
||||||
|
hasPrev: ev.value > 0,
|
||||||
|
hasNext: pageCount == null || ev.value < (pageCount! - 1),
|
||||||
));
|
));
|
||||||
|
if (state.isPlay) {
|
||||||
_prepareNextPage();
|
_prepareNextPage();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onNextPage(_NextPage ev, Emitter<_State> emit) {
|
void _onNextPage(_NextPage ev, Emitter<_State> emit) {
|
||||||
_log.info(ev);
|
_log.info(ev);
|
||||||
emit(state.copyWith(nextPage: ev.value));
|
emit(state.copyWith(
|
||||||
|
nextPage: ev.value,
|
||||||
|
shouldAnimateNextPage: true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onToggleTimeline(_ToggleTimeline ev, Emitter<_State> emit) {
|
||||||
|
_log.info(ev);
|
||||||
|
final next = !state.isShowTimeline;
|
||||||
|
emit(state.copyWith(
|
||||||
|
isShowTimeline: next,
|
||||||
|
hasShownTimeline: state.hasShownTimeline || next,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRequestPage(_RequestPage ev, Emitter<_State> emit) {
|
||||||
|
_log.info(ev);
|
||||||
|
emit(state.copyWith(
|
||||||
|
nextPage: ev.value,
|
||||||
|
shouldAnimateNextPage: false,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
static ({List<int> shuffled, int initial, int? count}) _parseConfig({
|
static ({List<int> shuffled, int initial, int? count}) _parseConfig({
|
||||||
|
@ -152,8 +214,23 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// for photos, we wait for a fixed amount of time defined in config
|
// for photos, we wait for a fixed amount of time defined in config
|
||||||
await Future.delayed(config.duration);
|
_pageChangeTimer?.cancel();
|
||||||
_gotoNextPage();
|
_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() {
|
void _gotoNextPage() {
|
||||||
|
@ -163,16 +240,14 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||||
final nextPage = state.page + 1;
|
final nextPage = state.page + 1;
|
||||||
if (pageCount != null && nextPage >= pageCount!) {
|
if (pageCount != null && nextPage >= pageCount!) {
|
||||||
// end reached
|
// end reached
|
||||||
_log.info("[_gotoNextSlide] Reached the end");
|
_log.info("[_gotoNextPage] Reached the end");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_log.info("[_gotoNextSlide] Next page: $nextPage");
|
_log.info("[_gotoNextPage] To page: $nextPage");
|
||||||
|
_pageChangeTimer?.cancel();
|
||||||
add(_NextPage(nextPage));
|
add(_NextPage(nextPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
FileDescriptor _getFileByPageIndex(int pageIndex) =>
|
|
||||||
files[convertPageToFileIndex(pageIndex)];
|
|
||||||
|
|
||||||
final Account account;
|
final Account account;
|
||||||
final List<FileDescriptor> files;
|
final List<FileDescriptor> files;
|
||||||
final int startIndex;
|
final int startIndex;
|
||||||
|
@ -181,7 +256,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
|
||||||
late final List<int> _shuffledIndex;
|
late final List<int> _shuffledIndex;
|
||||||
late final int initialPage;
|
late final int initialPage;
|
||||||
late final int? pageCount;
|
late final int? pageCount;
|
||||||
|
Timer? _pageChangeTimer;
|
||||||
|
|
||||||
final _subscriptions = <StreamSubscription>[];
|
final _subscriptions = <StreamSubscription>[];
|
||||||
Timer? _showUiTimer;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,15 @@ class _State {
|
||||||
required this.hasInit,
|
required this.hasInit,
|
||||||
required this.page,
|
required this.page,
|
||||||
required this.nextPage,
|
required this.nextPage,
|
||||||
|
required this.shouldAnimateNextPage,
|
||||||
required this.currentFile,
|
required this.currentFile,
|
||||||
required this.isShowUi,
|
required this.isShowUi,
|
||||||
|
required this.isPlay,
|
||||||
|
required this.isVideoCompleted,
|
||||||
|
required this.hasPrev,
|
||||||
|
required this.hasNext,
|
||||||
|
required this.isShowTimeline,
|
||||||
|
required this.hasShownTimeline,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory _State.init({
|
factory _State.init({
|
||||||
|
@ -18,8 +25,15 @@ class _State {
|
||||||
hasInit: false,
|
hasInit: false,
|
||||||
page: 0,
|
page: 0,
|
||||||
nextPage: 0,
|
nextPage: 0,
|
||||||
|
shouldAnimateNextPage: true,
|
||||||
currentFile: initialFile,
|
currentFile: initialFile,
|
||||||
isShowUi: false,
|
isShowUi: false,
|
||||||
|
isPlay: true,
|
||||||
|
isVideoCompleted: false,
|
||||||
|
hasPrev: false,
|
||||||
|
hasNext: false,
|
||||||
|
isShowTimeline: false,
|
||||||
|
hasShownTimeline: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -28,8 +42,15 @@ class _State {
|
||||||
final bool hasInit;
|
final bool hasInit;
|
||||||
final int page;
|
final int page;
|
||||||
final int nextPage;
|
final int nextPage;
|
||||||
|
final bool shouldAnimateNextPage;
|
||||||
final FileDescriptor currentFile;
|
final FileDescriptor currentFile;
|
||||||
final bool isShowUi;
|
final bool isShowUi;
|
||||||
|
final bool isPlay;
|
||||||
|
final bool isVideoCompleted;
|
||||||
|
final bool hasPrev;
|
||||||
|
final bool hasNext;
|
||||||
|
final bool isShowTimeline;
|
||||||
|
final bool hasShownTimeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _Event {}
|
abstract class _Event {}
|
||||||
|
@ -62,12 +83,42 @@ class _PreloadSidePages implements _Event {
|
||||||
|
|
||||||
@toString
|
@toString
|
||||||
class _VideoCompleted implements _Event {
|
class _VideoCompleted implements _Event {
|
||||||
const _VideoCompleted(this.page);
|
const _VideoCompleted();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => _$toString();
|
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
|
@toString
|
||||||
|
@ -89,3 +140,21 @@ class _NextPage implements _Event {
|
||||||
|
|
||||||
final int value;
|
final int value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _ToggleTimeline implements _Event {
|
||||||
|
const _ToggleTimeline();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@toString
|
||||||
|
class _RequestPage implements _Event {
|
||||||
|
const _RequestPage(this.value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
final int value;
|
||||||
|
}
|
||||||
|
|
116
app/lib/widget/slideshow_viewer/timeline.dart
Normal file
116
app/lib/widget/slideshow_viewer/timeline.dart
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
part of '../slideshow_viewer.dart';
|
||||||
|
|
||||||
|
class _Timeline extends StatefulWidget {
|
||||||
|
const _Timeline();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _TimelineState();
|
||||||
|
|
||||||
|
static const width = 96.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelineState extends State<_Timeline> {
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiBlocListener(
|
||||||
|
listeners: [
|
||||||
|
_BlocListenerT(
|
||||||
|
selector: (state) => state.page,
|
||||||
|
listener: (context, page) {
|
||||||
|
if (_lastInteraction == null ||
|
||||||
|
clock.now().difference(_lastInteraction!) >
|
||||||
|
const Duration(seconds: 10)) {
|
||||||
|
_controller.animateTo(
|
||||||
|
page * _Timeline.width,
|
||||||
|
duration: k.animationDurationShort,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Container(
|
||||||
|
width: _Timeline.width,
|
||||||
|
color: Colors.black.withOpacity(.65),
|
||||||
|
child: NotificationListener<UserScrollNotification>(
|
||||||
|
onNotification: (notification) {
|
||||||
|
_lastInteraction = clock.now();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: ScrollConfiguration.of(context).copyWith(
|
||||||
|
scrollbars: false,
|
||||||
|
overscroll: false,
|
||||||
|
),
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
controller: _controller,
|
||||||
|
itemCount: context.bloc.pageCount,
|
||||||
|
itemBuilder: (context, i) => _BlocSelector<int>(
|
||||||
|
selector: (state) => state.page,
|
||||||
|
builder: (context, page) => _TimelineItem(
|
||||||
|
index: i,
|
||||||
|
file: context.bloc.getFileByPageIndex(i),
|
||||||
|
isSelected: i == page,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
late final _controller = ScrollController();
|
||||||
|
DateTime? _lastInteraction;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelineItem extends StatelessWidget {
|
||||||
|
const _TimelineItem({
|
||||||
|
required this.index,
|
||||||
|
required this.file,
|
||||||
|
required this.isSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
color: isSelected
|
||||||
|
? Theme.of(context).colorScheme.secondaryContainer
|
||||||
|
: Colors.transparent,
|
||||||
|
child: PhotoListImage(
|
||||||
|
account: context.bloc.account,
|
||||||
|
previewUrl: NetworkRectThumbnail.imageUrlForFile(
|
||||||
|
context.bloc.account,
|
||||||
|
file,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isSelected)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
context.addEvent(_RequestPage(index));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
final FileDescriptor file;
|
||||||
|
final bool isSelected;
|
||||||
|
}
|
|
@ -5,11 +5,12 @@ class _AppBar extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
return SizedBox(
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
// + status bar height
|
// + status bar height
|
||||||
height: kToolbarHeight + MediaQuery.of(context).padding.top,
|
height: kToolbarHeight + MediaQuery.of(context).padding.top,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
|
@ -24,6 +25,7 @@ class _AppBar extends StatelessWidget {
|
||||||
AppBar(
|
AppBar(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
|
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
|
||||||
|
@ -33,6 +35,96 @@ class _AppBar extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ControlBar extends StatelessWidget {
|
||||||
|
const _ControlBar();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: kToolbarHeight,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color.fromARGB(0, 0, 0, 0),
|
||||||
|
Color.fromARGB(192, 0, 0, 0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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, size: 36),
|
||||||
|
)
|
||||||
|
: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.addEvent(const _SetPlay());
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.play_arrow_outlined, size: 36),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_BlocSelector<bool>(
|
||||||
|
selector: (state) => state.hasNext,
|
||||||
|
builder: (context, hasNext) => IconButton(
|
||||||
|
onPressed: hasNext
|
||||||
|
? () {
|
||||||
|
context.addEvent(const _RequestNextPage());
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.skip_next_outlined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.addEvent(const _ToggleTimeline());
|
||||||
|
},
|
||||||
|
icon: _BlocSelector<bool>(
|
||||||
|
selector: (state) => state.isShowTimeline,
|
||||||
|
builder: (context, isShowTimeline) => isShowTimeline
|
||||||
|
? const Icon(Icons.view_timeline)
|
||||||
|
: const Icon(Icons.view_timeline_outlined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +137,11 @@ class _Body extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (context.state.isShowTimeline) {
|
||||||
|
context.addEvent(const _ToggleTimeline());
|
||||||
|
} else {
|
||||||
context.addEvent(const _ToggleShowUi());
|
context.addEvent(const _ToggleShowUi());
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
|
@ -67,6 +163,34 @@ class _Body extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
_BlocSelector<bool>(
|
||||||
|
selector: (state) => state.isShowUi,
|
||||||
|
builder: (context, isShowUi) => AnimatedVisibility(
|
||||||
|
opacity: isShowUi ? 1 : 0,
|
||||||
|
duration: k.animationDurationNormal,
|
||||||
|
child: const Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: _ControlBar(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_BlocSelector<bool>(
|
||||||
|
selector: (state) => state.isShowTimeline,
|
||||||
|
builder: (context, isShowTimeline) => AnimatedPositionedDirectional(
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
end: isShowTimeline ? 0 : -_Timeline.width,
|
||||||
|
duration: k.animationDurationNormal,
|
||||||
|
// needed because Timeline rely on some late var
|
||||||
|
child: _BlocSelector<bool>(
|
||||||
|
selector: (state) => state.hasShownTimeline,
|
||||||
|
builder: (context, hasShownTimeline) => Visibility(
|
||||||
|
visible: hasShownTimeline,
|
||||||
|
child: const _Timeline(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -85,24 +209,28 @@ class _PageViewerState extends State<_PageViewer> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiBlocListener(
|
return MultiBlocListener(
|
||||||
listeners: [
|
listeners: [
|
||||||
_BlocListenerT(
|
_BlocListener(
|
||||||
selector: (state) => state.nextPage,
|
listenWhen: (previous, current) =>
|
||||||
|
previous.nextPage != current.nextPage,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
_controller.nextPage(
|
if (state.shouldAnimateNextPage) {
|
||||||
|
_controller.animateToPage(
|
||||||
|
state.nextPage,
|
||||||
duration: k.animationDurationLong,
|
duration: k.animationDurationLong,
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
_controller.jumpToPage(state.nextPage);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: HorizontalPageViewer(
|
child: HorizontalPageViewer(
|
||||||
pageCount: context.bloc.pageCount,
|
pageCount: context.bloc.pageCount,
|
||||||
pageBuilder: (context, index) {
|
pageBuilder: (context, index) => FractionallySizedBox(
|
||||||
return FractionallySizedBox(
|
|
||||||
widthFactor: 1 / _viewportFraction,
|
widthFactor: 1 / _viewportFraction,
|
||||||
child: _PageView.ofPage(context, index),
|
child: _PageView.ofPage(context, index),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
initialPage: context.bloc.initialPage,
|
initialPage: context.bloc.initialPage,
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
viewportFraction: _viewportFraction,
|
viewportFraction: _viewportFraction,
|
||||||
|
@ -143,7 +271,7 @@ class _PageView extends StatelessWidget {
|
||||||
return _VideoPageView(
|
return _VideoPageView(
|
||||||
file: file,
|
file: file,
|
||||||
onCompleted: () {
|
onCompleted: () {
|
||||||
context.addEvent(_VideoCompleted(page));
|
context.addEvent(const _VideoCompleted());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in a new issue