mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor: bottom player border, player queue using shadcn drawer
This commit is contained in:
parent
04190f2dda
commit
2488da2279
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -13,6 +13,7 @@
|
|||||||
"RGBO",
|
"RGBO",
|
||||||
"riverpod",
|
"riverpod",
|
||||||
"Scrobblenaut",
|
"Scrobblenaut",
|
||||||
|
"shadcn",
|
||||||
"skeletonizer",
|
"skeletonizer",
|
||||||
"songlink",
|
"songlink",
|
||||||
"speechiness",
|
"speechiness",
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
|
@ -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);
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
},
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user