#437 tv: viewer overlay
This commit is contained in:
parent
737dac8da1
commit
3be4f661cc
35 changed files with 435 additions and 247 deletions
|
@ -203,7 +203,7 @@ class Settings extends ChangeNotifier {
|
|||
await settingsStore.init();
|
||||
_appliedLocale = null;
|
||||
if (monitorPlatformSettings) {
|
||||
_platformSettingsChangeChannel.receiveBroadcastStream().listen((event) => _onPlatformSettingsChange(event as Map?));
|
||||
_platformSettingsChangeChannel.receiveBroadcastStream().listen((event) => _onPlatformSettingsChanged(event as Map?));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -246,10 +246,13 @@ class Settings extends ChangeNotifier {
|
|||
FavouriteFilter.instance,
|
||||
RecentlyAddedFilter.instance,
|
||||
];
|
||||
showOverlayOnOpening = false;
|
||||
showOverlayMinimap = false;
|
||||
showOverlayThumbnailPreview = false;
|
||||
viewerGestureSideTapNext = false;
|
||||
viewerUseCutout = true;
|
||||
viewerMaxBrightness = false;
|
||||
videoControls = VideoControls.playSeek;
|
||||
videoControls = VideoControls.none;
|
||||
videoGestureDoubleTapTogglePlay = false;
|
||||
videoGestureSideDoubleTapSeek = false;
|
||||
enableBin = false;
|
||||
|
@ -886,7 +889,7 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
// platform settings
|
||||
|
||||
void _onPlatformSettingsChange(Map? fields) {
|
||||
void _onPlatformSettingsChanged(Map? fields) {
|
||||
fields?.forEach((key, value) {
|
||||
switch (key) {
|
||||
case platformAccelerometerRotationKey:
|
||||
|
|
|
@ -104,9 +104,7 @@ class DurationsData {
|
|||
final Duration staggeredAnimation;
|
||||
final Duration staggeredAnimationPageTarget;
|
||||
final Duration quickChooserAnimation;
|
||||
|
||||
// grid animations
|
||||
final Duration gridTvFocusAnimation;
|
||||
final Duration tvImageFocusAnimation;
|
||||
|
||||
// viewer animations
|
||||
final Duration viewerVerticalPageScrollAnimation;
|
||||
|
@ -126,7 +124,7 @@ class DurationsData {
|
|||
this.staggeredAnimation = const Duration(milliseconds: 375),
|
||||
this.staggeredAnimationPageTarget = const Duration(milliseconds: 800),
|
||||
this.quickChooserAnimation = const Duration(milliseconds: 100),
|
||||
this.gridTvFocusAnimation = const Duration(milliseconds: 150),
|
||||
this.tvImageFocusAnimation = const Duration(milliseconds: 150),
|
||||
this.viewerVerticalPageScrollAnimation = const Duration(milliseconds: 500),
|
||||
this.viewerOverlayAnimation = const Duration(milliseconds: 200),
|
||||
this.viewerOverlayChangeAnimation = const Duration(milliseconds: 150),
|
||||
|
@ -144,7 +142,7 @@ class DurationsData {
|
|||
staggeredAnimation: Duration.zero,
|
||||
staggeredAnimationPageTarget: Duration.zero,
|
||||
quickChooserAnimation: Duration.zero,
|
||||
gridTvFocusAnimation: Duration.zero,
|
||||
tvImageFocusAnimation: Duration.zero,
|
||||
viewerVerticalPageScrollAnimation: Duration.zero,
|
||||
viewerOverlayAnimation: Duration.zero,
|
||||
viewerOverlayChangeAnimation: Duration.zero,
|
||||
|
|
|
@ -154,7 +154,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
_screenSize = _getScreenSize();
|
||||
_shouldUseBoldFontLoader = AccessibilityService.shouldUseBoldFont();
|
||||
_dynamicColorPaletteLoader = DynamicColorPlugin.getCorePalette();
|
||||
_mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChange(event as String?));
|
||||
_mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChanged(event as String?));
|
||||
_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?));
|
||||
_analysisCompletionChannel.receiveBroadcastStream().listen((event) => _onAnalysisCompletion());
|
||||
_errorChannel.receiveBroadcastStream().listen((event) => _onError(event as String?));
|
||||
|
@ -451,7 +451,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
_mediaStoreSource.updateDerivedFilters();
|
||||
}
|
||||
|
||||
void _onMediaStoreChange(String? uri) {
|
||||
void _onMediaStoreChanged(String? uri) {
|
||||
if (uri != null) _changedUris.add(uri);
|
||||
if (_changedUris.isNotEmpty) {
|
||||
_mediaStoreChangeDebouncer(() async {
|
||||
|
@ -460,7 +460,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
final tempUris = await _mediaStoreSource.refreshUris(todo);
|
||||
if (tempUris.isNotEmpty) {
|
||||
_changedUris.addAll(tempUris);
|
||||
_onMediaStoreChange(null);
|
||||
_onMediaStoreChanged(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
duration: context.read<DurationsData>().iconAnimation,
|
||||
vsync: this,
|
||||
);
|
||||
_isSelectingNotifier.addListener(_onActivityChange);
|
||||
_isSelectingNotifier.addListener(_onActivityChanged);
|
||||
_registerWidget(widget);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
|
@ -121,8 +121,10 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
@override
|
||||
void dispose() {
|
||||
_unregisterWidget(widget);
|
||||
_queryBarFocusNode.dispose();
|
||||
_queryFocusRequestNotifier.removeListener(_onQueryFocusRequest);
|
||||
_isSelectingNotifier.removeListener(_onActivityChange);
|
||||
_isSelectingNotifier.removeListener(_onActivityChanged);
|
||||
_isSelectingNotifier.dispose();
|
||||
_browseToSelectAnimation.dispose();
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
|
@ -529,7 +531,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
);
|
||||
}
|
||||
|
||||
void _onActivityChange() {
|
||||
void _onActivityChanged() {
|
||||
if (context.read<Selection<AvesEntry>>().isSelecting) {
|
||||
_browseToSelectAnimation.forward();
|
||||
} else {
|
||||
|
|
|
@ -191,7 +191,7 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
|
|||
return AnimatedScale(
|
||||
scale: focusedItem == entry ? 1 : .9,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
duration: context.select<DurationsData, Duration>((v) => v.gridTvFocusAnimation),
|
||||
duration: context.select<DurationsData, Duration>((v) => v.tvImageFocusAnimation),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
|
@ -408,13 +408,13 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
|
|||
void _registerWidget(_CollectionScrollView widget) {
|
||||
widget.collection.filterChangeNotifier.addListener(_scrollToTop);
|
||||
widget.collection.sortSectionChangeNotifier.addListener(_scrollToTop);
|
||||
widget.scrollController.addListener(_onScrollChange);
|
||||
widget.scrollController.addListener(_onScrollChanged);
|
||||
}
|
||||
|
||||
void _unregisterWidget(_CollectionScrollView widget) {
|
||||
widget.collection.filterChangeNotifier.removeListener(_scrollToTop);
|
||||
widget.collection.sortSectionChangeNotifier.removeListener(_scrollToTop);
|
||||
widget.scrollController.removeListener(_onScrollChange);
|
||||
widget.scrollController.removeListener(_onScrollChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -570,7 +570,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
|
|||
|
||||
void _scrollToTop() => widget.scrollController.jumpTo(0);
|
||||
|
||||
void _onScrollChange() {
|
||||
void _onScrollChanged() {
|
||||
widget.isScrollingNotifier.value = true;
|
||||
_stopScrollMonitoringTimer();
|
||||
_scrollMonitoringTimer = Timer(Durations.collectionScrollMonitoringTimerDelay, () {
|
||||
|
|
|
@ -59,7 +59,7 @@ class _QueryBarState extends State<QueryBar> {
|
|||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: widget.focusNode ?? FocusNode(),
|
||||
focusNode: widget.focusNode,
|
||||
decoration: InputDecoration(
|
||||
icon: Padding(
|
||||
padding: widget.leadingPadding ?? const EdgeInsetsDirectional.only(start: 16),
|
||||
|
|
|
@ -53,7 +53,7 @@ class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin {
|
|||
parent: _angleAnimationController,
|
||||
curve: widget.curve,
|
||||
));
|
||||
_angleAnimationController.addStatusListener(_onAnimationStatusChange);
|
||||
_angleAnimationController.addStatusListener(_onAnimationStatusChanged);
|
||||
_registerWidget(widget);
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_angleAnimationController.removeStatusListener(_onAnimationStatusChange);
|
||||
_angleAnimationController.removeStatusListener(_onAnimationStatusChanged);
|
||||
_angleAnimationController.dispose();
|
||||
_unregisterWidget(widget);
|
||||
super.dispose();
|
||||
|
@ -101,7 +101,7 @@ class _SweeperState extends State<Sweeper> with SingleTickerProviderStateMixin {
|
|||
);
|
||||
}
|
||||
|
||||
void _onAnimationStatusChange(AnimationStatus status) {
|
||||
void _onAnimationStatusChanged(AnimationStatus status) {
|
||||
setState(() {});
|
||||
if (status == AnimationStatus.completed) {
|
||||
widget.onSweepEnd?.call();
|
||||
|
|
|
@ -70,7 +70,7 @@ class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBin
|
|||
void didUpdateWidget(covariant GridItemTracker<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.tileLayout != widget.tileLayout) {
|
||||
_onLayoutChange();
|
||||
_onLayoutChanged();
|
||||
}
|
||||
_saveLayoutMetrics();
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBin
|
|||
final orientation = _windowOrientation;
|
||||
if (_lastOrientation != orientation) {
|
||||
_lastOrientation = orientation;
|
||||
_onLayoutChange();
|
||||
_onLayoutChanged();
|
||||
_saveLayoutMetrics();
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBin
|
|||
}
|
||||
}
|
||||
|
||||
void _onLayoutChange() {
|
||||
void _onLayoutChanged() {
|
||||
if (scrollController.positions.length != 1) return;
|
||||
|
||||
// do not track when view shows top edge
|
||||
|
|
|
@ -162,13 +162,13 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
_tapped = false;
|
||||
_subscriptions.add(covers.packageChangeStream.listen(_onCoverColorChange));
|
||||
_subscriptions.add(covers.colorChangeStream.listen(_onCoverColorChange));
|
||||
_subscriptions.add(covers.packageChangeStream.listen(_onCoverColorChanged));
|
||||
_subscriptions.add(covers.colorChangeStream.listen(_onCoverColorChanged));
|
||||
_subscriptions.add(settings.updateStream.where((event) => event.key == Settings.themeColorModeKey).listen((_) {
|
||||
// delay so that contextual colors reflect the new settings
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
_onCoverColorChange(null);
|
||||
_onCoverColorChanged(null);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
@ -207,7 +207,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
_outlineColor = context.read<AvesColorsData>().neutral;
|
||||
}
|
||||
|
||||
void _onCoverColorChange(Set<CollectionFilter>? event) {
|
||||
void _onCoverColorChanged(Set<CollectionFilter>? event) {
|
||||
if (event == null || event.contains(filter)) {
|
||||
_initColorLoader();
|
||||
setState(() {});
|
||||
|
|
|
@ -98,12 +98,12 @@ class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProv
|
|||
}
|
||||
_subscriptions.add(_leafletMapController.mapEventStream.listen((event) => _updateVisibleRegion()));
|
||||
widget.clusterListenable.addListener(_updateMarkers);
|
||||
widget.boundsNotifier.addListener(_onBoundsChange);
|
||||
widget.boundsNotifier.addListener(_onBoundsChanged);
|
||||
}
|
||||
|
||||
void _unregisterWidget(EntryLeafletMap<T> widget) {
|
||||
widget.clusterListenable.removeListener(_updateMarkers);
|
||||
widget.boundsNotifier.removeListener(_onBoundsChange);
|
||||
widget.boundsNotifier.removeListener(_onBoundsChanged);
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
|
@ -230,7 +230,7 @@ class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProv
|
|||
);
|
||||
}
|
||||
|
||||
void _onBoundsChange() => _debouncer(_onIdle);
|
||||
void _onBoundsChanged() => _debouncer(_onIdle);
|
||||
|
||||
void _onIdle() {
|
||||
if (!mounted) return;
|
||||
|
|
|
@ -78,14 +78,14 @@ class _ThumbnailScrollerState extends State<ThumbnailScroller> {
|
|||
void _registerWidget(ThumbnailScroller widget) {
|
||||
final scrollOffset = indexToScrollOffset(indexNotifier.value ?? 0);
|
||||
_scrollController = ScrollController(initialScrollOffset: scrollOffset);
|
||||
_scrollController.addListener(_onScrollChange);
|
||||
widget.indexNotifier.addListener(_onIndexChange);
|
||||
_scrollController.addListener(_onScrollChanged);
|
||||
widget.indexNotifier.addListener(_onIndexChanged);
|
||||
}
|
||||
|
||||
void _unregisterWidget(ThumbnailScroller widget) {
|
||||
_scrollController.removeListener(_onScrollChange);
|
||||
_scrollController.removeListener(_onScrollChanged);
|
||||
_scrollController.dispose();
|
||||
widget.indexNotifier.removeListener(_onIndexChange);
|
||||
widget.indexNotifier.removeListener(_onIndexChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -180,7 +180,7 @@ class _ThumbnailScrollerState extends State<ThumbnailScroller> {
|
|||
}
|
||||
}
|
||||
|
||||
void _onScrollChange() {
|
||||
void _onScrollChanged() {
|
||||
if (!_isAnimating) {
|
||||
final index = scrollOffsetToIndex(_scrollController.offset);
|
||||
if (indexNotifier.value != index) {
|
||||
|
@ -190,7 +190,7 @@ class _ThumbnailScrollerState extends State<ThumbnailScroller> {
|
|||
}
|
||||
}
|
||||
|
||||
void _onIndexChange() {
|
||||
void _onIndexChanged() {
|
||||
if (!_isScrolling && !_isAnimating) {
|
||||
final index = indexNotifier.value;
|
||||
if (index != null) {
|
||||
|
|
|
@ -45,13 +45,13 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
_patternTextController.text = settings.entryRenamingPattern;
|
||||
_patternTextController.addListener(_onUserPatternChange);
|
||||
_onUserPatternChange();
|
||||
_patternTextController.addListener(_onUserPatternChanged);
|
||||
_onUserPatternChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_patternTextController.removeListener(_onUserPatternChange);
|
||||
_patternTextController.removeListener(_onUserPatternChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -196,7 +196,7 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
|||
);
|
||||
}
|
||||
|
||||
void _onUserPatternChange() {
|
||||
void _onUserPatternChanged() {
|
||||
_namingPatternNotifier.value = NamingPattern.from(
|
||||
userPattern: _patternTextController.text,
|
||||
entryCount: entryCount,
|
||||
|
|
|
@ -44,6 +44,13 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
_initTopTags();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_newTagTextFocusNode.dispose();
|
||||
_expandedSectionNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
|
|
@ -38,6 +38,9 @@ class _CreateAlbumDialogState extends State<CreateAlbumDialog> {
|
|||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_nameFieldFocusNode.removeListener(_onFocus);
|
||||
_nameFieldFocusNode.dispose();
|
||||
_existsNotifier.dispose();
|
||||
_isValidNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
|
@ -102,14 +102,16 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
|
|||
duration: context.read<DurationsData>().iconAnimation,
|
||||
vsync: this,
|
||||
);
|
||||
_isSelectingNotifier.addListener(_onActivityChange);
|
||||
_isSelectingNotifier.addListener(_onActivityChanged);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _updateAppBarHeight());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_queryBarFocusNode.dispose();
|
||||
_queryFocusRequestNotifier.removeListener(_onQueryFocusRequest);
|
||||
_isSelectingNotifier.removeListener(_onActivityChange);
|
||||
_isSelectingNotifier.removeListener(_onActivityChanged);
|
||||
_isSelectingNotifier.dispose();
|
||||
_browseToSelectAnimation.dispose();
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
|
@ -368,7 +370,7 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
|
|||
}
|
||||
}
|
||||
|
||||
void _onActivityChange() {
|
||||
void _onActivityChanged() {
|
||||
if (context.read<Selection<FilterGridItem<T>>>().isSelecting) {
|
||||
_browseToSelectAnimation.forward();
|
||||
} else {
|
||||
|
|
|
@ -365,7 +365,7 @@ class _FilterGridContentState<T extends CollectionFilter> extends State<_FilterG
|
|||
return AnimatedScale(
|
||||
scale: focusedItem == gridItem ? 1 : .9,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
duration: context.select<DurationsData, Duration>((v) => v.gridTvFocusAnimation),
|
||||
duration: context.select<DurationsData, Duration>((v) => v.tvImageFocusAnimation),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -132,12 +132,12 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
|||
parent: _overlayAnimationController,
|
||||
curve: Curves.easeOutQuad,
|
||||
);
|
||||
_overlayVisible.addListener(_onOverlayVisibleChange);
|
||||
_overlayVisible.addListener(_onOverlayVisibleChanged);
|
||||
|
||||
_subscriptions.add(_mapController.idleUpdates.listen((event) => _onIdle(event.bounds)));
|
||||
_subscriptions.add(openingCollection.source.eventBus.on<CatalogMetadataChangedEvent>().listen((e) => _updateRegionCollection()));
|
||||
|
||||
_selectedIndexNotifier.addListener(_onThumbnailIndexChange);
|
||||
_selectedIndexNotifier.addListener(_onThumbnailIndexChanged);
|
||||
Future.delayed(Durations.pageTransitionAnimation * timeDilation + const Duration(seconds: 1), () {
|
||||
final regionEntries = regionCollection?.sortedEntries ?? [];
|
||||
final initialEntry = widget.initialEntry ?? regionEntries.firstOrNull;
|
||||
|
@ -150,7 +150,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
|||
}
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onOverlayVisibleChange(animate: false));
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onOverlayVisibleChanged(animate: false));
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -161,9 +161,9 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
|||
_dotEntryNotifier.value?.metadataChangeNotifier.removeListener(_onMarkerEntryMetadataChanged);
|
||||
_dotEntryNotifier.removeListener(_onSelectedEntryChanged);
|
||||
_overlayAnimationController.dispose();
|
||||
_overlayVisible.removeListener(_onOverlayVisibleChange);
|
||||
_overlayVisible.removeListener(_onOverlayVisibleChanged);
|
||||
_mapController.dispose();
|
||||
_selectedIndexNotifier.removeListener(_onThumbnailIndexChange);
|
||||
_selectedIndexNotifier.removeListener(_onThumbnailIndexChanged);
|
||||
regionCollection?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
@ -380,7 +380,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
|||
: 0;
|
||||
_selectedIndexNotifier.value = selectedIndex;
|
||||
// force update, as the region entries may change without a change of index
|
||||
_onThumbnailIndexChange();
|
||||
_onThumbnailIndexChanged();
|
||||
}
|
||||
|
||||
AvesEntry? _getRegionEntry(int? index) {
|
||||
|
@ -395,7 +395,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
|||
|
||||
void _onThumbnailTap(int index) => _goToViewer(_getRegionEntry(index));
|
||||
|
||||
void _onThumbnailIndexChange() => _onEntrySelected(_getRegionEntry(_selectedIndexNotifier.value));
|
||||
void _onThumbnailIndexChanged() => _onEntrySelected(_getRegionEntry(_selectedIndexNotifier.value));
|
||||
|
||||
void _onEntrySelected(AvesEntry? selectedEntry) {
|
||||
_dotEntryNotifier.value?.metadataChangeNotifier.removeListener(_onMarkerEntryMetadataChanged);
|
||||
|
@ -457,7 +457,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
|
|||
|
||||
void _toggleOverlay() => _overlayVisible.value = !_overlayVisible.value;
|
||||
|
||||
Future<void> _onOverlayVisibleChange({bool animate = true}) async {
|
||||
Future<void> _onOverlayVisibleChanged({bool animate = true}) async {
|
||||
if (_overlayVisible.value) {
|
||||
if (animate) {
|
||||
await _overlayAnimationController.forward();
|
||||
|
|
|
@ -69,12 +69,12 @@ class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProvid
|
|||
|
||||
void _registerWidget(FloatingNavBar widget) {
|
||||
_lastOffset = null;
|
||||
widget.scrollController?.addListener(_onScrollChange);
|
||||
widget.scrollController?.addListener(_onScrollChanged);
|
||||
_subscriptions.add(widget.events.listen(_onDraggableScrollBarEvent));
|
||||
}
|
||||
|
||||
void _unregisterWidget(FloatingNavBar widget) {
|
||||
widget.scrollController?.removeListener(_onScrollChange);
|
||||
widget.scrollController?.removeListener(_onScrollChanged);
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
|
@ -88,7 +88,7 @@ class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProvid
|
|||
);
|
||||
}
|
||||
|
||||
void _onScrollChange() {
|
||||
void _onScrollChanged() {
|
||||
final scrollController = widget.scrollController;
|
||||
if (scrollController == null) return;
|
||||
|
||||
|
|
|
@ -57,11 +57,11 @@ class _AppBottomNavBarState extends State<AppBottomNavBar> {
|
|||
}
|
||||
|
||||
void _registerWidget(AppBottomNavBar widget) {
|
||||
widget.currentCollection?.filterChangeNotifier.addListener(_onCollectionFilterChange);
|
||||
widget.currentCollection?.filterChangeNotifier.addListener(_onCollectionFilterChanged);
|
||||
}
|
||||
|
||||
void _unregisterWidget(AppBottomNavBar widget) {
|
||||
widget.currentCollection?.filterChangeNotifier.removeListener(_onCollectionFilterChange);
|
||||
widget.currentCollection?.filterChangeNotifier.removeListener(_onCollectionFilterChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -112,7 +112,7 @@ class _AppBottomNavBarState extends State<AppBottomNavBar> {
|
|||
);
|
||||
}
|
||||
|
||||
void _onCollectionFilterChange() => setState(() {});
|
||||
void _onCollectionFilterChanged() => setState(() {});
|
||||
|
||||
int _getCurrentIndex(BuildContext context, List<AvesBottomNavItem> items) {
|
||||
// current route may be null during navigation
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/model/device.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';
|
||||
|
@ -19,6 +20,7 @@ class ViewerOverlayPage extends StatelessWidget {
|
|||
body: SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
if (!device.isTelevision)
|
||||
SettingsSwitchListTile(
|
||||
selector: (context, s) => s.showOverlayOnOpening,
|
||||
onChanged: (v) => settings.showOverlayOnOpening = v,
|
||||
|
@ -54,11 +56,13 @@ class ViewerOverlayPage extends StatelessWidget {
|
|||
);
|
||||
},
|
||||
),
|
||||
if (!device.isTelevision)
|
||||
SettingsSwitchListTile(
|
||||
selector: (context, s) => s.showOverlayMinimap,
|
||||
onChanged: (v) => settings.showOverlayMinimap = v,
|
||||
title: context.l10n.settingsViewerShowMinimap,
|
||||
),
|
||||
if (!device.isTelevision)
|
||||
SettingsSwitchListTile(
|
||||
selector: (context, s) => s.showOverlayThumbnailPreview,
|
||||
onChanged: (v) => settings.showOverlayThumbnailPreview = v,
|
||||
|
|
|
@ -92,6 +92,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
case EntryAction.videoCaptureFrame:
|
||||
return !device.isReadOnly && targetEntry.isVideo;
|
||||
case EntryAction.videoToggleMute:
|
||||
return !device.isTelevision && targetEntry.isVideo;
|
||||
case EntryAction.videoSelectStreams:
|
||||
case EntryAction.videoSetSpeed:
|
||||
case EntryAction.videoSettings:
|
||||
|
@ -106,8 +107,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
return device.canPinShortcut;
|
||||
case EntryAction.edit:
|
||||
return !device.isReadOnly;
|
||||
case EntryAction.info:
|
||||
case EntryAction.copyToClipboard:
|
||||
return !device.isTelevision;
|
||||
case EntryAction.info:
|
||||
case EntryAction.open:
|
||||
case EntryAction.setAs:
|
||||
case EntryAction.share:
|
||||
|
@ -174,7 +176,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
|
||||
switch (action) {
|
||||
case EntryAction.info:
|
||||
ShowInfoNotification().dispatch(context);
|
||||
ShowInfoPageNotification().dispatch(context);
|
||||
break;
|
||||
case EntryAction.addShortcut:
|
||||
_addShortcut(context, targetEntry);
|
||||
|
|
|
@ -48,18 +48,18 @@ class ViewerController {
|
|||
}) {
|
||||
_initialScale = initialScale;
|
||||
_autopilotNotifier = ValueNotifier(autopilot);
|
||||
_autopilotNotifier.addListener(_onAutopilotChange);
|
||||
_onAutopilotChange();
|
||||
_autopilotNotifier.addListener(_onAutopilotChanged);
|
||||
_onAutopilotChanged();
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_autopilotNotifier.removeListener(_onAutopilotChange);
|
||||
_autopilotNotifier.removeListener(_onAutopilotChanged);
|
||||
_clearAutopilotAnimations();
|
||||
_stopPlayTimer();
|
||||
_streamController.close();
|
||||
}
|
||||
|
||||
void _onAutopilotChange() {
|
||||
void _onAutopilotChanged() {
|
||||
_clearAutopilotAnimations();
|
||||
_stopPlayTimer();
|
||||
if (autopilot && autopilotInterval != null) {
|
||||
|
|
|
@ -3,6 +3,8 @@ import 'dart:math';
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
|
@ -11,6 +13,7 @@ import 'package:aves/widgets/viewer/controller.dart';
|
|||
import 'package:aves/widgets/viewer/entry_horizontal_pager.dart';
|
||||
import 'package:aves/widgets/viewer/info/info_page.dart';
|
||||
import 'package:aves/widgets/viewer/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/video/conductor.dart';
|
||||
import 'package:aves_magnifier/aves_magnifier.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -50,6 +53,7 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
|
|||
final List<StreamSubscription> _subscriptions = [];
|
||||
final ValueNotifier<double> _backgroundOpacityNotifier = ValueNotifier(1);
|
||||
final ValueNotifier<bool> _isVerticallyScrollingNotifier = ValueNotifier(false);
|
||||
final ValueNotifier<bool> _isImageFocusedNotifier = ValueNotifier(true);
|
||||
Timer? _verticalScrollMonitoringTimer;
|
||||
AvesEntry? _oldEntry;
|
||||
Future<double>? _systemBrightness;
|
||||
|
@ -83,6 +87,9 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
|
|||
void dispose() {
|
||||
_unregisterWidget(widget);
|
||||
_stopScrollMonitoringTimer();
|
||||
_backgroundOpacityNotifier.dispose();
|
||||
_isVerticallyScrollingNotifier.dispose();
|
||||
_isImageFocusedNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -174,10 +181,19 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
|
|||
}
|
||||
|
||||
Widget _buildImagePage() {
|
||||
final isTelevision = device.isTelevision;
|
||||
|
||||
Widget? child;
|
||||
Map<ShortcutActivator, Intent>? shortcuts;
|
||||
Map<ShortcutActivator, Intent>? shortcuts = {
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp): isTelevision ? const TvShowLessInfoIntent() : const LeaveIntent(),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown): isTelevision ? const TvShowMoreInfoIntent() : const ShowInfoIntent(),
|
||||
};
|
||||
|
||||
if (hasCollection) {
|
||||
shortcuts.addAll(const {
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft): ShowPreviousIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight): ShowNextIntent(),
|
||||
});
|
||||
child = MultiEntryScroller(
|
||||
collection: collection!,
|
||||
viewerController: widget.viewerController,
|
||||
|
@ -185,23 +201,28 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
|
|||
onPageChanged: widget.onHorizontalPageChanged,
|
||||
onViewDisposed: widget.onViewDisposed,
|
||||
);
|
||||
shortcuts = const {
|
||||
SingleActivator(LogicalKeyboardKey.arrowLeft): ShowPreviousIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowRight): ShowNextIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp): LeaveIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown): ShowInfoIntent(),
|
||||
};
|
||||
} else if (entry != null) {
|
||||
child = SingleEntryScroller(
|
||||
viewerController: widget.viewerController,
|
||||
entry: entry!,
|
||||
);
|
||||
shortcuts = const {
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp): LeaveIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown): ShowInfoIntent(),
|
||||
};
|
||||
}
|
||||
if (child != null) {
|
||||
if (device.isTelevision) {
|
||||
child = ValueListenableBuilder<bool>(
|
||||
valueListenable: _isImageFocusedNotifier,
|
||||
builder: (context, isImageFocused, child) {
|
||||
return AnimatedScale(
|
||||
scale: isImageFocused ? 1 : .7,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
duration: context.select<DurationsData, Duration>((v) => v.tvImageFocusAnimation),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return FocusableActionDetector(
|
||||
autofocus: true,
|
||||
shortcuts: shortcuts,
|
||||
|
@ -209,8 +230,25 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
|
|||
ShowPreviousIntent: CallbackAction<Intent>(onInvoke: (intent) => _goToHorizontalPage(-1, animate: false)),
|
||||
ShowNextIntent: CallbackAction<Intent>(onInvoke: (intent) => _goToHorizontalPage(1, animate: false)),
|
||||
LeaveIntent: CallbackAction<Intent>(onInvoke: (intent) => Navigator.pop(context)),
|
||||
ShowInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => ShowInfoNotification().dispatch(context)),
|
||||
ShowInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => ShowInfoPageNotification().dispatch(context)),
|
||||
TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context)),
|
||||
TvShowMoreInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowMoreInfoNotification().dispatch(context)),
|
||||
ActivateIntent: CallbackAction<Intent>(onInvoke: (intent) {
|
||||
if (isTelevision) {
|
||||
final _entry = entry;
|
||||
if (_entry != null && _entry.isVideo) {
|
||||
final controller = context.read<VideoConductor>().getController(_entry);
|
||||
if (controller != null) {
|
||||
VideoActionNotification(controller: controller, action: EntryAction.videoTogglePlay).dispatch(context);
|
||||
}
|
||||
} else {
|
||||
const ToggleOverlayNotification().dispatch(context);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
onFocusChange: (focused) => _isImageFocusedNotifier.value = focused,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
@ -311,3 +349,11 @@ class LeaveIntent extends Intent {
|
|||
class ShowInfoIntent extends Intent {
|
||||
const ShowInfoIntent();
|
||||
}
|
||||
|
||||
class TvShowLessInfoIntent extends Intent {
|
||||
const TvShowLessInfoIntent();
|
||||
}
|
||||
|
||||
class TvShowMoreInfoIntent extends Intent {
|
||||
const TvShowMoreInfoIntent();
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
_currentEntryIndex = max(0, entry != null ? entries.indexOf(entry) : -1);
|
||||
_currentVerticalPage = ValueNotifier(imagePage);
|
||||
_horizontalPager = PageController(initialPage: _currentEntryIndex);
|
||||
_verticalPager = PageController(initialPage: _currentVerticalPage.value)..addListener(_onVerticalPageControllerChange);
|
||||
_verticalPager = PageController(initialPage: _currentVerticalPage.value)..addListener(_onVerticalPageControllerChanged);
|
||||
_overlayAnimationController = AnimationController(
|
||||
duration: context.read<DurationsData>().viewerOverlayAnimation,
|
||||
vsync: this,
|
||||
|
@ -142,7 +142,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
curve: Curves.easeOutQuad,
|
||||
));
|
||||
_overlayVisible.value = settings.showOverlayOnOpening && !viewerController.autopilot;
|
||||
_overlayVisible.addListener(_onOverlayVisibleChange);
|
||||
_overlayVisible.addListener(_onOverlayVisibleChanged);
|
||||
_videoActionDelegate = VideoActionDelegate(
|
||||
collection: collection,
|
||||
);
|
||||
|
@ -164,19 +164,19 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
cleanEntryControllers(entryNotifier.value);
|
||||
_videoActionDelegate.dispose();
|
||||
_overlayAnimationController.dispose();
|
||||
_overlayVisible.removeListener(_onOverlayVisibleChange);
|
||||
_verticalPager.removeListener(_onVerticalPageControllerChange);
|
||||
_overlayVisible.removeListener(_onOverlayVisibleChanged);
|
||||
_verticalPager.removeListener(_onVerticalPageControllerChanged);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_unregisterWidget(widget);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _registerWidget(EntryViewerStack widget) {
|
||||
widget.collection?.addListener(_onCollectionChange);
|
||||
widget.collection?.addListener(_onCollectionChanged);
|
||||
}
|
||||
|
||||
void _unregisterWidget(EntryViewerStack widget) {
|
||||
widget.collection?.removeListener(_onCollectionChange);
|
||||
widget.collection?.removeListener(_onCollectionChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -199,13 +199,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
final viewStateConductor = context.read<ViewStateConductor>();
|
||||
return WillPopScope(
|
||||
onWillPop: () {
|
||||
if (_currentVerticalPage.value == infoPage) {
|
||||
// back from info to image
|
||||
_goToVerticalPage(imagePage);
|
||||
} else {
|
||||
if (!_isEntryTracked) _trackEntry();
|
||||
_popVisual();
|
||||
}
|
||||
_onWillPop();
|
||||
return SynchronousFuture(false);
|
||||
},
|
||||
child: ValueListenableProvider<HeroInfo?>.value(
|
||||
|
@ -243,10 +237,19 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
}
|
||||
} else if (notification is ToggleOverlayNotification) {
|
||||
_overlayVisible.value = notification.visible ?? !_overlayVisible.value;
|
||||
} else if (notification is ShowInfoNotification) {
|
||||
} else if (notification is TvShowLessInfoNotification) {
|
||||
if (_overlayVisible.value) {
|
||||
_overlayVisible.value = false;
|
||||
} else {
|
||||
_onWillPop();
|
||||
}
|
||||
} else if (notification is TvShowMoreInfoNotification) {
|
||||
if (!_overlayVisible.value) {
|
||||
_overlayVisible.value = true;
|
||||
}
|
||||
} else if (notification is ShowInfoPageNotification) {
|
||||
// remove focus, if any, to prevent viewer shortcuts activation from the Info page
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
_goToVerticalPage(infoPage);
|
||||
_showInfoPage();
|
||||
} else if (notification is JumpToPreviousEntryNotification) {
|
||||
_jumpToHorizontalPageByDelta(-1);
|
||||
} else if (notification is JumpToNextEntryNotification) {
|
||||
|
@ -458,7 +461,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
builder: (context, mqHeight, child) {
|
||||
// when orientation change, the `PageController` offset is not updated right away
|
||||
// and it does not trigger its listeners when it does, so we force a refresh in the next frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onVerticalPageControllerChange());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onVerticalPageControllerChanged());
|
||||
return AnimatedBuilder(
|
||||
animation: _verticalScrollNotifier,
|
||||
builder: (context, child) => Positioned(
|
||||
|
@ -492,7 +495,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
}
|
||||
}
|
||||
|
||||
void _onVerticalPageControllerChange() {
|
||||
void _onVerticalPageControllerChanged() {
|
||||
if (!_isEntryTracked && _verticalPager.hasClients && _verticalPager.page?.floor() == transitionPage) {
|
||||
_trackEntry();
|
||||
}
|
||||
|
@ -520,6 +523,12 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
);
|
||||
}
|
||||
|
||||
void _showInfoPage() {
|
||||
// remove focus, if any, to prevent viewer shortcuts activation from the Info page
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
_goToVerticalPage(infoPage);
|
||||
}
|
||||
|
||||
Future<void> _goToVerticalPage(int page) async {
|
||||
final animationDuration = context.read<DurationsData>().viewerVerticalPageScrollAnimation;
|
||||
if (animationDuration > Duration.zero) {
|
||||
|
@ -574,7 +583,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
_updateEntry();
|
||||
}
|
||||
|
||||
void _onCollectionChange() {
|
||||
void _onCollectionChanged() {
|
||||
_updateEntry();
|
||||
}
|
||||
|
||||
|
@ -588,7 +597,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
if (index != -1) {
|
||||
_onHorizontalPageChanged(index);
|
||||
}
|
||||
_onCollectionChange();
|
||||
_onCollectionChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -600,7 +609,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
final collectionEntries = collection!.sortedEntries;
|
||||
removedEntries.forEach(collectionEntries.remove);
|
||||
if (collectionEntries.isNotEmpty) {
|
||||
_onCollectionChange();
|
||||
_onCollectionChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -630,6 +639,16 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
await initEntryControllers(newEntry);
|
||||
}
|
||||
|
||||
void _onWillPop() {
|
||||
if (_currentVerticalPage.value == infoPage) {
|
||||
// back from info to image
|
||||
_goToVerticalPage(imagePage);
|
||||
} else {
|
||||
if (!_isEntryTracked) _trackEntry();
|
||||
_popVisual();
|
||||
}
|
||||
}
|
||||
|
||||
void _popVisual() {
|
||||
if (Navigator.canPop(context)) {
|
||||
void pop() {
|
||||
|
@ -689,11 +708,11 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
// wait for MaterialPageRoute.transitionDuration
|
||||
// to show overlay after hero animation is complete
|
||||
await Future.delayed(ModalRoute.of(context)!.transitionDuration * timeDilation);
|
||||
await _onOverlayVisibleChange();
|
||||
await _onOverlayVisibleChanged();
|
||||
_overlayInitialized = true;
|
||||
}
|
||||
|
||||
Future<void> _onOverlayVisibleChange({bool animate = true}) async {
|
||||
Future<void> _onOverlayVisibleChanged({bool animate = true}) async {
|
||||
if (_overlayVisible.value) {
|
||||
await AvesApp.showSystemUI();
|
||||
AvesApp.setSystemUIStyle(context);
|
||||
|
|
|
@ -61,11 +61,11 @@ class _LocationSectionState extends State<LocationSection> {
|
|||
}
|
||||
|
||||
void _registerWidget(LocationSection widget) {
|
||||
widget.entry.metadataChangeNotifier.addListener(_onMetadataChange);
|
||||
widget.entry.metadataChangeNotifier.addListener(_onMetadataChanged);
|
||||
}
|
||||
|
||||
void _unregisterWidget(LocationSection widget) {
|
||||
widget.entry.metadataChangeNotifier.removeListener(_onMetadataChange);
|
||||
widget.entry.metadataChangeNotifier.removeListener(_onMetadataChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -150,7 +150,7 @@ class _LocationSectionState extends State<LocationSection> {
|
|||
mapCollection.dispose();
|
||||
}
|
||||
|
||||
void _onMetadataChange() {
|
||||
void _onMetadataChanged() {
|
||||
setState(() {});
|
||||
|
||||
final location = entry.latLng;
|
||||
|
|
|
@ -10,7 +10,13 @@ import 'package:flutter/widgets.dart';
|
|||
class ShowImageNotification extends Notification {}
|
||||
|
||||
@immutable
|
||||
class ShowInfoNotification extends Notification {}
|
||||
class ShowInfoPageNotification extends Notification {}
|
||||
|
||||
@immutable
|
||||
class TvShowLessInfoNotification extends Notification {}
|
||||
|
||||
@immutable
|
||||
class TvShowMoreInfoNotification extends Notification {}
|
||||
|
||||
@immutable
|
||||
class ToggleOverlayNotification extends Notification {
|
||||
|
|
|
@ -1,17 +1,22 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||
import 'package:aves/widgets/viewer/entry_vertical_pager.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/controller.dart';
|
||||
import 'package:aves/widgets/viewer/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/multipage.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/thumbnail_preview.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/wallpaper_buttons.dart';
|
||||
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
|
@ -119,11 +124,30 @@ class _BottomOverlayContent extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
||||
final FocusScopeNode _buttonRowFocusScopeNode = FocusScopeNode();
|
||||
late Animation<double> _buttonScale, _thumbnailOpacity;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registerWidget(widget);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _BottomOverlayContent oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_unregisterWidget(oldWidget);
|
||||
_registerWidget(widget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unregisterWidget(widget);
|
||||
_buttonRowFocusScopeNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _registerWidget(_BottomOverlayContent widget) {
|
||||
final animationController = widget.animationController;
|
||||
_buttonScale = CurvedAnimation(
|
||||
parent: animationController,
|
||||
|
@ -134,6 +158,11 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
|||
parent: animationController,
|
||||
curve: Curves.easeOutQuad,
|
||||
);
|
||||
animationController.addStatusListener(_onAnimationStatusChanged);
|
||||
}
|
||||
|
||||
void _unregisterWidget(_BottomOverlayContent widget) {
|
||||
widget.animationController.removeStatusListener(_onAnimationStatusChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -153,7 +182,11 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
|||
selector: (context, mq) => mq.size.width,
|
||||
builder: (context, mqWidth, child) {
|
||||
final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero);
|
||||
final viewerButtonRow = SafeArea(
|
||||
final viewerButtonRow = FocusableActionDetector(
|
||||
focusNode: _buttonRowFocusScopeNode,
|
||||
shortcuts: device.isTelevision ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null,
|
||||
actions: {TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))},
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
minimum: EdgeInsets.only(
|
||||
|
@ -171,6 +204,7 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
|||
collection: widget.collection,
|
||||
scale: _buttonScale,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null;
|
||||
|
@ -228,7 +262,14 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
|||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _onAnimationStatusChanged(AnimationStatus status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
_buttonRowFocusScopeNode.children.firstOrNull?.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -49,11 +49,11 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
|||
}
|
||||
|
||||
void _registerWidget(MultiPageOverlay widget) {
|
||||
widget.controller.pageNotifier.addListener(_onPageChange);
|
||||
widget.controller.pageNotifier.addListener(_onPageChanged);
|
||||
}
|
||||
|
||||
void _unregisterWidget(MultiPageOverlay widget) {
|
||||
widget.controller.pageNotifier.removeListener(_onPageChange);
|
||||
widget.controller.pageNotifier.removeListener(_onPageChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -74,7 +74,7 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
|||
);
|
||||
}
|
||||
|
||||
void _onPageChange() {
|
||||
void _onPageChanged() {
|
||||
if (_previousPage != null) {
|
||||
final info = controller.info;
|
||||
if (info != null) {
|
||||
|
|
|
@ -35,7 +35,7 @@ class _ViewerThumbnailPreviewState extends State<ViewerThumbnailPreview> {
|
|||
void initState() {
|
||||
super.initState();
|
||||
_entryIndexNotifier.value = widget.displayedIndex;
|
||||
_entryIndexNotifier.addListener(_onScrollerIndexChange);
|
||||
_entryIndexNotifier.addListener(_onScrollerIndexChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -49,7 +49,7 @@ class _ViewerThumbnailPreviewState extends State<ViewerThumbnailPreview> {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_entryIndexNotifier.removeListener(_onScrollerIndexChange);
|
||||
_entryIndexNotifier.removeListener(_onScrollerIndexChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@ class _ViewerThumbnailPreviewState extends State<ViewerThumbnailPreview> {
|
|||
);
|
||||
}
|
||||
|
||||
void _onScrollerIndexChange() => _debouncer(() {
|
||||
void _onScrollerIndexChanged() => _debouncer(() {
|
||||
if (mounted) {
|
||||
JumpToEntryNotification(index: _entryIndexNotifier.value).dispatch(context);
|
||||
}
|
||||
|
|
|
@ -59,8 +59,8 @@ class _PlayTogglerState extends State<PlayToggler> with SingleTickerProviderStat
|
|||
void _registerWidget(PlayToggler widget) {
|
||||
final controller = widget.controller;
|
||||
if (controller != null) {
|
||||
_subscriptions.add(controller.statusStream.listen(_onStatusChange));
|
||||
_onStatusChange(controller.status);
|
||||
_subscriptions.add(controller.statusStream.listen(_onStatusChanged));
|
||||
_onStatusChanged(controller.status);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ class _PlayTogglerState extends State<PlayToggler> with SingleTickerProviderStat
|
|||
);
|
||||
}
|
||||
|
||||
void _onStatusChange(VideoStatus status) {
|
||||
void _onStatusChanged(VideoStatus status) {
|
||||
final status = _playPauseAnimation.status;
|
||||
if (isPlaying && status != AnimationStatus.forward && status != AnimationStatus.completed) {
|
||||
_playPauseAnimation.forward();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
|
@ -12,6 +13,7 @@ import 'package:aves/widgets/common/app_bar/quick_choosers/tag_button.dart';
|
|||
import 'package:aves/widgets/common/basic/menu.dart';
|
||||
import 'package:aves/widgets/common/basic/popup_menu_button.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/settings/common/quick_actions/action_button.dart';
|
||||
import 'package:aves/widgets/viewer/action/entry_action_delegate.dart';
|
||||
import 'package:aves/widgets/viewer/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
|
@ -47,9 +49,17 @@ class ViewerButtons extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (device.isTelevision) {
|
||||
return _TvButtonRowContent(
|
||||
scale: scale,
|
||||
mainEntry: mainEntry,
|
||||
pageEntry: pageEntry,
|
||||
collection: collection,
|
||||
);
|
||||
}
|
||||
|
||||
final actionDelegate = EntryActionDelegate(mainEntry, pageEntry, collection);
|
||||
final trashed = mainEntry.trashed;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
|
@ -82,6 +92,44 @@ class ViewerButtons extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _TvButtonRowContent extends StatelessWidget {
|
||||
final Animation<double> scale;
|
||||
final AvesEntry mainEntry, pageEntry;
|
||||
final CollectionLens? collection;
|
||||
|
||||
const _TvButtonRowContent({
|
||||
required this.scale,
|
||||
required this.mainEntry,
|
||||
required this.pageEntry,
|
||||
required this.collection,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final actionDelegate = EntryActionDelegate(mainEntry, pageEntry, collection);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...EntryActions.topLevel,
|
||||
...EntryActions.export,
|
||||
...EntryActions.video,
|
||||
].where(actionDelegate.isVisible).map((action) {
|
||||
// TODO TLAD [tv] togglers cf `_buildOverlayButton`
|
||||
// TODO TLAD [tv] use `scale`
|
||||
final enabled = actionDelegate.canApply(action);
|
||||
return ActionButton(
|
||||
text: action.getText(context),
|
||||
icon: action.getIcon(),
|
||||
enabled: enabled,
|
||||
onPressed: enabled ? () => actionDelegate.onActionSelected(context, action) : null,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ViewerButtonRowContent extends StatelessWidget {
|
||||
final List<EntryAction> quickActions, topLevelActions, exportActions, videoActions;
|
||||
final Animation<double> scale;
|
||||
|
@ -342,7 +390,7 @@ class ViewerButtonRowContent extends StatelessWidget {
|
|||
}
|
||||
|
||||
PopupMenuItem<EntryAction> _buildRotateAndFlipMenuItems(BuildContext context) {
|
||||
final actionDelegate = EntryActionDelegate(mainEntry, pageEntry, collection);
|
||||
final actionDelegate = _entryActionDelegate;
|
||||
|
||||
Widget buildDivider() => const SizedBox(
|
||||
height: 16,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/entry_images.dart';
|
||||
import 'package:aves/model/panorama.dart';
|
||||
|
@ -42,13 +43,13 @@ class _PanoramaPageState extends State<PanoramaPage> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_overlayVisible.addListener(_onOverlayVisibleChange);
|
||||
_overlayVisible.addListener(_onOverlayVisibleChanged);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _initOverlay());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_overlayVisible.removeListener(_onOverlayVisibleChange);
|
||||
_overlayVisible.removeListener(_onOverlayVisibleChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -96,7 +97,22 @@ class _PanoramaPageState extends State<PanoramaPage> {
|
|||
Positioned(
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: TooltipTheme(
|
||||
child: _buildOverlay(context),
|
||||
),
|
||||
const TopGestureAreaProtector(),
|
||||
const SideGestureAreaProtector(),
|
||||
const BottomGestureAreaProtector(),
|
||||
],
|
||||
),
|
||||
resizeToAvoidBottomInset: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverlay(BuildContext context) {
|
||||
if (device.isTelevision) return const SizedBox();
|
||||
|
||||
return TooltipTheme(
|
||||
data: TooltipTheme.of(context).copyWith(
|
||||
preferBelow: false,
|
||||
),
|
||||
|
@ -132,15 +148,6 @@ class _PanoramaPageState extends State<PanoramaPage> {
|
|||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const TopGestureAreaProtector(),
|
||||
const SideGestureAreaProtector(),
|
||||
const BottomGestureAreaProtector(),
|
||||
],
|
||||
),
|
||||
resizeToAvoidBottomInset: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -169,10 +176,10 @@ class _PanoramaPageState extends State<PanoramaPage> {
|
|||
// wait for MaterialPageRoute.transitionDuration
|
||||
// to show overlay after page animation is complete
|
||||
await Future.delayed(ModalRoute.of(context)!.transitionDuration * timeDilation);
|
||||
await _onOverlayVisibleChange();
|
||||
await _onOverlayVisibleChanged();
|
||||
}
|
||||
|
||||
Future<void> _onOverlayVisibleChange() async {
|
||||
Future<void> _onOverlayVisibleChanged() async {
|
||||
if (_overlayVisible.value) {
|
||||
await AvesApp.showSystemUI();
|
||||
AvesApp.setSystemUIStyle(context);
|
||||
|
|
|
@ -499,14 +499,14 @@ extension ExtraFijkPlayer on FijkPlayer {
|
|||
await setDataSource(uri, autoPlay: false, showCover: false);
|
||||
|
||||
final completer = Completer();
|
||||
void onChange() {
|
||||
void onChanged() {
|
||||
switch (state) {
|
||||
case FijkState.prepared:
|
||||
removeListener(onChange);
|
||||
removeListener(onChanged);
|
||||
completer.complete();
|
||||
break;
|
||||
case FijkState.error:
|
||||
removeListener(onChange);
|
||||
removeListener(onChanged);
|
||||
completer.completeError(value.exception);
|
||||
break;
|
||||
default:
|
||||
|
@ -514,7 +514,7 @@ extension ExtraFijkPlayer on FijkPlayer {
|
|||
}
|
||||
}
|
||||
|
||||
addListener(onChange);
|
||||
addListener(onChanged);
|
||||
await prepareAsync();
|
||||
return completer.future;
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> {
|
|||
if (entry.isMultiPage) {
|
||||
await _initMultiPageController(entry);
|
||||
}
|
||||
void listener() => _onMetadataChange(entry);
|
||||
void listener() => _onMetadataChanged(entry);
|
||||
_metadataChangeListeners[entry] = listener;
|
||||
entry.metadataChangeNotifier.addListener(listener);
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
}
|
||||
|
||||
void _onMetadataChange(AvesEntry entry) {
|
||||
void _onMetadataChanged(AvesEntry entry) {
|
||||
debugPrint('reinitialize controllers for entry=$entry because metadata changed');
|
||||
cleanEntryControllers(entry);
|
||||
initEntryControllers(entry);
|
||||
|
@ -149,7 +149,7 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> {
|
|||
videoPageEntries.forEach((entry) => videoConductor.getOrCreateController(entry, maxControllerCount: videoPageEntries.length));
|
||||
|
||||
// auto play/pause when changing page
|
||||
Future<void> _onPageChange() async {
|
||||
Future<void> _onPageChanged() async {
|
||||
await pauseVideoControllers();
|
||||
if (videoAutoPlayEnabled || (entry.isMotionPhoto && shouldAutoPlayMotionPhoto)) {
|
||||
final page = multiPageController.page;
|
||||
|
@ -165,9 +165,9 @@ mixin EntryViewControllerMixin<T extends StatefulWidget> on State<T> {
|
|||
}
|
||||
}
|
||||
|
||||
_multiPageControllerPageListeners[multiPageController] = _onPageChange;
|
||||
multiPageController.pageNotifier.addListener(_onPageChange);
|
||||
await _onPageChange();
|
||||
_multiPageControllerPageListeners[multiPageController] = _onPageChanged;
|
||||
multiPageController.pageNotifier.addListener(_onPageChanged);
|
||||
await _onPageChanged();
|
||||
|
||||
if (entry.isMotionPhoto && shouldAutoPlayMotionPhoto) {
|
||||
await Future.delayed(Durations.motionPhotoAutoPlayDelay);
|
||||
|
|
|
@ -103,7 +103,7 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
|
|||
// no bounce at the bottom, to avoid video controller displacement
|
||||
curve: Curves.easeOutQuad,
|
||||
);
|
||||
_overlayVisible.addListener(_onOverlayVisibleChange);
|
||||
_overlayVisible.addListener(_onOverlayVisibleChanged);
|
||||
_videoActionDelegate = VideoActionDelegate(
|
||||
collection: null,
|
||||
);
|
||||
|
@ -112,7 +112,7 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
|
|||
initialScale: const ScaleLevel(ref: ScaleReference.covered),
|
||||
);
|
||||
initEntryControllers(entry);
|
||||
_onOverlayVisibleChange();
|
||||
_onOverlayVisibleChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -121,7 +121,7 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
|
|||
_viewerController.dispose();
|
||||
_videoActionDelegate.dispose();
|
||||
_overlayAnimationController.dispose();
|
||||
_overlayVisible.removeListener(_onOverlayVisibleChange);
|
||||
_overlayVisible.removeListener(_onOverlayVisibleChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -232,7 +232,7 @@ class _EntryEditorState extends State<EntryEditor> with EntryViewControllerMixin
|
|||
|
||||
// overlay
|
||||
|
||||
Future<void> _onOverlayVisibleChange({bool animate = true}) async {
|
||||
Future<void> _onOverlayVisibleChanged({bool animate = true}) async {
|
||||
if (_overlayVisible.value) {
|
||||
await AvesApp.showSystemUI();
|
||||
AvesApp.setSystemUIStyle(context);
|
||||
|
|
Loading…
Reference in a new issue