feat(mobile): slideshow view (#28421)
* feat(mobile): slideshow view * move slideshow settings to metadata store * remove watch in initState * wrap progress bar in safearea * show slideshow button on remote albums * fix crash on unknown assets * always show slideshow option * add zoom effect * add padding to slideshow settings * chore: styling tweak --------- Co-authored-by: Alex <alex.tran1502@gmail.com>pull/27715/head^2
parent
df016f9228
commit
0ef04d9baa
|
|
@ -18,3 +18,7 @@ enum CleanupStep { selectDate, scan, delete }
|
||||||
enum AssetKeepType { none, photosOnly, videosOnly }
|
enum AssetKeepType { none, photosOnly, videosOnly }
|
||||||
|
|
||||||
enum AssetDateAggregation { start, end }
|
enum AssetDateAggregation { start, end }
|
||||||
|
|
||||||
|
enum SlideshowLook { contain, cover, blurredBackground }
|
||||||
|
|
||||||
|
enum SlideshowDirection { forward, backward, shuffle }
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:immich_mobile/domain/models/config/map_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
|
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/viewer_config.dart';
|
import 'package:immich_mobile/domain/models/config/viewer_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/slideshow_config.dart';
|
||||||
|
|
||||||
class AppConfig {
|
class AppConfig {
|
||||||
final ThemeConfig theme;
|
final ThemeConfig theme;
|
||||||
|
|
@ -12,6 +13,7 @@ class AppConfig {
|
||||||
final TimelineConfig timeline;
|
final TimelineConfig timeline;
|
||||||
final ImageConfig image;
|
final ImageConfig image;
|
||||||
final ViewerConfig viewer;
|
final ViewerConfig viewer;
|
||||||
|
final SlideshowConfig slideshow;
|
||||||
|
|
||||||
const AppConfig({
|
const AppConfig({
|
||||||
this.theme = const .new(),
|
this.theme = const .new(),
|
||||||
|
|
@ -20,6 +22,7 @@ class AppConfig {
|
||||||
this.timeline = const .new(),
|
this.timeline = const .new(),
|
||||||
this.image = const .new(),
|
this.image = const .new(),
|
||||||
this.viewer = const .new(),
|
this.viewer = const .new(),
|
||||||
|
this.slideshow = const .new(),
|
||||||
});
|
});
|
||||||
|
|
||||||
AppConfig copyWith({
|
AppConfig copyWith({
|
||||||
|
|
@ -29,6 +32,7 @@ class AppConfig {
|
||||||
TimelineConfig? timeline,
|
TimelineConfig? timeline,
|
||||||
ImageConfig? image,
|
ImageConfig? image,
|
||||||
ViewerConfig? viewer,
|
ViewerConfig? viewer,
|
||||||
|
SlideshowConfig? slideshow,
|
||||||
}) => .new(
|
}) => .new(
|
||||||
theme: theme ?? this.theme,
|
theme: theme ?? this.theme,
|
||||||
cleanup: cleanup ?? this.cleanup,
|
cleanup: cleanup ?? this.cleanup,
|
||||||
|
|
@ -36,6 +40,7 @@ class AppConfig {
|
||||||
timeline: timeline ?? this.timeline,
|
timeline: timeline ?? this.timeline,
|
||||||
image: image ?? this.image,
|
image: image ?? this.image,
|
||||||
viewer: viewer ?? this.viewer,
|
viewer: viewer ?? this.viewer,
|
||||||
|
slideshow: slideshow ?? this.slideshow,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -47,12 +52,13 @@ class AppConfig {
|
||||||
other.map == map &&
|
other.map == map &&
|
||||||
other.timeline == timeline &&
|
other.timeline == timeline &&
|
||||||
other.image == image &&
|
other.image == image &&
|
||||||
other.viewer == viewer);
|
other.viewer == viewer &&
|
||||||
|
other.slideshow == slideshow);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer);
|
int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer)';
|
'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow)';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
|
||||||
|
class SlideshowConfig {
|
||||||
|
final bool transition;
|
||||||
|
final bool repeat;
|
||||||
|
final int duration;
|
||||||
|
final SlideshowLook look;
|
||||||
|
final SlideshowDirection direction;
|
||||||
|
|
||||||
|
const SlideshowConfig({
|
||||||
|
this.transition = true,
|
||||||
|
this.repeat = true,
|
||||||
|
this.duration = 5,
|
||||||
|
this.look = SlideshowLook.contain,
|
||||||
|
this.direction = SlideshowDirection.forward,
|
||||||
|
});
|
||||||
|
|
||||||
|
SlideshowConfig copyWith({
|
||||||
|
bool? transition,
|
||||||
|
bool? repeat,
|
||||||
|
int? duration,
|
||||||
|
SlideshowLook? look,
|
||||||
|
SlideshowDirection? direction,
|
||||||
|
}) => SlideshowConfig(
|
||||||
|
transition: transition ?? this.transition,
|
||||||
|
repeat: repeat ?? this.repeat,
|
||||||
|
duration: duration ?? this.duration,
|
||||||
|
look: look ?? this.look,
|
||||||
|
direction: direction ?? this.direction,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is SlideshowConfig &&
|
||||||
|
other.transition == transition &&
|
||||||
|
other.repeat == repeat &&
|
||||||
|
other.duration == duration &&
|
||||||
|
other.look == look &&
|
||||||
|
other.direction == direction);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(transition, repeat, duration, look, direction);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'SlideshowConfig(transition: $transition, repeat: $repeat, duration: $duration, look: $look, direction: $direction)';
|
||||||
|
}
|
||||||
|
|
@ -64,7 +64,19 @@ enum MetadataKey<T extends Object> {
|
||||||
),
|
),
|
||||||
cleanupKeepAlbumIds<List<String>>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)),
|
cleanupKeepAlbumIds<List<String>>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)),
|
||||||
cleanupCutoffDaysAgo<int>(.appConfig, 'cleanup.cutoffDaysAgo', -1),
|
cleanupCutoffDaysAgo<int>(.appConfig, 'cleanup.cutoffDaysAgo', -1),
|
||||||
cleanupDefaultsInitialized<bool>(.appConfig, 'cleanup.defaultsInitialized', false);
|
cleanupDefaultsInitialized<bool>(.appConfig, 'cleanup.defaultsInitialized', false),
|
||||||
|
|
||||||
|
// Slideshow
|
||||||
|
slideshowTransition<bool>(.appConfig, 'slideshow.transition', true),
|
||||||
|
slideshowRepeat<bool>(.appConfig, 'slideshow.repeat', true),
|
||||||
|
slideshowDuration<int>(.appConfig, 'slideshow.duration', 5),
|
||||||
|
slideshowLook<SlideshowLook>(.appConfig, 'slideshow.look', SlideshowLook.contain, _EnumCodec(SlideshowLook.values)),
|
||||||
|
slideshowDirection<SlideshowDirection>(
|
||||||
|
.appConfig,
|
||||||
|
'slideshow.direction',
|
||||||
|
SlideshowDirection.forward,
|
||||||
|
_EnumCodec(SlideshowDirection.values),
|
||||||
|
);
|
||||||
|
|
||||||
final MetadataDomain domain;
|
final MetadataDomain domain;
|
||||||
final String name;
|
final String name;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ enum StoreKey<T> {
|
||||||
readonlyModeEnabled<bool>._(138),
|
readonlyModeEnabled<bool>._(138),
|
||||||
albumGridView<bool>._(140),
|
albumGridView<bool>._(140),
|
||||||
|
|
||||||
|
// Image viewer navigation settings
|
||||||
|
tapToNavigate<bool>._(141),
|
||||||
|
|
||||||
// Experimental stuff
|
// Experimental stuff
|
||||||
enableBackup<bool>._(1003),
|
enableBackup<bool>._(1003),
|
||||||
useWifiForUploadVideos<bool>._(1004),
|
useWifiForUploadVideos<bool>._(1004),
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,13 @@ extension<T extends Object> on MetadataDomain<T> {
|
||||||
autoPlayVideo: repo._read(.viewerAutoPlayVideo),
|
autoPlayVideo: repo._read(.viewerAutoPlayVideo),
|
||||||
tapToNavigate: repo._read(.viewerTapToNavigate),
|
tapToNavigate: repo._read(.viewerTapToNavigate),
|
||||||
),
|
),
|
||||||
|
slideshow: .new(
|
||||||
|
transition: repo._read(.slideshowTransition),
|
||||||
|
repeat: repo._read(.slideshowRepeat),
|
||||||
|
duration: repo._read(.slideshowDuration),
|
||||||
|
look: repo._read(.slideshowLook),
|
||||||
|
direction: repo._read(.slideshowDirection),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
case .systemConfig:
|
case .systemConfig:
|
||||||
repo._systemConfig = .new(logLevel: repo._read(.logLevel));
|
repo._systemConfig = .new(logLevel: repo._read(.logLevel));
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,350 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/slideshow_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/settings.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||||
|
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftSlideshowPage extends ConsumerStatefulWidget {
|
||||||
|
final TimelineService timeline;
|
||||||
|
|
||||||
|
const DriftSlideshowPage({super.key, required this.timeline});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<DriftSlideshowPage> createState() => _DriftSlideshowPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> {
|
||||||
|
late final SlideshowConfig _config;
|
||||||
|
late final PageController _pageController;
|
||||||
|
late final Stopwatch _stopwatch;
|
||||||
|
late Timer _timer;
|
||||||
|
late int _index;
|
||||||
|
late int _nextIndex;
|
||||||
|
bool _paused = false;
|
||||||
|
bool _showAppBar = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
initState() {
|
||||||
|
super.initState();
|
||||||
|
_config = ref.read(appConfigProvider.select((s) => s.slideshow));
|
||||||
|
final asset = ref.read(assetViewerProvider).currentAsset;
|
||||||
|
_index = asset == null ? 0 : widget.timeline.getIndex(asset.heroTag) ?? 0;
|
||||||
|
_pageController = PageController(initialPage: _index);
|
||||||
|
_stopwatch = Stopwatch();
|
||||||
|
_createTimer();
|
||||||
|
_updateNextIndex();
|
||||||
|
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
dispose() {
|
||||||
|
_timer.cancel();
|
||||||
|
_stopwatch.stop();
|
||||||
|
_pageController.dispose();
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _play() {
|
||||||
|
final asset = widget.timeline.getAssetSafe(_index)!;
|
||||||
|
|
||||||
|
if (asset.isImage) {
|
||||||
|
_createTimer();
|
||||||
|
} else if (ref.read(videoPlayerProvider(asset.heroTag)).status == VideoPlaybackStatus.paused) {
|
||||||
|
ref.read(videoPlayerProvider(asset.heroTag).notifier).play();
|
||||||
|
} else {
|
||||||
|
_nextPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateNextIndex();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_paused = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pause() {
|
||||||
|
_timer.cancel();
|
||||||
|
_stopwatch.stop();
|
||||||
|
|
||||||
|
final asset = widget.timeline.getAssetSafe(_index)!;
|
||||||
|
|
||||||
|
if (!asset.isImage) {
|
||||||
|
ref.read(videoPlayerProvider(asset.heroTag).notifier).pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_paused = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateNextIndex() {
|
||||||
|
_nextIndex = switch (_config.direction) {
|
||||||
|
SlideshowDirection.forward => _index + 1,
|
||||||
|
SlideshowDirection.backward => _index - 1,
|
||||||
|
SlideshowDirection.shuffle => widget.timeline.getIndex(widget.timeline.getRandomAsset().heroTag)!,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!widget.timeline.hasRange(_nextIndex, 1)) {
|
||||||
|
widget.timeline.preloadAssets(_nextIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _nextPage() async {
|
||||||
|
if (_nextIndex < 0 || _nextIndex >= widget.timeline.totalAssets) {
|
||||||
|
if (_config.repeat) {
|
||||||
|
final wrapped = _config.direction == SlideshowDirection.forward ? 0 : widget.timeline.totalAssets - 1;
|
||||||
|
await widget.timeline.preloadAssets(wrapped);
|
||||||
|
_pageController.jumpToPage(wrapped);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widget.timeline.hasRange(_nextIndex, 1)) {
|
||||||
|
await widget.timeline.preloadAssets(_nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_config.direction == SlideshowDirection.shuffle || !_config.transition) {
|
||||||
|
_pageController.jumpToPage(_nextIndex);
|
||||||
|
} else {
|
||||||
|
unawaited(_pageController.animateToPage(_nextIndex, duration: Durations.long2, curve: Curves.easeIn));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createTimer() {
|
||||||
|
_timer = Timer(Duration(milliseconds: _config.duration * 1000 - _stopwatch.elapsedMilliseconds), () {
|
||||||
|
_stopwatch.stop();
|
||||||
|
_stopwatch.reset();
|
||||||
|
_nextPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
_stopwatch.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _pageChanged(int page) {
|
||||||
|
final asset = widget.timeline.getAssetSafe(page)!;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_index = page;
|
||||||
|
|
||||||
|
if (!asset.isImage) {
|
||||||
|
_paused = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_timer.cancel();
|
||||||
|
_stopwatch.stop();
|
||||||
|
_stopwatch.reset();
|
||||||
|
|
||||||
|
if (!_paused && asset.isImage) {
|
||||||
|
_createTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateNextIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapUp() async {
|
||||||
|
await SystemChrome.setEnabledSystemUIMode(_showAppBar ? SystemUiMode.immersive : SystemUiMode.edgeToEdge);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
setState(() {
|
||||||
|
_showAppBar = !_showAppBar;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getProgressBar(BuildContext context) {
|
||||||
|
final asset = widget.timeline.getAssetSafe(_index);
|
||||||
|
|
||||||
|
if (asset == null) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.isImage) {
|
||||||
|
final elapsed = _stopwatch.elapsedMilliseconds;
|
||||||
|
final duration = _config.duration * 1000;
|
||||||
|
|
||||||
|
return TweenAnimationBuilder(
|
||||||
|
key: Key(_index.toString()),
|
||||||
|
tween: Tween<double>(begin: elapsed / duration.toDouble(), end: _paused ? elapsed / duration.toDouble() : 1.0),
|
||||||
|
duration: Duration(milliseconds: _paused ? 1 : max(duration - elapsed, 1)),
|
||||||
|
builder: (context, value, _) => LinearProgressIndicator(
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.zero),
|
||||||
|
minHeight: 5,
|
||||||
|
value: value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return LinearProgressIndicator(
|
||||||
|
color: context.colorScheme.primary,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.zero),
|
||||||
|
minHeight: 5,
|
||||||
|
value:
|
||||||
|
ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.position)).inMilliseconds /
|
||||||
|
asset.duration.inMilliseconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getBlur(BuildContext context, int index) {
|
||||||
|
final asset = widget.timeline.getAssetSafe(index);
|
||||||
|
|
||||||
|
if (asset == null) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImageFiltered(
|
||||||
|
imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: getFullImageProvider(asset, size: Size(context.width, context.height)),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Container(color: Colors.black.withValues(alpha: 0.2)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getPhotoView(BuildContext context, int index) {
|
||||||
|
final asset = widget.timeline.getAssetSafe(index);
|
||||||
|
|
||||||
|
if (asset == null) {
|
||||||
|
return const Center(child: ImmichLoadingIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
final scale = _config.look == SlideshowLook.cover
|
||||||
|
? PhotoViewComputedScale.covered
|
||||||
|
: PhotoViewComputedScale.contained;
|
||||||
|
final isCurrent = _index == index;
|
||||||
|
final imageProvider = getFullImageProvider(asset, size: context.sizeData);
|
||||||
|
|
||||||
|
if (asset.isImage) {
|
||||||
|
final zoomOut = index % 2 == 1;
|
||||||
|
final elapsed = _stopwatch.elapsedMilliseconds;
|
||||||
|
final duration = _config.duration * 1000;
|
||||||
|
final progress = zoomOut ? 1.0 - elapsed / duration.toDouble() : elapsed / duration.toDouble();
|
||||||
|
|
||||||
|
return TweenAnimationBuilder(
|
||||||
|
tween: Tween<double>(
|
||||||
|
begin: progress,
|
||||||
|
end: _paused
|
||||||
|
? progress
|
||||||
|
: zoomOut
|
||||||
|
? 0.0
|
||||||
|
: 1.0,
|
||||||
|
),
|
||||||
|
duration: Duration(milliseconds: _paused ? 1 : max(duration - elapsed, 1)),
|
||||||
|
builder: (context, value, _) => PhotoView(
|
||||||
|
imageProvider: imageProvider,
|
||||||
|
index: index,
|
||||||
|
disableScaleGestures: true,
|
||||||
|
gaplessPlayback: true,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
initialScale: scale * (1.0 + value / 10.0),
|
||||||
|
controller: PhotoViewController(),
|
||||||
|
onTapUp: (_, _, _) => _onTapUp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final status = ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.status));
|
||||||
|
final position = ref.read(videoPlayerProvider(asset.heroTag)).position;
|
||||||
|
|
||||||
|
if (status == VideoPlaybackStatus.completed && isCurrent && position.inMicroseconds > 0) {
|
||||||
|
_nextPage();
|
||||||
|
} else if (status == VideoPlaybackStatus.playing) {
|
||||||
|
ref.read(videoPlayerProvider(asset.heroTag).notifier).setLoop(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PhotoView.customChild(
|
||||||
|
onTapUp: (_, _, _) => _onTapUp(),
|
||||||
|
disableScaleGestures: true,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
initialScale: scale,
|
||||||
|
child: NativeVideoViewer(
|
||||||
|
asset: asset,
|
||||||
|
isCurrent: isCurrent,
|
||||||
|
image: Image(image: imageProvider, fit: BoxFit.contain, alignment: Alignment.center),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: PreferredSize(
|
||||||
|
preferredSize: Size(AppBar().preferredSize.width, AppBar().preferredSize.height + 5),
|
||||||
|
child: IgnorePointer(
|
||||||
|
ignoring: !_showAppBar,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: _showAppBar ? 1.0 : 0.0,
|
||||||
|
duration: Durations.short2,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
AppBar(
|
||||||
|
backgroundColor: context.scaffoldBackgroundColor,
|
||||||
|
title: Text("slideshow".t(context: context)),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _paused ? _play : _pause,
|
||||||
|
icon: Icon(_paused ? Icons.play_arrow : Icons.pause),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_pause();
|
||||||
|
context.pushRoute(SettingsSubRoute(section: SettingSection.assetViewer));
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_getProgressBar(context),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
extendBody: true,
|
||||||
|
extendBodyBehindAppBar: true,
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
body: PhotoViewGestureDetectorScope(
|
||||||
|
axis: Axis.horizontal,
|
||||||
|
child: PageView.builder(
|
||||||
|
controller: _pageController,
|
||||||
|
physics: const FastClampingScrollPhysics(),
|
||||||
|
itemCount: widget.timeline.totalAssets,
|
||||||
|
onPageChanged: _pageChanged,
|
||||||
|
itemBuilder: (context, index) => Stack(
|
||||||
|
children: [
|
||||||
|
if (_config.look == SlideshowLook.blurredBackground) _getBlur(context, index),
|
||||||
|
_getPhotoView(context, index),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -50,10 +50,13 @@ class BaseActionButton extends ConsumerWidget {
|
||||||
final iconColor = this.iconColor;
|
final iconColor = this.iconColor;
|
||||||
|
|
||||||
return MenuItemButton(
|
return MenuItemButton(
|
||||||
style: MenuItemButton.styleFrom(alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16)),
|
style: MenuItemButton.styleFrom(
|
||||||
leadingIcon: Icon(iconData, color: iconColor),
|
alignment: Alignment.centerLeft,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
|
),
|
||||||
|
leadingIcon: Icon(iconData, color: iconColor, size: 20),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: Text(label, style: TextStyle(fontSize: 16, color: iconColor)),
|
child: Text(label, style: TextStyle(fontSize: 15, color: iconColor)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
class SlideshowActionButton extends ConsumerWidget {
|
||||||
|
final bool iconOnly;
|
||||||
|
final bool menuItem;
|
||||||
|
|
||||||
|
const SlideshowActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return BaseActionButton(
|
||||||
|
iconData: Icons.slideshow,
|
||||||
|
label: "slideshow".t(context: context),
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
menuItem: menuItem,
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
|
maxWidth: 100,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -60,6 +60,7 @@ import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_recently_added.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_recently_added.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/drift_slideshow.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_video.page.dart';
|
||||||
|
|
@ -189,6 +190,7 @@ class AppRouter extends RootStackRouter {
|
||||||
AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
AutoRoute(page: CleanupPreviewRoute.page, guards: [_authGuard, _duplicateGuard]),
|
AutoRoute(page: CleanupPreviewRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
|
AutoRoute(page: DriftSlideshowRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||||
// required to handle all deeplinks in deep_link.service.dart
|
// required to handle all deeplinks in deep_link.service.dart
|
||||||
// auto_route_library#1722
|
// auto_route_library#1722
|
||||||
RedirectRoute(path: '*', redirectTo: '/'),
|
RedirectRoute(path: '*', redirectTo: '/'),
|
||||||
|
|
|
||||||
|
|
@ -1095,6 +1095,53 @@ class DriftSearchRoute extends PageRouteInfo<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftSlideshowPage]
|
||||||
|
class DriftSlideshowRoute extends PageRouteInfo<DriftSlideshowRouteArgs> {
|
||||||
|
DriftSlideshowRoute({
|
||||||
|
Key? key,
|
||||||
|
required TimelineService timeline,
|
||||||
|
List<PageRouteInfo>? children,
|
||||||
|
}) : super(
|
||||||
|
DriftSlideshowRoute.name,
|
||||||
|
args: DriftSlideshowRouteArgs(key: key, timeline: timeline),
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'DriftSlideshowRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
final args = data.argsAs<DriftSlideshowRouteArgs>();
|
||||||
|
return DriftSlideshowPage(key: args.key, timeline: args.timeline);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DriftSlideshowRouteArgs {
|
||||||
|
const DriftSlideshowRouteArgs({this.key, required this.timeline});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final TimelineService timeline;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DriftSlideshowRouteArgs{key: $key, timeline: $timeline}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
if (other is! DriftSlideshowRouteArgs) return false;
|
||||||
|
return key == other.key && timeline == other.timeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => key.hashCode ^ timeline.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [DriftTrashPage]
|
/// [DriftTrashPage]
|
||||||
class DriftTrashRoute extends PageRouteInfo<void> {
|
class DriftTrashRoute extends PageRouteInfo<void> {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_pi
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/slideshow_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||||
|
|
@ -73,6 +74,7 @@ enum ActionButtonType {
|
||||||
similarPhotos,
|
similarPhotos,
|
||||||
setProfilePicture,
|
setProfilePicture,
|
||||||
viewInTimeline,
|
viewInTimeline,
|
||||||
|
slideshow,
|
||||||
download,
|
download,
|
||||||
upload,
|
upload,
|
||||||
openInBrowser,
|
openInBrowser,
|
||||||
|
|
@ -179,6 +181,7 @@ enum ActionButtonType {
|
||||||
context.timelineOrigin != TimelineOrigin.localAlbum &&
|
context.timelineOrigin != TimelineOrigin.localAlbum &&
|
||||||
context.isOwner,
|
context.isOwner,
|
||||||
ActionButtonType.cast => context.isCasting || context.asset.hasRemote,
|
ActionButtonType.cast => context.isCasting || context.asset.hasRemote,
|
||||||
|
ActionButtonType.slideshow => true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,6 +203,7 @@ enum ActionButtonType {
|
||||||
iconOnly: iconOnly,
|
iconOnly: iconOnly,
|
||||||
menuItem: menuItem,
|
menuItem: menuItem,
|
||||||
),
|
),
|
||||||
|
ActionButtonType.slideshow => SlideshowActionButton(iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.archive => ArchiveActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
ActionButtonType.archive => ArchiveActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem),
|
||||||
ActionButtonType.unarchive => UnArchiveActionButton(
|
ActionButtonType.unarchive => UnArchiveActionButton(
|
||||||
source: context.source,
|
source: context.source,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
||||||
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart';
|
import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart';
|
||||||
|
|
||||||
class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget {
|
class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget {
|
||||||
|
|
@ -89,6 +90,10 @@ class _MesmerizingSliverAppBarState extends ConsumerState<RemoteAlbumSliverAppBa
|
||||||
onPressed: () => context.maybePop(),
|
onPressed: () => context.maybePop(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider))),
|
||||||
|
icon: Icon(Icons.slideshow_outlined, color: actionIconColor, shadows: actionIconShadows),
|
||||||
|
),
|
||||||
if (currentAlbum.isActivityEnabled && currentAlbum.isShared)
|
if (currentAlbum.isActivityEnabled && currentAlbum.isShared)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.chat_outlined, color: actionIconColor, shadows: actionIconShadows),
|
icon: Icon(Icons.chat_outlined, color: actionIconColor, shadows: actionIconShadows),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart';
|
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart';
|
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/video_viewer_settings.dart';
|
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/video_viewer_settings.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/slideshow_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||||
|
|
||||||
class AssetViewerSettings extends StatelessWidget {
|
class AssetViewerSettings extends StatelessWidget {
|
||||||
|
|
@ -13,6 +14,7 @@ class AssetViewerSettings extends StatelessWidget {
|
||||||
const ImageViewerQualitySetting(),
|
const ImageViewerQualitySetting(),
|
||||||
const ImageViewerTapToNavigateSetting(),
|
const ImageViewerTapToNavigateSetting(),
|
||||||
const VideoViewerSettings(),
|
const VideoViewerSettings(),
|
||||||
|
const SlideshowSettings(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return SettingsSubPageScaffold(settings: assetViewerSetting, showDivider: true);
|
return SettingsSubPageScaffold(settings: assetViewerSetting, showDivider: true);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||||
|
|
||||||
|
class SlideshowSettings extends HookConsumerWidget {
|
||||||
|
const SlideshowSettings({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final slideshow = ref.read(appConfigProvider).slideshow;
|
||||||
|
final useTransition = useState(slideshow.transition);
|
||||||
|
final useRepeat = useState(slideshow.repeat);
|
||||||
|
final useDuration = useState(slideshow.duration);
|
||||||
|
final useLook = useState(slideshow.look);
|
||||||
|
final useDirection = useState(slideshow.direction);
|
||||||
|
|
||||||
|
useValueChanged<bool, void>(useTransition.value, (_, __) {
|
||||||
|
ref.read(metadataProvider).write(.slideshowTransition, useTransition.value);
|
||||||
|
});
|
||||||
|
useValueChanged<bool, void>(useRepeat.value, (_, __) {
|
||||||
|
ref.read(metadataProvider).write(.slideshowRepeat, useRepeat.value);
|
||||||
|
});
|
||||||
|
useValueChanged<int, void>(useDuration.value, (_, __) {
|
||||||
|
ref.read(metadataProvider).write(.slideshowDuration, useDuration.value);
|
||||||
|
});
|
||||||
|
useValueChanged<SlideshowLook, void>(useLook.value, (_, __) {
|
||||||
|
ref.read(metadataProvider).write(.slideshowLook, useLook.value);
|
||||||
|
});
|
||||||
|
useValueChanged<SlideshowDirection, void>(useDirection.value, (_, __) {
|
||||||
|
ref.read(metadataProvider).write(.slideshowDirection, useDirection.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SettingGroupTitle(
|
||||||
|
title: 'slideshow'.t(context: context),
|
||||||
|
icon: Icons.slideshow_outlined,
|
||||||
|
),
|
||||||
|
SettingsSwitchListTile(
|
||||||
|
valueNotifier: useTransition,
|
||||||
|
title: "show_slideshow_transition".t(context: context),
|
||||||
|
enabled: useDirection.value != SlideshowDirection.shuffle,
|
||||||
|
),
|
||||||
|
SettingsSwitchListTile(
|
||||||
|
valueNotifier: useRepeat,
|
||||||
|
title: "slideshow_repeat".t(context: context),
|
||||||
|
subtitle: "slideshow_repeat_description".t(context: context),
|
||||||
|
),
|
||||||
|
SettingsSliderListTile(
|
||||||
|
valueNotifier: useDuration,
|
||||||
|
text: "duration".t(context: context),
|
||||||
|
minValue: 5,
|
||||||
|
noDivisons: 5,
|
||||||
|
maxValue: 30,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 20),
|
||||||
|
child: SettingsSubTitle(title: 'look'.t(context: context)),
|
||||||
|
),
|
||||||
|
SettingsRadioListTile(
|
||||||
|
groups: [
|
||||||
|
SettingsRadioGroup(
|
||||||
|
title: 'contain'.t(context: context),
|
||||||
|
value: SlideshowLook.contain,
|
||||||
|
),
|
||||||
|
SettingsRadioGroup(
|
||||||
|
title: 'cover'.t(context: context),
|
||||||
|
value: SlideshowLook.cover,
|
||||||
|
),
|
||||||
|
SettingsRadioGroup(
|
||||||
|
title: 'blurred_background'.t(context: context),
|
||||||
|
value: SlideshowLook.blurredBackground,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
groupBy: useLook.value,
|
||||||
|
onRadioChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
useLook.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 20),
|
||||||
|
child: SettingsSubTitle(title: 'direction'.t(context: context)),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 32),
|
||||||
|
child: SettingsRadioListTile(
|
||||||
|
groups: [
|
||||||
|
SettingsRadioGroup(
|
||||||
|
title: 'forward'.t(context: context),
|
||||||
|
value: SlideshowDirection.forward,
|
||||||
|
),
|
||||||
|
SettingsRadioGroup(
|
||||||
|
title: 'backward'.t(context: context),
|
||||||
|
value: SlideshowDirection.backward,
|
||||||
|
),
|
||||||
|
SettingsRadioGroup(
|
||||||
|
title: 'shuffle'.t(context: context),
|
||||||
|
value: SlideshowDirection.shuffle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
groupBy: useDirection.value,
|
||||||
|
onRadioChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
useDirection.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue