#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
|
- Map: edit cluster location
|
||||||
- Accessibility: optional alternative to pinch-to-zoom thumbnails
|
- Accessibility: optional alternative to pinch-to-zoom thumbnails
|
||||||
- Lithuanian translation (thanks Gediminas Murauskas)
|
- 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)
|
- Chinese (Traditional) translation (thanks pemibe)
|
||||||
- Ukrainian translation (thanks Olexandr Mazur)
|
- 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/favourite.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/mime.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/defaults.dart';
|
||||||
import 'package:aves/model/settings/enums/enums.dart';
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
import 'package:aves/model/settings/enums/map_style.dart';
|
import 'package:aves/model/settings/enums/map_style.dart';
|
||||||
|
@ -250,7 +249,6 @@ class Settings extends ChangeNotifier {
|
||||||
null,
|
null,
|
||||||
MimeFilter.video,
|
MimeFilter.video,
|
||||||
FavouriteFilter.instance,
|
FavouriteFilter.instance,
|
||||||
RecentlyAddedFilter.instance,
|
|
||||||
];
|
];
|
||||||
drawerPageBookmarks = [
|
drawerPageBookmarks = [
|
||||||
AlbumListPage.routeName,
|
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/licenses.dart';
|
||||||
import 'package:aves/widgets/about/translators.dart';
|
import 'package:aves/widgets/about/translators.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.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/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/navigation/tv_rail.dart';
|
import 'package:aves/widgets/navigation/tv_rail.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -47,7 +48,8 @@ class AboutPage extends StatelessWidget {
|
||||||
|
|
||||||
if (device.isTelevision) {
|
if (device.isTelevision) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: TvPopScope(
|
body: AvesPopScope(
|
||||||
|
handlers: const [TvNavigationPopHandler.pop],
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
TvRail(
|
TvRail(
|
||||||
|
|
|
@ -140,7 +140,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
// - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28
|
// - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28
|
||||||
// - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0)
|
// - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0)
|
||||||
final ValueNotifier<PageTransitionsBuilder> _pageTransitionsBuilderNotifier = ValueNotifier(const FadeUpwardsPageTransitionsBuilder());
|
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);
|
final ValueNotifier<AppMode> _appModeNotifier = ValueNotifier(AppMode.main);
|
||||||
|
|
||||||
// observers are not registered when using the same list object with different items
|
// 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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_pageTransitionsBuilderNotifier.dispose();
|
_pageTransitionsBuilderNotifier.dispose();
|
||||||
_navigationModeNotifier.dispose();
|
_tvMediaQueryModifierNotifier.dispose();
|
||||||
_appModeNotifier.dispose();
|
_appModeNotifier.dispose();
|
||||||
_subscriptions
|
_subscriptions
|
||||||
..forEach((sub) => sub.cancel())
|
..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`
|
// 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
|
// but we need to also check the non-standard Samsung field `bf` representing the bold font toggle
|
||||||
final shouldUseBoldFont = snapshot.data ?? false;
|
final shouldUseBoldFont = snapshot.data ?? false;
|
||||||
return ValueListenableBuilder<NavigationMode>(
|
final mq = MediaQuery.of(context).copyWith(
|
||||||
valueListenable: _navigationModeNotifier,
|
boldText: shouldUseBoldFont,
|
||||||
builder: (context, navigationMode, child) {
|
);
|
||||||
|
return ValueListenableBuilder<TvMediaQueryModifier?>(
|
||||||
|
valueListenable: _tvMediaQueryModifierNotifier,
|
||||||
|
builder: (context, modifier, child) {
|
||||||
return MediaQuery(
|
return MediaQuery(
|
||||||
data: MediaQuery.of(context).copyWith(
|
data: modifier?.call(mq) ?? mq,
|
||||||
boldText: shouldUseBoldFont,
|
|
||||||
navigationMode: navigationMode,
|
|
||||||
),
|
|
||||||
child: AvesColorsProvider(
|
child: AvesColorsProvider(
|
||||||
child: ValueListenableBuilder<PageTransitionsBuilder>(
|
child: ValueListenableBuilder<PageTransitionsBuilder>(
|
||||||
valueListenable: _pageTransitionsBuilderNotifier,
|
valueListenable: _pageTransitionsBuilderNotifier,
|
||||||
|
@ -409,7 +409,10 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
await device.init();
|
await device.init();
|
||||||
if (device.isTelevision) {
|
if (device.isTelevision) {
|
||||||
_pageTransitionsBuilderNotifier.value = const TvPageTransitionsBuilder();
|
_pageTransitionsBuilderNotifier.value = const TvPageTransitionsBuilder();
|
||||||
_navigationModeNotifier.value = NavigationMode.directional;
|
_tvMediaQueryModifierNotifier.value = (mq) => mq.copyWith(
|
||||||
|
textScaleFactor: 1.1,
|
||||||
|
navigationMode: NavigationMode.directional,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await mobileServices.init();
|
await mobileServices.init();
|
||||||
await settings.init(monitorPlatformSettings: true);
|
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/collection/collection_grid.dart';
|
||||||
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.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/pop/double_back.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/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_fab.dart';
|
import 'package:aves/widgets/common/identity/aves_fab.dart';
|
||||||
import 'package:aves/widgets/common/providers/query_provider.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/nav_bar/nav_bar.dart';
|
||||||
import 'package:aves/widgets/navigation/tv_rail.dart';
|
import 'package:aves/widgets/navigation/tv_rail.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
@ -52,6 +52,7 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
late CollectionLens _collection;
|
late CollectionLens _collection;
|
||||||
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
|
final StreamController<DraggableScrollBarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
|
||||||
|
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -76,6 +77,7 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
..forEach((sub) => sub.cancel())
|
..forEach((sub) => sub.cancel())
|
||||||
..clear();
|
..clear();
|
||||||
_collection.dispose();
|
_collection.dispose();
|
||||||
|
_doubleBackPopHandler.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,17 +91,21 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
final body = QueryProvider(
|
final body = QueryProvider(
|
||||||
initialQuery: liveFilter?.query,
|
initialQuery: liveFilter?.query,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) => WillPopScope(
|
builder: (context) {
|
||||||
onWillPop: () {
|
return AvesPopScope(
|
||||||
final selection = context.read<Selection<AvesEntry>>();
|
handlers: [
|
||||||
if (selection.isSelecting) {
|
(context) {
|
||||||
selection.browse();
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
return SynchronousFuture(false);
|
if (selection.isSelecting) {
|
||||||
}
|
selection.browse();
|
||||||
return SynchronousFuture(true);
|
return false;
|
||||||
},
|
}
|
||||||
child: const DoubleBackPopScope(
|
return true;
|
||||||
child: GestureAreaProtectorStack(
|
},
|
||||||
|
TvNavigationPopHandler.pop,
|
||||||
|
_doubleBackPopHandler.pop,
|
||||||
|
],
|
||||||
|
child: const GestureAreaProtectorStack(
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
bottom: false,
|
bottom: false,
|
||||||
|
@ -110,27 +116,25 @@ class _CollectionPageState extends State<CollectionPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget page;
|
Widget page;
|
||||||
if (device.isTelevision) {
|
if (device.isTelevision) {
|
||||||
page = TvPopScope(
|
page = Scaffold(
|
||||||
child: Scaffold(
|
body: Row(
|
||||||
body: Row(
|
children: [
|
||||||
children: [
|
TvRail(
|
||||||
TvRail(
|
controller: context.read<TvRailController>(),
|
||||||
controller: context.read<TvRailController>(),
|
currentCollection: _collection,
|
||||||
currentCollection: _collection,
|
),
|
||||||
),
|
Expanded(child: body),
|
||||||
Expanded(child: body),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
resizeToAvoidBottomInset: false,
|
|
||||||
extendBody: true,
|
|
||||||
),
|
),
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
extendBody: true,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
page = Selector<Settings, bool>(
|
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/collection/collection_page.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
|
// address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality
|
||||||
class TvPopScope extends StatelessWidget {
|
class TvNavigationPopHandler {
|
||||||
final Widget child;
|
static bool pop(BuildContext context) {
|
||||||
|
if (!device.isTelevision || _isHome(context)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const TvPopScope({
|
Navigator.pushAndRemoveUntil(
|
||||||
super.key,
|
context,
|
||||||
required this.child,
|
_getHomeRoute(),
|
||||||
});
|
(route) => false,
|
||||||
|
|
||||||
@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,
|
|
||||||
);
|
);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isHome(BuildContext context) {
|
static bool _isHome(BuildContext context) {
|
||||||
final homePage = settings.homePage;
|
final homePage = settings.homePage;
|
||||||
final currentRoute = context.currentRouteName;
|
final currentRoute = context.currentRouteName;
|
||||||
|
|
||||||
|
@ -53,7 +39,7 @@ class TvPopScope extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Route _getHomeRoute() {
|
static Route _getHomeRoute() {
|
||||||
switch (settings.homePage) {
|
switch (settings.homePage) {
|
||||||
case HomePageSetting.collection:
|
case HomePageSetting.collection:
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
|
@ -42,7 +42,6 @@ class AvesAppBar extends StatelessWidget {
|
||||||
child: AvesFloatingBar(
|
child: AvesFloatingBar(
|
||||||
builder: (context, backgroundColor, child) => Material(
|
builder: (context, backgroundColor, child) => Material(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
textStyle: Theme.of(context).appBarTheme.titleTextStyle,
|
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -63,18 +62,21 @@ class AvesAppBar extends StatelessWidget {
|
||||||
)
|
)
|
||||||
: const SizedBox(width: 16),
|
: const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Hero(
|
child: DefaultTextStyle(
|
||||||
tag: titleHeroTag,
|
style: Theme.of(context).appBarTheme.titleTextStyle!,
|
||||||
flightShuttleBuilder: _flightShuttleBuilder,
|
child: Hero(
|
||||||
transitionOnUserGestures: true,
|
tag: titleHeroTag,
|
||||||
child: AnimatedSwitcher(
|
flightShuttleBuilder: _flightShuttleBuilder,
|
||||||
duration: context.read<DurationsData>().iconAnimation,
|
transitionOnUserGestures: true,
|
||||||
child: Row(
|
child: AnimatedSwitcher(
|
||||||
key: ValueKey(transitionKey),
|
duration: context.read<DurationsData>().iconAnimation,
|
||||||
children: [
|
child: Row(
|
||||||
Expanded(child: title),
|
key: ValueKey(transitionKey),
|
||||||
...actions,
|
children: [
|
||||||
],
|
Expanded(child: title),
|
||||||
|
...actions,
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -4,7 +4,6 @@ import 'dart:math';
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/actions/chip_actions.dart';
|
import 'package:aves/model/actions/chip_actions.dart';
|
||||||
import 'package:aves/model/covers.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/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
|
@ -60,10 +59,9 @@ class AvesFilterChip extends StatefulWidget {
|
||||||
static const double minChipHeight = kMinInteractiveDimension;
|
static const double minChipHeight = kMinInteractiveDimension;
|
||||||
static const double minChipWidth = 80;
|
static const double minChipWidth = 80;
|
||||||
static const double iconSize = 18;
|
static const double iconSize = 18;
|
||||||
|
static const double fontSize = 14;
|
||||||
static const double decoratedContentVerticalPadding = 5;
|
static const double decoratedContentVerticalPadding = 5;
|
||||||
|
|
||||||
static double get fontSize => device.isTelevision ? 18 : 14;
|
|
||||||
|
|
||||||
const AvesFilterChip({
|
const AvesFilterChip({
|
||||||
super.key,
|
super.key,
|
||||||
required this.filter,
|
required this.filter,
|
||||||
|
|
|
@ -2,8 +2,9 @@ import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/utils/debouncer.dart';
|
import 'package:aves/utils/debouncer.dart';
|
||||||
import 'package:aves/widgets/common/behaviour/double_back_pop.dart';
|
import 'package:aves/widgets/common/behaviour/pop/double_back.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_app_bar.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/delegate.dart';
|
||||||
import 'package:aves/widgets/common/search/route.dart';
|
import 'package:aves/widgets/common/search/route.dart';
|
||||||
|
@ -29,6 +30,7 @@ class SearchPage extends StatefulWidget {
|
||||||
class _SearchPageState extends State<SearchPage> {
|
class _SearchPageState extends State<SearchPage> {
|
||||||
final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay);
|
final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay);
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
|
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -52,6 +54,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||||
_unregisterWidget(widget);
|
_unregisterWidget(widget);
|
||||||
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
widget.animation.removeStatusListener(_onAnimationStatusChanged);
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
|
_doubleBackPopHandler.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,12 +150,14 @@ class _SearchPageState extends State<SearchPage> {
|
||||||
),
|
),
|
||||||
actions: widget.delegate.buildActions(context),
|
actions: widget.delegate.buildActions(context),
|
||||||
),
|
),
|
||||||
body: TvPopScope(
|
body: AvesPopScope(
|
||||||
child: DoubleBackPopScope(
|
handlers: [
|
||||||
child: AnimatedSwitcher(
|
TvNavigationPopHandler.pop,
|
||||||
duration: const Duration(milliseconds: 300),
|
_doubleBackPopHandler.pop,
|
||||||
child: body,
|
],
|
||||||
),
|
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/theme/durations.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.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/common/identity/aves_expansion_tile.dart';
|
||||||
import 'package:aves/widgets/debug/android_apps.dart';
|
import 'package:aves/widgets/debug/android_apps.dart';
|
||||||
import 'package:aves/widgets/debug/android_codecs.dart';
|
import 'package:aves/widgets/debug/android_codecs.dart';
|
||||||
|
@ -41,35 +42,36 @@ class _AppDebugPageState extends State<AppDebugPage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TvPopScope(
|
return Directionality(
|
||||||
child: Directionality(
|
textDirection: TextDirection.ltr,
|
||||||
textDirection: TextDirection.ltr,
|
child: Scaffold(
|
||||||
child: Scaffold(
|
appBar: AppBar(
|
||||||
appBar: AppBar(
|
title: const Text('Debug'),
|
||||||
title: const Text('Debug'),
|
actions: [
|
||||||
actions: [
|
MenuIconTheme(
|
||||||
MenuIconTheme(
|
child: PopupMenuButton<AppDebugAction>(
|
||||||
child: PopupMenuButton<AppDebugAction>(
|
// key is expected by test driver
|
||||||
// key is expected by test driver
|
key: const Key('appbar-menu-button'),
|
||||||
key: const Key('appbar-menu-button'),
|
itemBuilder: (context) => AppDebugAction.values
|
||||||
itemBuilder: (context) => AppDebugAction.values
|
.map((v) => PopupMenuItem(
|
||||||
.map((v) => PopupMenuItem(
|
// key is expected by test driver
|
||||||
// key is expected by test driver
|
key: Key('menu-${v.name}'),
|
||||||
key: Key('menu-${v.name}'),
|
value: v,
|
||||||
value: v,
|
child: MenuRow(text: v.name),
|
||||||
child: MenuRow(text: v.name),
|
))
|
||||||
))
|
.toList(),
|
||||||
.toList(),
|
onSelected: (action) async {
|
||||||
onSelected: (action) async {
|
// wait for the popup menu to hide before proceeding with the action
|
||||||
// wait for the popup menu to hide before proceeding with the action
|
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
||||||
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
unawaited(_onActionSelected(action));
|
||||||
unawaited(_onActionSelected(action));
|
},
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
body: SafeArea(
|
),
|
||||||
|
body: AvesPopScope(
|
||||||
|
handlers: const [TvNavigationPopHandler.pop],
|
||||||
|
child: SafeArea(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
children: [
|
children: [
|
||||||
|
|
|
@ -306,7 +306,7 @@ class _TagCount extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'$count',
|
'$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/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.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/pop/double_back.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/build_context.dart';
|
||||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||||
import 'package:aves/widgets/common/grid/item_tracker.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/nav_bar/nav_bar.dart';
|
||||||
import 'package:aves/widgets/navigation/tv_rail.dart';
|
import 'package:aves/widgets/navigation/tv_rail.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -80,48 +80,34 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final body = QueryProvider(
|
final body = QueryProvider(
|
||||||
initialQuery: null,
|
initialQuery: null,
|
||||||
child: WillPopScope(
|
child: GestureAreaProtectorStack(
|
||||||
onWillPop: () {
|
child: SafeArea(
|
||||||
final selection = context.read<Selection<FilterGridItem<T>>>();
|
top: false,
|
||||||
if (selection.isSelecting) {
|
bottom: false,
|
||||||
selection.browse();
|
child: Selector<MediaQueryData, double>(
|
||||||
return SynchronousFuture(false);
|
selector: (context, mq) => mq.padding.top,
|
||||||
}
|
builder: (context, mqPaddingTop, child) {
|
||||||
return SynchronousFuture(true);
|
return ValueListenableBuilder<double>(
|
||||||
},
|
valueListenable: appBarHeightNotifier,
|
||||||
child: TvPopScope(
|
builder: (context, appBarHeight, child) {
|
||||||
child: DoubleBackPopScope(
|
return _FilterGrid<T>(
|
||||||
child: GestureAreaProtectorStack(
|
// key is expected by test driver
|
||||||
child: SafeArea(
|
key: const Key('filter-grid'),
|
||||||
top: false,
|
settingsRouteKey: settingsRouteKey,
|
||||||
bottom: false,
|
appBar: appBar,
|
||||||
child: Selector<MediaQueryData, double>(
|
appBarHeight: mqPaddingTop + appBarHeight,
|
||||||
selector: (context, mq) => mq.padding.top,
|
sections: sections,
|
||||||
builder: (context, mqPaddingTop, child) {
|
newFilters: newFilters,
|
||||||
return ValueListenableBuilder<double>(
|
sortFactor: sortFactor,
|
||||||
valueListenable: appBarHeightNotifier,
|
showHeaders: showHeaders,
|
||||||
builder: (context, appBarHeight, child) {
|
selectable: selectable,
|
||||||
return FilterGrid<T>(
|
applyQuery: applyQuery,
|
||||||
// key is expected by test driver
|
emptyBuilder: emptyBuilder,
|
||||||
key: const Key('filter-grid'),
|
heroType: heroType,
|
||||||
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 String? settingsRouteKey;
|
||||||
final Widget appBar;
|
final Widget appBar;
|
||||||
final double appBarHeight;
|
final double appBarHeight;
|
||||||
|
@ -182,7 +168,7 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
|
||||||
final Widget Function() emptyBuilder;
|
final Widget Function() emptyBuilder;
|
||||||
final HeroType heroType;
|
final HeroType heroType;
|
||||||
|
|
||||||
const FilterGrid({
|
const _FilterGrid({
|
||||||
super.key,
|
super.key,
|
||||||
required this.settingsRouteKey,
|
required this.settingsRouteKey,
|
||||||
required this.appBar,
|
required this.appBar,
|
||||||
|
@ -198,15 +184,17 @@ class FilterGrid<T extends CollectionFilter> extends StatefulWidget {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@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;
|
TileExtentController? _tileExtentController;
|
||||||
|
final DoubleBackPopHandler _doubleBackPopHandler = DoubleBackPopHandler();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tileExtentController?.dispose();
|
_tileExtentController?.dispose();
|
||||||
|
_doubleBackPopHandler.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,19 +208,33 @@ class _FilterGridState<T extends CollectionFilter> extends State<FilterGrid<T>>
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
horizontalPadding: 2,
|
horizontalPadding: 2,
|
||||||
);
|
);
|
||||||
return TileExtentControllerProvider(
|
return AvesPopScope(
|
||||||
controller: _tileExtentController!,
|
handlers: [
|
||||||
child: _FilterGridContent<T>(
|
(context) {
|
||||||
appBar: widget.appBar,
|
final selection = context.read<Selection<FilterGridItem<T>>>();
|
||||||
appBarHeight: widget.appBarHeight,
|
if (selection.isSelecting) {
|
||||||
sections: widget.sections,
|
selection.browse();
|
||||||
newFilters: widget.newFilters,
|
return false;
|
||||||
sortFactor: widget.sortFactor,
|
}
|
||||||
showHeaders: widget.showHeaders,
|
return true;
|
||||||
selectable: widget.selectable,
|
},
|
||||||
applyQuery: widget.applyQuery,
|
TvNavigationPopHandler.pop,
|
||||||
emptyBuilder: widget.emptyBuilder,
|
_doubleBackPopHandler.pop,
|
||||||
heroType: widget.heroType,
|
],
|
||||||
|
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/album.dart';
|
||||||
import 'package:aves/model/filters/filters.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/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.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/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_logo.dart';
|
import 'package:aves/widgets/common/identity/aves_logo.dart';
|
||||||
import 'package:aves/widgets/debug/app_debug_page.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/app_drawer.dart';
|
||||||
import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart';
|
import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart';
|
||||||
import 'package:aves/widgets/navigation/drawer/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 = ScrollController(initialScrollOffset: controller.offset);
|
||||||
_scrollController.addListener(_onScrollChanged);
|
_scrollController.addListener(_onScrollChanged);
|
||||||
|
|
||||||
final focusedIndex = controller.focusedIndex;
|
WidgetsBinding.instance.addPostFrameCallback((_) => _initFocus());
|
||||||
if (focusedIndex != null) {
|
|
||||||
controller.focusedIndex = null;
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
final nodes = _focusNode.children.toList();
|
|
||||||
if (focusedIndex < nodes.length) {
|
|
||||||
nodes[focusedIndex].requestFocus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -85,7 +78,7 @@ class _TvRailState extends State<TvRail> {
|
||||||
context.l10n.appName,
|
context.l10n.appName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 44,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.w300,
|
fontWeight: FontWeight.w300,
|
||||||
letterSpacing: 1.0,
|
letterSpacing: 1.0,
|
||||||
fontFeatures: [FontFeature.enable('smcp')],
|
fontFeatures: [FontFeature.enable('smcp')],
|
||||||
|
@ -94,16 +87,7 @@ class _TvRailState extends State<TvRail> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
final navEntries = <_NavEntry>[
|
final navEntries = _getNavEntries(context);
|
||||||
..._buildTypeLinks(),
|
|
||||||
..._buildAlbumLinks(context),
|
|
||||||
..._buildPageLinks(context),
|
|
||||||
...[
|
|
||||||
SettingsPage.routeName,
|
|
||||||
AboutPage.routeName,
|
|
||||||
if (!kReleaseMode) AppDebugPage.routeName,
|
|
||||||
].map(_routeNavEntry),
|
|
||||||
];
|
|
||||||
|
|
||||||
final rail = Focus(
|
final rail = Focus(
|
||||||
focusNode: _focusNode,
|
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() {
|
List<_NavEntry> _buildTypeLinks() {
|
||||||
final hiddenFilters = settings.hiddenFilters;
|
final hiddenFilters = settings.hiddenFilters;
|
||||||
final typeBookmarks = settings.drawerTypeBookmarks;
|
final typeBookmarks = settings.drawerTypeBookmarks;
|
||||||
|
@ -160,6 +173,7 @@ class _TvRailState extends State<TvRail> {
|
||||||
return _NavEntry(
|
return _NavEntry(
|
||||||
icon: DrawerFilterIcon(filter: filter),
|
icon: DrawerFilterIcon(filter: filter),
|
||||||
label: DrawerFilterTitle(filter: filter),
|
label: DrawerFilterTitle(filter: filter),
|
||||||
|
isHome: settings.homePage == HomePageSetting.collection && filter == null,
|
||||||
isSelected: isSelected(),
|
isSelected: isSelected(),
|
||||||
onSelection: () => _goToCollection(context, filter),
|
onSelection: () => _goToCollection(context, filter),
|
||||||
);
|
);
|
||||||
|
@ -181,6 +195,7 @@ class _TvRailState extends State<TvRail> {
|
||||||
return _NavEntry(
|
return _NavEntry(
|
||||||
icon: DrawerFilterIcon(filter: filter),
|
icon: DrawerFilterIcon(filter: filter),
|
||||||
label: DrawerFilterTitle(filter: filter),
|
label: DrawerFilterTitle(filter: filter),
|
||||||
|
isHome: false,
|
||||||
isSelected: isSelected(),
|
isSelected: isSelected(),
|
||||||
onSelection: () => _goToCollection(context, filter),
|
onSelection: () => _goToCollection(context, filter),
|
||||||
);
|
);
|
||||||
|
@ -195,6 +210,7 @@ class _TvRailState extends State<TvRail> {
|
||||||
_NavEntry _routeNavEntry(String routeName) => _NavEntry(
|
_NavEntry _routeNavEntry(String routeName) => _NavEntry(
|
||||||
icon: DrawerPageIcon(route: routeName),
|
icon: DrawerPageIcon(route: routeName),
|
||||||
label: DrawerPageTitle(route: routeName),
|
label: DrawerPageTitle(route: routeName),
|
||||||
|
isHome: settings.homePage == HomePageSetting.albums && routeName == AlbumListPage.routeName,
|
||||||
isSelected: context.currentRouteName == routeName,
|
isSelected: context.currentRouteName == routeName,
|
||||||
onSelection: () => _goTo(routeName),
|
onSelection: () => _goTo(routeName),
|
||||||
);
|
);
|
||||||
|
@ -226,14 +242,14 @@ class _TvRailState extends State<TvRail> {
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class _NavEntry {
|
class _NavEntry {
|
||||||
final Widget icon;
|
final Widget icon, label;
|
||||||
final Widget label;
|
final bool isHome, isSelected;
|
||||||
final bool isSelected;
|
|
||||||
final VoidCallback onSelection;
|
final VoidCallback onSelection;
|
||||||
|
|
||||||
const _NavEntry({
|
const _NavEntry({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.label,
|
required this.label,
|
||||||
|
required this.isHome,
|
||||||
required this.isSelected,
|
required this.isSelected,
|
||||||
required this.onSelection,
|
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/app_bar/app_bar_title.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.dart';
|
import 'package:aves/widgets/common/basic/insets.dart';
|
||||||
import 'package:aves/widgets/common/basic/menu.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/build_context.dart';
|
||||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||||
import 'package:aves/widgets/common/search/route.dart';
|
import 'package:aves/widgets/common/search/route.dart';
|
||||||
|
@ -74,7 +75,8 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
|
||||||
|
|
||||||
if (device.isTelevision) {
|
if (device.isTelevision) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: TvPopScope(
|
body: AvesPopScope(
|
||||||
|
handlers: const [TvNavigationPopHandler.pop],
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
TvRail(
|
TvRail(
|
||||||
|
|
|
@ -216,7 +216,7 @@ class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
|
||||||
valueListenable: _isImageFocusedNotifier,
|
valueListenable: _isImageFocusedNotifier,
|
||||||
builder: (context, isImageFocused, child) {
|
builder: (context, isImageFocused, child) {
|
||||||
return AnimatedScale(
|
return AnimatedScale(
|
||||||
scale: isImageFocused ? 1 : .7,
|
scale: isImageFocused ? 1 : .6,
|
||||||
curve: Curves.fastOutSlowIn,
|
curve: Curves.fastOutSlowIn,
|
||||||
duration: context.select<DurationsData, Duration>((v) => v.tvImageFocusAnimation),
|
duration: context.select<DurationsData, Duration>((v) => v.tvImageFocusAnimation),
|
||||||
child: child!,
|
child: child!,
|
||||||
|
|
Loading…
Reference in a new issue