diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart index c5c1e654b..4888d01bc 100644 --- a/lib/widgets/about/about_page.dart +++ b/lib/widgets/about/about_page.dart @@ -5,6 +5,7 @@ import 'package:aves/widgets/about/credits.dart'; import 'package:aves/widgets/about/licenses.dart'; import 'package:aves/widgets/about/translators.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/tv_edge_focus.dart'; import 'package:aves/widgets/common/behaviour/pop/scope.dart'; import 'package:aves/widgets/common/behaviour/pop/tv_navigation.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -28,7 +29,8 @@ class AboutPage extends StatelessWidget { sliver: SliverList( delegate: SliverChildListDelegate( [ - AppReference(showLogo: !useTvLayout), + const TvEdgeFocus(), + const AppReference(), if (!settings.useTvLayout) ...[ const Divider(), const BugReport(), diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index ce4a94ba4..fbab83d89 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -10,12 +10,7 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; class AppReference extends StatefulWidget { - final bool showLogo; - - const AppReference({ - super.key, - required this.showLogo, - }); + const AppReference({super.key}); @override State createState() => _AppReferenceState(); @@ -24,6 +19,13 @@ class AppReference extends StatefulWidget { class _AppReferenceState extends State { late Future _packageInfoLoader; + static const _appTitleStyle = TextStyle( + fontSize: 20, + fontWeight: FontWeight.normal, + letterSpacing: 1.0, + fontFeatures: [FontFeature.enable('smcp')], + ); + @override void initState() { super.initState(); @@ -44,28 +46,19 @@ class _AppReferenceState extends State { } Widget _buildAvesLine() { - const style = TextStyle( - fontSize: 20, - fontWeight: FontWeight.normal, - letterSpacing: 1.0, - fontFeatures: [FontFeature.enable('smcp')], - ); - return FutureBuilder( future: _packageInfoLoader, builder: (context, snapshot) { return Row( mainAxisSize: MainAxisSize.min, children: [ - if (widget.showLogo) ...[ - AvesLogo( - size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3, - ), - const SizedBox(width: 8), - ], + AvesLogo( + size: _appTitleStyle.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3, + ), + const SizedBox(width: 8), Text( '${context.l10n.appName} ${snapshot.data?.version}', - style: style, + style: _appTitleStyle, ), ], ); diff --git a/lib/widgets/about/credits.dart b/lib/widgets/about/credits.dart index e4422cce3..2fd508712 100644 --- a/lib/widgets/about/credits.dart +++ b/lib/widgets/about/credits.dart @@ -1,4 +1,4 @@ -import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/about/title.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; @@ -14,13 +14,7 @@ class AboutCredits extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ConstrainedBox( - constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Text(l10n.aboutCreditsSectionTitle, style: Constants.knownTitleTextStyle), - ), - ), + AboutSectionTitle(text: l10n.aboutCreditsSectionTitle), const SizedBox(height: 8), Text.rich( TextSpan( diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index 813776474..50b376ce3 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -1,8 +1,9 @@ import 'package:aves/app_flavor.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/ref/brand_colors.dart'; import 'package:aves/theme/colors.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/utils/dependencies.dart'; +import 'package:aves/widgets/about/title.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; @@ -50,30 +51,32 @@ class _LicensesState extends State { [ _buildHeader(), const SizedBox(height: 16), - AvesExpansionTile( - title: context.l10n.aboutLicensesAndroidLibrariesSectionTitle, - highlightColor: colors.fromBrandColor(BrandColors.android), - expandedNotifier: _expandedNotifier, - children: _platform.map((package) => LicenseRow(package: package)).toList(), - ), - AvesExpansionTile( - title: context.l10n.aboutLicensesFlutterPluginsSectionTitle, - highlightColor: colors.fromBrandColor(BrandColors.flutter), - expandedNotifier: _expandedNotifier, - children: _flutterPlugins.map((package) => LicenseRow(package: package)).toList(), - ), - AvesExpansionTile( - title: context.l10n.aboutLicensesFlutterPackagesSectionTitle, - highlightColor: colors.fromBrandColor(BrandColors.flutter), - expandedNotifier: _expandedNotifier, - children: _flutterPackages.map((package) => LicenseRow(package: package)).toList(), - ), - AvesExpansionTile( - title: context.l10n.aboutLicensesDartPackagesSectionTitle, - highlightColor: colors.fromBrandColor(BrandColors.flutter), - expandedNotifier: _expandedNotifier, - children: _dartPackages.map((package) => LicenseRow(package: package)).toList(), - ), + if (!settings.useTvLayout) ...[ + AvesExpansionTile( + title: context.l10n.aboutLicensesAndroidLibrariesSectionTitle, + highlightColor: colors.fromBrandColor(BrandColors.android), + expandedNotifier: _expandedNotifier, + children: _platform.map((package) => LicenseRow(package: package)).toList(), + ), + AvesExpansionTile( + title: context.l10n.aboutLicensesFlutterPluginsSectionTitle, + highlightColor: colors.fromBrandColor(BrandColors.flutter), + expandedNotifier: _expandedNotifier, + children: _flutterPlugins.map((package) => LicenseRow(package: package)).toList(), + ), + AvesExpansionTile( + title: context.l10n.aboutLicensesFlutterPackagesSectionTitle, + highlightColor: colors.fromBrandColor(BrandColors.flutter), + expandedNotifier: _expandedNotifier, + children: _flutterPackages.map((package) => LicenseRow(package: package)).toList(), + ), + AvesExpansionTile( + title: context.l10n.aboutLicensesDartPackagesSectionTitle, + highlightColor: colors.fromBrandColor(BrandColors.flutter), + expandedNotifier: _expandedNotifier, + children: _dartPackages.map((package) => LicenseRow(package: package)).toList(), + ), + ], Center( child: AvesOutlinedButton( label: context.l10n.aboutLicensesShowAllButtonLabel, @@ -104,13 +107,7 @@ class _LicensesState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ConstrainedBox( - constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Text(context.l10n.aboutLicensesSectionTitle, style: Constants.knownTitleTextStyle), - ), - ), + AboutSectionTitle(text: context.l10n.aboutLicensesSectionTitle), const SizedBox(height: 8), Text(context.l10n.aboutLicensesBanner), ], diff --git a/lib/widgets/about/policy_page.dart b/lib/widgets/about/policy_page.dart index ea4a1d113..3803bae2f 100644 --- a/lib/widgets/about/policy_page.dart +++ b/lib/widgets/about/policy_page.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/basic/markdown_container.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/material.dart'; @@ -14,6 +15,7 @@ class PolicyPage extends StatefulWidget { class _PolicyPageState extends State { late Future _termsLoader; + final ScrollController _scrollController = ScrollController(); static const termsPath = 'assets/terms.md'; static const termsDirection = TextDirection.ltr; @@ -28,26 +30,72 @@ class _PolicyPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(context.l10n.policyPageTitle), ), body: SafeArea( - child: Center( - child: FutureBuilder( - future: _termsLoader, - builder: (context, snapshot) { - if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return const SizedBox(); - final terms = snapshot.data!; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: MarkdownContainer( - data: terms, - textDirection: termsDirection, - ), - ); - }, + child: FocusableActionDetector( + autofocus: true, + shortcuts: const { + SingleActivator(LogicalKeyboardKey.arrowUp): _ScrollIntent.up(), + SingleActivator(LogicalKeyboardKey.arrowDown): _ScrollIntent.down(), + }, + actions: { + _ScrollIntent: CallbackAction<_ScrollIntent>(onInvoke: _onScrollIntent), + }, + child: Center( + child: FutureBuilder( + future: _termsLoader, + builder: (context, snapshot) { + if (snapshot.hasError || snapshot.connectionState != ConnectionState.done) return const SizedBox(); + final terms = snapshot.data!; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: MarkdownContainer( + scrollController: _scrollController, + data: terms, + textDirection: termsDirection, + ), + ); + }, + ), ), ), ), ); } + + void _onScrollIntent(_ScrollIntent intent) { + late int factor; + switch (intent.type) { + case _ScrollDirection.up: + factor = -1; + break; + case _ScrollDirection.down: + factor = 1; + break; + } + _scrollController.animateTo( + _scrollController.offset + factor * 150, + duration: const Duration(milliseconds: 500), + curve: Curves.easeOutCubic, + ); + } +} + +class _ScrollIntent extends Intent { + const _ScrollIntent({ + required this.type, + }); + + const _ScrollIntent.up() : type = _ScrollDirection.up; + + const _ScrollIntent.down() : type = _ScrollDirection.down; + + final _ScrollDirection type; +} + +enum _ScrollDirection { + up, + down, } diff --git a/lib/widgets/about/title.dart b/lib/widgets/about/title.dart new file mode 100644 index 000000000..bfb63b27d --- /dev/null +++ b/lib/widgets/about/title.dart @@ -0,0 +1,37 @@ +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:flutter/material.dart'; + +class AboutSectionTitle extends StatelessWidget { + final String text; + + const AboutSectionTitle({ + super.key, + required this.text, + }); + + @override + Widget build(BuildContext context) { + Widget child = Container( + alignment: AlignmentDirectional.centerStart, + constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), + child: Text(text, style: Constants.knownTitleTextStyle), + ); + + if (settings.useTvLayout) { + child = InkWell( + borderRadius: const BorderRadius.all(Radius.circular(123)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + child, + ], + ), + ), + ); + } + return child; + } +} diff --git a/lib/widgets/about/translators.dart b/lib/widgets/about/translators.dart index 6e7e01ff7..7d172bbaa 100644 --- a/lib/widgets/about/translators.dart +++ b/lib/widgets/about/translators.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/about/title.dart'; import 'package:aves/widgets/common/basic/text/change_highlight.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:collection/collection.dart'; @@ -55,23 +56,16 @@ class AboutTranslators extends StatelessWidget { @override Widget build(BuildContext context) { - final l10n = context.l10n; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ConstrainedBox( - constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Text(l10n.aboutTranslatorsSectionTitle, style: Constants.knownTitleTextStyle), - ), - ), + AboutSectionTitle(text: context.l10n.aboutTranslatorsSectionTitle), const SizedBox(height: 8), _RandomTextSpanHighlighter( spans: translators.map((v) => v.name).toList(), - highlightColor: Theme.of(context).colorScheme.onPrimary, + color: Theme.of(context).colorScheme.onPrimary, ), const SizedBox(height: 16), ], @@ -82,11 +76,11 @@ class AboutTranslators extends StatelessWidget { class _RandomTextSpanHighlighter extends StatefulWidget { final List spans; - final Color highlightColor; + final Color color; const _RandomTextSpanHighlighter({ required this.spans, - required this.highlightColor, + required this.color, }); @override @@ -103,18 +97,21 @@ class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter> void initState() { super.initState(); + final color = widget.color; _baseStyle = TextStyle( + color: color.withOpacity(.7), shadows: [ Shadow( - color: widget.highlightColor.withOpacity(0), + color: color.withOpacity(0), blurRadius: 0, ) ], ); final highlightStyle = TextStyle( + color: color.withOpacity(1), shadows: [ Shadow( - color: widget.highlightColor, + color: color.withOpacity(1), blurRadius: 3, ) ], @@ -133,7 +130,7 @@ class _RandomTextSpanHighlighterState extends State<_RandomTextSpanHighlighter> ..repeat(reverse: true); _animatedStyle = ShadowedTextStyleTween(begin: _baseStyle, end: highlightStyle).animate(CurvedAnimation( parent: _controller, - curve: Curves.linear, + curve: Curves.easeInOutCubic, )); } diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index 328bbf00e..8bcdf131b 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -540,6 +540,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { ? 'profile' : 'debug', 'has_mobile_services': mobileServices.isServiceAvailable, + 'is_television': device.isTelevision, 'locales': WidgetsBinding.instance.window.locales.join(', '), 'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})', }); diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 3198fd9f1..e23286a9b 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -58,6 +58,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware }) { final canWrite = !settings.isReadOnly; final isMain = appMode == AppMode.main; + final useTvLayout = settings.useTvLayout; switch (action) { // general case EntrySetAction.configureView: @@ -70,9 +71,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware return isSelecting && selectedItemCount == itemCount; // browsing case EntrySetAction.searchCollection: - return !settings.useTvLayout && appMode.canNavigate && !isSelecting; + return !useTvLayout && appMode.canNavigate && !isSelecting; case EntrySetAction.toggleTitleSearch: - return !isSelecting; + return !useTvLayout && !isSelecting; case EntrySetAction.addShortcut: return isMain && !isSelecting && device.canPinShortcut && !isTrash; case EntrySetAction.emptyBin: @@ -83,7 +84,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware case EntrySetAction.stats: return isMain; case EntrySetAction.rescan: - return !settings.useTvLayout && isMain && !isTrash; + return !useTvLayout && isMain && !isTrash; // selecting case EntrySetAction.share: case EntrySetAction.toggleFavourite: diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart index e99c52296..defc70ecf 100644 --- a/lib/widgets/common/action_mixins/entry_storage.dart +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -27,7 +27,7 @@ import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; -import 'package:aves/widgets/filter_grids/album_pick.dart'; +import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart'; import 'package:aves/widgets/viewer/notifications.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/common/basic/markdown_container.dart b/lib/widgets/common/basic/markdown_container.dart index a01fd3cb6..a8174795b 100644 --- a/lib/widgets/common/basic/markdown_container.dart +++ b/lib/widgets/common/basic/markdown_container.dart @@ -6,11 +6,13 @@ import 'package:flutter_markdown/flutter_markdown.dart'; class MarkdownContainer extends StatelessWidget { final String data; final TextDirection? textDirection; + final ScrollController? scrollController; const MarkdownContainer({ super.key, required this.data, this.textDirection, + this.scrollController, }); static const double maxWidth = 460; @@ -44,6 +46,7 @@ class MarkdownContainer extends StatelessWidget { data: data, selectable: true, onTapLink: (text, href, title) => AvesApp.launchUrl(href), + controller: scrollController, shrinkWrap: true, ), ), diff --git a/lib/widgets/common/basic/tv_edge_focus.dart b/lib/widgets/common/basic/tv_edge_focus.dart new file mode 100644 index 000000000..2c6ddad87 --- /dev/null +++ b/lib/widgets/common/basic/tv_edge_focus.dart @@ -0,0 +1,15 @@ +import 'package:aves/model/settings/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +// to be placed at the edges of lists and grids, +// so that TV can reach them with D-pad +class TvEdgeFocus extends StatelessWidget { + const TvEdgeFocus({super.key}); + + @override + Widget build(BuildContext context) { + final useTvLayout = context.select((s) => s.useTvLayout); + return useTvLayout ? const Focus(child: SizedBox()) : const SizedBox(); + } +} diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index bc941d63b..d6da38773 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -34,18 +34,10 @@ class SectionHeader extends StatelessWidget { Widget build(BuildContext context) { Widget child = _buildContent(context); if (settings.useTvLayout) { - final primaryColor = Theme.of(context).colorScheme.primary; - child = Material( - type: MaterialType.transparency, - child: InkResponse( - onTap: _onTap(context), - containedInkWell: true, - highlightShape: BoxShape.rectangle, - borderRadius: const BorderRadius.all(Radius.circular(123)), - hoverColor: primaryColor.withOpacity(0.04), - splashColor: primaryColor.withOpacity(0.12), - child: child, - ), + child = InkWell( + onTap: _onTap(context), + borderRadius: const BorderRadius.all(Radius.circular(123)), + child: child, ); } return Container( diff --git a/lib/widgets/common/identity/buttons/captioned_button.dart b/lib/widgets/common/identity/buttons/captioned_button.dart index dda3e7787..10cc09602 100644 --- a/lib/widgets/common/identity/buttons/captioned_button.dart +++ b/lib/widgets/common/identity/buttons/captioned_button.dart @@ -8,7 +8,7 @@ class CaptionedButton extends StatefulWidget { final Animation scale; final Widget captionText; final CaptionedIconButtonBuilder iconButtonBuilder; - final bool showCaption; + final bool autofocus, showCaption; final VoidCallback? onPressed; static const EdgeInsets padding = EdgeInsets.symmetric(horizontal: 8); @@ -21,6 +21,7 @@ class CaptionedButton extends StatefulWidget { CaptionedIconButtonBuilder? iconButtonBuilder, String? caption, Widget? captionText, + this.autofocus = false, this.showCaption = true, required this.onPressed, }) : assert(icon != null || iconButtonBuilder != null), @@ -57,6 +58,7 @@ class CaptionedButton extends StatefulWidget { class _CaptionedButtonState extends State { final FocusNode _focusNode = FocusNode(); final ValueNotifier _focusedNotifier = ValueNotifier(false); + bool _didAutofocus = false; @override void initState() { @@ -65,12 +67,21 @@ class _CaptionedButtonState extends State { _focusNode.addListener(_onFocusChanged); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _handleAutofocus(); + } + @override void didUpdateWidget(covariant CaptionedButton oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.onPressed != widget.onPressed) { _updateTraversal(); } + if (oldWidget.autofocus != widget.autofocus) { + _handleAutofocus(); + } } @override @@ -120,6 +131,13 @@ class _CaptionedButtonState extends State { ); } + void _handleAutofocus() { + if (!_didAutofocus && widget.autofocus) { + FocusScope.of(context).autofocus(_focusNode); + _didAutofocus = true; + } + } + void _onFocusChanged() => _focusedNotifier.value = _focusNode.hasFocus; void _updateTraversal() { diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart similarity index 79% rename from lib/widgets/filter_grids/album_pick.dart rename to lib/widgets/dialogs/pick_dialogs/album_pick_page.dart index 3a19327cd..7039ccfe4 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/dialogs/pick_dialogs/album_pick_page.dart @@ -13,6 +13,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart'; import 'package:aves/widgets/dialogs/filter_editors/create_album_dialog.dart'; @@ -128,6 +129,53 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { Selection> selection, AlbumChipSetActionDelegate actionDelegate, ) { + final itemCount = actionDelegate.allItems.length; + final isSelecting = selection.isSelecting; + final selectedItems = selection.selectedItems; + final selectedFilters = selectedItems.map((v) => v.filter).toSet(); + + bool isVisible(ChipSetAction action) => actionDelegate.isVisible( + action, + appMode: appMode, + isSelecting: isSelecting, + itemCount: itemCount, + selectedFilters: selectedFilters, + ); + + return settings.useTvLayout + ? _buildTelevisionActions( + context: context, + isVisible: isVisible, + actionDelegate: actionDelegate, + ) + : _buildMobileActions( + context: context, + isVisible: isVisible, + actionDelegate: actionDelegate, + ); + } + + List _buildTelevisionActions({ + required BuildContext context, + required bool Function(ChipSetAction action) isVisible, + required AlbumChipSetActionDelegate actionDelegate, + }) { + return [ + ...ChipSetActions.general, + ].where(isVisible).map((action) { + return CaptionedButton( + icon: action.getIcon(), + caption: action.getText(context), + onPressed: () => actionDelegate.onActionSelected(context, {}, action), + ); + }).toList(); + } + + List _buildMobileActions({ + required BuildContext context, + required bool Function(ChipSetAction action) isVisible, + required AlbumChipSetActionDelegate actionDelegate, + }) { return [ if (widget.moveType != null) IconButton( @@ -149,7 +197,7 @@ class _AlbumPickPageState extends State<_AlbumPickPage> { child: PopupMenuButton( itemBuilder: (context) { return [ - FilterGridAppBar.toMenuItem(context, ChipSetAction.configureView, enabled: true), + ...ChipSetActions.general.where(isVisible).map((action) => FilterGridAppBar.toMenuItem(context, action, enabled: true)), const PopupMenuDivider(), FilterGridAppBar.toMenuItem(context, ChipSetAction.toggleTitleSearch, enabled: true), ]; diff --git a/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart index 9cc912c76..418edc63f 100644 --- a/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart +++ b/lib/widgets/dialogs/pick_dialogs/app_pick_page.dart @@ -1,4 +1,5 @@ import 'package:aves/image_providers/app_icon_image_provider.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/basic/query_bar.dart'; @@ -37,8 +38,10 @@ class _AppPickPageState extends State { @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !useTvLayout, title: Text(context.l10n.appPickDialogTitle), ), body: SafeArea( @@ -57,7 +60,7 @@ class _AppPickPageState extends State { final packages = allPackages.where((package) => package.categoryLauncher).toList()..sort((a, b) => compareAsciiUpperCase(_displayName(a), _displayName(b))); return Column( children: [ - QueryBar(queryNotifier: _queryNotifier), + if (!useTvLayout) QueryBar(queryNotifier: _queryNotifier), ValueListenableBuilder( valueListenable: _queryNotifier, builder: (context, query, child) { diff --git a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart index 310d2b382..0a01f5ff1 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -70,6 +70,7 @@ abstract class ChipSetActionDelegate with FeedbackMi final selectedItemCount = selectedFilters.length; final hasSelection = selectedFilters.isNotEmpty; final isMain = appMode == AppMode.main; + final useTvLayout = settings.useTvLayout; switch (action) { // general case ChipSetAction.configureView: @@ -82,9 +83,9 @@ abstract class ChipSetActionDelegate with FeedbackMi return isSelecting && selectedItemCount == itemCount; // browsing case ChipSetAction.search: - return !settings.useTvLayout && appMode.canNavigate && !isSelecting; + return !useTvLayout && appMode.canNavigate && !isSelecting; case ChipSetAction.toggleTitleSearch: - return !isSelecting; + return !useTvLayout && !isSelecting; case ChipSetAction.createAlbum: return false; // browsing or selecting diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index beb3114f3..7f0e61c5b 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -115,15 +115,23 @@ class FilterGridPage extends StatelessWidget { ); if (useTvLayout) { + final canNavigate = context.select, bool>((v) => v.value.canNavigate); return Scaffold( - body: Row( - children: [ - TvRail( - controller: context.read(), - ), - Expanded(child: body), - ], - ), + body: canNavigate + ? Row( + children: [ + TvRail( + controller: context.read(), + ), + Expanded(child: body), + ], + ) + : DirectionalSafeArea( + top: false, + end: false, + bottom: false, + child: body, + ), resizeToAvoidBottomInset: false, extendBody: true, ); diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 25a94f89e..ed5108c40 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -277,11 +277,12 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin MapAction.zoomIn, MapAction.zoomOut, ] - .map((action) => Padding( + .mapIndexed((i, action) => Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: CaptionedButton( icon: action.getIcon(), caption: action.getText(context), + autofocus: i == 0, onPressed: () => MapActionDelegate(_mapController).onActionSelected(context, action), ), )) diff --git a/lib/widgets/settings/navigation/drawer_tab_albums.dart b/lib/widgets/settings/navigation/drawer_tab_albums.dart index faf8916d8..2f4cf2f02 100644 --- a/lib/widgets/settings/navigation/drawer_tab_albums.dart +++ b/lib/widgets/settings/navigation/drawer_tab_albums.dart @@ -3,7 +3,7 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/buttons/outlined_button.dart'; -import 'package:aves/widgets/filter_grids/album_pick.dart'; +import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart'; import 'package:aves/widgets/navigation/drawer/tile.dart'; import 'package:aves/widgets/settings/navigation/drawer_editor_banner.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/stats/mime_donut.dart b/lib/widgets/stats/mime_donut.dart index f7739612e..4aee3ade5 100644 --- a/lib/widgets/stats/mime_donut.dart +++ b/lib/widgets/stats/mime_donut.dart @@ -111,45 +111,37 @@ class _MimeDonutState extends State with AutomaticKeepAliveClientMixi ], ), ); - final primaryColor = Theme.of(context).colorScheme.primary; final legend = SizedBox( width: dim, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: seriesData - .map((d) => Material( - type: MaterialType.transparency, - child: InkResponse( - onTap: () => widget.onFilterSelection(MimeFilter(d.mimeType)), - containedInkWell: true, - highlightShape: BoxShape.rectangle, - borderRadius: const BorderRadius.all(Radius.circular(123)), - hoverColor: primaryColor.withOpacity(0.04), - splashColor: primaryColor.withOpacity(0.12), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(AIcons.disc, color: d.color), - const SizedBox(width: 8), - Flexible( - child: Text( - d.displayText, - overflow: TextOverflow.fade, - softWrap: false, - maxLines: 1, - ), + .map((d) => InkWell( + onTap: () => widget.onFilterSelection(MimeFilter(d.mimeType)), + borderRadius: const BorderRadius.all(Radius.circular(123)), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(AIcons.disc, color: d.color), + const SizedBox(width: 8), + Flexible( + child: Text( + d.displayText, + overflow: TextOverflow.fade, + softWrap: false, + maxLines: 1, ), - const SizedBox(width: 8), - Text( - numberFormat.format(d.entryCount), - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall!.color, - ), + ), + const SizedBox(width: 8), + Text( + numberFormat.format(d.entryCount), + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall!.color, ), - const SizedBox(width: 4), - ], - ), + ), + const SizedBox(width: 4), + ], ), )) .toList(), diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index 1edaad08a..85af4999f 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -15,6 +15,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/basic/tv_edge_focus.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; @@ -96,6 +97,7 @@ class _StatsPageState extends State { @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; return ValueListenableBuilder( valueListenable: _isPageAnimatingNotifier, builder: (context, animating, child) { @@ -196,6 +198,7 @@ class _StatsPageState extends State { ), ), children: [ + const TvEdgeFocus(), mimeDonuts, Histogram( entries: entries, @@ -218,7 +221,7 @@ class _StatsPageState extends State { return Scaffold( appBar: AppBar( - automaticallyImplyLeading: !settings.useTvLayout, + automaticallyImplyLeading: !useTvLayout, title: Text(l10n.statsPageTitle), ), body: GestureAreaProtectorStack( @@ -274,23 +277,15 @@ class _StatsPageState extends State { style: Constants.knownTitleTextStyle, ); if (settings.useTvLayout) { - final primaryColor = Theme.of(context).colorScheme.primary; header = Container( padding: const EdgeInsets.symmetric(vertical: 12), alignment: AlignmentDirectional.centerStart, - child: Material( - type: MaterialType.transparency, - child: InkResponse( - onTap: onHeaderPressed, - containedInkWell: true, - highlightShape: BoxShape.rectangle, - borderRadius: const BorderRadius.all(Radius.circular(123)), - hoverColor: primaryColor.withOpacity(0.04), - splashColor: primaryColor.withOpacity(0.12), - child: Padding( - padding: const EdgeInsets.all(16), - child: header, - ), + child: InkWell( + onTap: onHeaderPressed, + borderRadius: const BorderRadius.all(Radius.circular(123)), + child: Padding( + padding: const EdgeInsets.all(16), + child: header, ), ), ); diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 61577aa3b..1cd1d0a79 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -31,7 +31,7 @@ import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/entry_editors/rename_entry_dialog.dart'; import 'package:aves/widgets/dialogs/export_entry_dialog.dart'; -import 'package:aves/widgets/filter_grids/album_pick.dart'; +import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart'; import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart'; import 'package:aves/widgets/viewer/action/printer.dart'; import 'package:aves/widgets/viewer/action/single_entry_editor.dart';