tv: scrollable policy, focus improvements, pick page review

This commit is contained in:
Thibault Deckers 2023-01-18 11:52:39 +01:00
parent f09f3ede28
commit 7cf5538408
23 changed files with 313 additions and 167 deletions

View file

@ -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(),

View file

@ -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<AppReference> createState() => _AppReferenceState();
@ -24,6 +19,13 @@ class AppReference extends StatefulWidget {
class _AppReferenceState extends State<AppReference> {
late Future<PackageInfo> _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<AppReference> {
}
Widget _buildAvesLine() {
const style = TextStyle(
fontSize: 20,
fontWeight: FontWeight.normal,
letterSpacing: 1.0,
fontFeatures: [FontFeature.enable('smcp')],
);
return FutureBuilder<PackageInfo>(
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,
),
],
);

View file

@ -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(

View file

@ -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<Licenses> {
[
_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<Licenses> {
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),
],

View file

@ -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<PolicyPage> {
late Future<String> _termsLoader;
final ScrollController _scrollController = ScrollController();
static const termsPath = 'assets/terms.md';
static const termsDirection = TextDirection.ltr;
@ -28,26 +30,72 @@ class _PolicyPageState extends State<PolicyPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: !settings.useTvLayout,
title: Text(context.l10n.policyPageTitle),
),
body: SafeArea(
child: Center(
child: FutureBuilder<String>(
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<String>(
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,
}

View file

@ -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;
}
}

View file

@ -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<String> 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,
));
}

View file

@ -540,6 +540,7 @@ class _AvesAppState extends State<AvesApp> 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})',
});

View file

@ -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:

View file

@ -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';

View file

@ -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,
),
),

View file

@ -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<Settings, bool>((s) => s.useTvLayout);
return useTvLayout ? const Focus(child: SizedBox()) : const SizedBox();
}
}

View file

@ -34,18 +34,10 @@ class SectionHeader<T> 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(

View file

@ -8,7 +8,7 @@ class CaptionedButton extends StatefulWidget {
final Animation<double> 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<CaptionedButton> {
final FocusNode _focusNode = FocusNode();
final ValueNotifier<bool> _focusedNotifier = ValueNotifier(false);
bool _didAutofocus = false;
@override
void initState() {
@ -65,12 +67,21 @@ class _CaptionedButtonState extends State<CaptionedButton> {
_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<CaptionedButton> {
);
}
void _handleAutofocus() {
if (!_didAutofocus && widget.autofocus) {
FocusScope.of(context).autofocus(_focusNode);
_didAutofocus = true;
}
}
void _onFocusChanged() => _focusedNotifier.value = _focusNode.hasFocus;
void _updateTraversal() {

View file

@ -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<FilterGridItem<AlbumFilter>> 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<Widget> _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<Widget> _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<ChipSetAction>(
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),
];

View file

@ -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<AppPickPage> {
@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<AppPickPage> {
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<String>(
valueListenable: _queryNotifier,
builder: (context, query, child) {

View file

@ -70,6 +70,7 @@ abstract class ChipSetActionDelegate<T extends CollectionFilter> 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<T extends CollectionFilter> 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

View file

@ -115,15 +115,23 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
);
if (useTvLayout) {
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
return Scaffold(
body: Row(
children: [
TvRail(
controller: context.read<TvRailController>(),
),
Expanded(child: body),
],
),
body: canNavigate
? Row(
children: [
TvRail(
controller: context.read<TvRailController>(),
),
Expanded(child: body),
],
)
: DirectionalSafeArea(
top: false,
end: false,
bottom: false,
child: body,
),
resizeToAvoidBottomInset: false,
extendBody: true,
);

View file

@ -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),
),
))

View file

@ -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';

View file

@ -111,45 +111,37 @@ class _MimeDonutState extends State<MimeDonut> 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(),

View file

@ -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<StatsPage> {
@override
Widget build(BuildContext context) {
final useTvLayout = settings.useTvLayout;
return ValueListenableBuilder<bool>(
valueListenable: _isPageAnimatingNotifier,
builder: (context, animating, child) {
@ -196,6 +198,7 @@ class _StatsPageState extends State<StatsPage> {
),
),
children: [
const TvEdgeFocus(),
mimeDonuts,
Histogram(
entries: entries,
@ -218,7 +221,7 @@ class _StatsPageState extends State<StatsPage> {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: !settings.useTvLayout,
automaticallyImplyLeading: !useTvLayout,
title: Text(l10n.statsPageTitle),
),
body: GestureAreaProtectorStack(
@ -274,23 +277,15 @@ class _StatsPageState extends State<StatsPage> {
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,
),
),
);

View file

@ -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';