#437 tv: rail nav fix

This commit is contained in:
Thibault Deckers 2022-12-14 19:24:56 +01:00
parent 3be4f661cc
commit 207e8cd545
7 changed files with 185 additions and 123 deletions

View file

@ -8,6 +8,7 @@ import 'package:aves/widgets/common/basic/insets.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';
import 'package:provider/provider.dart';
class AboutPage extends StatelessWidget { class AboutPage extends StatelessWidget {
static const routeName = '/about'; static const routeName = '/about';
@ -47,7 +48,9 @@ class AboutPage extends StatelessWidget {
return Scaffold( return Scaffold(
body: Row( body: Row(
children: [ children: [
const TvRail(), TvRail(
controller: context.read<TvRailController>(),
),
Expanded(child: body), Expanded(child: body),
], ],
), ),

View file

@ -35,6 +35,7 @@ import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/navigation/tv_page_transitions.dart'; import 'package:aves/widgets/navigation/tv_page_transitions.dart';
import 'package:aves/widgets/navigation/tv_rail.dart';
import 'package:aves/widgets/welcome_page.dart'; import 'package:aves/widgets/welcome_page.dart';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
@ -126,6 +127,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
late final Future<void> _appSetup; late final Future<void> _appSetup;
late final Future<bool> _shouldUseBoldFontLoader; late final Future<bool> _shouldUseBoldFontLoader;
late final Future<CorePalette?> _dynamicColorPaletteLoader; late final Future<CorePalette?> _dynamicColorPaletteLoader;
final TvRailController _tvRailController = TvRailController();
final CollectionSource _mediaStoreSource = MediaStoreSource(); final CollectionSource _mediaStoreSource = MediaStoreSource();
final Debouncer _mediaStoreChangeDebouncer = Debouncer(delay: Durations.mediaContentChangeDebounceDelay); final Debouncer _mediaStoreChangeDebouncer = Debouncer(delay: Durations.mediaContentChangeDebounceDelay);
final Set<String> _changedUris = {}; final Set<String> _changedUris = {};
@ -173,109 +175,112 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
value: appModeNotifier, value: appModeNotifier,
child: Provider<CollectionSource>.value( child: Provider<CollectionSource>.value(
value: _mediaStoreSource, value: _mediaStoreSource,
child: DurationsProvider( child: Provider<TvRailController>.value(
child: HighlightInfoProvider( value: _tvRailController,
child: OverlaySupport( child: DurationsProvider(
child: FutureBuilder<void>( child: HighlightInfoProvider(
future: _appSetup, child: OverlaySupport(
builder: (context, snapshot) { child: FutureBuilder<void>(
final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done; future: _appSetup,
if (initialized) { builder: (context, snapshot) {
AvesApp.showSystemUI(); final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done;
} if (initialized) {
final home = initialized AvesApp.showSystemUI();
? _getFirstPage() }
: Scaffold( final home = initialized
body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(), ? _getFirstPage()
); : Scaffold(
return Selector<Settings, Tuple4<Locale?, bool, AvesThemeBrightness, bool>>( body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(),
selector: (context, s) => Tuple4(
s.locale,
s.initialized ? s.accessibilityAnimations.animate : true,
s.initialized ? s.themeBrightness : SettingsDefaults.themeBrightness,
s.initialized ? s.enableDynamicColor : SettingsDefaults.enableDynamicColor,
),
builder: (context, s, child) {
final settingsLocale = s.item1;
final areAnimationsEnabled = s.item2;
final themeBrightness = s.item3;
final enableDynamicColor = s.item4;
Constants.updateStylesForLocale(settings.appliedLocale);
return FutureBuilder<CorePalette?>(
future: _dynamicColorPaletteLoader,
builder: (context, snapshot) {
const defaultAccent = Themes.defaultAccent;
Color lightAccent = defaultAccent, darkAccent = defaultAccent;
if (enableDynamicColor) {
// `DynamicColorBuilder` from package `dynamic_color` provides light/dark
// palettes with a primary color from tones too dark/light (40/80),
// so we derive the color with adjusted tones (60/70)
final tonalPalette = snapshot.data?.primary;
lightAccent = Color(tonalPalette?.get(60) ?? defaultAccent.value);
darkAccent = Color(tonalPalette?.get(70) ?? defaultAccent.value);
}
final lightTheme = Themes.lightTheme(lightAccent, initialized);
final darkTheme = themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized);
return FutureBuilder<bool>(
future: _shouldUseBoldFontLoader,
builder: (context, snapshot) {
// 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 Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
// handle Android TV remote `select` button
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
},
child: MaterialApp(
navigatorKey: AvesApp.navigatorKey,
home: home,
navigatorObservers: _navigatorObservers,
builder: (context, child) {
if (initialized) {
WidgetsBinding.instance.addPostFrameCallback((_) => AvesApp.setSystemUIStyle(context));
}
return MediaQuery(
data: MediaQuery.of(context).copyWith(boldText: shouldUseBoldFont),
child: AvesColorsProvider(
child: ValueListenableBuilder<PageTransitionsBuilder>(
valueListenable: _pageTransitionsBuilderNotifier,
builder: (context, pageTransitionsBuilder, child) {
return Theme(
data: Theme.of(context).copyWith(
pageTransitionsTheme: areAnimationsEnabled
? PageTransitionsTheme(builders: {TargetPlatform.android: pageTransitionsBuilder})
// strip page transitions used by `MaterialPageRoute`
: const DirectPageTransitionsTheme(),
),
child: MediaQueryDataProvider(child: child!),
);
},
child: child,
),
),
);
},
onGenerateTitle: (context) => context.l10n.appName,
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeBrightness.appThemeMode,
locale: settingsLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AvesApp.supportedLocales,
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
scrollBehavior: StretchMaterialScrollBehavior(),
),
);
},
); );
}, return Selector<Settings, Tuple4<Locale?, bool, AvesThemeBrightness, bool>>(
); selector: (context, s) => Tuple4(
}, s.locale,
); s.initialized ? s.accessibilityAnimations.animate : true,
}, s.initialized ? s.themeBrightness : SettingsDefaults.themeBrightness,
s.initialized ? s.enableDynamicColor : SettingsDefaults.enableDynamicColor,
),
builder: (context, s, child) {
final settingsLocale = s.item1;
final areAnimationsEnabled = s.item2;
final themeBrightness = s.item3;
final enableDynamicColor = s.item4;
Constants.updateStylesForLocale(settings.appliedLocale);
return FutureBuilder<CorePalette?>(
future: _dynamicColorPaletteLoader,
builder: (context, snapshot) {
const defaultAccent = Themes.defaultAccent;
Color lightAccent = defaultAccent, darkAccent = defaultAccent;
if (enableDynamicColor) {
// `DynamicColorBuilder` from package `dynamic_color` provides light/dark
// palettes with a primary color from tones too dark/light (40/80),
// so we derive the color with adjusted tones (60/70)
final tonalPalette = snapshot.data?.primary;
lightAccent = Color(tonalPalette?.get(60) ?? defaultAccent.value);
darkAccent = Color(tonalPalette?.get(70) ?? defaultAccent.value);
}
final lightTheme = Themes.lightTheme(lightAccent, initialized);
final darkTheme = themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme(darkAccent, initialized) : Themes.darkTheme(darkAccent, initialized);
return FutureBuilder<bool>(
future: _shouldUseBoldFontLoader,
builder: (context, snapshot) {
// 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 Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
// handle Android TV remote `select` button
LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
},
child: MaterialApp(
navigatorKey: AvesApp.navigatorKey,
home: home,
navigatorObservers: _navigatorObservers,
builder: (context, child) {
if (initialized) {
WidgetsBinding.instance.addPostFrameCallback((_) => AvesApp.setSystemUIStyle(context));
}
return MediaQuery(
data: MediaQuery.of(context).copyWith(boldText: shouldUseBoldFont),
child: AvesColorsProvider(
child: ValueListenableBuilder<PageTransitionsBuilder>(
valueListenable: _pageTransitionsBuilderNotifier,
builder: (context, pageTransitionsBuilder, child) {
return Theme(
data: Theme.of(context).copyWith(
pageTransitionsTheme: areAnimationsEnabled
? PageTransitionsTheme(builders: {TargetPlatform.android: pageTransitionsBuilder})
// strip page transitions used by `MaterialPageRoute`
: const DirectPageTransitionsTheme(),
),
child: MediaQueryDataProvider(child: child!),
);
},
child: child,
),
),
);
},
onGenerateTitle: (context) => context.l10n.appName,
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeBrightness.appThemeMode,
locale: settingsLocale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AvesApp.supportedLocales,
// TODO TLAD remove custom scroll behavior when this is fixed: https://github.com/flutter/flutter/issues/82906
scrollBehavior: StretchMaterialScrollBehavior(),
),
);
},
);
},
);
},
);
},
),
), ),
), ),
), ),

View file

@ -121,7 +121,10 @@ class _CollectionPageState extends State<CollectionPage> {
return Scaffold( return Scaffold(
body: Row( body: Row(
children: [ children: [
TvRail(currentCollection: _collection), TvRail(
controller: context.read<TvRailController>(),
currentCollection: _collection,
),
Expanded(child: body), Expanded(child: body),
], ],
), ),

View file

@ -128,7 +128,9 @@ class FilterGridPage<T extends CollectionFilter> extends StatelessWidget {
return Scaffold( return Scaffold(
body: Row( body: Row(
children: [ children: [
const TvRail(), TvRail(
controller: context.read<TvRailController>(),
),
Expanded(child: body), Expanded(child: body),
], ],
), ),

View file

@ -20,12 +20,19 @@ 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';
class TvRailController {
int? focusedIndex;
double offset = 0;
}
class TvRail extends StatefulWidget { class TvRail extends StatefulWidget {
// collection loaded in the `CollectionPage`, if any // collection loaded in the `CollectionPage`, if any
final CollectionLens? currentCollection; final CollectionLens? currentCollection;
final TvRailController controller;
const TvRail({ const TvRail({
super.key, super.key,
required this.controller,
this.currentCollection, this.currentCollection,
}); });
@ -34,10 +41,41 @@ class TvRail extends StatefulWidget {
} }
class _TvRailState extends State<TvRail> { class _TvRailState extends State<TvRail> {
final _scrollController = ScrollController(); late final ScrollController _scrollController;
final FocusNode _focusNode = FocusNode();
TvRailController get controller => widget.controller;
CollectionLens? get currentCollection => widget.currentCollection; CollectionLens? get currentCollection => widget.currentCollection;
@override
void initState() {
super.initState();
_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();
debugPrint('TLAD focusedIndex=$focusedIndex < nodes.length=${nodes.length}');
if (focusedIndex < nodes.length) {
nodes[focusedIndex].requestFocus();
}
});
}
}
@override
void dispose() {
_scrollController.removeListener(_onScrollChanged);
_scrollController.dispose();
_focusNode.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final header = Row( final header = Row(
@ -64,21 +102,28 @@ class _TvRailState extends State<TvRail> {
...[ ...[
SettingsPage.routeName, SettingsPage.routeName,
AboutPage.routeName, AboutPage.routeName,
if (!kReleaseMode) AppDebugPage.routeName,
].map(_routeNavEntry), ].map(_routeNavEntry),
if (!kReleaseMode) _routeNavEntry(AppDebugPage.routeName),
]; ];
final rail = NavigationRail( final rail = Focus(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, focusNode: _focusNode,
extended: true, skipTraversal: true,
destinations: navEntries child: NavigationRail(
.map((v) => NavigationRailDestination( backgroundColor: Theme.of(context).scaffoldBackgroundColor,
icon: v.icon, extended: true,
label: v.label, destinations: navEntries
)) .map((v) => NavigationRailDestination(
.toList(), icon: v.icon,
selectedIndex: max(0, navEntries.indexWhere(((v) => v.isSelected))), label: v.label,
onDestinationSelected: (index) => navEntries[index].onSelection(), ))
.toList(),
selectedIndex: max(0, navEntries.indexWhere(((v) => v.isSelected))),
onDestinationSelected: (index) {
controller.focusedIndex = index;
navEntries[index].onSelection();
},
),
); );
return Column( return Column(
@ -177,6 +222,8 @@ class _TvRailState extends State<TvRail> {
(route) => false, (route) => false,
); );
} }
void _onScrollChanged() => controller.offset = _scrollController.offset;
} }
@immutable @immutable

View file

@ -18,7 +18,7 @@ class CrumbLine extends StatefulWidget {
} }
class _CrumbLineState extends State<CrumbLine> { class _CrumbLineState extends State<CrumbLine> {
final ScrollController _controller = ScrollController(); final ScrollController _scrollController = ScrollController();
VolumeRelativeDirectory get directory => widget.directory; VolumeRelativeDirectory get directory => widget.directory;
@ -28,8 +28,8 @@ class _CrumbLineState extends State<CrumbLine> {
if (oldWidget.directory.relativeDir.length < widget.directory.relativeDir.length) { if (oldWidget.directory.relativeDir.length < widget.directory.relativeDir.length) {
// scroll to show last crumb // scroll to show last crumb
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final extent = _controller.position.maxScrollExtent; final extent = _scrollController.position.maxScrollExtent;
_controller.animateTo( _scrollController.animateTo(
extent, extent,
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
curve: Curves.easeOutQuad, curve: Curves.easeOutQuad,
@ -53,7 +53,7 @@ class _CrumbLineState extends State<CrumbLine> {
), ),
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
controller: _controller, controller: _scrollController,
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
Widget _buildText(String text) => Padding( Widget _buildText(String text) => Padding(

View file

@ -75,7 +75,9 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
return Scaffold( return Scaffold(
body: Row( body: Row(
children: [ children: [
const TvRail(), TvRail(
controller: context.read<TvRailController>(),
),
Expanded( Expanded(
child: Column( child: Column(
children: [ children: [