Rewrite slideshow viewer

This commit is contained in:
Ming Ming 2024-07-10 01:00:24 +08:00
parent 90a426b562
commit 559af7f129
6 changed files with 653 additions and 230 deletions

View file

@ -12,7 +12,7 @@ part 'slideshow_dialog.g.dart';
@toString @toString
class SlideshowConfig { class SlideshowConfig {
SlideshowConfig({ const SlideshowConfig({
required this.duration, required this.duration,
required this.isShuffle, required this.isShuffle,
required this.isRepeat, required this.isRepeat,

View file

@ -1,9 +1,13 @@
import 'dart:async'; import 'dart:async';
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';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/bloc_util.dart';
import 'package:nc_photos/controller/account_controller.dart';
import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/k.dart' as k;
@ -17,24 +21,28 @@ import 'package:nc_photos/widget/viewer_mixin.dart';
import 'package:nc_photos/widget/wakelock_util.dart'; import 'package:nc_photos/widget/wakelock_util.dart';
import 'package:np_codegen/np_codegen.dart'; import 'package:np_codegen/np_codegen.dart';
import 'package:np_ui/np_ui.dart'; import 'package:np_ui/np_ui.dart';
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/state_event.dart';
part 'slideshow_viewer/view.dart';
class SlideshowViewerArguments { class SlideshowViewerArguments {
SlideshowViewerArguments( const SlideshowViewerArguments(
this.account, this.account,
this.streamFiles, this.files,
this.startIndex, this.startIndex,
this.config, this.config,
); );
final Account account; final Account account;
final List<FileDescriptor> streamFiles; final List<FileDescriptor> files;
final int startIndex; final int startIndex;
final SlideshowConfig config; final SlideshowConfig config;
} }
class SlideshowViewer extends StatefulWidget { class SlideshowViewer extends StatelessWidget {
static const routeName = "/slideshow-viewer"; static const routeName = "/slideshow-viewer";
static Route buildRoute(SlideshowViewerArguments args) => MaterialPageRoute( static Route buildRoute(SlideshowViewerArguments args) => MaterialPageRoute(
@ -44,7 +52,7 @@ class SlideshowViewer extends StatefulWidget {
const SlideshowViewer({ const SlideshowViewer({
super.key, super.key,
required this.account, required this.account,
required this.streamFiles, required this.files,
required this.startIndex, required this.startIndex,
required this.config, required this.config,
}); });
@ -53,44 +61,49 @@ class SlideshowViewer extends StatefulWidget {
: this( : this(
key: key, key: key,
account: args.account, account: args.account,
streamFiles: args.streamFiles, files: args.files,
startIndex: args.startIndex, startIndex: args.startIndex,
config: args.config, config: args.config,
); );
@override @override
createState() => _SlideshowViewerState(); Widget build(BuildContext context) {
return BlocProvider(
create: (context) => _Bloc(
account: context.read<AccountController>().account,
files: files,
startIndex: startIndex,
config: config,
)..add(const _Init()),
child: const _WrappedSlideshowViewer(),
);
}
final Account account; final Account account;
final List<FileDescriptor> streamFiles; final List<FileDescriptor> files;
final int startIndex; final int startIndex;
final SlideshowConfig config; final SlideshowConfig config;
} }
@npLog class _WrappedSlideshowViewer extends StatefulWidget {
class _SlideshowViewerState extends State<SlideshowViewer> const _WrappedSlideshowViewer();
with
DisposableManagerMixin<SlideshowViewer>,
ViewerControllersMixin<SlideshowViewer> {
@override @override
initState() { State<StatefulWidget> createState() => _WrappedSlideshowViewerState();
}
class _WrappedSlideshowViewerState extends State<_WrappedSlideshowViewer>
with
DisposableManagerMixin<_WrappedSlideshowViewer>,
ViewerControllersMixin<_WrappedSlideshowViewer> {
@override
void initState() {
super.initState(); super.initState();
_shuffledIndex = () {
final index = [for (var i = 0; i < widget.streamFiles.length; ++i) i];
if (widget.config.isShuffle) {
return index..shuffle();
} else if (widget.config.isReverse) {
return index.reversed.toList();
} else {
return index;
}
}();
_initSlideshow();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
} }
@override @override
initDisposables() { List<Disposable> initDisposables() {
return [ return [
...super.initDisposables(), ...super.initDisposables(),
WakelockControllerDisposable(), WakelockControllerDisposable(),
@ -98,210 +111,23 @@ class _SlideshowViewerState extends State<SlideshowViewer>
} }
@override @override
build(BuildContext context) { Widget build(BuildContext context) {
return Theme( return Theme(
data: buildDarkTheme(context), data: buildDarkTheme(context),
child: Scaffold( child: const Scaffold(
body: Builder( body: _Body(),
builder: _buildContent,
),
), ),
); );
} }
}
Widget _buildContent(BuildContext context) {
final int initialPage; // typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
if (widget.config.isShuffle) { // typedef _BlocListener = BlocListener<_Bloc, _State>;
// the original order is meaningless after shuffled typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
initialPage = 0; typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
} else if (widget.config.isReverse) {
initialPage = widget.streamFiles.length - 1 - widget.startIndex; extension on BuildContext {
} else { _Bloc get bloc => read<_Bloc>();
initialPage = widget.startIndex; // _State get state => bloc.state;
} void addEvent(_Event event) => bloc.add(event);
return GestureDetector(
onTap: () {
setState(() {
_setShowActionBar(!_isShowAppBar);
});
},
child: Stack(
children: [
Container(color: Colors.black),
HorizontalPageViewer(
pageCount:
widget.config.isRepeat ? null : widget.streamFiles.length,
pageBuilder: _buildPage,
initialPage: initialPage,
controller: _viewerController,
viewportFraction: _viewportFraction,
canSwitchPage: false,
),
_buildAppBar(context),
],
),
);
}
Widget _buildAppBar(BuildContext context) {
return Wrap(
children: [
AnimatedVisibility(
opacity: _isShowAppBar ? 1.0 : 0.0,
duration: k.animationDurationNormal,
child: Stack(
children: [
Container(
// + status bar height
height: kToolbarHeight + MediaQuery.of(context).padding.top,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment(0, -1),
end: Alignment(0, 1),
colors: [
Color.fromARGB(192, 0, 0, 0),
Color.fromARGB(0, 0, 0, 0),
],
),
),
),
AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.close),
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
onPressed: () {
Navigator.of(context).pop();
},
),
),
],
),
),
],
);
}
Widget _buildPage(BuildContext context, int index) {
final itemIndex = _transformIndex(index);
_log.info("[_buildPage] Page: $index, item: $itemIndex");
return FractionallySizedBox(
widthFactor: 1 / _viewportFraction,
child: _buildItemView(context, itemIndex),
);
}
Widget _buildItemView(BuildContext context, int index) {
final file = widget.streamFiles[index];
if (file_util.isSupportedImageFormat(file)) {
return _buildImageView(context, index);
} else if (file_util.isSupportedVideoFormat(file)) {
return _buildVideoView(context, index);
} else {
_log.shout("[_buildItemView] Unknown file format: ${file.fdMime}");
return Container();
}
}
Widget _buildImageView(BuildContext context, int index) {
return RemoteImageViewer(
account: widget.account,
file: widget.streamFiles[index],
canZoom: false,
onLoaded: () => _onImageLoaded(index),
);
}
Widget _buildVideoView(BuildContext context, int index) {
return VideoViewer(
account: widget.account,
file: widget.streamFiles[index],
onLoadFailure: () {
// error, next
Future.delayed(const Duration(seconds: 2), _onSlideshowTick);
},
onPause: () {
// video ended
Future.delayed(const Duration(seconds: 2), _onSlideshowTick);
},
isControlVisible: false,
canLoop: false,
);
}
void _onImageLoaded(int index) {
// currently pageview doesn't pre-load pages, we do it manually
// don't pre-load if user already navigated away
if (_viewerController.currentPage == index) {
_log.info("[_onImageLoaded] Pre-loading nearby images");
if (index > 0) {
final prevFile = widget.streamFiles[index - 1];
if (file_util.isSupportedImageFormat(prevFile)) {
RemoteImageViewer.preloadImage(widget.account, prevFile);
}
}
if (index + 1 < widget.streamFiles.length) {
final nextFile = widget.streamFiles[index + 1];
if (file_util.isSupportedImageFormat(nextFile)) {
RemoteImageViewer.preloadImage(widget.account, nextFile);
}
}
}
}
void _initSlideshow() {
_setupSlideTransition(widget.startIndex);
}
Future<void> _onSlideshowTick() async {
if (!mounted) {
return;
}
_log.info("[_onSlideshowTick] Next item");
final page = _viewerController.currentPage;
await _viewerController.nextPage(
duration: k.animationDurationLong, curve: Curves.easeInOut);
final newPage = _viewerController.currentPage;
if (page == newPage) {
// end reached
_log.info("[_onSlideshowTick] Reached the end");
return;
}
_setupSlideTransition(newPage);
unawaited(SystemChrome.restoreSystemUIOverlays());
}
void _setupSlideTransition(int index) {
final itemIndex = _transformIndex(index);
final item = widget.streamFiles[itemIndex];
if (file_util.isSupportedVideoFormat(item)) {
// for videos, we need to wait until it's ended
} else {
Future.delayed(widget.config.duration, _onSlideshowTick);
}
}
void _setShowActionBar(bool flag) {
_isShowAppBar = flag;
if (flag) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
}
/// Return the page index to the corresponding item index
int _transformIndex(int pageIndex) =>
_shuffledIndex[pageIndex % widget.streamFiles.length];
var _isShowAppBar = false;
final _viewerController = HorizontalPageViewerController();
// late final _SlideshowController _slideshowController;
late final List<int> _shuffledIndex;
static const _viewportFraction = 1.05;
} }

View file

@ -2,13 +2,125 @@
part of 'slideshow_viewer.dart'; part of 'slideshow_viewer.dart';
// **************************************************************************
// CopyWithLintRuleGenerator
// **************************************************************************
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class $_StateCopyWithWorker {
_State call(
{bool? hasInit,
int? page,
int? nextPage,
FileDescriptor? currentFile,
bool? isShowUi});
}
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
_$_StateCopyWithWorkerImpl(this.that);
@override
_State call(
{dynamic hasInit,
dynamic page,
dynamic nextPage,
dynamic currentFile,
dynamic isShowUi}) {
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);
}
final _State that;
}
extension $_StateCopyWith on _State {
$_StateCopyWithWorker get copyWith => _$copyWith;
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
}
// ************************************************************************** // **************************************************************************
// NpLogGenerator // NpLogGenerator
// ************************************************************************** // **************************************************************************
extension _$_SlideshowViewerStateNpLog on _SlideshowViewerState { extension _$_BlocNpLog on _Bloc {
// ignore: unused_element // ignore: unused_element
Logger get _log => log; Logger get _log => log;
static final log = Logger("widget.slideshow_viewer._SlideshowViewerState"); static final log = Logger("widget.slideshow_viewer._Bloc");
}
extension _$_BodyNpLog on _Body {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.slideshow_viewer._Body");
}
extension _$_PageViewNpLog on _PageView {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.slideshow_viewer._PageView");
}
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {hasInit: $hasInit, page: $page, nextPage: $nextPage, currentFile: ${currentFile.fdPath}, isShowUi: $isShowUi}";
}
}
extension _$_InitToString on _Init {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Init {}";
}
}
extension _$_ToggleShowUiToString on _ToggleShowUi {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_ToggleShowUi {}";
}
}
extension _$_PreloadSidePagesToString on _PreloadSidePages {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_PreloadSidePages {center: $center}";
}
}
extension _$_VideoCompletedToString on _VideoCompleted {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_VideoCompleted {page: $page}";
}
}
extension _$_SetCurrentPageToString on _SetCurrentPage {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetCurrentPage {value: $value}";
}
}
extension _$_NextPageToString on _NextPage {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_NextPage {value: $value}";
}
} }

View file

@ -0,0 +1,187 @@
part of '../slideshow_viewer.dart';
@npLog
class _Bloc extends Bloc<_Event, _State> with BlocLogger {
_Bloc({
required this.account,
required this.files,
required this.startIndex,
required this.config,
}) : super(_State.init(
initialFile: files[startIndex],
)) {
on<_Init>(_onInit);
on<_ToggleShowUi>(_onToggleShowUi);
on<_PreloadSidePages>(_onPreloadSidePages);
on<_VideoCompleted>(_onVideoCompleted);
on<_SetCurrentPage>(_onSetCurrentPage);
on<_NextPage>(_onNextPage);
}
@override
Future<void> close() {
_showUiTimer?.cancel();
for (final s in _subscriptions) {
s.cancel();
}
return super.close();
}
@override
String get tag => _log.fullName;
/// Convert the page index to the corresponding item index
int convertPageToFileIndex(int pageIndex) =>
_shuffledIndex[pageIndex % files.length];
void _onInit(_Init ev, Emitter<_State> emit) {
_log.info(ev);
final parsedConfig = _parseConfig(
files: files,
startIndex: startIndex,
config: config,
);
_shuffledIndex = parsedConfig.shuffled;
initialPage = parsedConfig.initial;
pageCount = parsedConfig.count;
emit(state.copyWith(
hasInit: true,
page: initialPage,
currentFile: _getFileByPageIndex(initialPage),
));
_prepareNextPage();
}
void _onToggleShowUi(_ToggleShowUi ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(isShowUi: !state.isShowUi));
if (state.isShowUi) {
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
_showUiTimer?.cancel();
_showUiTimer = Timer(
const Duration(seconds: 3),
() {
if (state.isShowUi) {
add(const _ToggleShowUi());
}
},
);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
}
void _onPreloadSidePages(_PreloadSidePages ev, Emitter<_State> emit) {
_log.info(ev);
// currently pageview doesn't pre-load pages, we do it manually
// don't pre-load if user already navigated away
if (state.page != ev.center) {
return;
}
_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)) {
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)) {
RemoteImageViewer.preloadImage(account, nextFile);
}
}
}
void _onVideoCompleted(_VideoCompleted ev, Emitter<_State> emit) {
_log.info(ev);
_gotoNextPage();
}
void _onSetCurrentPage(_SetCurrentPage ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(
page: ev.value,
currentFile: _getFileByPageIndex(ev.value),
));
_prepareNextPage();
}
void _onNextPage(_NextPage ev, Emitter<_State> emit) {
_log.info(ev);
emit(state.copyWith(nextPage: ev.value));
}
static ({List<int> shuffled, int initial, int? count}) _parseConfig({
required List<FileDescriptor> files,
required int startIndex,
required SlideshowConfig config,
}) {
final index = [for (var i = 0; i < files.length; ++i) i];
final count = config.isRepeat ? null : files.length;
if (config.isShuffle) {
return (
shuffled: index..shuffle(),
initial: 0,
count: count,
);
} else if (config.isReverse) {
return (
shuffled: index.reversed.toList(),
initial: files.length - 1 - startIndex,
count: count,
);
} else {
return (
shuffled: index,
initial: startIndex,
count: count,
);
}
}
Future<void> _prepareNextPage() async {
final file = state.currentFile;
if (file_util.isSupportedVideoFormat(file)) {
// for videos, we need to wait until it's ended
return;
}
// for photos, we wait for a fixed amount of time defined in config
await Future.delayed(config.duration);
_gotoNextPage();
}
void _gotoNextPage() {
if (isClosed) {
return;
}
final nextPage = state.page + 1;
if (pageCount != null && nextPage >= pageCount!) {
// end reached
_log.info("[_gotoNextSlide] Reached the end");
return;
}
_log.info("[_gotoNextSlide] Next page: $nextPage");
add(_NextPage(nextPage));
}
FileDescriptor _getFileByPageIndex(int pageIndex) =>
files[convertPageToFileIndex(pageIndex)];
final Account account;
final List<FileDescriptor> files;
final int startIndex;
final SlideshowConfig config;
late final List<int> _shuffledIndex;
late final int initialPage;
late final int? pageCount;
final _subscriptions = <StreamSubscription>[];
Timer? _showUiTimer;
}

View file

@ -0,0 +1,91 @@
part of '../slideshow_viewer.dart';
@genCopyWith
@toString
class _State {
const _State({
required this.hasInit,
required this.page,
required this.nextPage,
required this.currentFile,
required this.isShowUi,
});
factory _State.init({
required FileDescriptor initialFile,
}) =>
_State(
hasInit: false,
page: 0,
nextPage: 0,
currentFile: initialFile,
isShowUi: false,
);
@override
String toString() => _$toString();
final bool hasInit;
final int page;
final int nextPage;
final FileDescriptor currentFile;
final bool isShowUi;
}
abstract class _Event {}
@toString
class _Init implements _Event {
const _Init();
@override
String toString() => _$toString();
}
@toString
class _ToggleShowUi implements _Event {
const _ToggleShowUi();
@override
String toString() => _$toString();
}
@toString
class _PreloadSidePages implements _Event {
const _PreloadSidePages(this.center);
@override
String toString() => _$toString();
final int center;
}
@toString
class _VideoCompleted implements _Event {
const _VideoCompleted(this.page);
@override
String toString() => _$toString();
final int page;
}
@toString
class _SetCurrentPage implements _Event {
const _SetCurrentPage(this.value);
@override
String toString() => _$toString();
final int value;
}
@toString
class _NextPage implements _Event {
const _NextPage(this.value);
@override
String toString() => _$toString();
final int value;
}

View file

@ -0,0 +1,207 @@
part of '../slideshow_viewer.dart';
class _AppBar extends StatelessWidget {
const _AppBar();
@override
Widget build(BuildContext context) {
return Stack(
children: [
Container(
// + status bar height
height: kToolbarHeight + MediaQuery.of(context).padding.top,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromARGB(192, 0, 0, 0),
Color.fromARGB(0, 0, 0, 0),
],
),
),
),
AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.close),
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
onPressed: () {
Navigator.of(context).pop();
},
),
),
],
);
}
}
@npLog
class _Body extends StatelessWidget {
const _Body();
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
context.addEvent(const _ToggleShowUi());
},
child: Stack(
fit: StackFit.expand,
children: [
const ColoredBox(color: Colors.black),
_BlocSelector<bool>(
selector: (state) => state.hasInit,
builder: (context, hasInit) =>
hasInit ? const _PageViewer() : const SizedBox.shrink(),
),
_BlocSelector<bool>(
selector: (state) => state.isShowUi,
builder: (context, isShowUi) => AnimatedVisibility(
opacity: isShowUi ? 1 : 0,
duration: k.animationDurationNormal,
child: const Align(
alignment: Alignment.topCenter,
child: _AppBar(),
),
),
),
],
),
);
}
}
class _PageViewer extends StatefulWidget {
const _PageViewer();
@override
State<StatefulWidget> createState() => _PageViewerState();
}
class _PageViewerState extends State<_PageViewer> {
@override
Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
_BlocListenerT(
selector: (state) => state.nextPage,
listener: (context, state) {
_controller.nextPage(
duration: k.animationDurationLong,
curve: Curves.easeInOut,
);
},
),
],
child: HorizontalPageViewer(
pageCount: context.bloc.pageCount,
pageBuilder: (context, index) {
return FractionallySizedBox(
widthFactor: 1 / _viewportFraction,
child: _PageView.ofPage(context, index),
);
},
initialPage: context.bloc.initialPage,
controller: _controller,
viewportFraction: _viewportFraction,
canSwitchPage: false,
onPageChanged: (from, to) {
context.addEvent(_SetCurrentPage(to));
},
),
);
}
final _controller = HorizontalPageViewerController();
}
@npLog
class _PageView extends StatelessWidget {
const _PageView._({
required this.page,
required this.itemIndex,
});
factory _PageView.ofPage(BuildContext context, int page) => _PageView._(
page: page,
itemIndex: context.bloc.convertPageToFileIndex(page),
);
@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(_VideoCompleted(page));
},
);
} else {
_log.shout("[build] Unknown file format: ${file.fdMime}");
return const SizedBox.shrink();
}
}
final int page;
final int itemIndex;
}
class _ImagePageView extends StatelessWidget {
const _ImagePageView({
required this.file,
this.onLoaded,
});
@override
Widget build(BuildContext context) {
return RemoteImageViewer(
account: context.bloc.account,
file: file,
canZoom: false,
onLoaded: onLoaded,
);
}
final FileDescriptor file;
final VoidCallback? onLoaded;
}
class _VideoPageView extends StatelessWidget {
const _VideoPageView({
required this.file,
this.onCompleted,
});
@override
Widget build(BuildContext context) {
return VideoViewer(
account: context.bloc.account,
file: file,
onLoadFailure: () {
// error, next
Future.delayed(const Duration(seconds: 2), onCompleted);
},
onPause: () {
// video ended
Future.delayed(const Duration(seconds: 2), onCompleted);
},
isControlVisible: false,
canLoop: false,
);
}
final FileDescriptor file;
final VoidCallback? onCompleted;
}
const _viewportFraction = 1.05;