add navigation rail for laptop

This commit is contained in:
Pato05 2023-10-16 16:54:56 +02:00
parent 5ba7e932e3
commit 6aa596177f
No known key found for this signature in database
GPG key ID: F53CA394104BA0CB
4 changed files with 320 additions and 132 deletions

View file

@ -298,20 +298,17 @@ class _LoginMainWrapperState extends State<LoginMainWrapper> {
class MainScreen extends StatefulWidget { class MainScreen extends StatefulWidget {
const MainScreen({super.key}); const MainScreen({super.key});
static MainScreenState of(BuildContext context) =>
context.findAncestorStateOfType<MainScreenState>()!;
@override @override
State<MainScreen> createState() => _MainScreenState(); MainScreenState createState() => MainScreenState();
} }
class _MainScreenState extends State<MainScreen> class MainScreenState extends State<MainScreen>
with SingleTickerProviderStateMixin, WidgetsBindingObserver { with SingleTickerProviderStateMixin, WidgetsBindingObserver {
final _logger = Logger('_MainScreenState'); final _logger = Logger('_MainScreenState');
final _selected = ValueNotifier<int>(0); final _selected = ValueNotifier<int>(0);
final _destinations = <int, String>{
0: '/',
1: '/podcasts',
2: '/library',
3: '/search',
};
StreamSubscription? _urlLinkStream; StreamSubscription? _urlLinkStream;
int _keyPressed = 0; int _keyPressed = 0;
bool textFieldVisited = false; bool textFieldVisited = false;
@ -320,6 +317,9 @@ class _MainScreenState extends State<MainScreen>
final playerScreenFocusNode = FocusScopeNode(); final playerScreenFocusNode = FocusScopeNode();
final playerBarFocusNode = FocusNode(); final playerBarFocusNode = FocusNode();
final _fancyScaffoldKey = GlobalKey<FancyScaffoldState>(); final _fancyScaffoldKey = GlobalKey<FancyScaffoldState>();
final routeObserver = RouteObserver();
late bool _isDesktop;
@override @override
void initState() { void initState() {
@ -445,7 +445,7 @@ class _MainScreenState extends State<MainScreen>
// Movement to navigation bar and back // Movement to navigation bar and back
if ((event is RawKeyUpEvent && textFieldVisited) || if ((event is RawKeyUpEvent && textFieldVisited) ||
event is RawKeyDownEvent) { event is RawKeyDownEvent) {
// only handl if we're running on android // only handle if we're running on android
if (event.data is! RawKeyEventDataAndroid) return; if (event.data is! RawKeyEventDataAndroid) return;
int keyCode = (event.data as RawKeyEventDataAndroid).keyCode; int keyCode = (event.data as RawKeyEventDataAndroid).keyCode;
_logger.fine('KEY PRESSED: $keyCode'); _logger.fine('KEY PRESSED: $keyCode');
@ -518,108 +518,203 @@ class _MainScreenState extends State<MainScreen>
return playerBarFocusNode.requestFocus(); return playerBarFocusNode.requestFocus();
} }
final _destinationRoutes = <int, String>{
0: '/',
1: '/podcasts',
2: '/library',
3: '/search',
};
final _destinations = <NavigationDestination>[
NavigationDestination(
icon: const Icon(Icons.home_outlined),
selectedIcon: const Icon(Icons.home),
label: 'Home'.i18n),
NavigationDestination(
icon: const Icon(Icons.podcasts), label: 'Podcasts'.i18n),
NavigationDestination(
icon: const Icon(Icons.library_music_outlined),
selectedIcon: const Icon(Icons.library_music),
label: 'Library'.i18n),
NavigationDestination(icon: const Icon(Icons.search), label: 'Search'.i18n),
];
final _navigationRailDestinationRoutes = <int, String>{
0: '/',
1: '/podcasts',
2: '/library/tracks',
3: '/library/albums',
4: '/library/artists',
5: '/library/playlists',
6: '/library/history',
7: '/downloads',
8: '/settings',
};
final _navigationRailDestinations = <NavigationRailDestination>[
NavigationRailDestination(
icon: const Icon(Icons.home_outlined),
selectedIcon: const Icon(Icons.home),
label: Text('Home'.i18n),
),
NavigationRailDestination(
icon: const Icon(Icons.podcasts),
label: Text('Podcasts'.i18n),
),
NavigationRailDestination(
icon: const Icon(Icons.audiotrack_outlined),
selectedIcon: const Icon(Icons.audiotrack),
label: Text('Tracks'.i18n),
),
NavigationRailDestination(
icon: const Icon(Icons.album_outlined),
selectedIcon: const Icon(Icons.album),
label: Text('Albums'.i18n),
),
NavigationRailDestination(
icon: const Icon(Icons.recent_actors_outlined),
selectedIcon: const Icon(Icons.recent_actors),
label: Text('Artists'.i18n),
),
NavigationRailDestination(
icon: const Icon(Icons.playlist_play),
label: Text('Playlists'.i18n),
),
NavigationRailDestination(
icon: const Icon(Icons.history),
label: Text('History'.i18n),
),
NavigationRailDestination(
icon: const Icon(Icons.download_outlined),
selectedIcon: const Icon(Icons.download),
label: Text('Downloads'.i18n),
),
NavigationRailDestination(
icon: const Icon(Icons.settings_outlined),
selectedIcon: const Icon(Icons.settings),
label: Text('Settings'.i18n),
),
];
void _onDestinationSelected(int s,
{bool useNavigationRailDestinations = false}) {
//Pop all routes until home screen
navigatorKey.currentState!.popUntil((route) => route.isFirst);
navigatorKey.currentState!.pushReplacementNamed(
useNavigationRailDestinations
? _navigationRailDestinationRoutes[s]!
: _destinationRoutes[s]!,
arguments: true);
if (_selected.value != s) _selected.value = s;
}
Widget? buildBottomBar(bool isDesktop) {
if (isDesktop) return null;
if (_selected.value > _destinations.length - 1) _selected.value = 3;
return FocusScope(
node: navigationBarFocusNode,
child: ValueListenableBuilder<int>(
valueListenable: _selected,
builder: (context, value, _) {
return NavigationBar(
selectedIndex: value,
onDestinationSelected: _onDestinationSelected,
destinations: _destinations,
);
}),
);
}
Widget? _buildNavigationRail(bool isDesktop) {
if (!isDesktop) return null;
return ValueListenableBuilder(
valueListenable: _selected,
builder: (context, selected, _) {
return ExtensibleNavigationRail(
destinations: _navigationRailDestinations,
selectedIndex: selected,
onDestinationSelected: (int s) =>
_onDestinationSelected(s, useNavigationRailDestinations: true),
);
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return RawKeyboardListener( return RawKeyboardListener(
focusNode: FocusNode(), focusNode: FocusNode(),
onKey: _handleKey, onKey: _handleKey,
child: FancyScaffold( child: LayoutBuilder(builder: (context, constraints) {
key: _fancyScaffoldKey, // check if we're running on a desktop platform
bottomNavigationBar: FocusScope( final isLandscape = constraints.maxWidth > constraints.maxHeight;
node: navigationBarFocusNode, _isDesktop = isLandscape && constraints.maxWidth > 1024;
child: ValueListenableBuilder<int>( return FancyScaffold(
valueListenable: _selected, key: _fancyScaffoldKey,
builder: (context, value, _) { bodyDrawer: _buildNavigationRail(_isDesktop),
return NavigationBar( bottomNavigationBar: buildBottomBar(_isDesktop),
selectedIndex: value, bottomPanel: PlayerBar(
onDestinationSelected: (int s) async { focusNode: playerBarFocusNode,
//Pop all routes until home screen onTap: () =>
navigatorKey.currentState! _fancyScaffoldKey.currentState!.dragController.fling(),
.popUntil((route) => route.isFirst); shouldHaveHero: false,
navigatorKey.currentState!.pushReplacementNamed( ),
_destinations[s]!, bottomPanelHeight: 68.0,
arguments: true); expandedPanel: FocusScope(
node: playerScreenFocusNode,
if (_selected.value != s) _selected.value = s;
},
destinations: <NavigationDestination>[
NavigationDestination(
icon: const Icon(Icons.home_outlined),
selectedIcon: const Icon(Icons.home),
label: 'Home'.i18n),
NavigationDestination(
icon: const Icon(Icons.podcasts),
label: 'Podcasts'.i18n),
NavigationDestination(
icon: const Icon(Icons.library_music_outlined),
selectedIcon: const Icon(Icons.library_music),
label: 'Library'.i18n),
NavigationDestination(
icon: const Icon(Icons.search),
label: 'Search'.i18n),
],
);
}),
),
bottomPanel: PlayerBar(
focusNode: playerBarFocusNode,
onTap: () =>
_fancyScaffoldKey.currentState!.dragController.fling(),
shouldHaveHero: false,
),
bottomPanelHeight: 68.0,
expandedPanel: FocusScope(
node: playerScreenFocusNode,
skipTraversal: true,
canRequestFocus: true,
child: const PlayerScreen(),
),
onAnimationStatusChange: (status) {
if (status == AnimationStatus.dismissed) {
return playerBarFocusNode.requestFocus();
}
_logger.fine('requesting focus to playerScreen');
playerScreenFocusNode.requestFocus();
},
body: Focus(
focusNode: screenFocusNode,
skipTraversal: true, skipTraversal: true,
canRequestFocus: false, canRequestFocus: true,
child: _MainRouteNavigator( child: const PlayerScreen(),
navigatorKey: navigatorKey, ),
routes: { onAnimationStatusChange: (status) {
Navigator.defaultRouteName: (context) => const HomeScreen(), if (status == AnimationStatus.dismissed) {
'/podcasts': (context) => HomePageScreen( return playerBarFocusNode.requestFocus();
cacheable: true, }
channel:
const DeezerChannel(target: 'channels/podcasts'), _logger.fine('requesting focus to playerScreen');
title: 'Podcasts'.i18n, playerScreenFocusNode.requestFocus();
), },
'/library': (context) => const LibraryScreen(), body: Focus(
'/library/tracks': (context) => const LibraryTracks(), focusNode: screenFocusNode,
'/library/albums': (context) => const LibraryAlbums(), skipTraversal: true,
'/library/artists': (context) => const LibraryArtists(), canRequestFocus: false,
'/library/playlists': (context) => const LibraryPlaylists(), child: _MainRouteNavigator(
'/library/history': (context) => const HistoryScreen(), observers: [routeObserver],
'/search': (context) => const SearchScreen(), navigatorKey: navigatorKey,
'/settings': (context) => const SettingsScreen(), routes: {
'/downloads': (context) => const DownloadsScreen(), Navigator.defaultRouteName: (context) =>
}, const HomeScreen(),
)))); '/podcasts': (context) => HomePageScreen(
cacheable: true,
channel: const DeezerChannel(
target: 'channels/podcasts'),
title: 'Podcasts'.i18n,
),
'/library': (context) => const LibraryScreen(),
'/library/tracks': (context) => const LibraryTracks(),
'/library/albums': (context) => const LibraryAlbums(),
'/library/artists': (context) => const LibraryArtists(),
'/library/playlists': (context) =>
const LibraryPlaylists(),
'/library/history': (context) => const HistoryScreen(),
'/search': (context) => const SearchScreen(),
'/settings': (context) => const SettingsScreen(),
'/downloads': (context) => const DownloadsScreen(),
},
)));
}));
} }
} }
// hella simple null-safe reimplementation of custom_navigator, which is NOT null-safe // hella simple null-safe reimplementation of custom_navigator, which is NOT null-safe
class _MainRouteNavigator extends StatefulWidget { class _MainRouteNavigator extends StatefulWidget {
final Map<String, WidgetBuilder> routes; final Map<String, WidgetBuilder> routes;
final Map<String, RouteFactory>? customRoutes;
final GlobalKey<NavigatorState> navigatorKey; final GlobalKey<NavigatorState> navigatorKey;
final List<NavigatorObserver> observers;
const _MainRouteNavigator({ const _MainRouteNavigator({
Key? key, Key? key,
required this.routes, required this.routes,
this.customRoutes,
required this.navigatorKey, required this.navigatorKey,
this.observers = const <NavigatorObserver>[],
}) : super(key: key); }) : super(key: key);
@override @override
@ -630,6 +725,7 @@ class _MainRouteNavigatorState extends State<_MainRouteNavigator>
with WidgetsBindingObserver { with WidgetsBindingObserver {
@override @override
void initState() { void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState(); super.initState();
} }
@ -658,6 +754,7 @@ class _MainRouteNavigatorState extends State<_MainRouteNavigator>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Navigator( return Navigator(
observers: widget.observers,
key: widget.navigatorKey, key: widget.navigatorKey,
initialRoute: Navigator.defaultRouteName, initialRoute: Navigator.defaultRouteName,
onGenerateRoute: _onGenerateRoute, onGenerateRoute: _onGenerateRoute,
@ -687,52 +784,94 @@ class _MainRouteNavigatorState extends State<_MainRouteNavigator>
} }
} }
class ExtensibleNavigationRail extends StatefulWidget {
final List<NavigationRailDestination> destinations;
final int selectedIndex;
final void Function(int)? onDestinationSelected;
const ExtensibleNavigationRail({
super.key,
required this.destinations,
required this.selectedIndex,
this.onDestinationSelected,
});
@override
State<ExtensibleNavigationRail> createState() =>
_ExtensibleNavigationRailState();
}
class _ExtensibleNavigationRailState extends State<ExtensibleNavigationRail> {
bool _extended = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _extended = true),
onExit: (_) => setState(() => _extended = false),
child: NavigationRail(
extended: _extended,
destinations: widget.destinations,
selectedIndex: widget.selectedIndex,
onDestinationSelected: widget.onDestinationSelected,
),
);
}
}
// class FreezerDrawer extends StatelessWidget { // class FreezerDrawer extends StatelessWidget {
// const FreezerDrawer({Key? key}) : super(key: key); // final double? width;
// const FreezerDrawer({super.key, this.width});
// //
// @override // @override
// Widget build(BuildContext context) { // Widget build(BuildContext context) {
// return Drawer( // return NavigationDrawer(
// child: ListView(children: [ // onDestinationSelected: print,
// children: [
// const DrawerHeader(child: FreezerTitle()), // const DrawerHeader(child: FreezerTitle()),
// NavigationDrawerDestination(
// icon: const Icon(Icons.home_outlined),
// selectedIcon: const Icon(Icons.home),
// label: Text('Home'.i18n)),
// FreezerDrawerTile( // FreezerDrawerTile(
// title: 'Home'.i18n, icon: Icon(Icons.home), route: '/'), // title: 'Home'.i18n,
// Divider(), // icon: const Icon(Icons.home),
// route: Navigator.defaultRouteName),
// const Divider(),
// FreezerDrawerTile( // FreezerDrawerTile(
// title: 'Tracks'.i18n, // title: 'Tracks'.i18n,
// icon: Icon(Icons.audiotrack), // icon: const Icon(Icons.audiotrack),
// route: '/library/tracks'), // route: '/library/tracks'),
// FreezerDrawerTile( // FreezerDrawerTile(
// title: 'Albums'.i18n, // title: 'Albums'.i18n,
// icon: Icon(Icons.album), // icon: const Icon(Icons.album),
// route: '/library/albums'), // route: '/library/albums'),
// FreezerDrawerTile( // FreezerDrawerTile(
// title: 'Artists'.i18n, // title: 'Artists'.i18n,
// icon: Icon(Icons.recent_actors), // icon: const Icon(Icons.recent_actors),
// route: '/library/artists'), // route: '/library/artists'),
// FreezerDrawerTile( // FreezerDrawerTile(
// title: 'Playlists'.i18n, // title: 'Playlists'.i18n,
// icon: Icon(Icons.playlist_play), // icon: const Icon(Icons.playlist_play),
// route: '/library/playlists'), // route: '/library/playlists'),
// Divider(), // const Divider(),
// FreezerDrawerTile( // FreezerDrawerTile(
// title: 'Downloads'.i18n, // title: 'Downloads'.i18n,
// icon: Icon(Icons.download), // icon: const Icon(Icons.download),
// route: '/downloads'), // route: '/downloads'),
// FreezerDrawerTile( // FreezerDrawerTile(
// title: 'History'.i18n, // title: 'History'.i18n,
// icon: Icon(Icons.history), // icon: const Icon(Icons.history),
// route: '/library/history'), // route: '/library/history'),
// FreezerDrawerTile( // FreezerDrawerTile(
// title: 'Settings'.i18n, // title: 'Settings'.i18n,
// icon: Icon(Icons.settings), // icon: const Icon(Icons.settings),
// route: '/settings'), // route: '/settings'),
// ]), // ],
// ); // );
// } // }
// } // }
// //
// class FreezerDrawerTile extends StatelessWidget { // class FreezerDrawerTile extends StatefulWidget {
// final Widget? icon; // final Widget? icon;
// final String title; // final String title;
// final String route; // final String route;
@ -741,26 +880,59 @@ class _MainRouteNavigatorState extends State<_MainRouteNavigator>
// : super(key: key); // : super(key: key);
// //
// @override // @override
// State<FreezerDrawerTile> createState() => _FreezerDrawerTileState();
// }
//
// class _FreezerDrawerTileState extends State<FreezerDrawerTile> with RouteAware {
// bool _isHighlighted = false;
// late final RouteObserver _routeObserver;
//
// @override
// void didChangeDependencies() {
// _routeObserver = MainScreen.of(context).routeObserver;
// _routeObserver.subscribe(this, ModalRoute.of(context)!);
// super.didChangeDependencies();
// }
//
// @override
// void dispose() {
// _routeObserver.unsubscribe(this);
// super.dispose();
// }
//
// @override
// void didPushNext() => _update();
//
// @override
// void didPopNext() => _update();
//
// void _update() {
// final highlighted = ModalRoute.of(context)?.settings.name == widget.route;
//
// if (highlighted != _isHighlighted) {
// setState(() => _isHighlighted = highlighted);
// }
// }
//
// @override
// Widget build(BuildContext context) { // Widget build(BuildContext context) {
// print(route);
// return Padding( // return Padding(
// padding: const EdgeInsets.symmetric(horizontal: 8.0), // padding: const EdgeInsets.symmetric(horizontal: 8.0),
// child: Material( // child: Material(
// borderRadius: BorderRadius.circular(8.0), // borderRadius: BorderRadius.circular(8.0),
// clipBehavior: Clip.antiAlias, // clipBehavior: Clip.antiAlias,
// color: ModalRoute.of(context)?.settings.name == route // color: _isHighlighted
// ? Theme.of(context).brightness == Brightness.dark // ? Theme.of(context).brightness == Brightness.dark
// ? Colors.white12 // ? Colors.white12
// : Colors.black12 // : Colors.black12
// : null, // : null,
// child: ListTile( // child: ListTile(
// selected: ModalRoute.of(context)?.settings.name == route, // selected: _isHighlighted,
// leading: icon, // leading: widget.icon,
// visualDensity: VisualDensity.compact, // visualDensity: VisualDensity.compact,
// title: Text(title), // title: Text(widget.title),
// onTap: () { // onTap: () {
// Navigator.of(context).pop(); // navigatorKey.currentState!.pushReplacementNamed(widget.route);
// navigatorKey.currentState!.pushReplacementNamed(route);
// }), // }),
// ), // ),
// ); // );

View file

@ -13,7 +13,9 @@ class FancyScaffold extends StatefulWidget {
final Widget bottomPanel; final Widget bottomPanel;
final double bottomPanelHeight; final double bottomPanelHeight;
final Widget expandedPanel; final Widget expandedPanel;
final Widget bottomNavigationBar; final Widget? bottomNavigationBar;
final Widget? drawer;
final Widget? bodyDrawer;
final Widget body; final Widget body;
final void Function(AnimationStatus)? onAnimationStatusChange; final void Function(AnimationStatus)? onAnimationStatusChange;
@ -21,9 +23,11 @@ class FancyScaffold extends StatefulWidget {
required this.bottomPanel, required this.bottomPanel,
required this.bottomPanelHeight, required this.bottomPanelHeight,
required this.expandedPanel, required this.expandedPanel,
required this.bottomNavigationBar,
required this.body, required this.body,
this.onAnimationStatusChange, this.onAnimationStatusChange,
this.bottomNavigationBar,
this.bodyDrawer,
this.drawer,
super.key, super.key,
}); });
@ -61,7 +65,8 @@ class FancyScaffoldState extends State<FancyScaffold>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final systemPadding = MediaQuery.of(context).viewPadding; final systemPadding = MediaQuery.of(context).viewPadding;
final defaultBottomPadding = 80.0 + systemPadding.bottom; final defaultBottomPadding =
(widget.bottomNavigationBar == null ? 0 : 80.0) + systemPadding.bottom;
final screenHeight = MediaQuery.of(context).size.height; final screenHeight = MediaQuery.of(context).size.height;
final sizeAnimation = Tween<double>( final sizeAnimation = Tween<double>(
begin: widget.bottomPanelHeight / MediaQuery.of(context).size.height, begin: widget.bottomPanelHeight / MediaQuery.of(context).size.height,
@ -81,17 +86,24 @@ class FancyScaffoldState extends State<FancyScaffold>
children: [ children: [
Positioned.fill( Positioned.fill(
child: Scaffold( child: Scaffold(
body: widget.body, body: widget.bodyDrawer != null
? Row(children: [
widget.bodyDrawer!,
Expanded(child: widget.body)
])
: widget.body,
drawer: widget.drawer,
bottomNavigationBar: Column( bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox(height: widget.bottomPanelHeight), SizedBox(height: widget.bottomPanelHeight),
SizeTransition( if (widget.bottomNavigationBar != null)
axisAlignment: -1.0, SizeTransition(
sizeFactor: axisAlignment: -1.0,
Tween(begin: 1.0, end: 0.0).animate(sizeAnimation), sizeFactor:
child: widget.bottomNavigationBar, Tween(begin: 1.0, end: 0.0).animate(sizeAnimation),
), child: widget.bottomNavigationBar,
),
], ],
), ),
), ),

View file

@ -754,9 +754,11 @@ packages:
just_audio_media_kit: just_audio_media_kit:
dependency: "direct main" dependency: "direct main"
description: description:
path: "../just_audio_media_kit" path: "."
relative: true ref: HEAD
source: path resolved-ref: "8ccec63c67c0c206c6df3570e46f60b2a45dbb24"
url: "https://github.com/Pato05/just_audio_media_kit.git"
source: git
version: "0.0.1" version: "0.0.1"
just_audio_platform_interface: just_audio_platform_interface:
dependency: transitive dependency: transitive

View file

@ -93,6 +93,8 @@ dependencies:
isar: ^3.1.0+1 isar: ^3.1.0+1
isar_flutter_libs: ^3.1.0+1 isar_flutter_libs: ^3.1.0+1
flutter_background_service: ^5.0.1 flutter_background_service: ^5.0.1
#deezcryptor:
#path: deezcryptor/
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: