diff --git a/lib/components/root/bottom_player.dart b/lib/components/root/bottom_player.dart index 851e37da0..701767c3c 100644 --- a/lib/components/root/bottom_player.dart +++ b/lib/components/root/bottom_player.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/gestures.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -8,6 +10,7 @@ import 'package:spotube/components/player/player_overlay.dart'; import 'package:spotube/components/player/player_track_details.dart'; import 'package:spotube/components/player/player_controls.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; +import 'package:spotube/hooks/use_brightness_value.dart'; import 'package:spotube/models/logger.dart'; import 'package:flutter/material.dart'; import 'package:spotube/provider/playlist_queue_provider.dart'; @@ -37,6 +40,13 @@ class BottomPlayer extends HookConsumerWidget { [playlist?.activeTrack.album?.images], ); + final bg = Theme.of(context).colorScheme.surfaceVariant; + + final bgColor = useBrightnessValue( + Color.lerp(bg, Colors.white, 0.7), + Color.lerp(bg, Colors.black, 0.45)!, + ); + // returning an empty non spacious Container as the overlay will take // place in the global overlay stack aka [_entries] if (layoutMode == LayoutMode.compact || @@ -45,74 +55,80 @@ class BottomPlayer extends HookConsumerWidget { return PlayerOverlay(albumArt: albumArt); } - return DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - ), - child: Material( - type: MaterialType.transparency, - textStyle: Theme.of(context).textTheme.bodyMedium!, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded(child: PlayerTrackDetails(albumArt: albumArt)), - // controls - Flexible( - flex: 3, - child: PlayerControls(), - ), - // add to saved tracks - Expanded( - flex: 1, - child: Wrap( - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - children: [ - Container( - height: 20, - constraints: const BoxConstraints(maxWidth: 200), - child: HookBuilder(builder: (context) { - final volumeState = ref.watch(VolumeProvider.provider); - final volumeNotifier = - ref.watch(VolumeProvider.provider.notifier); - final volume = useState(volumeState); - - useEffect(() { - if (volume.value != volumeState) { - volume.value = volumeState; - } - return null; - }, [volumeState]); + return ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: DecoratedBox( + decoration: BoxDecoration(color: bgColor?.withOpacity(0.8)), + child: Material( + type: MaterialType.transparency, + textStyle: Theme.of(context).textTheme.bodyMedium!, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded(child: PlayerTrackDetails(albumArt: albumArt)), + // controls + Flexible( + flex: 3, + child: PlayerControls(), + ), + // add to saved tracks + Expanded( + flex: 1, + child: Wrap( + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + Container( + height: 20, + constraints: const BoxConstraints(maxWidth: 200), + child: HookBuilder(builder: (context) { + final volumeState = + ref.watch(VolumeProvider.provider); + final volumeNotifier = + ref.watch(VolumeProvider.provider.notifier); + final volume = useState(volumeState); - return Listener( - onPointerSignal: (event) async { - if (event is PointerScrollEvent) { - if (event.scrollDelta.dy > 0) { - final value = volume.value - .2; - volumeNotifier.setVolume(value < 0 ? 0 : value); - } else { - final value = volume.value + .2; - volumeNotifier.setVolume(value > 1 ? 1 : value); + useEffect(() { + if (volume.value != volumeState) { + volume.value = volumeState; } - } - }, - child: Slider( - min: 0, - max: 1, - value: volume.value, - onChanged: (v) { - volume.value = v; - }, - onChangeEnd: volumeNotifier.setVolume, - ), - ); - }), + return null; + }, [volumeState]); + + return Listener( + onPointerSignal: (event) async { + if (event is PointerScrollEvent) { + if (event.scrollDelta.dy > 0) { + final value = volume.value - .2; + volumeNotifier + .setVolume(value < 0 ? 0 : value); + } else { + final value = volume.value + .2; + volumeNotifier + .setVolume(value > 1 ? 1 : value); + } + } + }, + child: Slider( + min: 0, + max: 1, + value: volume.value, + onChanged: (v) { + volume.value = v; + }, + onChangeEnd: volumeNotifier.setVolume, + ), + ); + }), + ), + PlayerActions() + ], ), - PlayerActions() - ], - ), - ) - ], + ) + ], + ), + ), ), ), ); diff --git a/lib/components/root/sidebar.dart b/lib/components/root/sidebar.dart index 5cffb0aae..83fe94d85 100644 --- a/lib/components/root/sidebar.dart +++ b/lib/components/root/sidebar.dart @@ -2,12 +2,15 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter/material.dart'; +import 'package:sidebarx/sidebarx.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/hooks/use_breakpoints.dart'; +import 'package:spotube/hooks/use_brightness_value.dart'; +import 'package:spotube/hooks/use_sidebarx_controller.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/downloader_provider.dart'; @@ -55,6 +58,34 @@ class Sidebar extends HookConsumerWidget { final layoutMode = ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); + final controller = useSidebarXController( + selectedIndex: selectedIndex, + extended: breakpoints > Breakpoints.md, + ); + + final bg = Theme.of(context).colorScheme.surfaceVariant; + + final bgColor = useBrightnessValue( + Color.lerp(bg, Colors.white, 0.7), + Color.lerp(bg, Colors.black, 0.45)!, + ); + + useEffect(() { + controller.addListener(() { + onSelectedIndexChanged(controller.selectedIndex); + }); + return null; + }, [controller]); + + useEffect(() { + if (breakpoints > Breakpoints.md && !controller.extended) { + controller.setExtended(true); + } else if (breakpoints <= Breakpoints.md && controller.extended) { + controller.setExtended(false); + } + return null; + }, [breakpoints, controller]); + if (layoutMode == LayoutMode.compact || (breakpoints.isSm && layoutMode == LayoutMode.adaptive)) { return Scaffold(body: child); @@ -62,41 +93,61 @@ class Sidebar extends HookConsumerWidget { return Row( children: [ - NavigationRail( - selectedIndex: selectedIndex, - onDestinationSelected: onSelectedIndexChanged, - destinations: sidebarTileList.map( + SidebarX( + controller: controller, + items: sidebarTileList.map( (e) { - return NavigationRailDestination( - icon: Badge( - backgroundColor: Theme.of(context).primaryColor, - isLabelVisible: e.title == "Library" && downloadCount > 0, - label: Text( - downloadCount.toString(), - style: const TextStyle( - color: Colors.white, - fontSize: 10, - ), - ), - child: Icon(e.icon), - ), - label: Text( - e.title, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), + return SidebarXItem( + // iconWidget: Badge( + // backgroundColor: Theme.of(context).primaryColor, + // isLabelVisible: e.title == "Library" && downloadCount > 0, + // label: Text( + // downloadCount.toString(), + // style: const TextStyle( + // color: Colors.white, + // fontSize: 10, + // ), + // ), + // child: Icon(e.icon), + // ), + icon: e.icon, + label: e.title, ); }, ).toList(), - extended: breakpoints > Breakpoints.md, - leading: const SidebarHeader(), - trailing: const Expanded( - child: Align( - alignment: Alignment.bottomCenter, - child: SidebarFooter(), + headerBuilder: (_, __) => const SidebarHeader(), + footerBuilder: (_, __) => const Padding( + padding: EdgeInsets.only(bottom: 5), + child: SidebarFooter(), + ), + showToggleButton: false, + theme: SidebarXTheme( + width: 60, + margin: const EdgeInsets.all(10).copyWith(bottom: 122), + ), + extendedTheme: SidebarXTheme( + width: 250, + margin: const EdgeInsets.all(10).copyWith(bottom: 122, left: 0), + padding: const EdgeInsets.symmetric(horizontal: 6), + decoration: BoxDecoration( + color: bgColor?.withOpacity(0.8), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(10), + bottomRight: Radius.circular(10), + )), + selectedItemDecoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), ), + selectedIconTheme: IconThemeData( + color: Theme.of(context).colorScheme.primary, + ), + selectedTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w600, + ), + itemTextPadding: const EdgeInsets.only(left: 10), + selectedItemTextPadding: const EdgeInsets.only(left: 10), ), ), Expanded(child: child) diff --git a/lib/hooks/use_sidebarx_controller.dart b/lib/hooks/use_sidebarx_controller.dart new file mode 100644 index 000000000..5af921b75 --- /dev/null +++ b/lib/hooks/use_sidebarx_controller.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:sidebarx/sidebarx.dart'; + +/// Creates [SidebarXController] that will be disposed automatically. +/// +/// See also: +/// - [SidebarXController] +SidebarXController useSidebarXController({ + required int selectedIndex, + bool? extended, + List? keys, +}) { + return use( + _SidebarXControllerHook( + selectedIndex: selectedIndex, + extended: extended, + keys: keys, + ), + ); +} + +class _SidebarXControllerHook extends Hook { + const _SidebarXControllerHook({ + required this.selectedIndex, + this.extended, + List? keys, + }) : super(keys: keys); + + final int selectedIndex; + final bool? extended; + + @override + HookState> createState() => + _SidebarXControllerHookState(); +} + +class _SidebarXControllerHookState + extends HookState { + late final SidebarXController controller; + + @override + void initHook() { + super.initHook(); + controller = SidebarXController( + selectedIndex: hook.selectedIndex, + extended: hook.extended, + ); + } + + @override + SidebarXController build(BuildContext context) => controller; + + @override + void dispose() => controller.dispose(); + + @override + String get debugLabel => 'useSidebarXController'; +} diff --git a/pubspec.lock b/pubspec.lock index 77f779f29..3304ba0e2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1393,6 +1393,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + sidebarx: + dependency: "direct main" + description: + name: sidebarx + sha256: "26a8392ceddb659c8f2c688beba6c04bcbf520b4d5decb143c5fd7253653081f" + url: "https://pub.dev" + source: hosted + version: "0.15.0" simple_circular_progress_bar: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e68bc0d8f..9f501bb3b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dependencies: queue: ^3.1.0+1 scroll_to_index: ^3.0.1 shared_preferences: ^2.0.11 + sidebarx: ^0.15.0 simple_circular_progress_bar: ^1.0.2 skeleton_text: ^3.0.0 spotify: