#437 tv: rail nav fix
This commit is contained in:
parent
3be4f661cc
commit
207e8cd545
7 changed files with 185 additions and 123 deletions
|
@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
Loading…
Reference in a new issue