diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 44d1f9c5a..f07066f25 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -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: diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index 770124e4f..a91ba9123 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -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, diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 96b628113..d3b81762b 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -154,7 +154,7 @@ class _AvesAppState extends State 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 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 with WidgetsBindingObserver { final tempUris = await _mediaStoreSource.refreshUris(todo); if (tempUris.isNotEmpty) { _changedUris.addAll(tempUris); - _onMediaStoreChange(null); + _onMediaStoreChanged(null); } }); } diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index a05caa289..f0dc33d06 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -102,7 +102,7 @@ class _CollectionAppBarState extends State with SingleTickerPr duration: context.read().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 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 with SingleTickerPr ); } - void _onActivityChange() { + void _onActivityChanged() { if (context.read>().isSelecting) { _browseToSelectAnimation.forward(); } else { diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 76e1368f0..b82d43022 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -191,7 +191,7 @@ class _CollectionGridContentState extends State<_CollectionGridContent> { return AnimatedScale( scale: focusedItem == entry ? 1 : .9, curve: Curves.fastOutSlowIn, - duration: context.select((v) => v.gridTvFocusAnimation), + duration: context.select((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, () { diff --git a/lib/widgets/common/basic/query_bar.dart b/lib/widgets/common/basic/query_bar.dart index 9cde4c3cb..4eb44b912 100644 --- a/lib/widgets/common/basic/query_bar.dart +++ b/lib/widgets/common/basic/query_bar.dart @@ -59,7 +59,7 @@ class _QueryBarState extends State { Expanded( child: TextField( controller: _controller, - focusNode: widget.focusNode ?? FocusNode(), + focusNode: widget.focusNode, decoration: InputDecoration( icon: Padding( padding: widget.leadingPadding ?? const EdgeInsetsDirectional.only(start: 16), diff --git a/lib/widgets/common/fx/sweeper.dart b/lib/widgets/common/fx/sweeper.dart index a02add989..3b387834f 100644 --- a/lib/widgets/common/fx/sweeper.dart +++ b/lib/widgets/common/fx/sweeper.dart @@ -53,7 +53,7 @@ class _SweeperState extends State with SingleTickerProviderStateMixin { parent: _angleAnimationController, curve: widget.curve, )); - _angleAnimationController.addStatusListener(_onAnimationStatusChange); + _angleAnimationController.addStatusListener(_onAnimationStatusChanged); _registerWidget(widget); } @@ -66,7 +66,7 @@ class _SweeperState extends State 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 with SingleTickerProviderStateMixin { ); } - void _onAnimationStatusChange(AnimationStatus status) { + void _onAnimationStatusChanged(AnimationStatus status) { setState(() {}); if (status == AnimationStatus.completed) { widget.onSweepEnd?.call(); diff --git a/lib/widgets/common/grid/item_tracker.dart b/lib/widgets/common/grid/item_tracker.dart index 33e3d7472..a693ec0bd 100644 --- a/lib/widgets/common/grid/item_tracker.dart +++ b/lib/widgets/common/grid/item_tracker.dart @@ -70,7 +70,7 @@ class _GridItemTrackerState extends State> with WidgetsBin void didUpdateWidget(covariant GridItemTracker oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.tileLayout != widget.tileLayout) { - _onLayoutChange(); + _onLayoutChanged(); } _saveLayoutMetrics(); } @@ -83,7 +83,7 @@ class _GridItemTrackerState extends State> with WidgetsBin final orientation = _windowOrientation; if (_lastOrientation != orientation) { _lastOrientation = orientation; - _onLayoutChange(); + _onLayoutChanged(); _saveLayoutMetrics(); } } @@ -147,7 +147,7 @@ class _GridItemTrackerState extends State> with WidgetsBin } } - void _onLayoutChange() { + void _onLayoutChanged() { if (scrollController.positions.length != 1) return; // do not track when view shows top edge diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index e260f128c..78a7122ca 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -162,13 +162,13 @@ class _AvesFilterChipState extends State { 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 { _outlineColor = context.read().neutral; } - void _onCoverColorChange(Set? event) { + void _onCoverColorChanged(Set? event) { if (event == null || event.contains(filter)) { _initColorLoader(); setState(() {}); diff --git a/lib/widgets/common/map/leaflet/map.dart b/lib/widgets/common/map/leaflet/map.dart index 8e2b15d07..358972f3f 100644 --- a/lib/widgets/common/map/leaflet/map.dart +++ b/lib/widgets/common/map/leaflet/map.dart @@ -98,12 +98,12 @@ class _EntryLeafletMapState extends State> with TickerProv } _subscriptions.add(_leafletMapController.mapEventStream.listen((event) => _updateVisibleRegion())); widget.clusterListenable.addListener(_updateMarkers); - widget.boundsNotifier.addListener(_onBoundsChange); + widget.boundsNotifier.addListener(_onBoundsChanged); } void _unregisterWidget(EntryLeafletMap 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 extends State> with TickerProv ); } - void _onBoundsChange() => _debouncer(_onIdle); + void _onBoundsChanged() => _debouncer(_onIdle); void _onIdle() { if (!mounted) return; diff --git a/lib/widgets/common/thumbnail/scroller.dart b/lib/widgets/common/thumbnail/scroller.dart index 4c8162149..259621305 100644 --- a/lib/widgets/common/thumbnail/scroller.dart +++ b/lib/widgets/common/thumbnail/scroller.dart @@ -78,14 +78,14 @@ class _ThumbnailScrollerState extends State { 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 { } } - void _onScrollChange() { + void _onScrollChanged() { if (!_isAnimating) { final index = scrollOffsetToIndex(_scrollController.offset); if (indexNotifier.value != index) { @@ -190,7 +190,7 @@ class _ThumbnailScrollerState extends State { } } - void _onIndexChange() { + void _onIndexChanged() { if (!_isScrolling && !_isAnimating) { final index = indexNotifier.value; if (index != null) { diff --git a/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart b/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart index b181d625f..43b29de6e 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_set_page.dart @@ -45,13 +45,13 @@ class _RenameEntrySetPageState extends State { 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 { ); } - void _onUserPatternChange() { + void _onUserPatternChanged() { _namingPatternNotifier.value = NamingPattern.from( userPattern: _patternTextController.text, entryCount: entryCount, diff --git a/lib/widgets/dialogs/entry_editors/tag_editor_page.dart b/lib/widgets/dialogs/entry_editors/tag_editor_page.dart index b29b3dae3..96d961f02 100644 --- a/lib/widgets/dialogs/entry_editors/tag_editor_page.dart +++ b/lib/widgets/dialogs/entry_editors/tag_editor_page.dart @@ -44,6 +44,13 @@ class _TagEditorPageState extends State { _initTopTags(); } + @override + void dispose() { + _newTagTextFocusNode.dispose(); + _expandedSectionNotifier.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final l10n = context.l10n; diff --git a/lib/widgets/dialogs/filter_editors/create_album_dialog.dart b/lib/widgets/dialogs/filter_editors/create_album_dialog.dart index c06f4a3eb..5bf025ba2 100644 --- a/lib/widgets/dialogs/filter_editors/create_album_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/create_album_dialog.dart @@ -38,6 +38,9 @@ class _CreateAlbumDialogState extends State { void dispose() { _nameController.dispose(); _nameFieldFocusNode.removeListener(_onFocus); + _nameFieldFocusNode.dispose(); + _existsNotifier.dispose(); + _isValidNotifier.dispose(); super.dispose(); } diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart index 84dad963b..67d717e1a 100644 --- a/lib/widgets/filter_grids/common/app_bar.dart +++ b/lib/widgets/filter_grids/common/app_bar.dart @@ -102,14 +102,16 @@ class _FilterGridAppBarState().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>>().isSelecting) { _browseToSelectAnimation.forward(); } else { diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 2e178f3fc..e3aceba6e 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -365,7 +365,7 @@ class _FilterGridContentState extends State<_FilterG return AnimatedScale( scale: focusedItem == gridItem ? 1 : .9, curve: Curves.fastOutSlowIn, - duration: context.select((v) => v.gridTvFocusAnimation), + duration: context.select((v) => v.tvImageFocusAnimation), child: child!, ); }, diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 22f9ac9db..5d59205a6 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -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().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 _onOverlayVisibleChange({bool animate = true}) async { + Future _onOverlayVisibleChanged({bool animate = true}) async { if (_overlayVisible.value) { if (animate) { await _overlayAnimationController.forward(); diff --git a/lib/widgets/navigation/nav_bar/floating.dart b/lib/widgets/navigation/nav_bar/floating.dart index 661568727..9c2ae9946 100644 --- a/lib/widgets/navigation/nav_bar/floating.dart +++ b/lib/widgets/navigation/nav_bar/floating.dart @@ -69,12 +69,12 @@ class _FloatingNavBarState extends State 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 with SingleTickerProvid ); } - void _onScrollChange() { + void _onScrollChanged() { final scrollController = widget.scrollController; if (scrollController == null) return; diff --git a/lib/widgets/navigation/nav_bar/nav_bar.dart b/lib/widgets/navigation/nav_bar/nav_bar.dart index 8061c13d1..33018e6fa 100644 --- a/lib/widgets/navigation/nav_bar/nav_bar.dart +++ b/lib/widgets/navigation/nav_bar/nav_bar.dart @@ -57,11 +57,11 @@ class _AppBottomNavBarState extends State { } 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 { ); } - void _onCollectionFilterChange() => setState(() {}); + void _onCollectionFilterChanged() => setState(() {}); int _getCurrentIndex(BuildContext context, List items) { // current route may be null during navigation diff --git a/lib/widgets/settings/viewer/overlay.dart b/lib/widgets/settings/viewer/overlay.dart index ff980c231..dab35532b 100644 --- a/lib/widgets/settings/viewer/overlay.dart +++ b/lib/widgets/settings/viewer/overlay.dart @@ -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,11 +20,12 @@ class ViewerOverlayPage extends StatelessWidget { body: SafeArea( child: ListView( children: [ - SettingsSwitchListTile( - selector: (context, s) => s.showOverlayOnOpening, - onChanged: (v) => settings.showOverlayOnOpening = v, - title: context.l10n.settingsViewerShowOverlayOnOpening, - ), + if (!device.isTelevision) + SettingsSwitchListTile( + selector: (context, s) => s.showOverlayOnOpening, + onChanged: (v) => settings.showOverlayOnOpening = v, + title: context.l10n.settingsViewerShowOverlayOnOpening, + ), SettingsSwitchListTile( selector: (context, s) => s.showOverlayInfo, onChanged: (v) => settings.showOverlayInfo = v, @@ -54,16 +56,18 @@ class ViewerOverlayPage extends StatelessWidget { ); }, ), - SettingsSwitchListTile( - selector: (context, s) => s.showOverlayMinimap, - onChanged: (v) => settings.showOverlayMinimap = v, - title: context.l10n.settingsViewerShowMinimap, - ), - SettingsSwitchListTile( - selector: (context, s) => s.showOverlayThumbnailPreview, - onChanged: (v) => settings.showOverlayThumbnailPreview = v, - title: context.l10n.settingsViewerShowOverlayThumbnails, - ), + 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, + title: context.l10n.settingsViewerShowOverlayThumbnails, + ), ], ), ), diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 060454a56..f8e926cbb 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -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); diff --git a/lib/widgets/viewer/controller.dart b/lib/widgets/viewer/controller.dart index a9a39319c..7a5d141d0 100644 --- a/lib/widgets/viewer/controller.dart +++ b/lib/widgets/viewer/controller.dart @@ -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) { diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index 6dcbc353b..381b3e226 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -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 { final List _subscriptions = []; final ValueNotifier _backgroundOpacityNotifier = ValueNotifier(1); final ValueNotifier _isVerticallyScrollingNotifier = ValueNotifier(false); + final ValueNotifier _isImageFocusedNotifier = ValueNotifier(true); Timer? _verticalScrollMonitoringTimer; AvesEntry? _oldEntry; Future? _systemBrightness; @@ -83,6 +87,9 @@ class _ViewerVerticalPageViewState extends State { void dispose() { _unregisterWidget(widget); _stopScrollMonitoringTimer(); + _backgroundOpacityNotifier.dispose(); + _isVerticallyScrollingNotifier.dispose(); + _isImageFocusedNotifier.dispose(); super.dispose(); } @@ -174,10 +181,19 @@ class _ViewerVerticalPageViewState extends State { } Widget _buildImagePage() { + final isTelevision = device.isTelevision; + Widget? child; - Map? shortcuts; + Map? 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 { 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( + valueListenable: _isImageFocusedNotifier, + builder: (context, isImageFocused, child) { + return AnimatedScale( + scale: isImageFocused ? 1 : .7, + curve: Curves.fastOutSlowIn, + duration: context.select((v) => v.tvImageFocusAnimation), + child: child!, + ); + }, + child: child, + ); + } + return FocusableActionDetector( autofocus: true, shortcuts: shortcuts, @@ -209,8 +230,25 @@ class _ViewerVerticalPageViewState extends State { ShowPreviousIntent: CallbackAction(onInvoke: (intent) => _goToHorizontalPage(-1, animate: false)), ShowNextIntent: CallbackAction(onInvoke: (intent) => _goToHorizontalPage(1, animate: false)), LeaveIntent: CallbackAction(onInvoke: (intent) => Navigator.pop(context)), - ShowInfoIntent: CallbackAction(onInvoke: (intent) => ShowInfoNotification().dispatch(context)), + ShowInfoIntent: CallbackAction(onInvoke: (intent) => ShowInfoPageNotification().dispatch(context)), + TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context)), + TvShowMoreInfoIntent: CallbackAction(onInvoke: (intent) => TvShowMoreInfoNotification().dispatch(context)), + ActivateIntent: CallbackAction(onInvoke: (intent) { + if (isTelevision) { + final _entry = entry; + if (_entry != null && _entry.isVideo) { + final controller = context.read().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(); +} diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 8234b638d..9c67c340f 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -118,7 +118,7 @@ class _EntryViewerStackState extends State 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().viewerOverlayAnimation, vsync: this, @@ -142,7 +142,7 @@ class _EntryViewerStackState extends State 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 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 with EntryViewContr final viewStateConductor = context.read(); 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.value( @@ -243,10 +237,19 @@ class _EntryViewerStackState extends State 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 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 with EntryViewContr } } - void _onVerticalPageControllerChange() { + void _onVerticalPageControllerChanged() { if (!_isEntryTracked && _verticalPager.hasClients && _verticalPager.page?.floor() == transitionPage) { _trackEntry(); } @@ -520,6 +523,12 @@ class _EntryViewerStackState extends State with EntryViewContr ); } + void _showInfoPage() { + // remove focus, if any, to prevent viewer shortcuts activation from the Info page + FocusManager.instance.primaryFocus?.unfocus(); + _goToVerticalPage(infoPage); + } + Future _goToVerticalPage(int page) async { final animationDuration = context.read().viewerVerticalPageScrollAnimation; if (animationDuration > Duration.zero) { @@ -574,7 +583,7 @@ class _EntryViewerStackState extends State with EntryViewContr _updateEntry(); } - void _onCollectionChange() { + void _onCollectionChanged() { _updateEntry(); } @@ -588,7 +597,7 @@ class _EntryViewerStackState extends State with EntryViewContr if (index != -1) { _onHorizontalPageChanged(index); } - _onCollectionChange(); + _onCollectionChanged(); } } @@ -600,7 +609,7 @@ class _EntryViewerStackState extends State with EntryViewContr final collectionEntries = collection!.sortedEntries; removedEntries.forEach(collectionEntries.remove); if (collectionEntries.isNotEmpty) { - _onCollectionChange(); + _onCollectionChanged(); return; } } @@ -630,6 +639,16 @@ class _EntryViewerStackState extends State 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 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 _onOverlayVisibleChange({bool animate = true}) async { + Future _onOverlayVisibleChanged({bool animate = true}) async { if (_overlayVisible.value) { await AvesApp.showSystemUI(); AvesApp.setSystemUIStyle(context); diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index cb86baa0e..ae316d050 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -61,11 +61,11 @@ class _LocationSectionState extends State { } 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 { mapCollection.dispose(); } - void _onMetadataChange() { + void _onMetadataChanged() { setState(() {}); final location = entry.latLng; diff --git a/lib/widgets/viewer/notifications.dart b/lib/widgets/viewer/notifications.dart index 68e477744..889a82127 100644 --- a/lib/widgets/viewer/notifications.dart +++ b/lib/widgets/viewer/notifications.dart @@ -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 { diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index 0dcab4ebe..8523e3f4a 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -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 _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 @@ -144,16 +173,20 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> { final isWallpaperMode = context.read>().value == AppMode.setWallpaper; return AnimatedBuilder( - animation: Listenable.merge([ - mainEntry.metadataChangeNotifier, - pageEntry.metadataChangeNotifier, - ]), - builder: (context, child) { - return Selector( - selector: (context, mq) => mq.size.width, - builder: (context, mqWidth, child) { - final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero); - final viewerButtonRow = SafeArea( + animation: Listenable.merge([ + mainEntry.metadataChangeNotifier, + pageEntry.metadataChangeNotifier, + ]), + builder: (context, child) { + return Selector( + selector: (context, mq) => mq.size.width, + builder: (context, mqWidth, child) { + final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero); + final viewerButtonRow = FocusableActionDetector( + focusNode: _buttonRowFocusScopeNode, + shortcuts: device.isTelevision ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, + actions: {TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))}, + child: SafeArea( top: false, bottom: false, minimum: EdgeInsets.only( @@ -171,64 +204,72 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> { collection: widget.collection, scale: _buttonScale, ), - ); + ), + ); - final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null; - final collapsedPageScroller = mainEntry.isMotionPhoto; + final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null; + final collapsedPageScroller = mainEntry.isMotionPhoto; - return SizedBox( - width: mqWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (showMultiPageOverlay && !collapsedPageScroller) - Padding( - padding: const EdgeInsets.only(bottom: 8), - child: FadeTransition( - opacity: _thumbnailOpacity, - child: MultiPageOverlay( - controller: multiPageController, - availableWidth: mqWidth, - scrollable: true, - ), + return SizedBox( + width: mqWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showMultiPageOverlay && !collapsedPageScroller) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: FadeTransition( + opacity: _thumbnailOpacity, + child: MultiPageOverlay( + controller: multiPageController, + availableWidth: mqWidth, + scrollable: true, ), ), - (showMultiPageOverlay && collapsedPageScroller) - ? Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SafeArea( - top: false, - bottom: false, - child: Padding( - padding: const EdgeInsets.only(bottom: 8), - child: MultiPageOverlay( - controller: multiPageController, - availableWidth: mqWidth, - scrollable: false, - ), + ), + (showMultiPageOverlay && collapsedPageScroller) + ? Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SafeArea( + top: false, + bottom: false, + child: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: MultiPageOverlay( + controller: multiPageController, + availableWidth: mqWidth, + scrollable: false, ), ), - Expanded(child: viewerButtonRow), - ], - ) - : viewerButtonRow, - if (settings.showOverlayThumbnailPreview && !isWallpaperMode) - FadeTransition( - opacity: _thumbnailOpacity, - child: ViewerThumbnailPreview( - availableWidth: mqWidth, - displayedIndex: widget.index, - entries: widget.entries, - ), + ), + Expanded(child: viewerButtonRow), + ], + ) + : viewerButtonRow, + if (settings.showOverlayThumbnailPreview && !isWallpaperMode) + FadeTransition( + opacity: _thumbnailOpacity, + child: ViewerThumbnailPreview( + availableWidth: mqWidth, + displayedIndex: widget.index, + entries: widget.entries, ), - ], - ), - ); - }, - ); - }); + ), + ], + ), + ); + }, + ); + }, + ); + } + + void _onAnimationStatusChanged(AnimationStatus status) { + if (status == AnimationStatus.completed) { + _buttonRowFocusScopeNode.children.firstOrNull?.requestFocus(); + } } } diff --git a/lib/widgets/viewer/overlay/multipage.dart b/lib/widgets/viewer/overlay/multipage.dart index 708d542b0..d1313890b 100644 --- a/lib/widgets/viewer/overlay/multipage.dart +++ b/lib/widgets/viewer/overlay/multipage.dart @@ -49,11 +49,11 @@ class _MultiPageOverlayState extends State { } 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 { ); } - void _onPageChange() { + void _onPageChanged() { if (_previousPage != null) { final info = controller.info; if (info != null) { diff --git a/lib/widgets/viewer/overlay/thumbnail_preview.dart b/lib/widgets/viewer/overlay/thumbnail_preview.dart index a77c1c3e8..962826fc9 100644 --- a/lib/widgets/viewer/overlay/thumbnail_preview.dart +++ b/lib/widgets/viewer/overlay/thumbnail_preview.dart @@ -35,7 +35,7 @@ class _ViewerThumbnailPreviewState extends State { void initState() { super.initState(); _entryIndexNotifier.value = widget.displayedIndex; - _entryIndexNotifier.addListener(_onScrollerIndexChange); + _entryIndexNotifier.addListener(_onScrollerIndexChanged); } @override @@ -49,7 +49,7 @@ class _ViewerThumbnailPreviewState extends State { @override void dispose() { - _entryIndexNotifier.removeListener(_onScrollerIndexChange); + _entryIndexNotifier.removeListener(_onScrollerIndexChanged); super.dispose(); } @@ -64,7 +64,7 @@ class _ViewerThumbnailPreviewState extends State { ); } - void _onScrollerIndexChange() => _debouncer(() { + void _onScrollerIndexChanged() => _debouncer(() { if (mounted) { JumpToEntryNotification(index: _entryIndexNotifier.value).dispatch(context); } diff --git a/lib/widgets/viewer/overlay/video/play_toggler.dart b/lib/widgets/viewer/overlay/video/play_toggler.dart index 3bce59e4c..27599cdce 100644 --- a/lib/widgets/viewer/overlay/video/play_toggler.dart +++ b/lib/widgets/viewer/overlay/video/play_toggler.dart @@ -59,8 +59,8 @@ class _PlayTogglerState extends State 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 with SingleTickerProviderStat ); } - void _onStatusChange(VideoStatus status) { + void _onStatusChanged(VideoStatus status) { final status = _playPauseAnimation.status; if (isPlaying && status != AnimationStatus.forward && status != AnimationStatus.completed) { _playPauseAnimation.forward(); diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index 1290a337d..9ac7c046d 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -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 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 quickActions, topLevelActions, exportActions, videoActions; final Animation scale; @@ -342,7 +390,7 @@ class ViewerButtonRowContent extends StatelessWidget { } PopupMenuItem _buildRotateAndFlipMenuItems(BuildContext context) { - final actionDelegate = EntryActionDelegate(mainEntry, pageEntry, collection); + final actionDelegate = _entryActionDelegate; Widget buildDivider() => const SizedBox( height: 16, diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index afc7a4a8f..29f6d2e20 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -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 { @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,43 +97,7 @@ class _PanoramaPageState extends State { Positioned( right: 0, bottom: 0, - child: TooltipTheme( - data: TooltipTheme.of(context).copyWith( - preferBelow: false, - ), - child: ValueListenableBuilder( - valueListenable: _overlayVisible, - builder: (context, overlayVisible, child) { - return Visibility( - visible: overlayVisible, - child: Selector( - selector: (context, mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom), - builder: (context, mqPaddingBottom, child) { - return SafeArea( - bottom: false, - child: Padding( - padding: const EdgeInsets.all(8) + EdgeInsets.only(bottom: mqPaddingBottom), - child: child, - ), - ); - }, - child: OverlayButton( - child: ValueListenableBuilder( - valueListenable: _sensorControl, - builder: (context, sensorControl, child) { - return IconButton( - icon: Icon(sensorControl == SensorControl.None ? AIcons.sensorControlEnabled : AIcons.sensorControlDisabled), - onPressed: _toggleSensor, - tooltip: sensorControl == SensorControl.None ? context.l10n.panoramaEnableSensorControl : context.l10n.panoramaDisableSensorControl, - ); - }, - ), - ), - ), - ); - }, - ), - ), + child: _buildOverlay(context), ), const TopGestureAreaProtector(), const SideGestureAreaProtector(), @@ -144,6 +109,48 @@ class _PanoramaPageState extends State { ); } + Widget _buildOverlay(BuildContext context) { + if (device.isTelevision) return const SizedBox(); + + return TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + ), + child: ValueListenableBuilder( + valueListenable: _overlayVisible, + builder: (context, overlayVisible, child) { + return Visibility( + visible: overlayVisible, + child: Selector( + selector: (context, mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom), + builder: (context, mqPaddingBottom, child) { + return SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.all(8) + EdgeInsets.only(bottom: mqPaddingBottom), + child: child, + ), + ); + }, + child: OverlayButton( + child: ValueListenableBuilder( + valueListenable: _sensorControl, + builder: (context, sensorControl, child) { + return IconButton( + icon: Icon(sensorControl == SensorControl.None ? AIcons.sensorControlEnabled : AIcons.sensorControlDisabled), + onPressed: _toggleSensor, + tooltip: sensorControl == SensorControl.None ? context.l10n.panoramaEnableSensorControl : context.l10n.panoramaDisableSensorControl, + ); + }, + ), + ), + ), + ); + }, + ), + ); + } + void _toggleSensor() { switch (_sensorControl.value) { case SensorControl.None: @@ -169,10 +176,10 @@ class _PanoramaPageState extends State { // 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 _onOverlayVisibleChange() async { + Future _onOverlayVisibleChanged() async { if (_overlayVisible.value) { await AvesApp.showSystemUI(); AvesApp.setSystemUIStyle(context); diff --git a/lib/widgets/viewer/video/fijkplayer.dart b/lib/widgets/viewer/video/fijkplayer.dart index 157a67568..e2c62f5b9 100644 --- a/lib/widgets/viewer/video/fijkplayer.dart +++ b/lib/widgets/viewer/video/fijkplayer.dart @@ -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; } diff --git a/lib/widgets/viewer/visual/controller_mixin.dart b/lib/widgets/viewer/visual/controller_mixin.dart index 447a481e4..f955fe8a2 100644 --- a/lib/widgets/viewer/visual/controller_mixin.dart +++ b/lib/widgets/viewer/visual/controller_mixin.dart @@ -32,7 +32,7 @@ mixin EntryViewControllerMixin on State { 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 on State { } } - 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 on State { videoPageEntries.forEach((entry) => videoConductor.getOrCreateController(entry, maxControllerCount: videoPageEntries.length)); // auto play/pause when changing page - Future _onPageChange() async { + Future _onPageChanged() async { await pauseVideoControllers(); if (videoAutoPlayEnabled || (entry.isMotionPhoto && shouldAutoPlayMotionPhoto)) { final page = multiPageController.page; @@ -165,9 +165,9 @@ mixin EntryViewControllerMixin on State { } } - _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); diff --git a/lib/widgets/wallpaper_page.dart b/lib/widgets/wallpaper_page.dart index c3c1ba15c..fbac45bbc 100644 --- a/lib/widgets/wallpaper_page.dart +++ b/lib/widgets/wallpaper_page.dart @@ -103,7 +103,7 @@ class _EntryEditorState extends State 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 with EntryViewControllerMixin initialScale: const ScaleLevel(ref: ScaleReference.covered), ); initEntryControllers(entry); - _onOverlayVisibleChange(); + _onOverlayVisibleChanged(); } @override @@ -121,7 +121,7 @@ class _EntryEditorState extends State with EntryViewControllerMixin _viewerController.dispose(); _videoActionDelegate.dispose(); _overlayAnimationController.dispose(); - _overlayVisible.removeListener(_onOverlayVisibleChange); + _overlayVisible.removeListener(_onOverlayVisibleChanged); super.dispose(); } @@ -232,7 +232,7 @@ class _EntryEditorState extends State with EntryViewControllerMixin // overlay - Future _onOverlayVisibleChange({bool animate = true}) async { + Future _onOverlayVisibleChanged({bool animate = true}) async { if (_overlayVisible.value) { await AvesApp.showSystemUI(); AvesApp.setSystemUIStyle(context);