This commit is contained in:
Thibault Deckers 2022-12-19 16:32:40 +01:00
parent 35835cdbd8
commit fbc10cc6a8
18 changed files with 292 additions and 269 deletions

View file

@ -13,7 +13,7 @@ All notable changes to this project will be documented in this file.
- Map: edit cluster location
- Accessibility: optional alternative to pinch-to-zoom thumbnails
- Lithuanian translation (thanks Gediminas Murauskas)
- Norsk (Bokmål) translation (thanks Allan Nordhøy)
- Norwegian (Bokmål) translation (thanks Allan Nordhøy)
- Chinese (Traditional) translation (thanks pemibe)
- Ukrainian translation (thanks Olexandr Mazur)

View file

@ -9,7 +9,6 @@ import 'package:aves/model/device.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/filters/recent.dart';
import 'package:aves/model/settings/defaults.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/map_style.dart';
@ -250,7 +249,6 @@ class Settings extends ChangeNotifier {
null,
MimeFilter.video,
FavouriteFilter.instance,
RecentlyAddedFilter.instance,
];
drawerPageBookmarks = [
AlbumListPage.routeName,

View file

@ -5,7 +5,8 @@ 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/behaviour/tv_pop.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';
import 'package:aves/widgets/navigation/tv_rail.dart';
import 'package:flutter/material.dart';
@ -47,7 +48,8 @@ class AboutPage extends StatelessWidget {
if (device.isTelevision) {
return Scaffold(
body: TvPopScope(
body: AvesPopScope(
handlers: const [TvNavigationPopHandler.pop],
child: Row(
children: [
TvRail(

View file

@ -140,7 +140,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
// - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28
// - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0)
final ValueNotifier<PageTransitionsBuilder> _pageTransitionsBuilderNotifier = ValueNotifier(const FadeUpwardsPageTransitionsBuilder());
final ValueNotifier<NavigationMode> _navigationModeNotifier = ValueNotifier(NavigationMode.traditional);
final ValueNotifier<TvMediaQueryModifier?> _tvMediaQueryModifierNotifier = ValueNotifier(null);
final ValueNotifier<AppMode> _appModeNotifier = ValueNotifier(AppMode.main);
// observers are not registered when using the same list object with different items
@ -170,7 +170,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
@override
void dispose() {
_pageTransitionsBuilderNotifier.dispose();
_navigationModeNotifier.dispose();
_tvMediaQueryModifierNotifier.dispose();
_appModeNotifier.dispose();
_subscriptions
..forEach((sub) => sub.cancel())
@ -294,14 +294,14 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
// Flutter v3.4 already checks the system `Configuration.fontWeightAdjustment` to update `MediaQuery`
// but we need to also check the non-standard Samsung field `bf` representing the bold font toggle
final shouldUseBoldFont = snapshot.data ?? false;
return ValueListenableBuilder<NavigationMode>(
valueListenable: _navigationModeNotifier,
builder: (context, navigationMode, child) {
final mq = MediaQuery.of(context).copyWith(
boldText: shouldUseBoldFont,
);
return ValueListenableBuilder<TvMediaQueryModifier?>(
valueListenable: _tvMediaQueryModifierNotifier,
builder: (context, modifier, child) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(
boldText: shouldUseBoldFont,
navigationMode: navigationMode,
),
data: modifier?.call(mq) ?? mq,
child: AvesColorsProvider(
child: ValueListenableBuilder<PageTransitionsBuilder>(
valueListenable: _pageTransitionsBuilderNotifier,
@ -409,7 +409,10 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
await device.init();
if (device.isTelevision) {
_pageTransitionsBuilderNotifier.value = const TvPageTransitionsBuilder();
_navigationModeNotifier.value = NavigationMode.directional;
_tvMediaQueryModifierNotifier.value = (mq) => mq.copyWith(
textScaleFactor: 1.1,
navigationMode: NavigationMode.directional,
);
}
await mobileServices.init();
await settings.init(monitorPlatformSettings: true);
@ -523,3 +526,5 @@ class StretchMaterialScrollBehavior extends MaterialScrollBehavior {
);
}
}
typedef TvMediaQueryModifier = MediaQueryData Function(MediaQueryData);

View file

@ -16,8 +16,9 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/collection/collection_grid.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
import 'package:aves/widgets/common/behaviour/tv_pop.dart';
import 'package:aves/widgets/common/behaviour/pop/double_back.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';
import 'package:aves/widgets/common/identity/aves_fab.dart';
import 'package:aves/widgets/common/providers/query_provider.dart';
@ -26,7 +27,6 @@ import 'package:aves/widgets/navigation/drawer/app_drawer.dart';
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
import 'package:aves/widgets/navigation/tv_rail.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -52,6 +52,7 @@ class _CollectionPageState extends State<CollectionPage> {
final List<StreamSubscription> _subscriptions = [];
late CollectionLens _collection;
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override
void initState() {
@ -76,6 +77,7 @@ class _CollectionPageState extends State<CollectionPage> {
..forEach((sub) => sub.cancel())
..clear();
_collection.dispose();
_doubleBackPopHandler.dispose();
super.dispose();
}
@ -89,17 +91,21 @@ class _CollectionPageState extends State<CollectionPage> {
final body = QueryProvider(
initialQuery: liveFilter?.query,
child: Builder(
builder: (context) => WillPopScope(
onWillPop: () {
final selection = context.read<Selection<AvesEntry>>();
if (selection.isSelecting) {
selection.browse();
return SynchronousFuture(false);
}
return SynchronousFuture(true);
},
child: const DoubleBackPopScope(
child: GestureAreaProtectorStack(
builder: (context) {
return AvesPopScope(
handlers: [
(context) {
final selection = context.read<Selection<AvesEntry>>();
if (selection.isSelecting) {
selection.browse();
return false;
}
return true;
},
TvNavigationPopHandler.pop,
_doubleBackPopHandler.pop,
],
child: const GestureAreaProtectorStack(
child: SafeArea(
top: false,
bottom: false,
@ -110,27 +116,25 @@ class _CollectionPageState extends State<CollectionPage> {
),
),
),
),
),
);
},
),
);
Widget page;
if (device.isTelevision) {
page = TvPopScope(
child: Scaffold(
body: Row(
children: [
TvRail(
controller: context.read<TvRailController>(),
currentCollection: _collection,
),
Expanded(child: body),
],
),
resizeToAvoidBottomInset: false,
extendBody: true,
page = Scaffold(
body: Row(
children: [
TvRail(
controller: context.read<TvRailController>(),
currentCollection: _collection,
),
Expanded(child: body),
],
),
resizeToAvoidBottomInset: false,
extendBody: true,
);
} else {
page = Selector<Settings, bool>(

View file

@ -1,56 +0,0 @@
import 'dart:async';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:overlay_support/overlay_support.dart';
class DoubleBackPopScope extends StatefulWidget {
final Widget child;
const DoubleBackPopScope({
super.key,
required this.child,
});
@override
State<DoubleBackPopScope> createState() => _DoubleBackPopScopeState();
}
class _DoubleBackPopScopeState extends State<DoubleBackPopScope> with FeedbackMixin {
bool _backOnce = false;
Timer? _backTimer;
@override
void dispose() {
_stopBackTimer();
super.dispose();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
if (!Navigator.canPop(context) && settings.mustBackTwiceToExit && !_backOnce) {
_backOnce = true;
_stopBackTimer();
_backTimer = Timer(Durations.doubleBackTimerDelay, () => _backOnce = false);
toast(
context.l10n.doubleBackExitMessage,
duration: Durations.doubleBackTimerDelay,
);
return SynchronousFuture(false);
}
return SynchronousFuture(true);
},
child: widget.child,
);
}
void _stopBackTimer() {
_backTimer?.cancel();
}
}

View file

@ -0,0 +1,34 @@
import 'dart:async';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
import 'package:overlay_support/overlay_support.dart';
class DoubleBackPopHandler {
bool _backOnce = false;
Timer? _backTimer;
void dispose() {
_stopBackTimer();
}
bool pop(BuildContext context) {
if (!Navigator.canPop(context) && settings.mustBackTwiceToExit && !_backOnce) {
_backOnce = true;
_stopBackTimer();
_backTimer = Timer(Durations.doubleBackTimerDelay, () => _backOnce = false);
toast(
context.l10n.doubleBackExitMessage,
duration: Durations.doubleBackTimerDelay,
);
return false;
}
return true;
}
void _stopBackTimer() {
_backTimer?.cancel();
}
}

View file

@ -0,0 +1,23 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
// as of Flutter v3.3.10, the resolution order of multiple `WillPopScope` is random
// so this widget combines multiple handlers with a guaranteed order
class AvesPopScope extends StatelessWidget {
final List<bool Function(BuildContext context)> handlers;
final Widget child;
const AvesPopScope({
super.key,
required this.handlers,
required this.child,
});
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () => SynchronousFuture(handlers.fold(true, (prev, v) => prev ? v(context) : false)),
child: child,
);
}
}

View file

@ -7,39 +7,25 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
class TvPopScope extends StatelessWidget {
final Widget child;
class TvNavigationPopHandler {
static bool pop(BuildContext context) {
if (!device.isTelevision || _isHome(context)) {
return true;
}
const TvPopScope({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
if (!device.isTelevision || _isHome(context)) {
return SynchronousFuture(true);
}
Navigator.pushAndRemoveUntil(
context,
_getHomeRoute(),
(route) => false,
);
return SynchronousFuture(false);
},
child: child,
Navigator.pushAndRemoveUntil(
context,
_getHomeRoute(),
(route) => false,
);
return false;
}
bool _isHome(BuildContext context) {
static bool _isHome(BuildContext context) {
final homePage = settings.homePage;
final currentRoute = context.currentRouteName;
@ -53,7 +39,7 @@ class TvPopScope extends StatelessWidget {
}
}
Route _getHomeRoute() {
static Route _getHomeRoute() {
switch (settings.homePage) {
case HomePageSetting.collection:
return MaterialPageRoute(

View file

@ -42,7 +42,6 @@ class AvesAppBar extends StatelessWidget {
child: AvesFloatingBar(
builder: (context, backgroundColor, child) => Material(
color: backgroundColor,
textStyle: Theme.of(context).appBarTheme.titleTextStyle,
child: child,
),
child: Column(
@ -63,18 +62,21 @@ class AvesAppBar extends StatelessWidget {
)
: const SizedBox(width: 16),
Expanded(
child: Hero(
tag: titleHeroTag,
flightShuttleBuilder: _flightShuttleBuilder,
transitionOnUserGestures: true,
child: AnimatedSwitcher(
duration: context.read<DurationsData>().iconAnimation,
child: Row(
key: ValueKey(transitionKey),
children: [
Expanded(child: title),
...actions,
],
child: DefaultTextStyle(
style: Theme.of(context).appBarTheme.titleTextStyle!,
child: Hero(
tag: titleHeroTag,
flightShuttleBuilder: _flightShuttleBuilder,
transitionOnUserGestures: true,
child: AnimatedSwitcher(
duration: context.read<DurationsData>().iconAnimation,
child: Row(
key: ValueKey(transitionKey),
children: [
Expanded(child: title),
...actions,
],
),
),
),
),

View file

@ -4,7 +4,6 @@ import 'dart:math';
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/chip_actions.dart';
import 'package:aves/model/covers.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/location.dart';
@ -60,10 +59,9 @@ class AvesFilterChip extends StatefulWidget {
static const double minChipHeight = kMinInteractiveDimension;
static const double minChipWidth = 80;
static const double iconSize = 18;
static const double fontSize = 14;
static const double decoratedContentVerticalPadding = 5;
static double get fontSize => device.isTelevision ? 18 : 14;
const AvesFilterChip({
super.key,
required this.filter,

View file

@ -2,8 +2,9 @@ import 'dart:ui';
import 'package:aves/theme/durations.dart';
import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
import 'package:aves/widgets/common/behaviour/tv_pop.dart';
import 'package:aves/widgets/common/behaviour/pop/double_back.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/identity/aves_app_bar.dart';
import 'package:aves/widgets/common/search/delegate.dart';
import 'package:aves/widgets/common/search/route.dart';
@ -29,6 +30,7 @@ class SearchPage extends StatefulWidget {
class _SearchPageState extends State<SearchPage> {
final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay);
final FocusNode _focusNode = FocusNode();
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override
void initState() {
@ -52,6 +54,7 @@ class _SearchPageState extends State<SearchPage> {
_unregisterWidget(widget);
widget.animation.removeStatusListener(_onAnimationStatusChanged);
_focusNode.dispose();
_doubleBackPopHandler.dispose();
super.dispose();
}
@ -147,12 +150,14 @@ class _SearchPageState extends State<SearchPage> {
),
actions: widget.delegate.buildActions(context),
),
body: TvPopScope(
child: DoubleBackPopScope(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: body,
),
body: AvesPopScope(
handlers: [
TvNavigationPopHandler.pop,
_doubleBackPopHandler.pop,
],
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: body,
),
),
);

View file

@ -9,7 +9,8 @@ import 'package:aves/services/analysis_service.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/behaviour/tv_pop.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/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/debug/android_apps.dart';
import 'package:aves/widgets/debug/android_codecs.dart';
@ -41,35 +42,36 @@ class _AppDebugPageState extends State<AppDebugPage> {
@override
Widget build(BuildContext context) {
return TvPopScope(
child: Directionality(
textDirection: TextDirection.ltr,
child: Scaffold(
appBar: AppBar(
title: const Text('Debug'),
actions: [
MenuIconTheme(
child: PopupMenuButton<AppDebugAction>(
// key is expected by test driver
key: const Key('appbar-menu-button'),
itemBuilder: (context) => AppDebugAction.values
.map((v) => PopupMenuItem(
// key is expected by test driver
key: Key('menu-${v.name}'),
value: v,
child: MenuRow(text: v.name),
))
.toList(),
onSelected: (action) async {
// wait for the popup menu to hide before proceeding with the action
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
unawaited(_onActionSelected(action));
},
),
return Directionality(
textDirection: TextDirection.ltr,
child: Scaffold(
appBar: AppBar(
title: const Text('Debug'),
actions: [
MenuIconTheme(
child: PopupMenuButton<AppDebugAction>(
// key is expected by test driver
key: const Key('appbar-menu-button'),
itemBuilder: (context) => AppDebugAction.values
.map((v) => PopupMenuItem(
// key is expected by test driver
key: Key('menu-${v.name}'),
value: v,
child: MenuRow(text: v.name),
))
.toList(),
onSelected: (action) async {
// wait for the popup menu to hide before proceeding with the action
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
unawaited(_onActionSelected(action));
},
),
],
),
body: SafeArea(
),
],
),
body: AvesPopScope(
handlers: const [TvNavigationPopHandler.pop],
child: SafeArea(
child: ListView(
padding: const EdgeInsets.all(8),
children: [

View file

@ -306,7 +306,7 @@ class _TagCount extends StatelessWidget {
),
child: Text(
'$count',
style: TextStyle(fontSize: AvesFilterChip.fontSize),
style: const TextStyle(fontSize: AvesFilterChip.fontSize),
),
);
}

View file

@ -13,8 +13,9 @@ import 'package:aves/theme/colors.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
import 'package:aves/widgets/common/behaviour/tv_pop.dart';
import 'package:aves/widgets/common/behaviour/pop/double_back.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';
import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/grid/item_tracker.dart';
@ -40,7 +41,6 @@ import 'package:aves/widgets/navigation/drawer/app_drawer.dart';
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
import 'package:aves/widgets/navigation/tv_rail.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:provider/provider.dart';
@ -80,48 +80,34 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
Widget build(BuildContext context) {
final body = QueryProvider(
initialQuery: null,
child: WillPopScope(
onWillPop: () {
final selection = context.read<Selection<FilterGridItem<T>>>();
if (selection.isSelecting) {
selection.browse();
return SynchronousFuture(false);
}
return SynchronousFuture(true);
},
child: TvPopScope(
child: DoubleBackPopScope(
child: GestureAreaProtectorStack(
child: SafeArea(
top: false,
bottom: false,
child: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.padding.top,
builder: (context, mqPaddingTop, child) {
return ValueListenableBuilder<double>(
valueListenable: appBarHeightNotifier,
builder: (context, appBarHeight, child) {
return FilterGrid<T>(
// key is expected by test driver
key: const Key('filter-grid'),
settingsRouteKey: settingsRouteKey,
appBar: appBar,
appBarHeight: mqPaddingTop + appBarHeight,
sections: sections,
newFilters: newFilters,
sortFactor: sortFactor,
showHeaders: showHeaders,
selectable: selectable,
applyQuery: applyQuery,
emptyBuilder: emptyBuilder,
heroType: heroType,
);
},
);
},
),
),
),
child: GestureAreaProtectorStack(
child: SafeArea(
top: false,
bottom: false,
child: Selector<MediaQueryData, double>(
selector: (context, mq) => mq.padding.top,
builder: (context, mqPaddingTop, child) {
return ValueListenableBuilder<double>(
valueListenable: appBarHeightNotifier,
builder: (context, appBarHeight, child) {
return _FilterGrid<T>(
// key is expected by test driver
key: const Key('filter-grid'),
settingsRouteKey: settingsRouteKey,
appBar: appBar,
appBarHeight: mqPaddingTop + appBarHeight,
sections: sections,
newFilters: newFilters,
sortFactor: sortFactor,
showHeaders: showHeaders,
selectable: selectable,
applyQuery: applyQuery,
emptyBuilder: emptyBuilder,
heroType: heroType,
);
},
);
},
),
),
),
@ -170,7 +156,7 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
}
}
class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
class _FilterGrid<T extends CollectionFilter> extends StatefulWidget {
final String? settingsRouteKey;
final Widget appBar;
final double appBarHeight;
@ -182,7 +168,7 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
final Widget Function() emptyBuilder;
final HeroType heroType;
const FilterGrid({
const _FilterGrid({
super.key,
required this.settingsRouteKey,
required this.appBar,
@ -198,15 +184,17 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
});
@override
State<FilterGrid<T>> createState() => _FilterGridState<T>();
State<_FilterGrid<T>> createState() => _FilterGridState<T>();
}
class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>> {
class _FilterGridState<T extends CollectionFilter> extends State<_FilterGrid<T>> {
TileExtentController? _tileExtentController;
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
@override
void dispose() {
_tileExtentController?.dispose();
_doubleBackPopHandler.dispose();
super.dispose();
}
@ -220,19 +208,33 @@ class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>>
spacing: 8,
horizontalPadding: 2,
);
return TileExtentControllerProvider(
controller: _tileExtentController!,
child: _FilterGridContent<T>(
appBar: widget.appBar,
appBarHeight: widget.appBarHeight,
sections: widget.sections,
newFilters: widget.newFilters,
sortFactor: widget.sortFactor,
showHeaders: widget.showHeaders,
selectable: widget.selectable,
applyQuery: widget.applyQuery,
emptyBuilder: widget.emptyBuilder,
heroType: widget.heroType,
return AvesPopScope(
handlers: [
(context) {
final selection = context.read<Selection<FilterGridItem<T>>>();
if (selection.isSelecting) {
selection.browse();
return false;
}
return true;
},
TvNavigationPopHandler.pop,
_doubleBackPopHandler.pop,
],
child: TileExtentControllerProvider(
controller: _tileExtentController!,
child: _FilterGridContent<T>(
appBar: widget.appBar,
appBarHeight: widget.appBarHeight,
sections: widget.sections,
newFilters: widget.newFilters,
sortFactor: widget.sortFactor,
showHeaders: widget.showHeaders,
selectable: widget.selectable,
applyQuery: widget.applyQuery,
emptyBuilder: widget.emptyBuilder,
heroType: widget.heroType,
),
),
);
}

View file

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
@ -11,6 +12,7 @@ import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_logo.dart';
import 'package:aves/widgets/debug/app_debug_page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/navigation/drawer/app_drawer.dart';
import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart';
import 'package:aves/widgets/navigation/drawer/tile.dart';
@ -55,16 +57,7 @@ class _TvRailState extends State<TvRail> {
_scrollController = ScrollController(initialScrollOffset: controller.offset);
_scrollController.addListener(_onScrollChanged);
final focusedIndex = controller.focusedIndex;
if (focusedIndex != null) {
controller.focusedIndex = null;
WidgetsBinding.instance.addPostFrameCallback((_) {
final nodes = _focusNode.children.toList();
if (focusedIndex < nodes.length) {
nodes[focusedIndex].requestFocus();
}
});
}
WidgetsBinding.instance.addPostFrameCallback((_) => _initFocus());
}
@override
@ -85,7 +78,7 @@ class _TvRailState extends State<TvRail> {
context.l10n.appName,
style: const TextStyle(
color: Colors.white,
fontSize: 44,
fontSize: 32,
fontWeight: FontWeight.w300,
letterSpacing: 1.0,
fontFeatures: [FontFeature.enable('smcp')],
@ -94,16 +87,7 @@ class _TvRailState extends State<TvRail> {
],
);
final navEntries = <_NavEntry>[
..._buildTypeLinks(),
..._buildAlbumLinks(context),
..._buildPageLinks(context),
...[
SettingsPage.routeName,
AboutPage.routeName,
if (!kReleaseMode) AppDebugPage.routeName,
].map(_routeNavEntry),
];
final navEntries = _getNavEntries(context);
final rail = Focus(
focusNode: _focusNode,
@ -147,6 +131,35 @@ class _TvRailState extends State<TvRail> {
);
}
void _initFocus() {
var index = controller.focusedIndex ?? -1;
controller.focusedIndex = null;
if (index == -1) {
final navEntries = _getNavEntries(context);
index = navEntries.indexWhere((v) => v.isHome);
}
final nodes = _focusNode.children.toList();
if (0 <= index && index < nodes.length) {
nodes[index].requestFocus();
}
}
List<_NavEntry> _getNavEntries(BuildContext context) {
final navEntries = <_NavEntry>[
..._buildTypeLinks(),
..._buildAlbumLinks(context),
..._buildPageLinks(context),
...[
SettingsPage.routeName,
AboutPage.routeName,
if (!kReleaseMode) AppDebugPage.routeName,
].map(_routeNavEntry),
];
return navEntries;
}
List<_NavEntry> _buildTypeLinks() {
final hiddenFilters = settings.hiddenFilters;
final typeBookmarks = settings.drawerTypeBookmarks;
@ -160,6 +173,7 @@ class _TvRailState extends State<TvRail> {
return _NavEntry(
icon: DrawerFilterIcon(filter: filter),
label: DrawerFilterTitle(filter: filter),
isHome: settings.homePage == HomePageSetting.collection && filter == null,
isSelected: isSelected(),
onSelection: () => _goToCollection(context, filter),
);
@ -181,6 +195,7 @@ class _TvRailState extends State<TvRail> {
return _NavEntry(
icon: DrawerFilterIcon(filter: filter),
label: DrawerFilterTitle(filter: filter),
isHome: false,
isSelected: isSelected(),
onSelection: () => _goToCollection(context, filter),
);
@ -195,6 +210,7 @@ class _TvRailState extends State<TvRail> {
_NavEntry _routeNavEntry(String routeName) => _NavEntry(
icon: DrawerPageIcon(route: routeName),
label: DrawerPageTitle(route: routeName),
isHome: settings.homePage == HomePageSetting.albums && routeName == AlbumListPage.routeName,
isSelected: context.currentRouteName == routeName,
onSelection: () => _goTo(routeName),
);
@ -226,14 +242,14 @@ class _TvRailState extends State<TvRail> {
@immutable
class _NavEntry {
final Widget icon;
final Widget label;
final bool isSelected;
final Widget icon, label;
final bool isHome, isSelected;
final VoidCallback onSelection;
const _NavEntry({
required this.icon,
required this.label,
required this.isHome,
required this.isSelected,
required this.onSelection,
});

View file

@ -13,7 +13,8 @@ import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/behaviour/tv_pop.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';
import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/search/route.dart';
@ -74,7 +75,8 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
if (device.isTelevision) {
return Scaffold(
body: TvPopScope(
body: AvesPopScope(
handlers: const [TvNavigationPopHandler.pop],
child: Row(
children: [
TvRail(

View file

@ -216,7 +216,7 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
valueListenable: _isImageFocusedNotifier,
builder: (context, isImageFocused, child) {
return AnimatedScale(
scale: isImageFocused ? 1 : .7,
scale: isImageFocused ? 1 : .6,
curve: Curves.fastOutSlowIn,
duration: context.select<DurationsData, Duration>((v) => v.tvImageFocusAnimation),
child: child!,