refactor: bottom player border, player queue using shadcn drawer

This commit is contained in:
Kingkor Roy Tirtho 2024-12-21 14:38:54 +06:00
parent 04190f2dda
commit 2488da2279
10 changed files with 397 additions and 457 deletions

View File

@ -13,6 +13,7 @@
"RGBO", "RGBO",
"riverpod", "riverpod",
"Scrobblenaut", "Scrobblenaut",
"shadcn",
"skeletonizer", "skeletonizer",
"songlink", "songlink",
"speechiness", "speechiness",

View File

@ -127,4 +127,5 @@ abstract class SpotubeIcons {
static const cache = FeatherIcons.hardDrive; static const cache = FeatherIcons.hardDrive;
static const export = Icons.file_open_outlined; static const export = Icons.file_open_outlined;
static const delete = FeatherIcons.trash2; static const delete = FeatherIcons.trash2;
static const open = FeatherIcons.externalLink;
} }

View File

@ -220,14 +220,14 @@ class Spotube extends HookConsumerWidget {
radius: .5, radius: .5,
iconTheme: const IconThemeProperties(), iconTheme: const IconThemeProperties(),
colorScheme: ColorSchemes.lightBlue(), colorScheme: ColorSchemes.lightBlue(),
surfaceOpacity: .9, surfaceOpacity: .8,
surfaceBlur: 10, surfaceBlur: 10,
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
radius: .5, radius: .5,
iconTheme: const IconThemeProperties(), iconTheme: const IconThemeProperties(),
colorScheme: ColorSchemes.darkNeutral(), colorScheme: ColorSchemes.darkNeutral(),
surfaceOpacity: .9, surfaceOpacity: .8,
surfaceBlur: 10, surfaceBlur: 10,
), ),
themeMode: themeMode, themeMode: themeMode,

View File

@ -3,6 +3,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'
show openDrawer, OverlayPosition;
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
@ -289,53 +292,53 @@ class PlayerView extends HookConsumerWidget {
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: OutlinedButton.icon( child: OutlinedButton.icon(
icon: const Icon(SpotubeIcons.queue), icon: const Icon(SpotubeIcons.queue),
label: Text(context.l10n.queue), label: Text(context.l10n.queue),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: bodyTextColor, foregroundColor: bodyTextColor,
side: BorderSide( side: BorderSide(
color: bodyTextColor ?? Colors.white, color: bodyTextColor ?? Colors.white,
),
), ),
onPressed: currentTrack != null ),
? () { // enabled: currentTrack != null,
showModalBottomSheet( onPressed: () {
context: context, openDrawer(
isDismissible: true, context: context,
enableDrag: true, barrierDismissible: true,
isScrollControlled: true, draggable: true,
backgroundColor: Colors.black12, barrierColor: Colors.black12,
barrierColor: Colors.black12, borderRadius: BorderRadius.circular(10),
shape: RoundedRectangleBorder( transformBackdrop: false,
borderRadius: position: OverlayPosition.bottom,
BorderRadius.circular(10), surfaceBlur: context.theme.surfaceBlur,
), surfaceOpacity: 0.7,
constraints: BoxConstraints( expands: true,
maxHeight: builder: (context) => Consumer(
MediaQuery.of(context) builder: (context, ref, _) {
.size final playlist = ref.watch(
.height * audioPlayerProvider,
.7, );
), final playlistNotifier = ref.read(
builder: (context) => Consumer( audioPlayerProvider.notifier);
builder: (context, ref, _) { return ConstrainedBox(
final playlist = ref.watch( constraints: BoxConstraints(
audioPlayerProvider, maxHeight: MediaQuery.of(context)
); .size
final playlistNotifier = ref .height *
.read(audioPlayerProvider 0.8,
.notifier); ),
return PlayerQueue child: PlayerQueue
.fromAudioPlayerNotifier( .fromAudioPlayerNotifier(
floating: false, floating: false,
playlist: playlist, playlist: playlist,
notifier: playlistNotifier, notifier: playlistNotifier,
); ),
}, );
), },
); ),
} );
: null), },
),
), ),
if (auth.asData?.value != null) if (auth.asData?.value != null)
const SizedBox(width: 10), const SizedBox(width: 10),

View File

@ -2,8 +2,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/modules/player/sibling_tracks_sheet.dart'; import 'package:spotube/modules/player/sibling_tracks_sheet.dart';
import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/heart_button/heart_button.dart'; import 'package:spotube/components/heart_button/heart_button.dart';
@ -82,7 +84,32 @@ class PlayerActions extends HookConsumerWidget {
icon: const Icon(SpotubeIcons.queue), icon: const Icon(SpotubeIcons.queue),
enabled: playlist.activeTrack != null, enabled: playlist.activeTrack != null,
onPressed: () { onPressed: () {
// Scaffold.of(context).openEndDrawer(); openDrawer(
context: context,
position: OverlayPosition.right,
transformBackdrop: false,
draggable: false,
surfaceBlur: context.theme.surfaceBlur,
surfaceOpacity: 0.7,
builder: (context) {
return Container(
constraints: const BoxConstraints(maxWidth: 800),
child: Consumer(
builder: (context, ref, _) {
final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier =
ref.read(audioPlayerProvider.notifier);
return PlayerQueue.fromAudioPlayerNotifier(
floating: true,
playlist: playlist,
notifier: playlistNotifier,
);
},
),
);
},
);
}, },
), ),
), ),
@ -100,6 +127,7 @@ class PlayerActions extends HookConsumerWidget {
draggable: true, draggable: true,
barrierColor: Colors.black.withValues(alpha: .2), barrierColor: Colors.black.withValues(alpha: .2),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
transformBackdrop: false,
builder: (context) { builder: (context) {
return SiblingTracksSheet(floating: floatingQueue); return SiblingTracksSheet(floating: floatingQueue);
}, },

View File

@ -1,14 +1,12 @@
import 'dart:ui'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/fallbacks/not_found.dart'; import 'package:spotube/components/fallbacks/not_found.dart';
@ -60,16 +58,6 @@ class PlayerQueue extends HookConsumerWidget {
final isSearching = useState(false); final isSearching = useState(false);
final tracks = playlist.tracks; final tracks = playlist.tracks;
final borderRadius = floating
? const BorderRadius.only(
topLeft: Radius.circular(10),
)
: const BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
);
final theme = Theme.of(context);
final headlineColor = theme.textTheme.headlineSmall?.color;
final filteredTracks = useMemoized( final filteredTracks = useMemoized(
() { () {
@ -92,217 +80,173 @@ class PlayerQueue extends HookConsumerWidget {
[tracks, searchText.value], [tracks, searchText.value],
); );
useEffect(() {
if (playlist.activeTrack == null) return null;
controller.scrollToIndex(
playlist.playlist.index,
preferPosition: AutoScrollPosition.middle,
);
return null;
}, []);
if (tracks.isEmpty) { if (tracks.isEmpty) {
return const NotFound(vertical: true); return const NotFound(vertical: true);
} }
return LayoutBuilder( return Stack(
builder: (context, constrains) { children: [
return ClipRRect( LayoutBuilder(
borderRadius: borderRadius, builder: (context, constrains) {
clipBehavior: Clip.hardEdge, final searchBar = ConstrainedBox(
child: BackdropFilter( constraints: BoxConstraints(
filter: ImageFilter.blur( maxHeight: 40,
sigmaX: 15, maxWidth:
sigmaY: 15, mediaQuery.smAndDown ? mediaQuery.size.width - 40 : 300,
),
child: Container(
padding: const EdgeInsets.only(
top: 5.0,
), ),
decoration: BoxDecoration( child: TextField(
color: onChanged: (value) {
theme.colorScheme.surfaceContainerHighest.withOpacity(0.5), searchText.value = value;
borderRadius: borderRadius,
),
child: CallbackShortcuts(
bindings: {
LogicalKeySet(LogicalKeyboardKey.escape): () {
if (!isSearching.value) {
Navigator.of(context).pop();
}
isSearching.value = false;
searchText.value = '';
}
}, },
child: InterScrollbar( placeholder: Text(context.l10n.search),
controller: controller, ),
child: CustomScrollView( );
controller: controller, return CallbackShortcuts(
slivers: [ bindings: {
if (!floating) LogicalKeySet(LogicalKeyboardKey.escape): () {
SliverToBoxAdapter( if (!isSearching.value) {
child: Center( Navigator.of(context).pop();
child: Container( }
height: 5, isSearching.value = false;
width: 100, searchText.value = '';
margin: const EdgeInsets.only(bottom: 5, top: 2), }
decoration: BoxDecoration( },
color: headlineColor, child: Column(
borderRadius: BorderRadius.circular(20), children: [
), if (isSearching.value && mediaQuery.smAndDown)
AppBar(
backgroundColor: Colors.transparent,
leading: [
if (mediaQuery.smAndDown)
IconButton.ghost(
icon: const Icon(
Icons.arrow_back_ios_new_outlined,
), ),
), onPressed: () {
), isSearching.value = false;
SliverAppBar( searchText.value = '';
floating: true, },
pinned: false, )
snap: false, ],
backgroundColor: Colors.transparent, surfaceBlur: 0,
elevation: 0, surfaceOpacity: 0,
automaticallyImplyLeading: false, child: searchBar,
title: BackdropFilter( )
filter: ImageFilter.blur( else
sigmaX: 10, AppBar(
sigmaY: 10, trailingGap: 0,
), backgroundColor: Colors.transparent,
child: SizedBox( surfaceBlur: 0,
height: kToolbarHeight, surfaceOpacity: 0,
child: mediaQuery.mdAndUp || !isSearching.value title: mediaQuery.mdAndUp || !isSearching.value
? Align( ? SizedBox(
alignment: Alignment.centerLeft, height: 30,
child: Text( child: AutoSizeText(
context.l10n context.l10n.tracks_in_queue(tracks.length),
.tracks_in_queue(tracks.length), maxLines: 1,
style: TextStyle(
color: headlineColor,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
)
: null,
),
),
actions: [
if (mediaQuery.mdAndUp || isSearching.value)
TextField(
onChanged: (value) {
searchText.value = value;
},
decoration: InputDecoration(
hintText: context.l10n.search,
isDense: true,
prefixIcon: mediaQuery.smAndDown
? IconButton(
icon: const Icon(
Icons.arrow_back_ios_new_outlined,
),
onPressed: () {
isSearching.value = false;
searchText.value = '';
},
style: IconButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size.square(20),
),
)
: const Icon(SpotubeIcons.filter),
constraints: BoxConstraints(
maxHeight: 40,
maxWidth: mediaQuery.smAndDown
? mediaQuery.size.width - 40
: 300,
),
), ),
) )
else : null,
IconButton.filledTonal( trailing: [
icon: const Icon(SpotubeIcons.filter), if (mediaQuery.mdAndUp)
onPressed: () { searchBar
isSearching.value = !isSearching.value; else
}, IconButton.ghost(
), icon: const Icon(SpotubeIcons.filter),
if (mediaQuery.mdAndUp || !isSearching.value) ...[ onPressed: () {
const SizedBox(width: 10), isSearching.value = !isSearching.value;
FilledButton( },
style: FilledButton.styleFrom( ),
backgroundColor: theme.scaffoldBackgroundColor if (mediaQuery.mdAndUp || !isSearching.value) ...[
.withOpacity(0.5), const SizedBox(width: 10),
foregroundColor: Tooltip(
theme.textTheme.headlineSmall?.color, tooltip: Text(context.l10n.clear_all),
), child: IconButton.outline(
child: Row( icon: const Icon(SpotubeIcons.playlistRemove),
children: [
const Icon(SpotubeIcons.playlistRemove),
const SizedBox(width: 5),
Text(context.l10n.clear_all),
],
),
onPressed: () { onPressed: () {
onStop(); onStop();
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
const SizedBox(width: 10), ),
],
], ],
), ],
const SliverGap(10), ),
SliverReorderableList( const Divider(),
onReorder: onReorder, Expanded(
itemCount: filteredTracks.length, child: InterScrollbar(
onReorderStart: (index) { controller: controller,
HapticFeedback.selectionClick(); child: CustomScrollView(
}, controller: controller,
onReorderEnd: (index) { slivers: [
HapticFeedback.selectionClick(); const SliverGap(10),
}, SliverReorderableList(
itemBuilder: (context, i) { onReorder: onReorder,
final track = filteredTracks.elementAt(i); itemCount: filteredTracks.length,
return AutoScrollTag( onReorderStart: (index) {
key: ValueKey<int>(i), HapticFeedback.selectionClick();
controller: controller, },
index: i, onReorderEnd: (index) {
child: Material( HapticFeedback.selectionClick();
color: Colors.transparent, },
child: TrackTile( itemBuilder: (context, i) {
playlist: playlist, final track = filteredTracks.elementAt(i);
return AutoScrollTag(
key: ValueKey<int>(i),
controller: controller,
index: i, index: i,
track: track, child: TrackTile(
onTap: () async { playlist: playlist,
if (playlist.activeTrack?.id == track.id) { index: i,
return; track: track,
} onTap: () async {
await onJump(track); if (playlist.activeTrack?.id == track.id) {
}, return;
leadingActions: [ }
if (!isSearching.value && await onJump(track);
searchText.value.isEmpty) },
Padding( leadingActions: [
padding: const EdgeInsets.only(left: 8.0), if (!isSearching.value &&
child: ReorderableDragStartListener( searchText.value.isEmpty)
index: i, Padding(
child: const Icon( padding:
SpotubeIcons.dragHandle, const EdgeInsets.only(left: 8.0),
child: ReorderableDragStartListener(
index: i,
child: const Icon(
SpotubeIcons.dragHandle,
),
), ),
), ),
), ],
], ),
), );
), },
); ),
}, const SliverGap(100),
],
), ),
const SliverGap(100), ),
],
), ),
), ],
), ),
), );
},
),
Positioned(
right: 20,
bottom: 20,
child: IconButton.secondary(
icon: const Icon(SpotubeIcons.open),
onPressed: () {
controller.scrollToIndex(
playlist.playlist.index,
preferPosition: AutoScrollPosition.middle,
);
},
), ),
); )
}, ],
); );
} }
} }

View File

@ -31,13 +31,18 @@ class VolumeSlider extends HookConsumerWidget {
} }
} }
}, },
child: Slider( child: SizedBox(
min: 0, height: 20,
max: 1, width: 100,
value: SliderValue.single(value), child: Slider(
onChanged: (v) => onChanged(v.value), min: 0,
max: 1,
value: SliderValue.single(value),
onChanged: (v) => onChanged(v.value),
),
), ),
); );
return Row( return Row(
mainAxisAlignment: mainAxisAlignment:
!fullWidth ? MainAxisAlignment.center : MainAxisAlignment.start, !fullWidth ? MainAxisAlignment.center : MainAxisAlignment.start,

View File

@ -1,9 +1,8 @@
import 'dart:ui';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
@ -16,7 +15,6 @@ import 'package:spotube/modules/player/volume_slider.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart'; import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -45,14 +43,6 @@ class BottomPlayer extends HookConsumerWidget {
[playlist.activeTrack?.album?.images], [playlist.activeTrack?.album?.images],
); );
final theme = Theme.of(context);
final bg = theme.colorScheme.background;
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 // returning an empty non spacious Container as the overlay will take
// place in the global overlay stack aka [_entries] // place in the global overlay stack aka [_entries]
if (layoutMode == LayoutMode.compact || if (layoutMode == LayoutMode.compact ||
@ -60,85 +50,81 @@ class BottomPlayer extends HookConsumerWidget {
return PlayerOverlay(albumArt: albumArt); return PlayerOverlay(albumArt: albumArt);
} }
return ClipRect( return SurfaceCard(
child: BackdropFilter( borderRadius: BorderRadius.zero,
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), surfaceBlur: context.theme.surfaceBlur,
child: DecoratedBox( child: Row(
decoration: BoxDecoration(color: bgColor?.withValues(alpha: .8)), mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Row( children: [
mainAxisAlignment: MainAxisAlignment.spaceBetween, Expanded(
child: PlayerTrackDetails(track: playlist.activeTrack),
),
// controls
const Flexible(
flex: 3,
child: Padding(
padding: EdgeInsets.only(top: 5),
child: PlayerControls(),
),
),
// add to saved tracks
Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
Expanded( PlayerActions(
child: PlayerTrackDetails(track: playlist.activeTrack), extraActions: [
), Tooltip(
// controls tooltip: Text(context.l10n.mini_player),
const Flexible( child: IconButton(
flex: 3, variance: ButtonVariance.ghost,
child: Padding( icon: const Icon(SpotubeIcons.miniPlayer),
padding: EdgeInsets.only(top: 5), onPressed: () async {
child: PlayerControls(), if (!kIsDesktop) return;
),
),
// add to saved tracks
Column(
children: [
PlayerActions(
extraActions: [
Tooltip(
tooltip: Text(context.l10n.mini_player),
child: IconButton(
variance: ButtonVariance.ghost,
icon: const Icon(SpotubeIcons.miniPlayer),
onPressed: () async {
if (!kIsDesktop) return;
final prevSize = await windowManager.getSize(); final prevSize = await windowManager.getSize();
await windowManager.setMinimumSize( await windowManager.setMinimumSize(
const Size(300, 300), const Size(300, 300),
); );
await windowManager.setAlwaysOnTop(true); await windowManager.setAlwaysOnTop(true);
if (!kIsLinux) { if (!kIsLinux) {
await windowManager.setHasShadow(false); await windowManager.setHasShadow(false);
}
await windowManager.setAlignment(Alignment.topRight);
await windowManager.setSize(const Size(400, 500));
await Future.delayed(
const Duration(milliseconds: 100),
() async {
if (context.mounted) {
context.go(
'/mini-player',
extra: prevSize,
);
} }
await windowManager
.setAlignment(Alignment.topRight);
await windowManager.setSize(const Size(400, 500));
await Future.delayed(
const Duration(milliseconds: 100),
() async {
if (context.mounted) {
context.go(
'/mini-player',
extra: prevSize,
);
}
},
);
}, },
), );
), },
], ),
), ),
Container(
height: 40,
constraints: const BoxConstraints(maxWidth: 250),
padding: const EdgeInsets.only(right: 10),
child: Consumer(builder: (context, ref, _) {
final volume = ref.watch(volumeProvider);
return VolumeSlider(
fullWidth: true,
value: volume,
onChanged: (value) {
ref.read(volumeProvider.notifier).setVolume(value);
},
);
}),
)
], ],
), ),
Container(
height: 40,
constraints: const BoxConstraints(maxWidth: 250),
padding: const EdgeInsets.only(right: 10),
child: Consumer(builder: (context, ref, _) {
final volume = ref.watch(volumeProvider);
return VolumeSlider(
fullWidth: true,
value: volume,
onChanged: (value) {
ref.read(volumeProvider.notifier).setVolume(value);
},
);
}),
)
], ],
), ),
), ],
), ),
); );
} }

View File

@ -96,32 +96,31 @@ class Sidebar extends HookConsumerWidget {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SafeArea( Column(
child: Column( children: [
children: [ Expanded(
Expanded( child: mediaQuery.lgAndUp
child: mediaQuery.lgAndUp ? NavigationSidebar(
? NavigationSidebar( index: selectedIndex,
index: selectedIndex, onSelected: (index) {
onSelected: (index) { final tile = sidebarTileList[index];
final tile = sidebarTileList[index]; ServiceUtils.pushNamed(context, tile.name);
ServiceUtils.pushNamed(context, tile.name); },
}, children: navigationButtons,
children: navigationButtons, )
) : NavigationRail(
: NavigationRail( alignment: NavigationRailAlignment.start,
alignment: NavigationRailAlignment.start, index: selectedIndex,
index: selectedIndex, onSelected: (index) {
onSelected: (index) { final tile = sidebarTileList[index];
final tile = sidebarTileList[index]; ServiceUtils.pushNamed(context, tile.name);
ServiceUtils.pushNamed(context, tile.name); },
}, children: navigationButtons,
children: navigationButtons, ),
), ),
), const SidebarFooter(),
const SidebarFooter(), const Gap(130)
], ],
),
), ),
const VerticalDivider(), const VerticalDivider(),
Expanded(child: child), Expanded(child: child),

View File

@ -1,14 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/side_bar_tiles.dart'; import 'package:spotube/collections/side_bar_tiles.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/framework/app_pop_scope.dart'; import 'package:spotube/components/framework/app_pop_scope.dart';
import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart'; import 'package:spotube/components/dialogs/replace_downloaded_dialog.dart';
import 'package:spotube/modules/root/bottom_player.dart'; import 'package:spotube/modules/root/bottom_player.dart';
import 'package:spotube/modules/root/sidebar.dart'; import 'package:spotube/modules/root/sidebar.dart';
@ -17,7 +16,6 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/hooks/configurators/use_endless_playback.dart'; import 'package:spotube/hooks/configurators/use_endless_playback.dart';
import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/home/home.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/glance/glance.dart'; import 'package:spotube/provider/glance/glance.dart';
import 'package:spotube/provider/server/routes/connect.dart'; import 'package:spotube/provider/server/routes/connect.dart';
import 'package:spotube/services/connectivity_adapter.dart'; import 'package:spotube/services/connectivity_adapter.dart';
@ -37,7 +35,7 @@ class RootApp extends HookConsumerWidget {
final showingDialogCompleter = useRef(Completer()..complete()); final showingDialogCompleter = useRef(Completer()..complete());
final downloader = ref.watch(downloadManagerProvider); final downloader = ref.watch(downloadManagerProvider);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final connectRoutes = ref.watch(serverConnectRoutesProvider); final connectRoutes = ref.watch(serverConnectRoutesProvider);
ref.listen(glanceProvider, (_, __) {}); ref.listen(glanceProvider, (_, __) {});
@ -50,64 +48,70 @@ class RootApp extends HookConsumerWidget {
final subscriptions = [ final subscriptions = [
ConnectionCheckerService.instance.onConnectivityChanged ConnectionCheckerService.instance.onConnectivityChanged
.listen((status) { .listen((status) {
if (!context.mounted) return;
if (status) { if (status) {
scaffoldMessenger.showSnackBar( showToast(
SnackBar( context: context,
content: Row( builder: (context, overlay) {
children: [ return SurfaceCard(
Icon( fillColor: theme.colorScheme.primary,
SpotubeIcons.wifi, child: Row(
color: theme.colorScheme.onPrimary, children: [
), Icon(
const SizedBox(width: 10), SpotubeIcons.wifi,
Text(context.l10n.connection_restored), color: theme.colorScheme.primaryForeground,
], ),
), const SizedBox(width: 10),
backgroundColor: theme.colorScheme.primary, Text(context.l10n.connection_restored),
showCloseIcon: true, ],
width: 350, ),
), );
},
); );
} else { } else {
scaffoldMessenger.showSnackBar( showToast(
SnackBar( context: context,
content: Row( builder: (context, overlay) {
children: [ return SurfaceCard(
Icon( fillColor: theme.colorScheme.destructive,
SpotubeIcons.noWifi, child: Row(
color: theme.colorScheme.onError, children: [
), Icon(
const SizedBox(width: 10), SpotubeIcons.noWifi,
Text(context.l10n.you_are_offline), color: theme.colorScheme.destructiveForeground,
], ),
), const SizedBox(width: 10),
backgroundColor: theme.colorScheme.error, Text(context.l10n.you_are_offline),
showCloseIcon: true, ],
width: 300, ),
), );
},
); );
} }
}), }),
connectRoutes.connectClientStream.listen((clientOrigin) { connectRoutes.connectClientStream.listen((clientOrigin) {
scaffoldMessenger.showSnackBar( if (!context.mounted) return;
SnackBar( showToast(
backgroundColor: Colors.yellow[600], context: context,
behavior: SnackBarBehavior.floating, builder: (context, overlay) {
content: Row( return SurfaceCard(
mainAxisSize: MainAxisSize.min, fillColor: Colors.yellow[600],
children: [ child: Row(
const Icon( mainAxisSize: MainAxisSize.min,
SpotubeIcons.error, children: [
color: Colors.black, const Icon(
), SpotubeIcons.error,
const SizedBox(width: 10), color: Colors.black,
Text( ),
context.l10n.connect_client_alert(clientOrigin), const SizedBox(width: 10),
style: const TextStyle(color: Colors.black), Text(
), context.l10n.connect_client_alert(clientOrigin),
], style: const TextStyle(color: Colors.black),
), ),
), ],
),
);
},
); );
}) })
]; ];
@ -156,7 +160,7 @@ class RootApp extends HookConsumerWidget {
useEndlessPlayback(ref); useEndlessPlayback(ref);
final backgroundColor = Theme.of(context).scaffoldBackgroundColor; final backgroundColor = Theme.of(context).colorScheme.background;
useEffect(() { useEffect(() {
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
@ -175,43 +179,12 @@ class RootApp extends HookConsumerWidget {
}, []); }, []);
final scaffold = Scaffold( final scaffold = Scaffold(
body: Sidebar(child: child), footers: const [
extendBody: true, BottomPlayer(),
drawerScrimColor: Colors.transparent, SpotubeNavigationBar(),
endDrawer: kIsDesktop ],
? Container( floatingFooter: true,
constraints: const BoxConstraints(maxWidth: 800), child: Sidebar(child: child),
decoration: BoxDecoration(
boxShadow: theme.brightness == Brightness.light
? null
: kElevationToShadow[8],
),
margin: const EdgeInsets.only(
top: 40,
bottom: 100,
),
child: Consumer(
builder: (context, ref, _) {
final playlist = ref.watch(audioPlayerProvider);
final playlistNotifier =
ref.read(audioPlayerProvider.notifier);
return PlayerQueue.fromAudioPlayerNotifier(
floating: true,
playlist: playlist,
notifier: playlistNotifier,
);
},
),
)
: null,
bottomNavigationBar: const Column(
mainAxisSize: MainAxisSize.min,
children: [
BottomPlayer(),
SpotubeNavigationBar(),
],
),
); );
if (!kIsAndroid) { if (!kIsAndroid) {