Merge branch 'slideshow-control'

This commit is contained in:
Ming Ming 2024-08-10 02:17:19 +08:00
commit 06b2c2189b
7 changed files with 544 additions and 71 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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