tv: scrollable policy, focus improvements, pick page review
This commit is contained in:
parent
f09f3ede28
commit
7cf5538408
23 changed files with 313 additions and 167 deletions
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
37
lib/widgets/about/title.dart
Normal file
37
lib/widgets/about/title.dart
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -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})',
|
||||
});
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
15
lib/widgets/common/basic/tv_edge_focus.dart
Normal file
15
lib/widgets/common/basic/tv_edge_focus.dart
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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),
|
||||
];
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
))
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue