From b0f613db2719ac00ad382f186c3c52bb1beb034e Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 6 Jan 2023 15:44:31 +0100 Subject: [PATCH] tv: button focus style, stats page --- lib/widgets/collection/app_bar.dart | 15 +- .../quick_choosers/common/button.dart | 3 + .../quick_choosers/rate_button.dart | 1 + .../quick_choosers/share_button.dart | 1 + .../quick_choosers/tag_button.dart | 1 + .../action_controls/togglers/favourite.dart | 3 + .../common/action_controls/togglers/mute.dart | 3 + .../common/action_controls/togglers/play.dart | 3 + .../togglers/title_search.dart | 3 + lib/widgets/common/fx/borders.dart | 10 +- .../identity/buttons/captioned_button.dart | 124 +++++--- .../identity/buttons/overlay_button.dart | 122 +++++--- lib/widgets/filter_grids/common/app_bar.dart | 11 +- lib/widgets/navigation/tv_rail.dart | 20 +- .../quick_actions/available_actions.dart | 8 +- .../settings/video/video_settings_page.dart | 2 + lib/widgets/stats/stats_page.dart | 82 ++++-- .../viewer/action/entry_action_delegate.dart | 4 +- .../viewer/overlay/viewer_buttons.dart | 274 +++++++++--------- 19 files changed, 448 insertions(+), 242 deletions(-) diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index f767e9c92..63ce8f10c 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:ui'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_set_actions.dart'; @@ -346,7 +345,13 @@ class _CollectionAppBarState extends State with SingleTickerPr ].where(isVisible).map((action) { final enabled = canApply(action); return CaptionedButton( - iconButton: _buildButtonIcon(context, action, enabled: enabled, selection: selection), + iconButtonBuilder: (context, focusNode) => _buildButtonIcon( + context, + action, + enabled: enabled, + selection: selection, + focusNode: focusNode, + ), captionText: _buildButtonCaption(context, action, enabled: enabled), onPressed: enabled ? () => _onActionSelected(action) : null, ); @@ -433,6 +438,7 @@ class _CollectionAppBarState extends State with SingleTickerPr BuildContext context, EntrySetAction action, { required bool enabled, + FocusNode? focusNode, required Selection selection, }) { final onPressed = enabled ? () => _onActionSelected(action) : null; @@ -445,12 +451,14 @@ class _CollectionAppBarState extends State with SingleTickerPr return TitleSearchToggler( queryEnabled: queryEnabled, onPressed: onPressed, + focusNode: focusNode, ); }, ); case EntrySetAction.toggleFavourite: return FavouriteToggler( entries: _getExpandedSelectedItems(selection), + focusNode: focusNode, onPressed: onPressed, ); default: @@ -458,6 +466,7 @@ class _CollectionAppBarState extends State with SingleTickerPr key: _getActionKey(action), icon: action.getIcon(), onPressed: onPressed, + focusNode: focusNode, tooltip: action.getText(context), ); } @@ -581,7 +590,7 @@ class _CollectionAppBarState extends State with SingleTickerPr void _onQueryFocusRequest() => _queryBarFocusNode.requestFocus(); void _updateStatusBarHeight() { - _statusBarHeight = EdgeInsets.fromWindowPadding(window.padding, window.devicePixelRatio).top; + _statusBarHeight = context.read().padding.top; _updateAppBarHeight(); } diff --git a/lib/widgets/common/action_controls/quick_choosers/common/button.dart b/lib/widgets/common/action_controls/quick_choosers/common/button.dart index d83f728ad..6b6636b0d 100644 --- a/lib/widgets/common/action_controls/quick_choosers/common/button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/common/button.dart @@ -8,12 +8,14 @@ import 'package:provider/provider.dart'; abstract class ChooserQuickButton extends StatefulWidget { final bool blurred; final ValueSetter? onChooserValue; + final FocusNode? focusNode; final VoidCallback? onPressed; const ChooserQuickButton({ super.key, required this.blurred, this.onChooserValue, + this.focusNode, required this.onPressed, }); } @@ -71,6 +73,7 @@ abstract class ChooserQuickButtonState, U> exten child: IconButton( icon: icon, onPressed: widget.onPressed, + focusNode: widget.focusNode, tooltip: _hasChooser ? null : tooltip, ), ); diff --git a/lib/widgets/common/action_controls/quick_choosers/rate_button.dart b/lib/widgets/common/action_controls/quick_choosers/rate_button.dart index cb3882d75..e14e1cb8e 100644 --- a/lib/widgets/common/action_controls/quick_choosers/rate_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/rate_button.dart @@ -8,6 +8,7 @@ class RateButton extends ChooserQuickButton { super.key, required super.blurred, super.onChooserValue, + super.focusNode, required super.onPressed, }); diff --git a/lib/widgets/common/action_controls/quick_choosers/share_button.dart b/lib/widgets/common/action_controls/quick_choosers/share_button.dart index d4b234071..379c6b9a2 100644 --- a/lib/widgets/common/action_controls/quick_choosers/share_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/share_button.dart @@ -13,6 +13,7 @@ class ShareButton extends ChooserQuickButton { required super.blurred, required this.entries, super.onChooserValue, + super.focusNode, required super.onPressed, }); diff --git a/lib/widgets/common/action_controls/quick_choosers/tag_button.dart b/lib/widgets/common/action_controls/quick_choosers/tag_button.dart index cb048e3e6..6735d644a 100644 --- a/lib/widgets/common/action_controls/quick_choosers/tag_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/tag_button.dart @@ -17,6 +17,7 @@ class TagButton extends ChooserQuickButton { super.key, required super.blurred, super.onChooserValue, + super.focusNode, required super.onPressed, }); diff --git a/lib/widgets/common/action_controls/togglers/favourite.dart b/lib/widgets/common/action_controls/togglers/favourite.dart index cfa4d9b4f..c19c646d1 100644 --- a/lib/widgets/common/action_controls/togglers/favourite.dart +++ b/lib/widgets/common/action_controls/togglers/favourite.dart @@ -12,12 +12,14 @@ import 'package:provider/provider.dart'; class FavouriteToggler extends StatefulWidget { final Set entries; final bool isMenuItem; + final FocusNode? focusNode; final VoidCallback? onPressed; const FavouriteToggler({ super.key, required this.entries, this.isMenuItem = false, + this.focusNode, this.onPressed, }); @@ -76,6 +78,7 @@ class _FavouriteTogglerState extends State { IconButton( icon: Icon(isFavourite ? isFavouriteIcon : isNotFavouriteIcon), onPressed: widget.onPressed, + focusNode: widget.focusNode, tooltip: isFavourite ? context.l10n.entryActionRemoveFavourite : context.l10n.entryActionAddFavourite, ), Sweeper( diff --git a/lib/widgets/common/action_controls/togglers/mute.dart b/lib/widgets/common/action_controls/togglers/mute.dart index 94c691f8c..e42bcab80 100644 --- a/lib/widgets/common/action_controls/togglers/mute.dart +++ b/lib/widgets/common/action_controls/togglers/mute.dart @@ -10,12 +10,14 @@ import 'package:flutter/material.dart'; class MuteToggler extends StatelessWidget { final AvesVideoController? controller; final bool isMenuItem; + final FocusNode? focusNode; final VoidCallback? onPressed; const MuteToggler({ super.key, required this.controller, this.isMenuItem = false, + this.focusNode, this.onPressed, }); @@ -40,6 +42,7 @@ class MuteToggler extends StatelessWidget { : IconButton( icon: icon, onPressed: canDo ? onPressed : null, + focusNode: focusNode, tooltip: text, ); }, diff --git a/lib/widgets/common/action_controls/togglers/play.dart b/lib/widgets/common/action_controls/togglers/play.dart index 814958b2b..354da0ddf 100644 --- a/lib/widgets/common/action_controls/togglers/play.dart +++ b/lib/widgets/common/action_controls/togglers/play.dart @@ -12,12 +12,14 @@ import 'package:provider/provider.dart'; class PlayToggler extends StatefulWidget { final AvesVideoController? controller; final bool isMenuItem; + final FocusNode? focusNode; final VoidCallback? onPressed; const PlayToggler({ super.key, required this.controller, this.isMenuItem = false, + this.focusNode, this.onPressed, }); @@ -86,6 +88,7 @@ class _PlayTogglerState extends State with SingleTickerProviderStat progress: _playPauseAnimation, ), onPressed: widget.onPressed, + focusNode: widget.focusNode, tooltip: text, ); } diff --git a/lib/widgets/common/action_controls/togglers/title_search.dart b/lib/widgets/common/action_controls/togglers/title_search.dart index 3b3d9ae55..dfe577994 100644 --- a/lib/widgets/common/action_controls/togglers/title_search.dart +++ b/lib/widgets/common/action_controls/togglers/title_search.dart @@ -8,12 +8,14 @@ import 'package:provider/provider.dart'; class TitleSearchToggler extends StatelessWidget { final bool queryEnabled, isMenuItem; + final FocusNode? focusNode; final VoidCallback? onPressed; const TitleSearchToggler({ super.key, required this.queryEnabled, this.isMenuItem = false, + this.focusNode, this.onPressed, }); @@ -29,6 +31,7 @@ class TitleSearchToggler extends StatelessWidget { : IconButton( icon: icon, onPressed: onPressed, + focusNode: focusNode, tooltip: text, ); } diff --git a/lib/widgets/common/fx/borders.dart b/lib/widgets/common/fx/borders.dart index 2e2fe275e..b9dc02a0d 100644 --- a/lib/widgets/common/fx/borders.dart +++ b/lib/widgets/common/fx/borders.dart @@ -13,15 +13,15 @@ class AvesBorder { // 1 device pixel for curves is too thin static double get curvedBorderWidth => window.devicePixelRatio > 2 ? 0.5 : 1.0; - static BorderSide straightSide(BuildContext context) => BorderSide( + static BorderSide straightSide(BuildContext context, {double? width}) => BorderSide( color: _borderColor(context), - width: straightBorderWidth, + width: width ?? straightBorderWidth, ); - static BorderSide curvedSide(BuildContext context) => BorderSide( + static BorderSide curvedSide(BuildContext context, {double? width}) => BorderSide( color: _borderColor(context), - width: curvedBorderWidth, + width: width ?? curvedBorderWidth, ); - static Border border(BuildContext context) => Border.fromBorderSide(curvedSide(context)); + static Border border(BuildContext context, {double? width}) => Border.fromBorderSide(curvedSide(context, width: width)); } diff --git a/lib/widgets/common/identity/buttons/captioned_button.dart b/lib/widgets/common/identity/buttons/captioned_button.dart index 6c8e01ada..dda3e7787 100644 --- a/lib/widgets/common/identity/buttons/captioned_button.dart +++ b/lib/widgets/common/identity/buttons/captioned_button.dart @@ -2,58 +2,39 @@ import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -class CaptionedButton extends StatelessWidget { +typedef CaptionedIconButtonBuilder = Widget Function(BuildContext context, FocusNode focusNode); + +class CaptionedButton extends StatefulWidget { final Animation scale; final Widget captionText; - final Widget iconButton; + final CaptionedIconButtonBuilder iconButtonBuilder; final bool showCaption; final VoidCallback? onPressed; + static const EdgeInsets padding = EdgeInsets.symmetric(horizontal: 8); + static const double iconTextPadding = 8; + CaptionedButton({ super.key, this.scale = kAlwaysCompleteAnimation, Widget? icon, - Widget? iconButton, + CaptionedIconButtonBuilder? iconButtonBuilder, String? caption, Widget? captionText, this.showCaption = true, required this.onPressed, - }) : assert(icon != null || iconButton != null), + }) : assert(icon != null || iconButtonBuilder != null), assert(caption != null || captionText != null), - iconButton = iconButton ?? IconButton(icon: icon!, onPressed: onPressed), + iconButtonBuilder = iconButtonBuilder ?? ((_, focusNode) => IconButton(icon: icon!, onPressed: onPressed, focusNode: focusNode)), captionText = captionText ?? CaptionedButtonText(text: caption!, enabled: onPressed != null); - static const double padding = 8; - @override - Widget build(BuildContext context) { - return SizedBox( - width: _width(context), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: padding), - OverlayButton( - scale: scale, - child: iconButton, - ), - if (showCaption) ...[ - const SizedBox(height: padding), - ScaleTransition( - scale: scale, - child: captionText, - ), - ], - const SizedBox(height: padding), - ], - ), - ); - } + State createState() => _CaptionedButtonState(); - static double _width(BuildContext context) => OverlayButton.getSize(context) + padding * 2; + static double getWidth(BuildContext context) => OverlayButton.getSize(context) + padding.horizontal; static Size getSize(BuildContext context, String text, {required bool showCaption}) { - final width = _width(context); + final width = getWidth(context); var height = width; if (showCaption) { final para = RenderParagraph( @@ -62,7 +43,7 @@ class CaptionedButton extends StatelessWidget { textScaleFactor: MediaQuery.textScaleFactorOf(context), maxLines: CaptionedButtonText.maxLines, )..layout(const BoxConstraints(), parentUsesSize: true); - height += para.getMaxIntrinsicHeight(width) + padding; + height += para.getMaxIntrinsicHeight(width) + padding.vertical; } return Size(width, height); } @@ -73,6 +54,81 @@ class CaptionedButton extends StatelessWidget { } } +class _CaptionedButtonState extends State { + final FocusNode _focusNode = FocusNode(); + final ValueNotifier _focusedNotifier = ValueNotifier(false); + + @override + void initState() { + super.initState(); + _updateTraversal(); + _focusNode.addListener(_onFocusChanged); + } + + @override + void didUpdateWidget(covariant CaptionedButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.onPressed != widget.onPressed) { + _updateTraversal(); + } + } + + @override + void dispose() { + _focusNode.dispose(); + _focusedNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: CaptionedButton.getWidth(context), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: CaptionedButton.padding.top), + OverlayButton( + scale: widget.scale, + focusNode: _focusNode, + child: widget.iconButtonBuilder(context, _focusNode), + ), + if (widget.showCaption) ...[ + const SizedBox(height: CaptionedButton.iconTextPadding), + ScaleTransition( + scale: widget.scale, + child: ValueListenableBuilder( + valueListenable: _focusedNotifier, + builder: (context, focused, child) { + final style = CaptionedButtonText.textStyle(context); + return AnimatedDefaultTextStyle( + style: focused + ? style.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + ) + : style, + duration: const Duration(milliseconds: 200), + child: widget.captionText, + ); + }, + ), + ), + ], + SizedBox(height: CaptionedButton.padding.bottom), + ], + ), + ); + } + + void _onFocusChanged() => _focusedNotifier.value = _focusNode.hasFocus; + + void _updateTraversal() { + final enabled = widget.onPressed != null; + _focusNode.skipTraversal = !enabled; + _focusNode.canRequestFocus = enabled; + } +} + class CaptionedButtonText extends StatelessWidget { final String text; final bool enabled; @@ -87,7 +143,7 @@ class CaptionedButtonText extends StatelessWidget { @override Widget build(BuildContext context) { - var style = textStyle(context); + var style = DefaultTextStyle.of(context).style; if (!enabled) { style = style.copyWith(color: style.color!.withOpacity(.2)); } diff --git a/lib/widgets/common/identity/buttons/overlay_button.dart b/lib/widgets/common/identity/buttons/overlay_button.dart index 1feee178a..2c894f994 100644 --- a/lib/widgets/common/identity/buttons/overlay_button.dart +++ b/lib/widgets/common/identity/buttons/overlay_button.dart @@ -4,60 +4,116 @@ import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:flutter/material.dart'; -class OverlayButton extends StatelessWidget { +class OverlayButton extends StatefulWidget { final Animation scale; final BorderRadius? borderRadius; + final FocusNode? focusNode; final Widget child; const OverlayButton({ super.key, this.scale = kAlwaysCompleteAnimation, this.borderRadius, + this.focusNode, required this.child, }); + @override + State createState() => _OverlayButtonState(); + + // icon (24) + icon padding (8) + button padding (16) + static double getSize(BuildContext context) => 48; +} + +class _OverlayButtonState extends State { + final ValueNotifier _focusedNotifier = ValueNotifier(false); + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant OverlayButton oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + _focusedNotifier.dispose(); + super.dispose(); + } + + void _registerWidget(OverlayButton widget) { + widget.focusNode?.addListener(_onFocusChanged); + } + + void _unregisterWidget(OverlayButton widget) { + widget.focusNode?.removeListener(_onFocusChanged); + } + @override Widget build(BuildContext context) { - final brightness = Theme.of(context).brightness; + final borderRadius = widget.borderRadius; + final blurred = settings.enableBlurEffect; + final overlayBackground = Themes.overlayBackgroundColor( + brightness: Theme.of(context).brightness, + blurred: blurred, + ); + return ScaleTransition( - scale: scale, - child: borderRadius != null - ? BlurredRRect( - enabled: blurred, - borderRadius: borderRadius, - child: Material( - type: MaterialType.button, - borderRadius: borderRadius, - color: Themes.overlayBackgroundColor(brightness: brightness, blurred: blurred), - child: Ink( - decoration: BoxDecoration( - border: AvesBorder.border(context), + scale: widget.scale, + child: ValueListenableBuilder( + valueListenable: _focusedNotifier, + builder: (context, focused, child) { + final border = AvesBorder.border( + context, + width: AvesBorder.curvedBorderWidth * (focused ? 2 : 1), + ); + return borderRadius != null + ? BlurredRRect( + enabled: blurred, + borderRadius: borderRadius, + child: Material( + type: MaterialType.button, borderRadius: borderRadius, + color: overlayBackground, + child: AnimatedContainer( + foregroundDecoration: BoxDecoration( + border: border, + borderRadius: borderRadius, + ), + duration: const Duration(milliseconds: 200), + child: widget.child, + ), ), - child: child, - ), - ), - ) - : BlurredOval( - enabled: blurred, - child: Material( - type: MaterialType.circle, - color: Themes.overlayBackgroundColor(brightness: brightness, blurred: blurred), - child: Ink( - decoration: BoxDecoration( - border: AvesBorder.border(context), - shape: BoxShape.circle, + ) + : BlurredOval( + enabled: blurred, + child: Material( + type: MaterialType.circle, + color: overlayBackground, + child: AnimatedContainer( + foregroundDecoration: BoxDecoration( + border: border, + shape: BoxShape.circle, + ), + duration: const Duration(milliseconds: 200), + child: widget.child, + ), ), - child: child, - ), - ), - ), + ); + }, + ), ); } - // icon (24) + icon padding (8) + button padding (16) + border (1 or 2) - static double getSize(BuildContext context) => 48.0 + AvesBorder.curvedBorderWidth * 2; + void _onFocusChanged() => _focusedNotifier.value = widget.focusNode?.hasFocus ?? false; } class ScalingOverlayTextButton extends StatelessWidget { diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart index 8279e143d..d5412d26e 100644 --- a/lib/widgets/filter_grids/common/app_bar.dart +++ b/lib/widgets/filter_grids/common/app_bar.dart @@ -288,7 +288,13 @@ class _FilterGridAppBarState _buildButtonIcon( + context, + actionDelegate, + action, + enabled: enabled, + focusNode: focusNode, + ), captionText: _buildButtonCaption(context, action, enabled: enabled), onPressed: enabled ? () => _onActionSelected(context, action, actionDelegate) : null, ); @@ -350,6 +356,7 @@ class _FilterGridAppBarState _onActionSelected(context, action, actionDelegate) : null; switch (action) { @@ -360,6 +367,7 @@ class _FilterGridAppBarState { @override void initState() { super.initState(); - _scrollController = ScrollController(initialScrollOffset: controller.offset); _scrollController.addListener(_onScrollChanged); + _registerWidget(widget); WidgetsBinding.instance.addPostFrameCallback((_) => _initFocus()); } + @override + void didUpdateWidget(covariant TvRail oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + @override void dispose() { + _unregisterWidget(widget); _scrollController.removeListener(_onScrollChanged); _scrollController.dispose(); _extendedNotifier.dispose(); @@ -73,6 +81,14 @@ class _TvRailState extends State { super.dispose(); } + void _registerWidget(TvRail widget) { + widget.currentCollection?.filterChangeNotifier.addListener(_onCollectionFilterChanged); + } + + void _unregisterWidget(TvRail widget) { + widget.currentCollection?.filterChangeNotifier.removeListener(_onCollectionFilterChanged); + } + @override Widget build(BuildContext context) { final navEntries = _getNavEntries(context); @@ -255,6 +271,8 @@ class _TvRailState extends State { } void _onScrollChanged() => controller.offset = _scrollController.offset; + + void _onCollectionFilterChanged() => setState(() {}); } @immutable diff --git a/lib/widgets/settings/common/quick_actions/available_actions.dart b/lib/widgets/settings/common/quick_actions/available_actions.dart index 9feb52162..66a724e34 100644 --- a/lib/widgets/settings/common/quick_actions/available_actions.dart +++ b/lib/widgets/settings/common/quick_actions/available_actions.dart @@ -17,7 +17,8 @@ class AvailableActionPanel extends StatelessWidget { final String Function(BuildContext context, T action) actionText; static const double spacing = 8; - static const padding = EdgeInsets.all(spacing); + static const double runSpacing = 20; + static const padding = EdgeInsets.symmetric(vertical: 16, horizontal: 8); const AvailableActionPanel({ super.key, @@ -56,7 +57,7 @@ class AvailableActionPanel extends StatelessWidget { child: Wrap( alignment: WrapAlignment.spaceEvenly, spacing: spacing, - runSpacing: spacing, + runSpacing: runSpacing, children: allActions.map((action) { final dragged = action == draggedAvailableAction.value; final enabled = dragged || !quickActions.contains(action); @@ -124,11 +125,10 @@ class AvailableActionPanel extends StatelessWidget { final buttonSizes = captions.map((v) => CaptionedButton.getSize(context, v, showCaption: true)); final actionsPerRun = (width - padding.horizontal + spacing) ~/ (buttonSizes.first.width + spacing); final runCount = (captions.length / actionsPerRun).ceil(); - var height = .0; + var height = runSpacing * (runCount - 1) + padding.vertical / 2; for (var i = 0; i < runCount; i++) { height += buttonSizes.skip(i * actionsPerRun).take(actionsPerRun).map((v) => v.height).max; } - height += spacing * (runCount - 1) + padding.vertical; return height; } } diff --git a/lib/widgets/settings/video/video_settings_page.dart b/lib/widgets/settings/video/video_settings_page.dart index 21187d01b..f01b7bf85 100644 --- a/lib/widgets/settings/video/video_settings_page.dart +++ b/lib/widgets/settings/video/video_settings_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:aves/widgets/settings/video/video.dart'; @@ -20,6 +21,7 @@ class _VideoSettingsPageState extends State { final theme = Theme.of(context); return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(context.l10n.settingsVideoPageTitle), ), body: Theme( diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index b240f6273..c2b27db4a 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -256,43 +256,71 @@ class _StatsPageState extends State { final totalEntryCount = entries.length; final hasMore = maxRowCount != null && entryCountMap.length > maxRowCount; - return [ - Padding( + final onHeaderPressed = hasMore + ? () => Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: StatsTopPage.routeName), + builder: (context) => StatsTopPage( + title: title, + tableBuilder: (context) => FilterTable( + totalEntryCount: totalEntryCount, + entryCountMap: entryCountMap, + filterBuilder: filterBuilder, + sortByCount: sortByCount, + maxRowCount: null, + onFilterSelection: (filter) => _onFilterSelection(context, filter), + ), + onFilterSelection: (filter) => _onFilterSelection(context, filter), + ), + ), + ) + : null; + Widget header = Text( + title, + style: Constants.knownTitleTextStyle, + ); + if (settings.useTvLayout) { + final colors = Theme.of(context).colorScheme; + header = Container( + padding: const EdgeInsets.symmetric(vertical: 12), + alignment: AlignmentDirectional.centerStart, + child: Material( + type: MaterialType.transparency, + child: InkResponse( + onTap: onHeaderPressed, + onHover: (_) {}, + highlightShape: BoxShape.rectangle, + borderRadius: const BorderRadius.all(Radius.circular(123)), + containedInkWell: true, + splashColor: colors.primary.withOpacity(0.12), + hoverColor: colors.primary.withOpacity(0.04), + child: Padding( + padding: const EdgeInsets.all(16), + child: header, + ), + ), + ), + ); + } else { + header = Padding( padding: const EdgeInsets.all(16), child: Row( children: [ - Text( - title, - style: Constants.knownTitleTextStyle, - ), + header, const Spacer(), IconButton( icon: const Icon(AIcons.next), - onPressed: hasMore - ? () => Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: StatsTopPage.routeName), - builder: (context) => StatsTopPage( - title: title, - tableBuilder: (context) => FilterTable( - totalEntryCount: totalEntryCount, - entryCountMap: entryCountMap, - filterBuilder: filterBuilder, - sortByCount: sortByCount, - maxRowCount: null, - onFilterSelection: (filter) => _onFilterSelection(context, filter), - ), - onFilterSelection: (filter) => _onFilterSelection(context, filter), - ), - ), - ) - : null, + onPressed: onHeaderPressed, tooltip: MaterialLocalizations.of(context).moreButtonTooltip, ), ], ), - ), + ); + } + + return [ + header, FilterTable( totalEntryCount: totalEntryCount, entryCountMap: entryCountMap, diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index fa7048fcb..36b0328a5 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -87,7 +87,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.print: return device.canPrint && !targetEntry.isVideo; case EntryAction.openMap: - return targetEntry.hasGps; + return !settings.useTvLayout && targetEntry.hasGps; case EntryAction.viewSource: return targetEntry.isSvg; case EntryAction.videoCaptureFrame: @@ -109,9 +109,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.edit: return canWrite; case EntryAction.copyToClipboard: + case EntryAction.open: return !settings.useTvLayout; case EntryAction.info: - case EntryAction.open: case EntryAction.setAs: case EntryAction.share: return true; diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index 24235bfcb..3ce1e4c6d 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -121,13 +121,14 @@ class _TvButtonRowContent extends StatelessWidget { final enabled = actionDelegate.canApply(action); return CaptionedButton( scale: scale, - iconButton: _buildButtonIcon( + iconButtonBuilder: (context, focusNode) => ViewerButtonRowContent._buildButtonIcon( context: context, action: action, mainEntry: mainEntry, pageEntry: pageEntry, videoController: videoController, actionDelegate: actionDelegate, + focusNode: focusNode, ), captionText: _buildButtonCaption( context: context, @@ -144,6 +145,39 @@ class _TvButtonRowContent extends StatelessWidget { }, ); } + + static Widget _buildButtonCaption({ + required BuildContext context, + required EntryAction action, + required AvesEntry mainEntry, + required AvesEntry pageEntry, + required AvesVideoController? videoController, + required bool enabled, + }) { + switch (action) { + case EntryAction.toggleFavourite: + final favouriteTargetEntry = mainEntry.isBurst ? pageEntry : mainEntry; + return FavouriteTogglerCaption( + entries: {favouriteTargetEntry}, + enabled: enabled, + ); + case EntryAction.videoToggleMute: + return MuteTogglerCaption( + controller: videoController, + enabled: enabled, + ); + case EntryAction.videoTogglePlay: + return PlayTogglerCaption( + controller: videoController, + enabled: enabled, + ); + default: + return CaptionedButtonText( + text: action.getText(context), + enabled: enabled, + ); + } + } } class ViewerButtonRowContent extends StatelessWidget { @@ -374,139 +408,115 @@ class ViewerButtonRowContent extends StatelessWidget { ), ); } -} -Widget _buildButtonIcon({ - required BuildContext context, - required EntryAction action, - required AvesEntry mainEntry, - required AvesEntry pageEntry, - required AvesVideoController? videoController, - required EntryActionDelegate actionDelegate, -}) { - Widget? child; - void onPressed() => actionDelegate.onActionSelected(context, action); + static Widget _buildButtonIcon({ + required BuildContext context, + required EntryAction action, + required AvesEntry mainEntry, + required AvesEntry pageEntry, + required AvesVideoController? videoController, + required EntryActionDelegate actionDelegate, + FocusNode? focusNode, + }) { + Widget? child; + void onPressed() => actionDelegate.onActionSelected(context, action); - ValueListenableBuilder _buildFromListenable(ValueListenable? enabledNotifier) { - return ValueListenableBuilder( - valueListenable: enabledNotifier ?? ValueNotifier(false), - builder: (context, canDo, child) => IconButton( - icon: child!, - onPressed: canDo ? onPressed : null, - tooltip: action.getText(context), - ), - child: action.getIcon(), - ); - } + ValueListenableBuilder _buildFromListenable(ValueListenable? enabledNotifier) { + return ValueListenableBuilder( + valueListenable: enabledNotifier ?? ValueNotifier(false), + builder: (context, canDo, child) => IconButton( + icon: child!, + onPressed: canDo ? onPressed : null, + focusNode: focusNode, + tooltip: action.getText(context), + ), + child: action.getIcon(), + ); + } - final blurred = settings.enableBlurEffect; - switch (action) { - case EntryAction.copy: - child = MoveButton( - copy: true, - blurred: blurred, - onChooserValue: (album) => actionDelegate.quickMove(context, album, copy: true), - onPressed: onPressed, - ); - break; - case EntryAction.move: - child = MoveButton( - copy: false, - blurred: blurred, - onChooserValue: (album) => actionDelegate.quickMove(context, album, copy: false), - onPressed: onPressed, - ); - break; - case EntryAction.share: - child = ShareButton( - blurred: blurred, - entries: {mainEntry}, - onChooserValue: (action) => actionDelegate.quickShare(context, action), - onPressed: onPressed, - ); - break; - case EntryAction.toggleFavourite: - final favouriteTargetEntry = mainEntry.isBurst ? pageEntry : mainEntry; - child = FavouriteToggler( - entries: {favouriteTargetEntry}, - onPressed: onPressed, - ); - break; - case EntryAction.videoToggleMute: - child = MuteToggler( - controller: videoController, - onPressed: onPressed, - ); - break; - case EntryAction.videoTogglePlay: - child = PlayToggler( - controller: videoController, - onPressed: onPressed, - ); - break; - case EntryAction.videoCaptureFrame: - child = _buildFromListenable(videoController?.canCaptureFrameNotifier); - break; - case EntryAction.videoSelectStreams: - child = _buildFromListenable(videoController?.canSelectStreamNotifier); - break; - case EntryAction.videoSetSpeed: - child = _buildFromListenable(videoController?.canSetSpeedNotifier); - break; - case EntryAction.editRating: - child = RateButton( - blurred: blurred, - onChooserValue: (rating) => actionDelegate.quickRate(context, rating), - onPressed: onPressed, - ); - break; - case EntryAction.editTags: - child = TagButton( - blurred: blurred, - onChooserValue: (filter) => actionDelegate.quickTag(context, filter), - onPressed: onPressed, - ); - break; - default: - child = IconButton( - icon: action.getIcon(), - onPressed: onPressed, - tooltip: action.getText(context), - ); - break; - } - return child; -} - -Widget _buildButtonCaption({ - required BuildContext context, - required EntryAction action, - required AvesEntry mainEntry, - required AvesEntry pageEntry, - required AvesVideoController? videoController, - required bool enabled, -}) { - switch (action) { - case EntryAction.toggleFavourite: - final favouriteTargetEntry = mainEntry.isBurst ? pageEntry : mainEntry; - return FavouriteTogglerCaption( - entries: {favouriteTargetEntry}, - enabled: enabled, - ); - case EntryAction.videoToggleMute: - return MuteTogglerCaption( - controller: videoController, - enabled: enabled, - ); - case EntryAction.videoTogglePlay: - return PlayTogglerCaption( - controller: videoController, - enabled: enabled, - ); - default: - return CaptionedButtonText( - text: action.getText(context), - enabled: enabled, - ); + final blurred = settings.enableBlurEffect; + switch (action) { + case EntryAction.copy: + child = MoveButton( + copy: true, + blurred: blurred, + onChooserValue: (album) => actionDelegate.quickMove(context, album, copy: true), + onPressed: onPressed, + ); + break; + case EntryAction.move: + child = MoveButton( + copy: false, + blurred: blurred, + onChooserValue: (album) => actionDelegate.quickMove(context, album, copy: false), + onPressed: onPressed, + ); + break; + case EntryAction.share: + child = ShareButton( + blurred: blurred, + entries: {mainEntry}, + onChooserValue: (action) => actionDelegate.quickShare(context, action), + focusNode: focusNode, + onPressed: onPressed, + ); + break; + case EntryAction.toggleFavourite: + final favouriteTargetEntry = mainEntry.isBurst ? pageEntry : mainEntry; + child = FavouriteToggler( + entries: {favouriteTargetEntry}, + focusNode: focusNode, + onPressed: onPressed, + ); + break; + case EntryAction.videoToggleMute: + child = MuteToggler( + controller: videoController, + focusNode: focusNode, + onPressed: onPressed, + ); + break; + case EntryAction.videoTogglePlay: + child = PlayToggler( + controller: videoController, + focusNode: focusNode, + onPressed: onPressed, + ); + break; + case EntryAction.videoCaptureFrame: + child = _buildFromListenable(videoController?.canCaptureFrameNotifier); + break; + case EntryAction.videoSelectStreams: + child = _buildFromListenable(videoController?.canSelectStreamNotifier); + break; + case EntryAction.videoSetSpeed: + child = _buildFromListenable(videoController?.canSetSpeedNotifier); + break; + case EntryAction.editRating: + child = RateButton( + blurred: blurred, + onChooserValue: (rating) => actionDelegate.quickRate(context, rating), + focusNode: focusNode, + onPressed: onPressed, + ); + break; + case EntryAction.editTags: + child = TagButton( + blurred: blurred, + onChooserValue: (filter) => actionDelegate.quickTag(context, filter), + focusNode: focusNode, + onPressed: onPressed, + ); + break; + default: + child = IconButton( + icon: action.getIcon(), + onPressed: onPressed, + focusNode: focusNode, + tooltip: action.getText(context), + ); + break; + } + return child; } }