This commit is contained in:
Thibault Deckers 2022-06-14 23:22:08 +09:00
parent 5317750506
commit 43b2a5c1c1
42 changed files with 1130 additions and 155 deletions

View file

@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- slideshow
- set wallpaper from any media - set wallpaper from any media
- optional dynamic accent color on Android 12+ - optional dynamic accent color on Android 12+
- support Android 13 (API 33) - support Android 13 (API 33)

View file

@ -1,4 +1,13 @@
enum AppMode { main, pickSingleMediaExternal, pickMultipleMediaExternal, pickMediaInternal, pickFilterInternal, setWallpaper, view } enum AppMode {
main,
pickSingleMediaExternal,
pickMultipleMediaExternal,
pickMediaInternal,
pickFilterInternal,
setWallpaper,
slideshow,
view,
}
extension ExtraAppMode on AppMode { extension ExtraAppMode on AppMode {
bool get canSearch => this == AppMode.main || this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal; bool get canSearch => this == AppMode.main || this == AppMode.pickSingleMediaExternal || this == AppMode.pickMultipleMediaExternal;

View file

@ -109,6 +109,9 @@
"videoActionSetSpeed": "Playback speed", "videoActionSetSpeed": "Playback speed",
"videoActionSettings": "Settings", "videoActionSettings": "Settings",
"slideshowActionResume": "Resume",
"slideshowActionShowInCollection": "Show in Collection",
"entryInfoActionEditDate": "Edit date & time", "entryInfoActionEditDate": "Edit date & time",
"entryInfoActionEditLocation": "Edit location", "entryInfoActionEditLocation": "Edit location",
"entryInfoActionEditRating": "Edit rating", "entryInfoActionEditRating": "Edit rating",
@ -185,10 +188,19 @@
"displayRefreshRatePreferHighest": "Highest rate", "displayRefreshRatePreferHighest": "Highest rate",
"displayRefreshRatePreferLowest": "Lowest rate", "displayRefreshRatePreferLowest": "Lowest rate",
"slideshowVideoPlaybackSkip": "Skip",
"slideshowVideoPlaybackMuted": "Play muted",
"slideshowVideoPlaybackWithSound": "Play with sound",
"themeBrightnessLight": "Light", "themeBrightnessLight": "Light",
"themeBrightnessDark": "Dark", "themeBrightnessDark": "Dark",
"themeBrightnessBlack": "Black", "themeBrightnessBlack": "Black",
"viewerTransitionFade": "Fade",
"viewerTransitionFadeZoomIn": "Fade & zoom in",
"viewerTransitionParallax": "Parallax",
"viewerTransitionSlide": "Slide",
"wallpaperTargetHome": "Home screen", "wallpaperTargetHome": "Home screen",
"wallpaperTargetLock": "Lock screen", "wallpaperTargetLock": "Lock screen",
"wallpaperTargetHomeLock": "Home and lock screens", "wallpaperTargetHomeLock": "Home and lock screens",
@ -397,6 +409,7 @@
"menuActionSelectAll": "Select all", "menuActionSelectAll": "Select all",
"menuActionSelectNone": "Select none", "menuActionSelectNone": "Select none",
"menuActionMap": "Map", "menuActionMap": "Map",
"menuActionSlideshow": "Slideshow",
"menuActionStats": "Stats", "menuActionStats": "Stats",
"viewDialogTabSort": "Sort", "viewDialogTabSort": "Sort",
@ -665,6 +678,17 @@
"settingsViewerShowOverlayThumbnails": "Show thumbnails", "settingsViewerShowOverlayThumbnails": "Show thumbnails",
"settingsViewerEnableOverlayBlurEffect": "Blur effect", "settingsViewerEnableOverlayBlurEffect": "Blur effect",
"settingsViewerSlideshowTile": "Slideshow",
"settingsViewerSlideshowTitle": "Slideshow",
"settingsSlideshowRepeat": "Repeat",
"settingsSlideshowShuffle": "Shuffle",
"settingsSlideshowTransitionTile": "Transition",
"settingsSlideshowTransitionTitle": "Transition",
"settingsSlideshowIntervalTile": "Interval",
"settingsSlideshowIntervalTitle": "Interval",
"settingsSlideshowVideoPlaybackTile": "Video playback",
"settingsSlideshowVideoPlaybackTitle": "Video Playback",
"settingsVideoPageTitle": "Video Settings", "settingsVideoPageTitle": "Video Settings",
"settingsSectionVideo": "Video", "settingsSectionVideo": "Video",
"settingsVideoShowVideos": "Show videos", "settingsVideoShowVideos": "Show videos",

View file

@ -13,6 +13,7 @@ enum ChipSetAction {
createAlbum, createAlbum,
// browsing or selecting // browsing or selecting
map, map,
slideshow,
stats, stats,
// selecting (single/multiple filters) // selecting (single/multiple filters)
delete, delete,
@ -36,6 +37,7 @@ class ChipSetActions {
ChipSetAction.search, ChipSetAction.search,
ChipSetAction.createAlbum, ChipSetAction.createAlbum,
ChipSetAction.map, ChipSetAction.map,
ChipSetAction.slideshow,
ChipSetAction.stats, ChipSetAction.stats,
]; ];
@ -47,6 +49,7 @@ class ChipSetActions {
ChipSetAction.rename, ChipSetAction.rename,
ChipSetAction.hide, ChipSetAction.hide,
ChipSetAction.map, ChipSetAction.map,
ChipSetAction.slideshow,
ChipSetAction.stats, ChipSetAction.stats,
]; ];
} }
@ -71,6 +74,8 @@ extension ExtraChipSetAction on ChipSetAction {
// browsing or selecting // browsing or selecting
case ChipSetAction.map: case ChipSetAction.map:
return context.l10n.menuActionMap; return context.l10n.menuActionMap;
case ChipSetAction.slideshow:
return context.l10n.menuActionSlideshow;
case ChipSetAction.stats: case ChipSetAction.stats:
return context.l10n.menuActionStats; return context.l10n.menuActionStats;
// selecting (single/multiple filters) // selecting (single/multiple filters)
@ -111,6 +116,8 @@ extension ExtraChipSetAction on ChipSetAction {
// browsing or selecting // browsing or selecting
case ChipSetAction.map: case ChipSetAction.map:
return AIcons.map; return AIcons.map;
case ChipSetAction.slideshow:
return AIcons.slideshow;
case ChipSetAction.stats: case ChipSetAction.stats:
return AIcons.stats; return AIcons.stats;
// selecting (single/multiple filters) // selecting (single/multiple filters)

View file

@ -15,6 +15,7 @@ enum EntrySetAction {
emptyBin, emptyBin,
// browsing or selecting // browsing or selecting
map, map,
slideshow,
stats, stats,
rescan, rescan,
// selecting // selecting
@ -48,6 +49,7 @@ class EntrySetActions {
EntrySetAction.toggleTitleSearch, EntrySetAction.toggleTitleSearch,
EntrySetAction.addShortcut, EntrySetAction.addShortcut,
EntrySetAction.map, EntrySetAction.map,
EntrySetAction.slideshow,
EntrySetAction.stats, EntrySetAction.stats,
EntrySetAction.rescan, EntrySetAction.rescan,
EntrySetAction.emptyBin, EntrySetAction.emptyBin,
@ -59,6 +61,7 @@ class EntrySetActions {
EntrySetAction.toggleTitleSearch, EntrySetAction.toggleTitleSearch,
EntrySetAction.addShortcut, EntrySetAction.addShortcut,
EntrySetAction.map, EntrySetAction.map,
EntrySetAction.slideshow,
EntrySetAction.stats, EntrySetAction.stats,
EntrySetAction.rescan, EntrySetAction.rescan,
]; ];
@ -72,6 +75,7 @@ class EntrySetActions {
EntrySetAction.rename, EntrySetAction.rename,
EntrySetAction.toggleFavourite, EntrySetAction.toggleFavourite,
EntrySetAction.map, EntrySetAction.map,
EntrySetAction.slideshow,
EntrySetAction.stats, EntrySetAction.stats,
EntrySetAction.rescan, EntrySetAction.rescan,
// editing actions are in their subsection // editing actions are in their subsection
@ -86,6 +90,7 @@ class EntrySetActions {
EntrySetAction.rename, EntrySetAction.rename,
EntrySetAction.toggleFavourite, EntrySetAction.toggleFavourite,
EntrySetAction.map, EntrySetAction.map,
EntrySetAction.slideshow,
EntrySetAction.stats, EntrySetAction.stats,
EntrySetAction.rescan, EntrySetAction.rescan,
// editing actions are in their subsection // editing actions are in their subsection
@ -125,6 +130,8 @@ extension ExtraEntrySetAction on EntrySetAction {
// browsing or selecting // browsing or selecting
case EntrySetAction.map: case EntrySetAction.map:
return context.l10n.menuActionMap; return context.l10n.menuActionMap;
case EntrySetAction.slideshow:
return context.l10n.menuActionSlideshow;
case EntrySetAction.stats: case EntrySetAction.stats:
return context.l10n.menuActionStats; return context.l10n.menuActionStats;
case EntrySetAction.rescan: case EntrySetAction.rescan:
@ -190,6 +197,8 @@ extension ExtraEntrySetAction on EntrySetAction {
// browsing or selecting // browsing or selecting
case EntrySetAction.map: case EntrySetAction.map:
return AIcons.map; return AIcons.map;
case EntrySetAction.slideshow:
return AIcons.slideshow;
case EntrySetAction.stats: case EntrySetAction.stats:
return AIcons.stats; return AIcons.stats;
case EntrySetAction.rescan: case EntrySetAction.rescan:

View file

@ -0,0 +1,30 @@
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
enum SlideshowAction {
resume,
showInCollection,
}
extension ExtraSlideshowAction on SlideshowAction {
String getText(BuildContext context) {
switch (this) {
case SlideshowAction.resume:
return context.l10n.slideshowActionResume;
case SlideshowAction.showInCollection:
return context.l10n.slideshowActionShowInCollection;
}
}
Widget getIcon() => Icon(_getIconData());
IconData _getIconData() {
switch (this) {
case SlideshowAction.resume:
return AIcons.play;
case SlideshowAction.showInCollection:
return AIcons.allCollection;
}
}
}

View file

@ -125,6 +125,13 @@ class SettingsDefaults {
// file picker // file picker
static const filePickerShowHiddenFiles = false; static const filePickerShowHiddenFiles = false;
// slideshow
static const slideshowRepeat = false;
static const slideshowShuffle = false;
static const slideshowTransition = ViewerTransition.fade;
static const slideshowVideoPlayback = SlideshowVideoPlayback.playMuted;
static const slideshowInterval = SlideshowInterval.s5;
// platform settings // platform settings
static const isRotationLocked = false; static const isRotationLocked = false;
static const areAnimationsRemoved = false; static const areAnimationsRemoved = false;

View file

@ -2,24 +2,30 @@ enum AccessibilityAnimations { system, disabled, enabled }
enum AccessibilityTimeout { system, appDefault, s3, s10, s30, s60, s120 } enum AccessibilityTimeout { system, appDefault, s3, s10, s30, s60, s120 }
enum AvesThemeColorMode { monochrome, polychrome }
enum AvesThemeBrightness { system, light, dark, black } enum AvesThemeBrightness { system, light, dark, black }
enum AvesThemeColorMode { monochrome, polychrome }
enum ConfirmationDialog { deleteForever, moveToBin, moveUndatedItems } enum ConfirmationDialog { deleteForever, moveToBin, moveUndatedItems }
enum CoordinateFormat { dms, decimal } enum CoordinateFormat { dms, decimal }
enum DisplayRefreshRateMode { auto, highest, lowest }
enum EntryBackground { black, white, checkered } enum EntryBackground { black, white, checkered }
enum HomePageSetting { collection, albums } enum HomePageSetting { collection, albums }
enum KeepScreenOn { never, viewerOnly, always } enum KeepScreenOn { never, viewerOnly, always }
enum DisplayRefreshRateMode { auto, highest, lowest } enum SlideshowInterval { s3, s5, s10, s30, s60 }
enum SlideshowVideoPlayback { skip, playMuted, playWithSound }
enum UnitSystem { metric, imperial } enum UnitSystem { metric, imperial }
enum VideoControls { play, playSeek, playOutside, none }
enum VideoLoopMode { never, shortOnly, always } enum VideoLoopMode { never, shortOnly, always }
enum VideoControls { play, playSeek, playOutside, none } enum ViewerTransition { slide, parallax, fade, fadeZoomIn }

View file

@ -0,0 +1,36 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraSlideshowInterval on SlideshowInterval {
String getName(BuildContext context) {
switch (this) {
case SlideshowInterval.s3:
return context.l10n.timeSeconds(3);
case SlideshowInterval.s5:
return context.l10n.timeSeconds(5);
case SlideshowInterval.s10:
return context.l10n.timeSeconds(10);
case SlideshowInterval.s30:
return context.l10n.timeSeconds(30);
case SlideshowInterval.s60:
return context.l10n.timeMinutes(1);
}
}
Duration getDuration() {
switch (this) {
case SlideshowInterval.s3:
return const Duration(seconds: 3);
case SlideshowInterval.s5:
return const Duration(seconds: 5);
case SlideshowInterval.s10:
return const Duration(seconds: 10);
case SlideshowInterval.s30:
return const Duration(seconds: 30);
case SlideshowInterval.s60:
return const Duration(minutes: 1);
}
}
}

View file

@ -0,0 +1,17 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraSlideshowVideoPlayback on SlideshowVideoPlayback {
String getName(BuildContext context) {
switch (this) {
case SlideshowVideoPlayback.skip:
return context.l10n.slideshowVideoPlaybackSkip;
case SlideshowVideoPlayback.playMuted:
return context.l10n.slideshowVideoPlaybackMuted;
case SlideshowVideoPlayback.playWithSound:
return context.l10n.slideshowVideoPlaybackWithSound;
}
}
}

View file

@ -0,0 +1,33 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/viewer/controller.dart';
import 'package:flutter/widgets.dart';
import 'enums.dart';
extension ExtraViewerTransition on ViewerTransition {
String getName(BuildContext context) {
switch (this) {
case ViewerTransition.fade:
return context.l10n.viewerTransitionFade;
case ViewerTransition.fadeZoomIn:
return context.l10n.viewerTransitionFadeZoomIn;
case ViewerTransition.parallax:
return context.l10n.viewerTransitionParallax;
case ViewerTransition.slide:
return context.l10n.viewerTransitionSlide;
}
}
TransitionBuilder builder(PageController pageController, int index) {
switch (this) {
case ViewerTransition.slide:
return PageTransitionEffects.slide(pageController, index, parallax: false);
case ViewerTransition.parallax:
return PageTransitionEffects.slide(pageController, index, parallax: true);
case ViewerTransition.fade:
return PageTransitionEffects.fade(pageController, index, zoomIn: false);
case ViewerTransition.fadeZoomIn:
return PageTransitionEffects.fade(pageController, index, zoomIn: true);
}
}
}

View file

@ -138,6 +138,13 @@ class Settings extends ChangeNotifier {
// file picker // file picker
static const filePickerShowHiddenFilesKey = 'file_picker_show_hidden_files'; static const filePickerShowHiddenFilesKey = 'file_picker_show_hidden_files';
// slideshow
static const slideshowRepeatKey = 'slideshow_loop';
static const slideshowShuffleKey = 'slideshow_shuffle';
static const slideshowTransitionKey = 'slideshow_transition';
static const slideshowVideoPlaybackKey = 'slideshow_video_playback';
static const slideshowIntervalKey = 'slideshow_interval';
// platform settings // platform settings
// cf Android `Settings.System.ACCELEROMETER_ROTATION` // cf Android `Settings.System.ACCELEROMETER_ROTATION`
static const platformAccelerometerRotationKey = 'accelerometer_rotation'; static const platformAccelerometerRotationKey = 'accelerometer_rotation';
@ -576,6 +583,28 @@ class Settings extends ChangeNotifier {
set filePickerShowHiddenFiles(bool newValue) => setAndNotify(filePickerShowHiddenFilesKey, newValue); set filePickerShowHiddenFiles(bool newValue) => setAndNotify(filePickerShowHiddenFilesKey, newValue);
// slideshow
bool get slideshowRepeat => getBoolOrDefault(slideshowRepeatKey, SettingsDefaults.slideshowRepeat);
set slideshowRepeat(bool newValue) => setAndNotify(slideshowRepeatKey, newValue);
bool get slideshowShuffle => getBoolOrDefault(slideshowShuffleKey, SettingsDefaults.slideshowShuffle);
set slideshowShuffle(bool newValue) => setAndNotify(slideshowShuffleKey, newValue);
ViewerTransition get slideshowTransition => getEnumOrDefault(slideshowTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values);
set slideshowTransition(ViewerTransition newValue) => setAndNotify(slideshowTransitionKey, newValue.toString());
SlideshowVideoPlayback get slideshowVideoPlayback => getEnumOrDefault(slideshowVideoPlaybackKey, SettingsDefaults.slideshowVideoPlayback, SlideshowVideoPlayback.values);
set slideshowVideoPlayback(SlideshowVideoPlayback newValue) => setAndNotify(slideshowVideoPlaybackKey, newValue.toString());
SlideshowInterval get slideshowInterval => getEnumOrDefault(slideshowIntervalKey, SettingsDefaults.slideshowInterval, SlideshowInterval.values);
set slideshowInterval(SlideshowInterval newValue) => setAndNotify(slideshowIntervalKey, newValue.toString());
// convenience methods // convenience methods
int? getInt(String key) => settingsStore.getInt(key); int? getInt(String key) => settingsStore.getInt(key);
@ -734,6 +763,8 @@ class Settings extends ChangeNotifier {
case subtitleShowOutlineKey: case subtitleShowOutlineKey:
case saveSearchHistoryKey: case saveSearchHistoryKey:
case filePickerShowHiddenFilesKey: case filePickerShowHiddenFilesKey:
case slideshowRepeatKey:
case slideshowShuffleKey:
if (newValue is bool) { if (newValue is bool) {
settingsStore.setBool(key, newValue); settingsStore.setBool(key, newValue);
} else { } else {
@ -761,6 +792,9 @@ class Settings extends ChangeNotifier {
case unitSystemKey: case unitSystemKey:
case accessibilityAnimationsKey: case accessibilityAnimationsKey:
case timeToTakeActionKey: case timeToTakeActionKey:
case slideshowTransitionKey:
case slideshowVideoPlaybackKey:
case slideshowIntervalKey:
if (newValue is String) { if (newValue is String) {
settingsStore.setString(key, newValue); settingsStore.setString(key, newValue);
} else { } else {

View file

@ -32,7 +32,7 @@ class CollectionLens with ChangeNotifier {
final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier(); final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier();
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
int? id; int? id;
bool listenToSource, groupBursts; bool listenToSource, groupBursts, fixedSort;
List<AvesEntry>? fixedSelection; List<AvesEntry>? fixedSelection;
List<AvesEntry> _filteredSortedEntries = []; List<AvesEntry> _filteredSortedEntries = [];
@ -45,6 +45,7 @@ class CollectionLens with ChangeNotifier {
this.id, this.id,
this.listenToSource = true, this.listenToSource = true,
this.groupBursts = true, this.groupBursts = true,
this.fixedSort = false,
this.fixedSelection, this.fixedSelection,
}) : filters = (filters ?? {}).whereNotNull().toSet(), }) : filters = (filters ?? {}).whereNotNull().toSet(),
sectionFactor = settings.collectionSectionFactor, sectionFactor = settings.collectionSectionFactor,
@ -203,6 +204,8 @@ class CollectionLens with ChangeNotifier {
} }
void _applySort() { void _applySort() {
if (fixedSort) return;
switch (sortFactor) { switch (sortFactor) {
case EntrySortFactor.date: case EntrySortFactor.date:
_filteredSortedEntries.sort(AvesEntry.compareByDate); _filteredSortedEntries.sort(AvesEntry.compareByDate);
@ -220,37 +223,43 @@ class CollectionLens with ChangeNotifier {
} }
void _applySection() { void _applySection() {
switch (sortFactor) { if (fixedSort) {
case EntrySortFactor.date: sections = Map.fromEntries([
switch (sectionFactor) { MapEntry(const SectionKey(), _filteredSortedEntries),
case EntryGroupFactor.album: ]);
sections = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory)); } else {
break; switch (sortFactor) {
case EntryGroupFactor.month: case EntrySortFactor.date:
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.monthTaken)); switch (sectionFactor) {
break; case EntryGroupFactor.album:
case EntryGroupFactor.day: sections = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.dayTaken)); break;
break; case EntryGroupFactor.month:
case EntryGroupFactor.none: sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.monthTaken));
sections = Map.fromEntries([ break;
MapEntry(const SectionKey(), _filteredSortedEntries), case EntryGroupFactor.day:
]); sections = groupBy<AvesEntry, EntryDateSectionKey>(_filteredSortedEntries, (entry) => EntryDateSectionKey(entry.dayTaken));
break; break;
} case EntryGroupFactor.none:
break; sections = Map.fromEntries([
case EntrySortFactor.name: MapEntry(const SectionKey(), _filteredSortedEntries),
final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory)); ]);
sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory!, b.directory!)); break;
break; }
case EntrySortFactor.rating: break;
sections = groupBy<AvesEntry, EntryRatingSectionKey>(_filteredSortedEntries, (entry) => EntryRatingSectionKey(entry.rating)); case EntrySortFactor.name:
break; final byAlbum = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
case EntrySortFactor.size: sections = SplayTreeMap<EntryAlbumSectionKey, List<AvesEntry>>.of(byAlbum, (a, b) => source.compareAlbumsByName(a.directory!, b.directory!));
sections = Map.fromEntries([ break;
MapEntry(const SectionKey(), _filteredSortedEntries), case EntrySortFactor.rating:
]); sections = groupBy<AvesEntry, EntryRatingSectionKey>(_filteredSortedEntries, (entry) => EntryRatingSectionKey(entry.rating));
break; break;
case EntrySortFactor.size:
sections = Map.fromEntries([
MapEntry(const SectionKey(), _filteredSortedEntries),
]);
break;
}
} }
sections = Map.unmodifiable(sections); sections = Map.unmodifiable(sections);
_sortedEntries = null; _sortedEntries = null;

View file

@ -104,6 +104,7 @@ class AIcons {
static const IconData setCover = MdiIcons.imageEditOutline; static const IconData setCover = MdiIcons.imageEditOutline;
static const IconData share = Icons.share_outlined; static const IconData share = Icons.share_outlined;
static const IconData show = Icons.visibility_outlined; static const IconData show = Icons.visibility_outlined;
static const IconData slideshow = Icons.slideshow_outlined;
static const IconData speed = Icons.speed_outlined; static const IconData speed = Icons.speed_outlined;
static const IconData stats = Icons.pie_chart_outline_outlined; static const IconData stats = Icons.pie_chart_outline_outlined;
static const IconData streams = Icons.translate_outlined; static const IconData streams = Icons.translate_outlined;

View file

@ -231,6 +231,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
case AppMode.pickMediaInternal: case AppMode.pickMediaInternal:
case AppMode.pickFilterInternal: case AppMode.pickFilterInternal:
case AppMode.setWallpaper: case AppMode.setWallpaper:
case AppMode.slideshow:
case AppMode.view: case AppMode.view:
break; break;
} }

View file

@ -477,6 +477,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
case EntrySetAction.addShortcut: case EntrySetAction.addShortcut:
// browsing or selecting // browsing or selecting
case EntrySetAction.map: case EntrySetAction.map:
case EntrySetAction.slideshow:
case EntrySetAction.stats: case EntrySetAction.stats:
case EntrySetAction.rescan: case EntrySetAction.rescan:
case EntrySetAction.emptyBin: case EntrySetAction.emptyBin:

View file

@ -35,6 +35,7 @@ import 'package:aves/widgets/dialogs/entry_editors/rename_entry_set_dialog.dart'
import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/map/map_page.dart';
import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/search/search_delegate.dart';
import 'package:aves/widgets/stats/stats_page.dart'; import 'package:aves/widgets/stats/stats_page.dart';
import 'package:aves/widgets/viewer/slideshow_page.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@ -73,6 +74,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
return appMode == AppMode.main && isTrash; return appMode == AppMode.main && isTrash;
// browsing or selecting // browsing or selecting
case EntrySetAction.map: case EntrySetAction.map:
case EntrySetAction.slideshow:
case EntrySetAction.stats: case EntrySetAction.stats:
return appMode == AppMode.main; return appMode == AppMode.main;
case EntrySetAction.rescan: case EntrySetAction.rescan:
@ -124,6 +126,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.emptyBin: case EntrySetAction.emptyBin:
return !isSelecting && hasItems; return !isSelecting && hasItems;
case EntrySetAction.map: case EntrySetAction.map:
case EntrySetAction.slideshow:
case EntrySetAction.stats: case EntrySetAction.stats:
case EntrySetAction.rescan: case EntrySetAction.rescan:
return (!isSelecting && hasItems) || (isSelecting && hasSelection); return (!isSelecting && hasItems) || (isSelecting && hasSelection);
@ -169,6 +172,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
case EntrySetAction.map: case EntrySetAction.map:
_goToMap(context); _goToMap(context);
break; break;
case EntrySetAction.slideshow:
_goToSlideshow(context);
break;
case EntrySetAction.stats: case EntrySetAction.stats:
_goToStats(context); _goToStats(context);
break; break;
@ -543,6 +549,27 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
); );
} }
void _goToSlideshow(BuildContext context) {
final collection = context.read<CollectionLens>();
final entries = _getTargetItems(context);
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: SlideshowPage.routeName),
builder: (context) {
return SlideshowPage(
collection: CollectionLens(
source: collection.source,
filters: collection.filters,
fixedSelection: entries.toList(),
),
);
},
),
);
}
void _goToStats(BuildContext context) { void _goToStats(BuildContext context) {
final collection = context.read<CollectionLens>(); final collection = context.read<CollectionLens>();
final entries = _getTargetItems(context); final entries = _getTargetItems(context);

View file

@ -55,6 +55,7 @@ class InteractiveTile extends StatelessWidget {
break; break;
case AppMode.pickFilterInternal: case AppMode.pickFilterInternal:
case AppMode.setWallpaper: case AppMode.setWallpaper:
case AppMode.slideshow:
case AppMode.view: case AppMode.view:
break; break;
} }

View file

@ -38,7 +38,7 @@ class AvesHighlightView extends StatelessWidget {
this.padding, this.padding,
this.textStyle, this.textStyle,
int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087 int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087
}) : source = input.replaceAll('\t', ' ' * tabSize); }) : source = input.replaceAll('\t', ' ' * tabSize);
List<TextSpan> _convert(List<Node> nodes) { List<TextSpan> _convert(List<Node> nodes) {
final spans = <TextSpan>[]; final spans = <TextSpan>[];

View file

@ -35,7 +35,7 @@ class ReselectableRadioListTile<T> extends StatelessWidget {
this.selected = false, this.selected = false,
this.controlAffinity = ListTileControlAffinity.platform, this.controlAffinity = ListTileControlAffinity.platform,
this.autofocus = false, this.autofocus = false,
}) : assert(!isThreeLine || subtitle != null); }) : assert(!isThreeLine || subtitle != null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -23,7 +23,7 @@ class AvesExpansionTile extends StatelessWidget {
this.initiallyExpanded = false, this.initiallyExpanded = false,
this.showHighlight = true, this.showHighlight = true,
required this.children, required this.children,
}) : value = value ?? title; }) : value = value ?? title;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -22,6 +22,7 @@ import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/map/map_page.dart';
import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/search/search_delegate.dart';
import 'package:aves/widgets/stats/stats_page.dart'; import 'package:aves/widgets/stats/stats_page.dart';
import 'package:aves/widgets/viewer/slideshow_page.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@ -65,6 +66,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
return false; return false;
// browsing or selecting // browsing or selecting
case ChipSetAction.map: case ChipSetAction.map:
case ChipSetAction.slideshow:
case ChipSetAction.stats: case ChipSetAction.stats:
return appMode == AppMode.main; return appMode == AppMode.main;
// selecting (single/multiple filters) // selecting (single/multiple filters)
@ -106,6 +108,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
return true; return true;
// browsing or selecting // browsing or selecting
case ChipSetAction.map: case ChipSetAction.map:
case ChipSetAction.slideshow:
case ChipSetAction.stats: case ChipSetAction.stats:
return (!isSelecting && hasItems) || (isSelecting && hasSelection); return (!isSelecting && hasItems) || (isSelecting && hasSelection);
// selecting (single/multiple filters) // selecting (single/multiple filters)
@ -146,6 +149,9 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
case ChipSetAction.map: case ChipSetAction.map:
_goToMap(context, filters); _goToMap(context, filters);
break; break;
case ChipSetAction.slideshow:
_goToSlideshow(context, filters);
break;
case ChipSetAction.stats: case ChipSetAction.stats:
_goToStats(context, filters); _goToStats(context, filters);
break; break;
@ -227,6 +233,23 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> with FeedbackMi
); );
} }
void _goToSlideshow(BuildContext context, Set<T> filters) {
Navigator.push(
context,
MaterialPageRoute(
settings: const RouteSettings(name: SlideshowPage.routeName),
builder: (context) {
return SlideshowPage(
collection: CollectionLens(
source: context.read<CollectionSource>(),
fixedSelection: _selectedEntries(context, filters).toList(),
),
);
},
),
);
}
void _goToStats(BuildContext context, Set<T> filters) { void _goToStats(BuildContext context, Set<T> filters) {
Navigator.push( Navigator.push(
context, context,

View file

@ -64,6 +64,7 @@ class _InteractiveFilterTileState<T extends CollectionFilter> extends State<Inte
break; break;
case AppMode.pickMediaInternal: case AppMode.pickMediaInternal:
case AppMode.setWallpaper: case AppMode.setWallpaper:
case AppMode.slideshow:
case AppMode.view: case AppMode.view:
break; break;
} }

View file

@ -162,6 +162,7 @@ class _HomePageState extends State<HomePage> {
case AppMode.pickMediaInternal: case AppMode.pickMediaInternal:
case AppMode.pickFilterInternal: case AppMode.pickFilterInternal:
case AppMode.setWallpaper: case AppMode.setWallpaper:
case AppMode.slideshow:
break; break;
} }
@ -286,7 +287,7 @@ class _HomePageState extends State<HomePage> {
default: default:
return DirectMaterialPageRoute( return DirectMaterialPageRoute(
settings: const RouteSettings(name: CollectionPage.routeName), settings: const RouteSettings(name: CollectionPage.routeName),
builder: (_) => CollectionPage( builder: (context) => CollectionPage(
source: source, source: source,
filters: filters, filters: filters,
), ),

View file

@ -414,12 +414,10 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: CollectionPage.routeName), settings: const RouteSettings(name: CollectionPage.routeName),
builder: (context) { builder: (context) => CollectionPage(
return CollectionPage( source: openingCollection.source,
source: openingCollection.source, filters: {...openingCollection.filters, filter},
filters: {...openingCollection.filters, filter}, ),
);
},
), ),
(route) => false, (route) => false,
); );

View file

@ -70,10 +70,10 @@ class SettingsTileDisplayEnableDynamicColor extends SettingsTile {
@override @override
Widget build(BuildContext context) => SettingsSwitchListTile( Widget build(BuildContext context) => SettingsSwitchListTile(
selector: (context, s) => s.enableDynamicColor, selector: (context, s) => s.enableDynamicColor,
onChanged: (v) => settings.enableDynamicColor = v, onChanged: (v) => settings.enableDynamicColor = v,
title: title(context), title: title(context),
); );
} }
class SettingsTileDisplayEnableBlurEffect extends SettingsTile { class SettingsTileDisplayEnableBlurEffect extends SettingsTile {
@ -82,10 +82,10 @@ class SettingsTileDisplayEnableBlurEffect extends SettingsTile {
@override @override
Widget build(BuildContext context) => SettingsSwitchListTile( Widget build(BuildContext context) => SettingsSwitchListTile(
selector: (context, s) => s.enableBlurEffect, selector: (context, s) => s.enableBlurEffect,
onChanged: (v) => settings.enableBlurEffect = v, onChanged: (v) => settings.enableBlurEffect = v,
title: title(context), title: title(context),
); );
} }
class SettingsTileDisplayDisplayRefreshRateMode extends SettingsTile { class SettingsTileDisplayDisplayRefreshRateMode extends SettingsTile {

View file

@ -6,7 +6,7 @@ import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class ViewerOverlayPage extends StatelessWidget { class ViewerOverlayPage extends StatelessWidget {
static const routeName = '/settings/viewer_overlay'; static const routeName = '/settings/viewer/overlay';
const ViewerOverlayPage({super.key}); const ViewerOverlayPage({super.key});

View file

@ -0,0 +1,63 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/slideshow_interval.dart';
import 'package:aves/model/settings/enums/slideshow_video_playback.dart';
import 'package:aves/model/settings/enums/viewer_transition.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/common/tiles.dart';
import 'package:flutter/material.dart';
class ViewerSlideshowPage extends StatelessWidget {
static const routeName = '/settings/viewer/slideshow';
const ViewerSlideshowPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.settingsViewerSlideshowTitle),
),
body: SafeArea(
child: ListView(
children: [
SettingsSwitchListTile(
selector: (context, s) => s.slideshowRepeat,
onChanged: (v) => settings.slideshowRepeat = v,
title: context.l10n.settingsSlideshowRepeat,
),
SettingsSwitchListTile(
selector: (context, s) => s.slideshowShuffle,
onChanged: (v) => settings.slideshowShuffle = v,
title: context.l10n.settingsSlideshowShuffle,
),
SettingsSelectionListTile<ViewerTransition>(
values: ViewerTransition.values,
getName: (context, v) => v.getName(context),
selector: (context, s) => s.slideshowTransition,
onSelection: (v) => settings.slideshowTransition = v,
tileTitle: context.l10n.settingsSlideshowTransitionTile,
dialogTitle: context.l10n.settingsSlideshowTransitionTitle,
),
SettingsSelectionListTile<SlideshowInterval>(
values: SlideshowInterval.values,
getName: (context, v) => v.getName(context),
selector: (context, s) => s.slideshowInterval,
onSelection: (v) => settings.slideshowInterval = v,
tileTitle: context.l10n.settingsSlideshowIntervalTile,
dialogTitle: context.l10n.settingsSlideshowIntervalTitle,
),
SettingsSelectionListTile<SlideshowVideoPlayback>(
values: SlideshowVideoPlayback.values,
getName: (context, v) => v.getName(context),
selector: (context, s) => s.slideshowVideoPlayback,
onSelection: (v) => settings.slideshowVideoPlayback = v,
tileTitle: context.l10n.settingsSlideshowVideoPlaybackTile,
dialogTitle: context.l10n.settingsSlideshowVideoPlaybackTitle,
),
],
),
),
);
}
}

View file

@ -11,6 +11,7 @@ import 'package:aves/widgets/settings/common/tiles.dart';
import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:aves/widgets/settings/settings_definition.dart';
import 'package:aves/widgets/settings/viewer/entry_background.dart'; import 'package:aves/widgets/settings/viewer/entry_background.dart';
import 'package:aves/widgets/settings/viewer/overlay.dart'; import 'package:aves/widgets/settings/viewer/overlay.dart';
import 'package:aves/widgets/settings/viewer/slideshow.dart';
import 'package:aves/widgets/settings/viewer/viewer_actions_editor.dart'; import 'package:aves/widgets/settings/viewer/viewer_actions_editor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -34,6 +35,7 @@ class ViewerSection extends SettingsSection {
return [ return [
SettingsTileViewerQuickActions(), SettingsTileViewerQuickActions(),
SettingsTileViewerOverlay(), SettingsTileViewerOverlay(),
SettingsTileViewerSlideshow(),
if (canSetCutoutMode) SettingsTileViewerCutoutMode(), if (canSetCutoutMode) SettingsTileViewerCutoutMode(),
SettingsTileViewerMaxBrightness(), SettingsTileViewerMaxBrightness(),
SettingsTileViewerMotionPhotoAutoPlay(), SettingsTileViewerMotionPhotoAutoPlay(),
@ -66,6 +68,18 @@ class SettingsTileViewerOverlay extends SettingsTile {
); );
} }
class SettingsTileViewerSlideshow extends SettingsTile {
@override
String title(BuildContext context) => context.l10n.settingsViewerSlideshowTile;
@override
Widget build(BuildContext context) => SettingsSubPageTile(
title: title(context),
routeName: ViewerSlideshowPage.routeName,
builder: (context) => const ViewerSlideshowPage(),
);
}
class SettingsTileViewerCutoutMode extends SettingsTile { class SettingsTileViewerCutoutMode extends SettingsTile {
@override @override
String title(BuildContext context) => context.l10n.settingsViewerUseCutout; String title(BuildContext context) => context.l10n.settingsViewerUseCutout;

View file

@ -5,7 +5,7 @@ import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ViewerActionEditorPage extends StatelessWidget { class ViewerActionEditorPage extends StatelessWidget {
static const routeName = '/settings/viewer_actions'; static const routeName = '/settings/viewer/actions';
const ViewerActionEditorPage({super.key}); const ViewerActionEditorPage({super.key});

View file

@ -0,0 +1,119 @@
import 'dart:async';
import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:flutter/widgets.dart';
class ViewerController {
final ValueNotifier<AvesEntry?> entryNotifier = ValueNotifier(null);
final ViewerTransition transition;
final Duration? autopilotInterval;
final bool repeat;
late final ValueNotifier<bool> _autopilotNotifier;
Timer? _playTimer;
final StreamController _streamController = StreamController.broadcast();
Stream<dynamic> get _events => _streamController.stream;
Stream<ViewerShowNextEvent> get showNextCommands => _events.where((event) => event is ViewerShowNextEvent).cast<ViewerShowNextEvent>();
Stream<ViewerOverlayToggleEvent> get overlayCommands => _events.where((event) => event is ViewerOverlayToggleEvent).cast<ViewerOverlayToggleEvent>();
bool get autopilot => _autopilotNotifier.value;
set autopilot(bool enabled) => _autopilotNotifier.value = enabled;
ViewerController({
this.transition = ViewerTransition.parallax,
this.repeat = false,
bool autopilot = false,
this.autopilotInterval,
}) {
_autopilotNotifier = ValueNotifier(autopilot);
_autopilotNotifier.addListener(_onAutopilotChange);
_onAutopilotChange();
}
void dispose() {
_autopilotNotifier.removeListener(_onAutopilotChange);
_stopPlayTimer();
_streamController.close();
}
void _stopPlayTimer() {
_playTimer?.cancel();
}
void _onAutopilotChange() {
_stopPlayTimer();
if (autopilot && autopilotInterval != null) {
_playTimer = Timer.periodic(autopilotInterval!, (_) => _streamController.add(ViewerShowNextEvent()));
_streamController.add(const ViewerOverlayToggleEvent(visible: false));
}
}
}
@immutable
class ViewerShowNextEvent {}
@immutable
class ViewerOverlayToggleEvent {
final bool? visible;
const ViewerOverlayToggleEvent({required this.visible});
}
class PageTransitionEffects {
static TransitionBuilder fade(
PageController pageController,
int index, {
required bool zoomIn,
}) =>
(context, child) {
double opacity = 0;
double dx = 0;
double scale = 1;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
opacity = (1 - position.abs()).clamp(0, 1);
dx = position * width;
if (zoomIn) {
scale = 1 + position;
}
}
return Opacity(
opacity: opacity,
child: Transform.translate(
offset: Offset(dx, 0),
child: Transform.scale(
scale: scale,
child: child,
),
),
);
};
static TransitionBuilder slide(
PageController pageController,
int index, {
required bool parallax,
}) =>
(context, child) {
double dx = 0;
if (pageController.hasClients && pageController.position.haveDimensions) {
final position = (pageController.page! - index).clamp(-1.0, 1.0);
final width = pageController.position.viewportDimension;
if (parallax) {
dx = position * width / 2;
}
}
return ClipRect(
child: Transform.translate(
offset: Offset(dx, 0),
child: child,
),
);
};
}

View file

@ -1,9 +1,11 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/enums/viewer_transition.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/common/magnifier/pan/gesture_detector_scope.dart'; import 'package:aves/widgets/common/magnifier/pan/gesture_detector_scope.dart';
import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart'; import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart';
import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart';
import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart';
@ -13,6 +15,7 @@ import 'package:provider/provider.dart';
class MultiEntryScroller extends StatefulWidget { class MultiEntryScroller extends StatefulWidget {
final CollectionLens collection; final CollectionLens collection;
final ViewerController viewerController;
final PageController pageController; final PageController pageController;
final ValueChanged<int> onPageChanged; final ValueChanged<int> onPageChanged;
final void Function(AvesEntry mainEntry, AvesEntry? pageEntry) onViewDisposed; final void Function(AvesEntry mainEntry, AvesEntry? pageEntry) onViewDisposed;
@ -20,6 +23,7 @@ class MultiEntryScroller extends StatefulWidget {
const MultiEntryScroller({ const MultiEntryScroller({
super.key, super.key,
required this.collection, required this.collection,
required this.viewerController,
required this.pageController, required this.pageController,
required this.onPageChanged, required this.onPageChanged,
required this.onViewDisposed, required this.onViewDisposed,
@ -32,6 +36,8 @@ class MultiEntryScroller extends StatefulWidget {
class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticKeepAliveClientMixin { class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticKeepAliveClientMixin {
List<AvesEntry> get entries => widget.collection.sortedEntries; List<AvesEntry> get entries => widget.collection.sortedEntries;
ViewerController get viewerController => widget.viewerController;
PageController get pageController => widget.pageController; PageController get pageController => widget.pageController;
@override @override
@ -51,45 +57,29 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
), ),
onPageChanged: widget.onPageChanged, onPageChanged: widget.onPageChanged,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final mainEntry = entries[index]; final mainEntry = entries[index % entries.length];
var child = mainEntry.isMultiPage final child = mainEntry.isMultiPage
? PageEntryBuilder( ? PageEntryBuilder(
multiPageController: context.read<MultiPageConductor>().getController(mainEntry), multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
builder: (pageEntry) => _buildViewer(mainEntry, pageEntry: pageEntry), builder: (pageEntry) => _buildViewer(mainEntry, pageEntry: pageEntry),
) )
: _buildViewer(mainEntry); : _buildViewer(mainEntry);
child = Selector<Settings, bool>( return Selector<Settings, bool>(
selector: (context, s) => s.accessibilityAnimations.animate, selector: (context, s) => s.accessibilityAnimations.animate,
builder: (context, animate, child) { builder: (context, animate, child) {
return animate if (!animate) return child!;
? AnimatedBuilder( return AnimatedBuilder(
animation: pageController, animation: pageController,
builder: (context, child) { builder: viewerController.transition.builder(pageController, index),
// parallax scrolling child: child,
double dx = 0; );
if (pageController.hasClients && pageController.position.haveDimensions) {
final delta = pageController.page! - index;
dx = delta * pageController.position.viewportDimension / 2;
}
return Transform.translate(
offset: Offset(dx, 0),
child: child,
);
},
child: child,
)
: child!;
}, },
child: child, child: child,
); );
return ClipRect(
child: child,
);
}, },
itemCount: entries.length, itemCount: viewerController.repeat ? null : entries.length,
), ),
); );
} }

View file

@ -2,14 +2,17 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart'; import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart';
import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/entry_horizontal_pager.dart'; import 'package:aves/widgets/viewer/entry_horizontal_pager.dart';
import 'package:aves/widgets/viewer/info/info_page.dart'; import 'package:aves/widgets/viewer/info/info_page.dart';
import 'package:aves/widgets/viewer/notifications.dart'; import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/overlay/notifications.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -19,6 +22,7 @@ import 'package:screen_brightness/screen_brightness.dart';
class ViewerVerticalPageView extends StatefulWidget { class ViewerVerticalPageView extends StatefulWidget {
final CollectionLens? collection; final CollectionLens? collection;
final ValueNotifier<AvesEntry?> entryNotifier; final ValueNotifier<AvesEntry?> entryNotifier;
final ViewerController viewerController;
final PageController horizontalPager, verticalPager; final PageController horizontalPager, verticalPager;
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged; final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
final VoidCallback onImagePageRequested; final VoidCallback onImagePageRequested;
@ -28,6 +32,7 @@ class ViewerVerticalPageView extends StatefulWidget {
super.key, super.key,
required this.collection, required this.collection,
required this.entryNotifier, required this.entryNotifier,
required this.viewerController,
required this.verticalPager, required this.verticalPager,
required this.horizontalPager, required this.horizontalPager,
required this.onVerticalPageChanged, required this.onVerticalPageChanged,
@ -41,6 +46,7 @@ class ViewerVerticalPageView extends StatefulWidget {
} }
class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> { class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
final List<StreamSubscription> _subscriptions = [];
final ValueNotifier<double> _backgroundOpacityNotifier = ValueNotifier(1); final ValueNotifier<double> _backgroundOpacityNotifier = ValueNotifier(1);
final ValueNotifier<bool> _isVerticallyScrollingNotifier = ValueNotifier(false); final ValueNotifier<bool> _isVerticallyScrollingNotifier = ValueNotifier(false);
Timer? _verticalScrollMonitoringTimer; Timer? _verticalScrollMonitoringTimer;
@ -80,12 +86,21 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
} }
void _registerWidget(ViewerVerticalPageView widget) { void _registerWidget(ViewerVerticalPageView widget) {
_subscriptions.add(widget.viewerController.showNextCommands.listen((event) {
_goToHorizontalPage(1, animate: true);
}));
_subscriptions.add(widget.viewerController.overlayCommands.listen((event) {
ToggleOverlayNotification(visible: event.visible).dispatch(context);
}));
widget.verticalPager.addListener(_onVerticalPageControllerChanged); widget.verticalPager.addListener(_onVerticalPageControllerChanged);
widget.entryNotifier.addListener(_onEntryChanged); widget.entryNotifier.addListener(_onEntryChanged);
if (_oldEntry != entry) _onEntryChanged(); if (_oldEntry != entry) _onEntryChanged();
} }
void _unregisterWidget(ViewerVerticalPageView widget) { void _unregisterWidget(ViewerVerticalPageView widget) {
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
widget.verticalPager.removeListener(_onVerticalPageControllerChanged); widget.verticalPager.removeListener(_onVerticalPageControllerChanged);
widget.entryNotifier.removeListener(_onEntryChanged); widget.entryNotifier.removeListener(_onEntryChanged);
_oldEntry?.imageChangeNotifier.removeListener(_onImageChanged); _oldEntry?.imageChangeNotifier.removeListener(_onImageChanged);
@ -96,34 +111,36 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
// fake page for opacity transition between collection and viewer // fake page for opacity transition between collection and viewer
const transitionPage = SizedBox(); const transitionPage = SizedBox();
final imagePage = _buildImagePage();
final infoPage = NotificationListener<ShowImageNotification>(
onNotification: (notification) {
widget.onImagePageRequested();
return true;
},
child: AnimatedBuilder(
animation: widget.verticalPager,
builder: (context, child) {
return Visibility(
visible: widget.verticalPager.page! > 1,
child: child!,
);
},
child: InfoPage(
collection: collection,
entryNotifier: widget.entryNotifier,
isScrollingNotifier: _isVerticallyScrollingNotifier,
),
),
);
final pages = [ final pages = [
transitionPage, transitionPage,
imagePage, _buildImagePage(),
infoPage,
]; ];
if (context.read<ValueNotifier<AppMode>>().value != AppMode.slideshow) {
final infoPage = NotificationListener<ShowImageNotification>(
onNotification: (notification) {
widget.onImagePageRequested();
return true;
},
child: AnimatedBuilder(
animation: widget.verticalPager,
builder: (context, child) {
return Visibility(
visible: widget.verticalPager.page! > 1,
child: child!,
);
},
child: InfoPage(
collection: collection,
entryNotifier: widget.entryNotifier,
isScrollingNotifier: _isVerticallyScrollingNotifier,
),
),
);
pages.add(infoPage);
}
return ValueListenableBuilder<double>( return ValueListenableBuilder<double>(
valueListenable: _backgroundOpacityNotifier, valueListenable: _backgroundOpacityNotifier,
builder: (context, backgroundOpacity, child) { builder: (context, backgroundOpacity, child) {
@ -155,6 +172,7 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
if (hasCollection) { if (hasCollection) {
child = MultiEntryScroller( child = MultiEntryScroller(
collection: collection!, collection: collection!,
viewerController: widget.viewerController,
pageController: widget.horizontalPager, pageController: widget.horizontalPager,
onPageChanged: widget.onHorizontalPageChanged, onPageChanged: widget.onHorizontalPageChanged,
onViewDisposed: widget.onViewDisposed, onViewDisposed: widget.onViewDisposed,
@ -179,8 +197,8 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
autofocus: true, autofocus: true,
shortcuts: shortcuts, shortcuts: shortcuts,
actions: { actions: {
ShowPreviousIntent: CallbackAction<Intent>(onInvoke: (intent) => _jumpHorizontalPage(-1)), ShowPreviousIntent: CallbackAction<Intent>(onInvoke: (intent) => _goToHorizontalPage(-1, animate: false)),
ShowNextIntent: CallbackAction<Intent>(onInvoke: (intent) => _jumpHorizontalPage(1)), ShowNextIntent: CallbackAction<Intent>(onInvoke: (intent) => _goToHorizontalPage(1, animate: false)),
LeaveIntent: CallbackAction<Intent>(onInvoke: (intent) => Navigator.pop(context)), LeaveIntent: CallbackAction<Intent>(onInvoke: (intent) => Navigator.pop(context)),
ShowInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => ShowInfoNotification().dispatch(context)), ShowInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => ShowInfoNotification().dispatch(context)),
}, },
@ -190,13 +208,24 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
return const SizedBox(); return const SizedBox();
} }
void _jumpHorizontalPage(int delta) { void _goToHorizontalPage(int delta, {required bool animate}) {
final pageController = widget.horizontalPager; final pageController = widget.horizontalPager;
final page = pageController.page?.round(); final page = pageController.page?.round();
final _collection = collection; final _collection = collection;
if (page != null && _collection != null) { if (page != null && _collection != null) {
final target = (page + delta).clamp(0, _collection.entryCount - 1); var target = page + delta;
pageController.jumpToPage(target); if (!widget.viewerController.repeat) {
target = target.clamp(0, _collection.entryCount - 1);
}
if (animate) {
pageController.animateToPage(
target,
duration: const Duration(seconds: 1),
curve: Curves.easeInOutCubic,
);
} else {
pageController.jumpToPage(target);
}
} }
} }

View file

@ -2,6 +2,7 @@ import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/entry_viewer_stack.dart'; import 'package:aves/widgets/viewer/entry_viewer_stack.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/overlay/bottom.dart'; import 'package:aves/widgets/viewer/overlay/bottom.dart';
@ -10,7 +11,7 @@ import 'package:aves/widgets/viewer/visual/conductor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class EntryViewerPage extends StatelessWidget { class EntryViewerPage extends StatefulWidget {
static const routeName = '/viewer'; static const routeName = '/viewer';
final CollectionLens? collection; final CollectionLens? collection;
@ -22,6 +23,23 @@ class EntryViewerPage extends StatelessWidget {
required this.initialEntry, required this.initialEntry,
}); });
@override
State<EntryViewerPage> createState() => _EntryViewerPageState();
static EdgeInsets snackBarMargin(BuildContext context) {
return EdgeInsets.only(bottom: ViewerBottomOverlay.actionSafeHeight(context));
}
}
class _EntryViewerPageState extends State<EntryViewerPage> {
final ViewerController _viewerController = ViewerController();
@override
void dispose() {
_viewerController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MediaQueryDataProvider( return MediaQueryDataProvider(
@ -30,8 +48,9 @@ class EntryViewerPage extends StatelessWidget {
child: VideoConductorProvider( child: VideoConductorProvider(
child: MultiPageConductorProvider( child: MultiPageConductorProvider(
child: EntryViewerStack( child: EntryViewerStack(
collection: collection, collection: widget.collection,
initialEntry: initialEntry, initialEntry: widget.initialEntry,
viewerController: _viewerController,
), ),
), ),
), ),
@ -45,10 +64,6 @@ class EntryViewerPage extends StatelessWidget {
), ),
); );
} }
static EdgeInsets snackBarMargin(BuildContext context) {
return EdgeInsets.only(bottom: ViewerBottomOverlay.actionSafeHeight(context));
}
} }
class ViewStateConductorProvider extends StatelessWidget { class ViewStateConductorProvider extends StatelessWidget {

View file

@ -16,6 +16,7 @@ import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/entry_vertical_pager.dart'; import 'package:aves/widgets/viewer/entry_vertical_pager.dart';
import 'package:aves/widgets/viewer/hero.dart'; import 'package:aves/widgets/viewer/hero.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart';
@ -23,6 +24,7 @@ import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/overlay/bottom.dart'; import 'package:aves/widgets/viewer/overlay/bottom.dart';
import 'package:aves/widgets/viewer/overlay/notifications.dart'; import 'package:aves/widgets/viewer/overlay/notifications.dart';
import 'package:aves/widgets/viewer/overlay/panorama.dart'; import 'package:aves/widgets/viewer/overlay/panorama.dart';
import 'package:aves/widgets/viewer/overlay/slideshow_buttons.dart';
import 'package:aves/widgets/viewer/overlay/top.dart'; import 'package:aves/widgets/viewer/overlay/top.dart';
import 'package:aves/widgets/viewer/overlay/video/video.dart'; import 'package:aves/widgets/viewer/overlay/video/video.dart';
import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart';
@ -42,11 +44,13 @@ import 'package:screen_brightness/screen_brightness.dart';
class EntryViewerStack extends StatefulWidget { class EntryViewerStack extends StatefulWidget {
final CollectionLens? collection; final CollectionLens? collection;
final AvesEntry initialEntry; final AvesEntry initialEntry;
final ViewerController viewerController;
const EntryViewerStack({ const EntryViewerStack({
super.key, super.key,
this.collection, this.collection,
required this.initialEntry, required this.initialEntry,
required this.viewerController,
}); });
@override @override
@ -54,7 +58,7 @@ class EntryViewerStack extends StatefulWidget {
} }
class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewControllerMixin, FeedbackMixin, SingleTickerProviderStateMixin, WidgetsBindingObserver { class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewControllerMixin, FeedbackMixin, SingleTickerProviderStateMixin, WidgetsBindingObserver {
late int _currentHorizontalPage; late int _currentEntryIndex;
late ValueNotifier<int> _currentVerticalPage; late ValueNotifier<int> _currentVerticalPage;
late PageController _horizontalPager, _verticalPager; late PageController _horizontalPager, _verticalPager;
final AChangeNotifier _verticalScrollNotifier = AChangeNotifier(); final AChangeNotifier _verticalScrollNotifier = AChangeNotifier();
@ -68,7 +72,9 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
bool _isEntryTracked = true; bool _isEntryTracked = true;
@override @override
final ValueNotifier<AvesEntry?> entryNotifier = ValueNotifier(null); late final ValueNotifier<AvesEntry?> entryNotifier;
ViewerController get viewerController => widget.viewerController;
CollectionLens? get collection => widget.collection; CollectionLens? get collection => widget.collection;
@ -103,10 +109,11 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
final entry = entries.firstWhereOrNull((entry) => entry.id == initialEntry.id) ?? entries.firstOrNull; final entry = entries.firstWhereOrNull((entry) => entry.id == initialEntry.id) ?? entries.firstOrNull;
// opening hero, with viewer as target // opening hero, with viewer as target
_heroInfoNotifier.value = HeroInfo(collection?.id, entry); _heroInfoNotifier.value = HeroInfo(collection?.id, entry);
entryNotifier = viewerController.entryNotifier;
entryNotifier.value = entry; entryNotifier.value = entry;
_currentHorizontalPage = max(0, entry != null ? entries.indexOf(entry) : -1); _currentEntryIndex = max(0, entry != null ? entries.indexOf(entry) : -1);
_currentVerticalPage = ValueNotifier(imagePage); _currentVerticalPage = ValueNotifier(imagePage);
_horizontalPager = PageController(initialPage: _currentHorizontalPage); _horizontalPager = PageController(initialPage: _currentEntryIndex);
_verticalPager = PageController(initialPage: _currentVerticalPage.value)..addListener(_onVerticalPageControllerChange); _verticalPager = PageController(initialPage: _currentVerticalPage.value)..addListener(_onVerticalPageControllerChange);
_overlayAnimationController = AnimationController( _overlayAnimationController = AnimationController(
duration: context.read<DurationsData>().viewerOverlayAnimation, duration: context.read<DurationsData>().viewerOverlayAnimation,
@ -126,7 +133,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
parent: _overlayAnimationController, parent: _overlayAnimationController,
curve: Curves.easeOutQuad, curve: Curves.easeOutQuad,
)); ));
_overlayVisible.value = settings.showOverlayOnOpening; _overlayVisible.value = settings.showOverlayOnOpening && !viewerController.autopilot;
_overlayVisible.addListener(_onOverlayVisibleChange); _overlayVisible.addListener(_onOverlayVisibleChange);
_videoActionDelegate = VideoActionDelegate( _videoActionDelegate = VideoActionDelegate(
collection: collection, collection: collection,
@ -233,7 +240,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
_goToVerticalPage(infoPage); _goToVerticalPage(infoPage);
} else if (notification is ViewEntryNotification) { } else if (notification is ViewEntryNotification) {
final index = notification.index; final index = notification.index;
if (_currentHorizontalPage != index) { if (_currentEntryIndex != index) {
_horizontalPager.jumpToPage(index); _horizontalPager.jumpToPage(index);
} }
} else if (notification is VideoActionNotification) { } else if (notification is VideoActionNotification) {
@ -250,6 +257,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
ViewerVerticalPageView( ViewerVerticalPageView(
collection: collection, collection: collection,
entryNotifier: entryNotifier, entryNotifier: entryNotifier,
viewerController: viewerController,
verticalPager: _verticalPager, verticalPager: _verticalPager,
horizontalPager: _horizontalPager, horizontalPager: _horizontalPager,
onVerticalPageChanged: _onVerticalPageChanged, onVerticalPageChanged: _onVerticalPageChanged,
@ -257,8 +265,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
onImagePageRequested: () => _goToVerticalPage(imagePage), onImagePageRequested: () => _goToVerticalPage(imagePage),
onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry), onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry),
), ),
_buildTopOverlay(), ..._buildOverlays(),
_buildBottomOverlay(),
const SideGestureAreaProtector(), const SideGestureAreaProtector(),
const BottomGestureAreaProtector(), const BottomGestureAreaProtector(),
], ],
@ -268,7 +275,40 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
); );
} }
Widget _buildTopOverlay() { List<Widget> _buildOverlays() {
if (context.read<ValueNotifier<AppMode>>().value == AppMode.slideshow) {
return [_buildSlideshowBottomOverlay()];
}
return [
_buildViewerTopOverlay(),
_buildViewerBottomOverlay(),
];
}
Widget _buildSlideshowBottomOverlay() {
return Selector<MediaQueryData, Size>(
selector: (context, mq) => mq.size,
builder: (context, mqSize, child) {
return SizedBox.fromSize(
size: mqSize,
child: Align(
alignment: AlignmentDirectional.bottomEnd,
child: TooltipTheme(
data: TooltipTheme.of(context).copyWith(
preferBelow: false,
),
child: SlideshowButtons(
scale: _overlayButtonScale,
),
),
),
);
},
);
}
Widget _buildViewerTopOverlay() {
Widget child = ValueListenableBuilder<AvesEntry?>( Widget child = ValueListenableBuilder<AvesEntry?>(
valueListenable: entryNotifier, valueListenable: entryNotifier,
builder: (context, mainEntry, child) { builder: (context, mainEntry, child) {
@ -278,7 +318,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
position: _overlayTopOffset, position: _overlayTopOffset,
child: ViewerTopOverlay( child: ViewerTopOverlay(
entries: entries, entries: entries,
index: _currentHorizontalPage, index: _currentEntryIndex,
hasCollection: hasCollection, hasCollection: hasCollection,
mainEntry: mainEntry, mainEntry: mainEntry,
scale: _overlayButtonScale, scale: _overlayButtonScale,
@ -314,7 +354,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
return child; return child;
} }
Widget _buildBottomOverlay() { Widget _buildViewerBottomOverlay() {
Widget child = ValueListenableBuilder<AvesEntry?>( Widget child = ValueListenableBuilder<AvesEntry?>(
valueListenable: entryNotifier, valueListenable: entryNotifier,
builder: (context, mainEntry, child) { builder: (context, mainEntry, child) {
@ -378,7 +418,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
if (extraBottomOverlay != null) extraBottomOverlay, if (extraBottomOverlay != null) extraBottomOverlay,
ViewerBottomOverlay( ViewerBottomOverlay(
entries: entries, entries: entries,
index: _currentHorizontalPage, index: _currentEntryIndex,
hasCollection: hasCollection, hasCollection: hasCollection,
animationController: _overlayAnimationController, animationController: _overlayAnimationController,
viewInsets: _frozenViewInsets, viewInsets: _frozenViewInsets,
@ -400,7 +440,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
return AnimatedBuilder( return AnimatedBuilder(
animation: _verticalScrollNotifier, animation: _verticalScrollNotifier,
builder: (context, child) => Positioned( builder: (context, child) => Positioned(
bottom: (_verticalPager.position.hasPixels ? _verticalPager.offset : 0) - mqHeight, bottom: (_verticalPager.hasClients && _verticalPager.position.hasPixels ? _verticalPager.offset : 0) - mqHeight,
child: child!, child: child!,
), ),
child: child, child: child,
@ -422,7 +462,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
} }
void _onVerticalPageControllerChange() { void _onVerticalPageControllerChange() {
if (!_isEntryTracked && _verticalPager.page?.floor() == transitionPage) { if (!_isEntryTracked && _verticalPager.hasClients && _verticalPager.page?.floor() == transitionPage) {
_trackEntry(); _trackEntry();
} }
_verticalScrollNotifier.notify(); _verticalScrollNotifier.notify();
@ -440,12 +480,10 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: CollectionPage.routeName), settings: const RouteSettings(name: CollectionPage.routeName),
builder: (context) { builder: (context) => CollectionPage(
return CollectionPage( source: baseCollection.source,
source: baseCollection.source, filters: {...baseCollection.filters, filter},
filters: {...baseCollection.filters, filter}, ),
);
},
), ),
(route) => false, (route) => false,
); );
@ -477,7 +515,10 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
} }
void _onHorizontalPageChanged(int page) { void _onHorizontalPageChanged(int page) {
_currentHorizontalPage = page; _currentEntryIndex = page;
if (viewerController.repeat) {
_currentEntryIndex %= entries.length;
}
_updateEntry(); _updateEntry();
} }
@ -521,14 +562,14 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
} }
Future<void> _updateEntry() async { Future<void> _updateEntry() async {
if (entries.isNotEmpty && _currentHorizontalPage >= entries.length) { if (entries.isNotEmpty && _currentEntryIndex >= entries.length) {
// as of Flutter v1.22.2, `PageView` does not call `onPageChanged` when the last page is deleted // as of Flutter v1.22.2, `PageView` does not call `onPageChanged` when the last page is deleted
// so we manually track the page change, and let the entry update follow // so we manually track the page change, and let the entry update follow
_onHorizontalPageChanged(entries.length - 1); _onHorizontalPageChanged(entries.length - 1);
return; return;
} }
final newEntry = _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null; final newEntry = _currentEntryIndex < entries.length ? entries[_currentEntryIndex] : null;
if (entryNotifier.value == newEntry) return; if (entryNotifier.value == newEntry) return;
cleanEntryControllers(entryNotifier.value); cleanEntryControllers(entryNotifier.value);
entryNotifier.value = newEntry; entryNotifier.value = newEntry;
@ -606,6 +647,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
} else { } else {
_overlayAnimationController.value = _overlayAnimationController.upperBound; _overlayAnimationController.value = _overlayAnimationController.upperBound;
} }
viewerController.autopilot = false;
} else { } else {
final mediaQuery = context.read<MediaQueryData>(); final mediaQuery = context.read<MediaQueryData>();
setState(() { setState(() {

View file

@ -7,8 +7,8 @@ import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart';
import 'package:aves/widgets/viewer/overlay/multipage.dart'; import 'package:aves/widgets/viewer/overlay/multipage.dart';
import 'package:aves/widgets/viewer/overlay/thumbnail_preview.dart'; import 'package:aves/widgets/viewer/overlay/thumbnail_preview.dart';
import 'package:aves/widgets/viewer/overlay/viewer_button_row.dart'; import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
import 'package:aves/widgets/viewer/overlay/wallpaper_button_row.dart'; import 'package:aves/widgets/viewer/overlay/wallpaper_buttons.dart';
import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -37,7 +37,7 @@ class ViewerBottomOverlay extends StatefulWidget {
State<StatefulWidget> createState() => _ViewerBottomOverlayState(); State<StatefulWidget> createState() => _ViewerBottomOverlayState();
static double actionSafeHeight(BuildContext context) { static double actionSafeHeight(BuildContext context) {
return ViewerButtonRow.preferredHeight(context) + (settings.showOverlayThumbnailPreview ? ViewerThumbnailPreview.preferredHeight : 0); return ViewerButtons.preferredHeight(context) + (settings.showOverlayThumbnailPreview ? ViewerThumbnailPreview.preferredHeight : 0);
} }
} }
@ -156,11 +156,11 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
right: viewInsetsPadding.right, right: viewInsetsPadding.right,
), ),
child: isWallpaperMode child: isWallpaperMode
? WallpaperButton( ? WallpaperButtons(
entry: pageEntry, entry: pageEntry,
scale: _buttonScale, scale: _buttonScale,
) )
: ViewerButtonRow( : ViewerButtons(
mainEntry: mainEntry, mainEntry: mainEntry,
pageEntry: pageEntry, pageEntry: pageEntry,
scale: _buttonScale, scale: _buttonScale,

View file

@ -0,0 +1,43 @@
import 'package:aves/model/actions/slideshow_actions.dart';
import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
import 'package:aves/widgets/viewer/slideshow_page.dart';
import 'package:flutter/material.dart';
class SlideshowButtons extends StatelessWidget {
final Animation<double> scale;
const SlideshowButtons({
super.key,
required this.scale,
});
@override
Widget build(BuildContext context) {
const padding = ViewerButtonRowContent.padding;
return SafeArea(
child: Padding(
padding: const EdgeInsets.only(left: padding / 2, right: padding / 2, bottom: padding),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SlideshowAction.resume,
SlideshowAction.showInCollection,
]
.map((action) => Padding(
padding: const EdgeInsets.symmetric(horizontal: padding / 2),
child: OverlayButton(
scale: scale,
child: IconButton(
icon: action.getIcon(),
onPressed: () => SlideshowActionNotification(action).dispatch(context),
tooltip: action.getText(context),
),
),
))
.toList(),
),
),
);
}
}

View file

@ -22,7 +22,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class ViewerButtonRow extends StatelessWidget { class ViewerButtons extends StatelessWidget {
final AvesEntry mainEntry; final AvesEntry mainEntry;
final AvesEntry pageEntry; final AvesEntry pageEntry;
final Animation<double> scale; final Animation<double> scale;
@ -35,7 +35,7 @@ class ViewerButtonRow extends StatelessWidget {
static double _buttonSize(BuildContext context) => OverlayButton.getSize(context); static double _buttonSize(BuildContext context) => OverlayButton.getSize(context);
const ViewerButtonRow({ const ViewerButtons({
super.key, super.key,
required this.mainEntry, required this.mainEntry,
required this.pageEntry, required this.pageEntry,

View file

@ -12,7 +12,7 @@ import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:aves/widgets/viewer/overlay/viewer_button_row.dart'; import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
import 'package:aves/widgets/viewer/video/conductor.dart'; import 'package:aves/widgets/viewer/video/conductor.dart';
import 'package:aves/widgets/viewer/visual/conductor.dart'; import 'package:aves/widgets/viewer/visual/conductor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -20,11 +20,11 @@ import 'package:flutter/services.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class WallpaperButton extends StatelessWidget with FeedbackMixin { class WallpaperButtons extends StatelessWidget with FeedbackMixin {
final AvesEntry entry; final AvesEntry entry;
final Animation<double> scale; final Animation<double> scale;
const WallpaperButton({ const WallpaperButtons({
super.key, super.key,
required this.entry, required this.entry,
required this.scale, required this.scale,

View file

@ -0,0 +1,142 @@
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/slideshow_actions.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/slideshow_interval.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/entry_viewer_stack.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class SlideshowPage extends StatefulWidget {
static const routeName = '/collection/slideshow';
final CollectionLens collection;
const SlideshowPage({
super.key,
required this.collection,
});
@override
State<SlideshowPage> createState() => _SlideshowPageState();
}
class _SlideshowPageState extends State<SlideshowPage> {
late final CollectionLens _slideshowCollection;
late final ViewerController _viewerController;
@override
void initState() {
super.initState();
final originalCollection = widget.collection;
var entries = originalCollection.sortedEntries;
if (settings.slideshowVideoPlayback == SlideshowVideoPlayback.skip) {
entries = entries.where((entry) => !MimeFilter.video.test(entry)).toList();
}
if (settings.slideshowShuffle) {
entries.shuffle();
}
_slideshowCollection = CollectionLens(
source: originalCollection.source,
listenToSource: false,
fixedSort: true,
fixedSelection: entries,
);
_viewerController = ViewerController(
transition: settings.slideshowTransition,
repeat: settings.slideshowRepeat,
autopilot: true,
autopilotInterval: settings.slideshowInterval.getDuration(),
);
}
@override
void dispose() {
_viewerController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final entries = _slideshowCollection.sortedEntries;
return ListenableProvider<ValueNotifier<AppMode>>.value(
value: ValueNotifier(AppMode.slideshow),
child: MediaQueryDataProvider(
child: Scaffold(
body: entries.isEmpty
? EmptyContent(
icon: AIcons.image,
text: context.l10n.collectionEmptyImages,
alignment: Alignment.center,
)
: ViewStateConductorProvider(
child: VideoConductorProvider(
child: MultiPageConductorProvider(
child: NotificationListener<SlideshowActionNotification>(
onNotification: (notification) {
_onActionSelected(notification.action);
return true;
},
child: EntryViewerStack(
collection: _slideshowCollection,
initialEntry: entries.first,
viewerController: _viewerController,
),
),
),
),
),
),
),
);
}
void _onActionSelected(SlideshowAction action) {
switch (action) {
case SlideshowAction.resume:
_viewerController.autopilot = true;
break;
case SlideshowAction.showInCollection:
_showInCollection();
break;
}
}
void _showInCollection() {
final entry = _viewerController.entryNotifier.value;
if (entry == null) return;
final source = _slideshowCollection.source;
final album = entry.directory;
final uri = entry.uri;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
settings: const RouteSettings(name: CollectionPage.routeName),
builder: (context) => CollectionPage(
source: source,
filters: album != null ? {AlbumFilter(album, source.getAlbumDisplayName(context, album))} : null,
highlightTest: (entry) => entry.uri == uri,
),
),
(route) => false,
);
}
}
class SlideshowActionNotification extends Notification {
final SlideshowAction action;
SlideshowActionNotification(this.action);
}

View file

@ -1,4 +1,6 @@
import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart';
@ -35,11 +37,27 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> {
} }
} }
bool _isSlideshow(BuildContext context) => context.read<ValueNotifier<AppMode>>().value == AppMode.slideshow;
bool _shouldAutoPlay(BuildContext context) {
if (_isSlideshow(context)) {
switch (settings.slideshowVideoPlayback) {
case SlideshowVideoPlayback.skip:
return false;
case SlideshowVideoPlayback.playMuted:
case SlideshowVideoPlayback.playWithSound:
return true;
}
}
return settings.enableVideoAutoPlay;
}
Future<void> _initVideoController(AvesEntry entry) async { Future<void> _initVideoController(AvesEntry entry) async {
final controller = context.read<VideoConductor>().getOrCreateController(entry); final controller = context.read<VideoConductor>().getOrCreateController(entry);
setState(() {}); setState(() {});
if (settings.enableVideoAutoPlay) { if (_shouldAutoPlay(context)) {
final resumeTimeMillis = await controller.getResumeTime(context); final resumeTimeMillis = await controller.getResumeTime(context);
await _playVideo(controller, () => entry == entryNotifier.value, resumeTimeMillis: resumeTimeMillis); await _playVideo(controller, () => entry == entryNotifier.value, resumeTimeMillis: resumeTimeMillis);
} }
@ -66,7 +84,7 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> {
// auto play/pause when changing page // auto play/pause when changing page
Future<void> _onPageChange() async { Future<void> _onPageChange() async {
await pauseVideoControllers(); await pauseVideoControllers();
if (settings.enableVideoAutoPlay || (entry.isMotionPhoto && settings.enableMotionPhotoAutoPlay)) { if (_shouldAutoPlay(context) || (entry.isMotionPhoto && settings.enableMotionPhotoAutoPlay)) {
final page = multiPageController.page; final page = multiPageController.page;
final pageInfo = multiPageInfo.getByIndex(page)!; final pageInfo = multiPageInfo.getByIndex(page)!;
if (pageInfo.isVideo) { if (pageInfo.isVideo) {
@ -109,6 +127,10 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> {
// so we play after a delay for increased stability // so we play after a delay for increased stability
await Future.delayed(const Duration(milliseconds: 300) * timeDilation); await Future.delayed(const Duration(milliseconds: 300) * timeDilation);
if (_isSlideshow(context) && settings.slideshowVideoPlayback == SlideshowVideoPlayback.playMuted && !videoController.isMuted) {
await videoController.toggleMute();
}
if (resumeTimeMillis != null) { if (resumeTimeMillis != null) {
await videoController.seekTo(resumeTimeMillis); await videoController.seekTo(resumeTimeMillis);
} else { } else {

View file

@ -1,99 +1,319 @@
{ {
"de": [ "de": [
"slideshowActionResume",
"slideshowActionShowInCollection",
"slideshowVideoPlaybackSkip",
"slideshowVideoPlaybackMuted",
"slideshowVideoPlaybackWithSound",
"viewerTransitionFade",
"viewerTransitionFadeZoomIn",
"viewerTransitionParallax",
"viewerTransitionSlide",
"wallpaperTargetHome", "wallpaperTargetHome",
"wallpaperTargetLock", "wallpaperTargetLock",
"wallpaperTargetHomeLock", "wallpaperTargetHomeLock",
"menuActionSlideshow",
"collectionEmptyGrantAccessButtonLabel", "collectionEmptyGrantAccessButtonLabel",
"settingsViewerSlideshowTile",
"settingsViewerSlideshowTitle",
"settingsSlideshowRepeat",
"settingsSlideshowShuffle",
"settingsSlideshowTransitionTile",
"settingsSlideshowTransitionTitle",
"settingsSlideshowIntervalTile",
"settingsSlideshowIntervalTitle",
"settingsSlideshowVideoPlaybackTile",
"settingsSlideshowVideoPlaybackTitle",
"settingsThemeEnableDynamicColor", "settingsThemeEnableDynamicColor",
"viewerSetWallpaperButtonLabel" "viewerSetWallpaperButtonLabel"
], ],
"es": [ "es": [
"slideshowActionResume",
"slideshowActionShowInCollection",
"slideshowVideoPlaybackSkip",
"slideshowVideoPlaybackMuted",
"slideshowVideoPlaybackWithSound",
"viewerTransitionFade",
"viewerTransitionFadeZoomIn",
"viewerTransitionParallax",
"viewerTransitionSlide",
"wallpaperTargetHome", "wallpaperTargetHome",
"wallpaperTargetLock", "wallpaperTargetLock",
"wallpaperTargetHomeLock", "wallpaperTargetHomeLock",
"menuActionSlideshow",
"collectionEmptyGrantAccessButtonLabel", "collectionEmptyGrantAccessButtonLabel",
"settingsShowBottomNavigationBar", "settingsShowBottomNavigationBar",
"settingsThumbnailShowTagIcon", "settingsThumbnailShowTagIcon",
"settingsViewerSlideshowTile",
"settingsViewerSlideshowTitle",
"settingsSlideshowRepeat",
"settingsSlideshowShuffle",
"settingsSlideshowTransitionTile",
"settingsSlideshowTransitionTitle",
"settingsSlideshowIntervalTile",
"settingsSlideshowIntervalTitle",
"settingsSlideshowVideoPlaybackTile",
"settingsSlideshowVideoPlaybackTitle",
"settingsThemeEnableDynamicColor", "settingsThemeEnableDynamicColor",
"viewerSetWallpaperButtonLabel" "viewerSetWallpaperButtonLabel"
], ],
"fr": [ "fr": [
"slideshowActionResume",
"slideshowActionShowInCollection",
"slideshowVideoPlaybackSkip",
"slideshowVideoPlaybackMuted",
"slideshowVideoPlaybackWithSound",
"viewerTransitionFade",
"viewerTransitionFadeZoomIn",
"viewerTransitionParallax",
"viewerTransitionSlide",
"wallpaperTargetHome", "wallpaperTargetHome",
"wallpaperTargetLock", "wallpaperTargetLock",
"wallpaperTargetHomeLock", "wallpaperTargetHomeLock",
"menuActionSlideshow",
"collectionEmptyGrantAccessButtonLabel", "collectionEmptyGrantAccessButtonLabel",
"settingsViewerSlideshowTile",
"settingsViewerSlideshowTitle",
"settingsSlideshowRepeat",
"settingsSlideshowShuffle",
"settingsSlideshowTransitionTile",
"settingsSlideshowTransitionTitle",
"settingsSlideshowIntervalTile",
"settingsSlideshowIntervalTitle",
"settingsSlideshowVideoPlaybackTile",
"settingsSlideshowVideoPlaybackTitle",
"settingsThemeEnableDynamicColor", "settingsThemeEnableDynamicColor",
"viewerSetWallpaperButtonLabel" "viewerSetWallpaperButtonLabel"
], ],
"id": [ "id": [
"slideshowActionResume",
"slideshowActionShowInCollection",
"slideshowVideoPlaybackSkip",
"slideshowVideoPlaybackMuted",
"slideshowVideoPlaybackWithSound",
"viewerTransitionFade",
"viewerTransitionFadeZoomIn",
"viewerTransitionParallax",
"viewerTransitionSlide",
"wallpaperTargetHome", "wallpaperTargetHome",
"wallpaperTargetLock", "wallpaperTargetLock",
"wallpaperTargetHomeLock", "wallpaperTargetHomeLock",
"menuActionSlideshow",
"collectionEmptyGrantAccessButtonLabel", "collectionEmptyGrantAccessButtonLabel",
"settingsViewerSlideshowTile",
"settingsViewerSlideshowTitle",
"settingsSlideshowRepeat",
"settingsSlideshowShuffle",
"settingsSlideshowTransitionTile",
"settingsSlideshowTransitionTitle",
"settingsSlideshowIntervalTile",
"settingsSlideshowIntervalTitle",
"settingsSlideshowVideoPlaybackTile",
"settingsSlideshowVideoPlaybackTitle",
"settingsThemeEnableDynamicColor", "settingsThemeEnableDynamicColor",
"viewerSetWallpaperButtonLabel" "viewerSetWallpaperButtonLabel"
], ],
"it": [ "it": [
"slideshowActionResume",
"slideshowActionShowInCollection",
"slideshowVideoPlaybackSkip",
"slideshowVideoPlaybackMuted",
"slideshowVideoPlaybackWithSound",
"viewerTransitionFade",
"viewerTransitionFadeZoomIn",
"viewerTransitionParallax",
"viewerTransitionSlide",
"wallpaperTargetHome", "wallpaperTargetHome",
"wallpaperTargetLock", "wallpaperTargetLock",
"wallpaperTargetHomeLock", "wallpaperTargetHomeLock",
"menuActionSlideshow",
"collectionEmptyGrantAccessButtonLabel", "collectionEmptyGrantAccessButtonLabel",
"settingsViewerSlideshowTile",
"settingsViewerSlideshowTitle",
"settingsSlideshowRepeat",
"settingsSlideshowShuffle",
"settingsSlideshowTransitionTile",
"settingsSlideshowTransitionTitle",
"settingsSlideshowIntervalTile",
"settingsSlideshowIntervalTitle",
"settingsSlideshowVideoPlaybackTile",
"settingsSlideshowVideoPlaybackTitle",
"settingsThemeEnableDynamicColor", "settingsThemeEnableDynamicColor",
"viewerSetWallpaperButtonLabel" "viewerSetWallpaperButtonLabel"
], ],
"ja": [ "ja": [
"slideshowActionResume",
"slideshowActionShowInCollection",
"slideshowVideoPlaybackSkip",
"slideshowVideoPlaybackMuted",
"slideshowVideoPlaybackWithSound",
"viewerTransitionFade",
"viewerTransitionFadeZoomIn",
"viewerTransitionParallax",
"viewerTransitionSlide",
"wallpaperTargetHome", "wallpaperTargetHome",
"wallpaperTargetLock", "wallpaperTargetLock",
"wallpaperTargetHomeLock", "wallpaperTargetHomeLock",
"menuActionSlideshow",
"collectionEmptyGrantAccessButtonLabel", "collectionEmptyGrantAccessButtonLabel",
"settingsViewerSlideshowTile",
"settingsViewerSlideshowTitle",
"settingsSlideshowRepeat",
"settingsSlideshowShuffle",
"settingsSlideshowTransitionTile",
"settingsSlideshowTransitionTitle",
"settingsSlideshowIntervalTile",
"settingsSlideshowIntervalTitle",
"settingsSlideshowVideoPlaybackTile",
"settingsSlideshowVideoPlaybackTitle",
"settingsThemeEnableDynamicColor", "settingsThemeEnableDynamicColor",
"viewerSetWallpaperButtonLabel" "viewerSetWallpaperButtonLabel"
], ],
"ko": [ "ko": [
"slideshowActionResume",
"slideshowActionShowInCollection",
"slideshowVideoPlaybackSkip",
"slideshowVideoPlaybackMuted",
"slideshowVideoPlaybackWithSound",
"viewerTransitionFade",
"viewerTransitionFadeZoomIn",
"viewerTransitionParallax",
"viewerTransitionSlide",
"wallpaperTargetHome", "wallpaperTargetHome",
"wallpaperTargetLock", "wallpaperTargetLock",
"wallpaperTargetHomeLock", "wallpaperTargetHomeLock",
"menuActionSlideshow",
"collectionEmptyGrantAccessButtonLabel", "collectionEmptyGrantAccessButtonLabel",
"settingsViewerSlideshowTile",
"settingsViewerSlideshowTitle",
"settingsSlideshowRepeat",
"settingsSlideshowShuffle",
"settingsSlideshowTransitionTile",
"settingsSlideshowTransitionTitle",
"settingsSlideshowIntervalTile",
"settingsSlideshowIntervalTitle",
"settingsSlideshowVideoPlaybackTile",
"settingsSlideshowVideoPlaybackTitle",
"settingsThemeEnableDynamicColor", "settingsThemeEnableDynamicColor",
"viewerSetWallpaperButtonLabel" "viewerSetWallpaperButtonLabel"
], ],
"pt": [ "pt": [
"slideshowActionResume",
"slideshowActionShowInCollection",
"slideshowVideoPlaybackSkip",
"slideshowVideoPlaybackMuted",
"slideshowVideoPlaybackWithSound",
"viewerTransitionFade",
"viewerTransitionFadeZoomIn",
"viewerTransitionParallax",
"viewerTransitionSlide",
"wallpaperTargetHome", "wallpaperTargetHome",
"wallpaperTargetLock", "wallpaperTargetLock",
"wallpaperTargetHomeLock", "wallpaperTargetHomeLock",
"menuActionSlideshow",
"collectionEmptyGrantAccessButtonLabel", "collectionEmptyGrantAccessButtonLabel",
"settingsViewerSlideshowTile",
"settingsViewerSlideshowTitle",
"settingsSlideshowRepeat",
"settingsSlideshowShuffle",
"settingsSlideshowTransitionTile",
"settingsSlideshowTransitionTitle",
"settingsSlideshowIntervalTile",
"settingsSlideshowIntervalTitle",
"settingsSlideshowVideoPlaybackTile",
"settingsSlideshowVideoPlaybackTitle",
"settingsThemeEnableDynamicColor", "settingsThemeEnableDynamicColor",
"viewerSetWallpaperButtonLabel" "viewerSetWallpaperButtonLabel"
], ],
"ru": [ "ru": [
"slideshowActionResume",
"slideshowActionShowInCollection",
"slideshowVideoPlaybackSkip",
"slideshowVideoPlaybackMuted",
"slideshowVideoPlaybackWithSound",
"viewerTransitionFade",
"viewerTransitionFadeZoomIn",
"viewerTransitionParallax",
"viewerTransitionSlide",
"wallpaperTargetHome", "wallpaperTargetHome",
"wallpaperTargetLock", "wallpaperTargetLock",
"wallpaperTargetHomeLock", "wallpaperTargetHomeLock",
"menuActionSlideshow",
"collectionEmptyGrantAccessButtonLabel", "collectionEmptyGrantAccessButtonLabel",
"settingsViewerSlideshowTile",
"settingsViewerSlideshowTitle",
"settingsSlideshowRepeat",
"settingsSlideshowShuffle",
"settingsSlideshowTransitionTile",
"settingsSlideshowTransitionTitle",
"settingsSlideshowIntervalTile",
"settingsSlideshowIntervalTitle",
"settingsSlideshowVideoPlaybackTile",
"settingsSlideshowVideoPlaybackTitle",
"settingsThemeEnableDynamicColor", "settingsThemeEnableDynamicColor",
"viewerSetWallpaperButtonLabel" "viewerSetWallpaperButtonLabel"
], ],
"tr": [ "tr": [
"slideshowActionResume",
"slideshowActionShowInCollection",
"slideshowVideoPlaybackSkip",
"slideshowVideoPlaybackMuted",
"slideshowVideoPlaybackWithSound",
"viewerTransitionFade",
"viewerTransitionFadeZoomIn",
"viewerTransitionParallax",
"viewerTransitionSlide",
"wallpaperTargetHome", "wallpaperTargetHome",
"wallpaperTargetLock", "wallpaperTargetLock",
"wallpaperTargetHomeLock", "wallpaperTargetHomeLock",
"menuActionSlideshow",
"settingsViewerSlideshowTile",
"settingsViewerSlideshowTitle",
"settingsSlideshowRepeat",
"settingsSlideshowShuffle",
"settingsSlideshowTransitionTile",
"settingsSlideshowTransitionTitle",
"settingsSlideshowIntervalTile",
"settingsSlideshowIntervalTitle",
"settingsSlideshowVideoPlaybackTile",
"settingsSlideshowVideoPlaybackTitle",
"viewerSetWallpaperButtonLabel" "viewerSetWallpaperButtonLabel"
], ],
"zh": [ "zh": [
"slideshowActionResume",
"slideshowActionShowInCollection",
"slideshowVideoPlaybackSkip",
"slideshowVideoPlaybackMuted",
"slideshowVideoPlaybackWithSound",
"viewerTransitionFade",
"viewerTransitionFadeZoomIn",
"viewerTransitionParallax",
"viewerTransitionSlide",
"wallpaperTargetHome", "wallpaperTargetHome",
"wallpaperTargetLock", "wallpaperTargetLock",
"wallpaperTargetHomeLock", "wallpaperTargetHomeLock",
"menuActionSlideshow",
"collectionEmptyGrantAccessButtonLabel", "collectionEmptyGrantAccessButtonLabel",
"settingsViewerSlideshowTile",
"settingsViewerSlideshowTitle",
"settingsSlideshowRepeat",
"settingsSlideshowShuffle",
"settingsSlideshowTransitionTile",
"settingsSlideshowTransitionTitle",
"settingsSlideshowIntervalTile",
"settingsSlideshowIntervalTitle",
"settingsSlideshowVideoPlaybackTile",
"settingsSlideshowVideoPlaybackTitle",
"settingsThemeEnableDynamicColor", "settingsThemeEnableDynamicColor",
"viewerSetWallpaperButtonLabel" "viewerSetWallpaperButtonLabel"
] ]