#437 tv: fixes
This commit is contained in:
parent
35835cdbd8
commit
fbc10cc6a8
18 changed files with 292 additions and 269 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
34
lib/widgets/common/behaviour/pop/double_back.dart
Normal file
34
lib/widgets/common/behaviour/pop/double_back.dart
Normal 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();
|
||||
}
|
||||
}
|
23
lib/widgets/common/behaviour/pop/scope.dart
Normal file
23
lib/widgets/common/behaviour/pop/scope.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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(
|
|
@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -306,7 +306,7 @@ class _TagCount extends StatelessWidget {
|
|||
),
|
||||
child: Text(
|
||||
'$count',
|
||||
style: TextStyle(fontSize: AvesFilterChip.fontSize),
|
||||
style: const TextStyle(fontSize: AvesFilterChip.fontSize),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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!,
|
||||
|
|
Loading…
Reference in a new issue