#163 slideshow
This commit is contained in:
parent
5317750506
commit
43b2a5c1c1
42 changed files with 1130 additions and 155 deletions
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
30
lib/model/actions/slideshow_actions.dart
Normal file
30
lib/model/actions/slideshow_actions.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
36
lib/model/settings/enums/slideshow_interval.dart
Normal file
36
lib/model/settings/enums/slideshow_interval.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
lib/model/settings/enums/slideshow_video_playback.dart
Normal file
17
lib/model/settings/enums/slideshow_video_playback.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
lib/model/settings/enums/viewer_transition.dart
Normal file
33
lib/model/settings/enums/viewer_transition.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>[];
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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});
|
||||||
|
|
||||||
|
|
63
lib/widgets/settings/viewer/slideshow.dart
Normal file
63
lib/widgets/settings/viewer/slideshow.dart
Normal 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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});
|
||||||
|
|
||||||
|
|
119
lib/widgets/viewer/controller.dart
Normal file
119
lib/widgets/viewer/controller.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(() {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
43
lib/widgets/viewer/overlay/slideshow_buttons.dart
Normal file
43
lib/widgets/viewer/overlay/slideshow_buttons.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
|
@ -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,
|
142
lib/widgets/viewer/slideshow_page.dart
Normal file
142
lib/widgets/viewer/slideshow_page.dart
Normal 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);
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue