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",
"riverpod",
"Scrobblenaut",
"shadcn",
"skeletonizer",
"songlink",
"speechiness",

View File

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

View File

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

View File

@ -3,6 +3,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.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/spotube_icons.dart';
@ -297,45 +300,45 @@ class PlayerView extends HookConsumerWidget {
color: bodyTextColor ?? Colors.white,
),
),
onPressed: currentTrack != null
? () {
showModalBottomSheet(
// enabled: currentTrack != null,
onPressed: () {
openDrawer(
context: context,
isDismissible: true,
enableDrag: true,
isScrollControlled: true,
backgroundColor: Colors.black12,
barrierDismissible: true,
draggable: true,
barrierColor: Colors.black12,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(10),
),
constraints: BoxConstraints(
maxHeight:
MediaQuery.of(context)
.size
.height *
.7,
),
borderRadius: BorderRadius.circular(10),
transformBackdrop: false,
position: OverlayPosition.bottom,
surfaceBlur: context.theme.surfaceBlur,
surfaceOpacity: 0.7,
expands: true,
builder: (context) => Consumer(
builder: (context, ref, _) {
final playlist = ref.watch(
audioPlayerProvider,
);
final playlistNotifier = ref
.read(audioPlayerProvider
.notifier);
return PlayerQueue
final playlistNotifier = ref.read(
audioPlayerProvider.notifier);
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context)
.size
.height *
0.8,
),
child: PlayerQueue
.fromAudioPlayerNotifier(
floating: false,
playlist: playlist,
notifier: playlistNotifier,
),
);
},
),
);
}
: null),
},
),
),
if (auth.asData?.value != null)
const SizedBox(width: 10),

View File

@ -2,8 +2,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/modules/player/player_queue.dart';
import 'package:spotube/modules/player/sibling_tracks_sheet.dart';
import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/heart_button/heart_button.dart';
@ -82,7 +84,32 @@ class PlayerActions extends HookConsumerWidget {
icon: const Icon(SpotubeIcons.queue),
enabled: playlist.activeTrack != null,
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,
barrierColor: Colors.black.withValues(alpha: .2),
borderRadius: BorderRadius.circular(10),
transformBackdrop: false,
builder: (context) {
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:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/fallbacks/not_found.dart';
@ -60,16 +58,6 @@ class PlayerQueue extends HookConsumerWidget {
final isSearching = useState(false);
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(
() {
@ -92,40 +80,28 @@ class PlayerQueue extends HookConsumerWidget {
[tracks, searchText.value],
);
useEffect(() {
if (playlist.activeTrack == null) return null;
controller.scrollToIndex(
playlist.playlist.index,
preferPosition: AutoScrollPosition.middle,
);
return null;
}, []);
if (tracks.isEmpty) {
return const NotFound(vertical: true);
}
return LayoutBuilder(
return Stack(
children: [
LayoutBuilder(
builder: (context, constrains) {
return ClipRRect(
borderRadius: borderRadius,
clipBehavior: Clip.hardEdge,
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 15,
sigmaY: 15,
final searchBar = ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 40,
maxWidth:
mediaQuery.smAndDown ? mediaQuery.size.width - 40 : 300,
),
child: Container(
padding: const EdgeInsets.only(
top: 5.0,
child: TextField(
onChanged: (value) {
searchText.value = value;
},
placeholder: Text(context.l10n.search),
),
decoration: BoxDecoration(
color:
theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
borderRadius: borderRadius,
),
child: CallbackShortcuts(
);
return CallbackShortcuts(
bindings: {
LogicalKeySet(LogicalKeyboardKey.escape): () {
if (!isSearching.value) {
@ -135,66 +111,14 @@ class PlayerQueue extends HookConsumerWidget {
searchText.value = '';
}
},
child: InterScrollbar(
controller: controller,
child: CustomScrollView(
controller: controller,
slivers: [
if (!floating)
SliverToBoxAdapter(
child: Center(
child: Container(
height: 5,
width: 100,
margin: const EdgeInsets.only(bottom: 5, top: 2),
decoration: BoxDecoration(
color: headlineColor,
borderRadius: BorderRadius.circular(20),
),
),
),
),
SliverAppBar(
floating: true,
pinned: false,
snap: false,
child: Column(
children: [
if (isSearching.value && mediaQuery.smAndDown)
AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
automaticallyImplyLeading: false,
title: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 10,
sigmaY: 10,
),
child: SizedBox(
height: kToolbarHeight,
child: mediaQuery.mdAndUp || !isSearching.value
? Align(
alignment: Alignment.centerLeft,
child: Text(
context.l10n
.tracks_in_queue(tracks.length),
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(
leading: [
if (mediaQuery.smAndDown)
IconButton.ghost(
icon: const Icon(
Icons.arrow_back_ios_new_outlined,
),
@ -202,22 +126,32 @@ class PlayerQueue extends HookConsumerWidget {
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,
),
),
],
surfaceBlur: 0,
surfaceOpacity: 0,
child: searchBar,
)
else
IconButton.filledTonal(
AppBar(
trailingGap: 0,
backgroundColor: Colors.transparent,
surfaceBlur: 0,
surfaceOpacity: 0,
title: mediaQuery.mdAndUp || !isSearching.value
? SizedBox(
height: 30,
child: AutoSizeText(
context.l10n.tracks_in_queue(tracks.length),
maxLines: 1,
),
)
: null,
trailing: [
if (mediaQuery.mdAndUp)
searchBar
else
IconButton.ghost(
icon: const Icon(SpotubeIcons.filter),
onPressed: () {
isSearching.value = !isSearching.value;
@ -225,29 +159,26 @@ class PlayerQueue extends HookConsumerWidget {
),
if (mediaQuery.mdAndUp || !isSearching.value) ...[
const SizedBox(width: 10),
FilledButton(
style: FilledButton.styleFrom(
backgroundColor: theme.scaffoldBackgroundColor
.withOpacity(0.5),
foregroundColor:
theme.textTheme.headlineSmall?.color,
),
child: Row(
children: [
const Icon(SpotubeIcons.playlistRemove),
const SizedBox(width: 5),
Text(context.l10n.clear_all),
],
),
Tooltip(
tooltip: Text(context.l10n.clear_all),
child: IconButton.outline(
icon: const Icon(SpotubeIcons.playlistRemove),
onPressed: () {
onStop();
Navigator.of(context).pop();
},
),
const SizedBox(width: 10),
),
],
],
),
const Divider(),
Expanded(
child: InterScrollbar(
controller: controller,
child: CustomScrollView(
controller: controller,
slivers: [
const SliverGap(10),
SliverReorderableList(
onReorder: onReorder,
@ -264,8 +195,6 @@ class PlayerQueue extends HookConsumerWidget {
key: ValueKey<int>(i),
controller: controller,
index: i,
child: Material(
color: Colors.transparent,
child: TrackTile(
playlist: playlist,
index: i,
@ -280,7 +209,8 @@ class PlayerQueue extends HookConsumerWidget {
if (!isSearching.value &&
searchText.value.isEmpty)
Padding(
padding: const EdgeInsets.only(left: 8.0),
padding:
const EdgeInsets.only(left: 8.0),
child: ReorderableDragStartListener(
index: i,
child: const Icon(
@ -290,7 +220,6 @@ class PlayerQueue extends HookConsumerWidget {
),
],
),
),
);
},
),
@ -299,10 +228,25 @@ class PlayerQueue extends HookConsumerWidget {
),
),
),
),
],
),
);
},
),
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: SizedBox(
height: 20,
width: 100,
child: Slider(
min: 0,
max: 1,
value: SliderValue.single(value),
onChanged: (v) => onChanged(v.value),
),
),
);
return Row(
mainAxisAlignment:
!fullWidth ? MainAxisAlignment.center : MainAxisAlignment.start,

View File

@ -1,9 +1,8 @@
import 'dart:ui';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/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/context.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/user_preferences/user_preferences_provider.dart';
@ -45,14 +43,6 @@ class BottomPlayer extends HookConsumerWidget {
[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
// place in the global overlay stack aka [_entries]
if (layoutMode == LayoutMode.compact ||
@ -60,11 +50,9 @@ class BottomPlayer extends HookConsumerWidget {
return PlayerOverlay(albumArt: albumArt);
}
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: DecoratedBox(
decoration: BoxDecoration(color: bgColor?.withValues(alpha: .8)),
return SurfaceCard(
borderRadius: BorderRadius.zero,
surfaceBlur: context.theme.surfaceBlur,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -81,6 +69,7 @@ class BottomPlayer extends HookConsumerWidget {
),
// add to saved tracks
Column(
mainAxisSize: MainAxisSize.min,
children: [
PlayerActions(
extraActions: [
@ -100,8 +89,7 @@ class BottomPlayer extends HookConsumerWidget {
if (!kIsLinux) {
await windowManager.setHasShadow(false);
}
await windowManager
.setAlignment(Alignment.topRight);
await windowManager.setAlignment(Alignment.topRight);
await windowManager.setSize(const Size(400, 500));
await Future.delayed(
const Duration(milliseconds: 100),
@ -138,8 +126,6 @@ class BottomPlayer extends HookConsumerWidget {
),
],
),
),
),
);
}
}

View File

@ -96,8 +96,7 @@ class Sidebar extends HookConsumerWidget {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SafeArea(
child: Column(
Column(
children: [
Expanded(
child: mediaQuery.lgAndUp
@ -120,9 +119,9 @@ class Sidebar extends HookConsumerWidget {
),
),
const SidebarFooter(),
const Gap(130)
],
),
),
const VerticalDivider(),
Expanded(child: child),
],

View File

@ -1,14 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.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/spotube_icons.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/modules/root/bottom_player.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/pages/home/home.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/server/routes/connect.dart';
import 'package:spotube/services/connectivity_adapter.dart';
@ -37,7 +35,7 @@ class RootApp extends HookConsumerWidget {
final showingDialogCompleter = useRef(Completer()..complete());
final downloader = ref.watch(downloadManagerProvider);
final scaffoldMessenger = ScaffoldMessenger.of(context);
final connectRoutes = ref.watch(serverConnectRoutesProvider);
ref.listen(glanceProvider, (_, __) {});
@ -50,50 +48,55 @@ class RootApp extends HookConsumerWidget {
final subscriptions = [
ConnectionCheckerService.instance.onConnectivityChanged
.listen((status) {
if (!context.mounted) return;
if (status) {
scaffoldMessenger.showSnackBar(
SnackBar(
content: Row(
showToast(
context: context,
builder: (context, overlay) {
return SurfaceCard(
fillColor: theme.colorScheme.primary,
child: Row(
children: [
Icon(
SpotubeIcons.wifi,
color: theme.colorScheme.onPrimary,
color: theme.colorScheme.primaryForeground,
),
const SizedBox(width: 10),
Text(context.l10n.connection_restored),
],
),
backgroundColor: theme.colorScheme.primary,
showCloseIcon: true,
width: 350,
),
);
},
);
} else {
scaffoldMessenger.showSnackBar(
SnackBar(
content: Row(
showToast(
context: context,
builder: (context, overlay) {
return SurfaceCard(
fillColor: theme.colorScheme.destructive,
child: Row(
children: [
Icon(
SpotubeIcons.noWifi,
color: theme.colorScheme.onError,
color: theme.colorScheme.destructiveForeground,
),
const SizedBox(width: 10),
Text(context.l10n.you_are_offline),
],
),
backgroundColor: theme.colorScheme.error,
showCloseIcon: true,
width: 300,
),
);
},
);
}
}),
connectRoutes.connectClientStream.listen((clientOrigin) {
scaffoldMessenger.showSnackBar(
SnackBar(
backgroundColor: Colors.yellow[600],
behavior: SnackBarBehavior.floating,
content: Row(
if (!context.mounted) return;
showToast(
context: context,
builder: (context, overlay) {
return SurfaceCard(
fillColor: Colors.yellow[600],
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
@ -107,7 +110,8 @@ class RootApp extends HookConsumerWidget {
),
],
),
),
);
},
);
})
];
@ -156,7 +160,7 @@ class RootApp extends HookConsumerWidget {
useEndlessPlayback(ref);
final backgroundColor = Theme.of(context).scaffoldBackgroundColor;
final backgroundColor = Theme.of(context).colorScheme.background;
useEffect(() {
SystemChrome.setSystemUIOverlayStyle(
@ -175,43 +179,12 @@ class RootApp extends HookConsumerWidget {
}, []);
final scaffold = Scaffold(
body: Sidebar(child: child),
extendBody: true,
drawerScrimColor: Colors.transparent,
endDrawer: kIsDesktop
? Container(
constraints: const BoxConstraints(maxWidth: 800),
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: [
footers: const [
BottomPlayer(),
SpotubeNavigationBar(),
],
),
floatingFooter: true,
child: Sidebar(child: child),
);
if (!kIsAndroid) {