tv: button focus style, stats page

This commit is contained in:
Thibault Deckers 2023-01-06 15:44:31 +01:00
parent 96f72fcdb3
commit b0f613db27
19 changed files with 448 additions and 242 deletions

View file

@ -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<CollectionAppBar> 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<CollectionAppBar> with SingleTickerPr
BuildContext context,
EntrySetAction action, {
required bool enabled,
FocusNode? focusNode,
required Selection<AvesEntry> selection,
}) {
final onPressed = enabled ? () => _onActionSelected(action) : null;
@ -445,12 +451,14 @@ class _CollectionAppBarState extends State<CollectionAppBar> 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<CollectionAppBar> with SingleTickerPr
key: _getActionKey(action),
icon: action.getIcon(),
onPressed: onPressed,
focusNode: focusNode,
tooltip: action.getText(context),
);
}
@ -581,7 +590,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
void _onQueryFocusRequest() => _queryBarFocusNode.requestFocus();
void _updateStatusBarHeight() {
_statusBarHeight = EdgeInsets.fromWindowPadding(window.padding, window.devicePixelRatio).top;
_statusBarHeight = context.read<MediaQueryData>().padding.top;
_updateAppBarHeight();
}

View file

@ -8,12 +8,14 @@ import 'package:provider/provider.dart';
abstract class ChooserQuickButton<T> extends StatefulWidget {
final bool blurred;
final ValueSetter<T>? 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<T extends ChooserQuickButton<U>, U> exten
child: IconButton(
icon: icon,
onPressed: widget.onPressed,
focusNode: widget.focusNode,
tooltip: _hasChooser ? null : tooltip,
),
);

View file

@ -8,6 +8,7 @@ class RateButton extends ChooserQuickButton<int> {
super.key,
required super.blurred,
super.onChooserValue,
super.focusNode,
required super.onPressed,
});

View file

@ -13,6 +13,7 @@ class ShareButton extends ChooserQuickButton<ShareAction> {
required super.blurred,
required this.entries,
super.onChooserValue,
super.focusNode,
required super.onPressed,
});

View file

@ -17,6 +17,7 @@ class TagButton extends ChooserQuickButton<CollectionFilter> {
super.key,
required super.blurred,
super.onChooserValue,
super.focusNode,
required super.onPressed,
});

View file

@ -12,12 +12,14 @@ import 'package:provider/provider.dart';
class FavouriteToggler extends StatefulWidget {
final Set<AvesEntry> 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<FavouriteToggler> {
IconButton(
icon: Icon(isFavourite ? isFavouriteIcon : isNotFavouriteIcon),
onPressed: widget.onPressed,
focusNode: widget.focusNode,
tooltip: isFavourite ? context.l10n.entryActionRemoveFavourite : context.l10n.entryActionAddFavourite,
),
Sweeper(

View file

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

View file

@ -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<PlayToggler> with SingleTickerProviderStat
progress: _playPauseAnimation,
),
onPressed: widget.onPressed,
focusNode: widget.focusNode,
tooltip: text,
);
}

View file

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

View file

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

View file

@ -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<double> 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<CaptionedButton> 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<CaptionedButton> {
final FocusNode _focusNode = FocusNode();
final ValueNotifier<bool> _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<bool>(
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));
}

View file

@ -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<double> 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<OverlayButton> createState() => _OverlayButtonState();
// icon (24) + icon padding (8) + button padding (16)
static double getSize(BuildContext context) => 48;
}
class _OverlayButtonState extends State<OverlayButton> {
final ValueNotifier<bool> _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<bool>(
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 {

View file

@ -288,7 +288,13 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
].where(isVisible).map((action) {
final enabled = canApply(action);
return CaptionedButton(
iconButton: _buildButtonIcon(context, actionDelegate, action, enabled: enabled),
iconButtonBuilder: (context, focusNode) => _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<T extends CollectionFilter, CSAD extends ChipSetAct
CSAD actionDelegate,
ChipSetAction action, {
required bool enabled,
FocusNode? focusNode,
}) {
final onPressed = enabled ? () => _onActionSelected(context, action, actionDelegate) : null;
switch (action) {
@ -360,6 +367,7 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
builder: (context, queryEnabled, child) {
return TitleSearchToggler(
queryEnabled: queryEnabled,
focusNode: focusNode,
onPressed: onPressed,
);
},
@ -368,6 +376,7 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
return IconButton(
icon: action.getIcon(),
onPressed: onPressed,
focusNode: focusNode,
tooltip: action.getText(context),
);
}

View file

@ -57,15 +57,23 @@ class _TvRailState extends State<TvRail> {
@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<TvRail> {
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<TvRail> {
}
void _onScrollChanged() => controller.offset = _scrollController.offset;
void _onCollectionFilterChanged() => setState(() {});
}
@immutable

View file

@ -17,7 +17,8 @@ class AvailableActionPanel<T extends Object> 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<T extends Object> 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<T extends Object> 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;
}
}

View file

@ -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<VideoSettingsPage> {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: !settings.useTvLayout,
title: Text(context.l10n.settingsVideoPageTitle),
),
body: Theme(

View file

@ -256,43 +256,71 @@ class _StatsPageState extends State<StatsPage> {
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,

View file

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

View file

@ -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<bool> _buildFromListenable(ValueListenable<bool>? enabledNotifier) {
return ValueListenableBuilder<bool>(
valueListenable: enabledNotifier ?? ValueNotifier(false),
builder: (context, canDo, child) => IconButton(
icon: child!,
onPressed: canDo ? onPressed : null,
tooltip: action.getText(context),
),
child: action.getIcon(),
);
}
ValueListenableBuilder<bool> _buildFromListenable(ValueListenable<bool>? enabledNotifier) {
return ValueListenableBuilder<bool>(
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;
}
}