refactor: show queue from side in desktop

This commit is contained in:
Kingkor Roy Tirtho 2023-11-08 18:51:19 +06:00
parent da04f068f9
commit a1cc44759b
4 changed files with 333 additions and 319 deletions

View File

@ -5,10 +5,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart' hide Offset; import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/player/player_queue.dart';
import 'package:spotube/components/player/sibling_tracks_sheet.dart'; import 'package:spotube/components/player/sibling_tracks_sheet.dart';
import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/components/shared/heart_button.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
@ -35,6 +35,7 @@ class PlayerActions extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final mediaQuery = MediaQuery.of(context);
final playlist = ref.watch(ProxyPlaylistNotifier.provider); final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final isLocalTrack = playlist.activeTrack is LocalTrack; final isLocalTrack = playlist.activeTrack is LocalTrack;
ref.watch(downloadManagerProvider); ref.watch(downloadManagerProvider);
@ -86,23 +87,7 @@ class PlayerActions extends HookConsumerWidget {
tooltip: context.l10n.queue, tooltip: context.l10n.queue,
onPressed: playlist.activeTrack != null onPressed: playlist.activeTrack != null
? () { ? () {
showModalBottomSheet( Scaffold.of(context).openEndDrawer();
context: context,
isDismissible: true,
enableDrag: true,
isScrollControlled: true,
backgroundColor: Colors.black12,
barrierColor: Colors.black12,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * .7,
),
builder: (context) {
return PlayerQueue(floating: floatingQueue);
},
);
} }
: null, : null,
), ),
@ -119,6 +104,7 @@ class PlayerActions extends HookConsumerWidget {
isScrollControlled: true, isScrollControlled: true,
backgroundColor: Colors.black12, backgroundColor: Colors.black12,
barrierColor: Colors.black12, barrierColor: Colors.black12,
elevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),

View File

@ -36,7 +36,9 @@ class PlayerQueue extends HookConsumerWidget {
final tracks = playlist.tracks; final tracks = playlist.tracks;
final borderRadius = floating final borderRadius = floating
? BorderRadius.circular(10) ? const BorderRadius.only(
topLeft: Radius.circular(10),
)
: const BorderRadius.only( : const BorderRadius.only(
topLeft: Radius.circular(10), topLeft: Radius.circular(10),
topRight: Radius.circular(10), topRight: Radius.circular(10),
@ -80,140 +82,177 @@ class PlayerQueue extends HookConsumerWidget {
return const NotFound(vertical: true); return const NotFound(vertical: true);
} }
return BackdropFilter( return ClipRRect(
filter: ImageFilter.blur( borderRadius: borderRadius,
sigmaX: 12.0, clipBehavior: Clip.hardEdge,
sigmaY: 12.0, child: BackdropFilter(
), filter: ImageFilter.blur(
child: Container( sigmaX: 15,
margin: EdgeInsets.all(floating ? 8.0 : 0), sigmaY: 15,
padding: const EdgeInsets.only(
top: 5.0,
), ),
decoration: BoxDecoration( child: Container(
color: theme.scaffoldBackgroundColor.withOpacity(0.5), padding: const EdgeInsets.only(
borderRadius: borderRadius, top: 5.0,
), ),
child: CallbackShortcuts( decoration: BoxDecoration(
bindings: { color: theme.colorScheme.surfaceVariant.withOpacity(0.5),
LogicalKeySet(LogicalKeyboardKey.escape): () { borderRadius: borderRadius,
if (!isSearching.value) { ),
Navigator.of(context).pop(); child: CallbackShortcuts(
bindings: {
LogicalKeySet(LogicalKeyboardKey.escape): () {
if (!isSearching.value) {
Navigator.of(context).pop();
}
isSearching.value = false;
searchText.value = '';
} }
isSearching.value = false; },
searchText.value = ''; child: LayoutBuilder(builder: (context, constraints) {
} return Column(
}, children: [
child: LayoutBuilder(builder: (context, constraints) { if (!floating)
return Column( Container(
children: [ height: 5,
Container( width: 100,
height: 5, margin: const EdgeInsets.only(bottom: 5, top: 2),
width: 100, decoration: BoxDecoration(
margin: const EdgeInsets.only(bottom: 5, top: 2), color: headlineColor,
decoration: BoxDecoration( borderRadius: BorderRadius.circular(20),
color: headlineColor,
borderRadius: BorderRadius.circular(20),
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (constraints.mdAndUp || !isSearching.value) ...[
const SizedBox(width: 10),
Text(
context.l10n.tracks_in_queue(tracks.length),
style: TextStyle(
color: headlineColor,
fontWeight: FontWeight.bold,
fontSize: 18,
),
), ),
const Spacer(), ),
], Row(
if (constraints.mdAndUp || isSearching.value) crossAxisAlignment: CrossAxisAlignment.center,
TextField( mainAxisAlignment: MainAxisAlignment.center,
onChanged: (value) { children: [
searchText.value = value; if (constraints.mdAndUp || !isSearching.value) ...[
}, const SizedBox(width: 10),
decoration: InputDecoration( Text(
hintText: context.l10n.search, context.l10n.tracks_in_queue(tracks.length),
isDense: true, style: TextStyle(
prefixIcon: constraints.smAndDown color: headlineColor,
? IconButton( fontWeight: FontWeight.bold,
icon: const Icon( fontSize: 18,
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: constraints.smAndDown
? constraints.maxWidth - 20
: 300,
), ),
), ),
) const Spacer(),
else ],
IconButton.filledTonal( if (constraints.mdAndUp || isSearching.value)
icon: const Icon(SpotubeIcons.filter), TextField(
onPressed: () { onChanged: (value) {
isSearching.value = !isSearching.value; searchText.value = value;
}, },
), decoration: InputDecoration(
if (constraints.mdAndUp || !isSearching.value) ...[ hintText: context.l10n.search,
const SizedBox(width: 10), isDense: true,
FilledButton( prefixIcon: constraints.smAndDown
style: FilledButton.styleFrom( ? IconButton(
backgroundColor: icon: const Icon(
theme.scaffoldBackgroundColor.withOpacity(0.5), Icons.arrow_back_ios_new_outlined,
foregroundColor: theme.textTheme.headlineSmall?.color, ),
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: constraints.smAndDown
? constraints.maxWidth - 20
: 300,
),
),
)
else
IconButton.filledTonal(
icon: const Icon(SpotubeIcons.filter),
onPressed: () {
isSearching.value = !isSearching.value;
},
), ),
child: Row( if (constraints.mdAndUp || !isSearching.value) ...[
children: [ const SizedBox(width: 10),
const Icon(SpotubeIcons.playlistRemove), FilledButton(
const SizedBox(width: 5), style: FilledButton.styleFrom(
Text(context.l10n.clear_all), 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),
],
),
onPressed: () {
playlistNotifier.stop();
Navigator.of(context).pop();
},
), ),
onPressed: () { const SizedBox(width: 10),
playlistNotifier.stop(); ],
Navigator.of(context).pop();
},
),
const SizedBox(width: 10),
], ],
], ),
), const SizedBox(height: 10),
const SizedBox(height: 10), if (!isSearching.value && searchText.value.isEmpty)
if (!isSearching.value && searchText.value.isEmpty) Flexible(
Flexible( child: InterScrollbar(
child: InterScrollbar( controller: controller,
controller: controller, child: ReorderableListView.builder(
child: ReorderableListView.builder( onReorder: (oldIndex, newIndex) {
onReorder: (oldIndex, newIndex) { playlistNotifier.moveTrack(oldIndex, newIndex);
playlistNotifier.moveTrack(oldIndex, newIndex); },
}, scrollController: controller,
scrollController: controller, itemCount: tracks.length,
itemCount: tracks.length, shrinkWrap: true,
shrinkWrap: true, buildDefaultDragHandles: false,
buildDefaultDragHandles: false, itemBuilder: (context, i) {
itemBuilder: (context, i) { final track = tracks.elementAt(i);
final track = tracks.elementAt(i); return AutoScrollTag(
return AutoScrollTag( key: ValueKey(i),
key: ValueKey(i), controller: controller,
controller: controller, index: i,
index: i, child: Padding(
child: Padding( padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: TrackTile(
index: i,
track: track,
onTap: () async {
if (playlist.activeTrack?.id == track.id) {
return;
}
await playlistNotifier.jumpToTrack(track);
},
leadingActions: [
ReorderableDragStartListener(
index: i,
child:
const Icon(SpotubeIcons.dragHandle),
),
],
),
),
);
},
),
),
)
else
Flexible(
child: InterScrollbar(
child: ListView.builder(
itemCount: filteredTracks.length,
itemBuilder: (context, i) {
final track = filteredTracks.elementAt(i);
return Padding(
padding: padding:
const EdgeInsets.symmetric(horizontal: 8.0), const EdgeInsets.symmetric(horizontal: 8.0),
child: TrackTile( child: TrackTile(
@ -225,47 +264,16 @@ class PlayerQueue extends HookConsumerWidget {
} }
await playlistNotifier.jumpToTrack(track); await playlistNotifier.jumpToTrack(track);
}, },
leadingActions: [
ReorderableDragStartListener(
index: i,
child: const Icon(SpotubeIcons.dragHandle),
),
],
), ),
), );
); },
}, ),
), ),
), ),
) ],
else );
Flexible( }),
child: InterScrollbar( ),
child: ListView.builder(
itemCount: filteredTracks.length,
itemBuilder: (context, i) {
final track = filteredTracks.elementAt(i);
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: TrackTile(
index: i,
track: track,
onTap: () async {
if (playlist.activeTrack?.id == track.id) {
return;
}
await playlistNotifier.jumpToTrack(track);
},
),
);
},
),
),
),
],
);
}),
), ),
), ),
); );

View File

@ -86,151 +86,153 @@ class SiblingTracksSheet extends HookConsumerWidget {
return null; return null;
}, [playlist.activeTrack]); }, [playlist.activeTrack]);
final itemBuilder = useCallback((YoutubeVideoInfo video) { final itemBuilder = useCallback(
return ListTile( (YoutubeVideoInfo video) {
title: Text(video.title), return ListTile(
leading: Padding( title: Text(video.title),
padding: const EdgeInsets.all(8.0), leading: Padding(
child: UniversalImage( padding: const EdgeInsets.all(8.0),
path: video.thumbnailUrl, child: UniversalImage(
height: 60, path: video.thumbnailUrl,
width: 60, height: 60,
width: 60,
),
), ),
), shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5),
borderRadius: BorderRadius.circular(5), ),
), trailing: Text(video.duration.toHumanReadableString()),
trailing: Text(video.duration.toHumanReadableString()), subtitle: Text(video.channelName),
subtitle: Text(video.channelName), enabled: playlist.isFetching != true,
enabled: playlist.isFetching != true, selected: playlist.isFetching != true &&
selected: playlist.isFetching != true && video.id == (playlist.activeTrack as SpotubeTrack).ytTrack.id,
video.id == (playlist.activeTrack as SpotubeTrack).ytTrack.id, selectedTileColor: theme.popupMenuTheme.color,
selectedTileColor: theme.popupMenuTheme.color, onTap: () {
onTap: () { if (playlist.isFetching == false &&
if (playlist.isFetching == false && video.id != (playlist.activeTrack as SpotubeTrack).ytTrack.id) {
video.id != (playlist.activeTrack as SpotubeTrack).ytTrack.id) { playlistNotifier.swapSibling(video);
playlistNotifier.swapSibling(video); Navigator.of(context).pop();
Navigator.of(context).pop(); }
} },
}, );
); },
}, [ [playlist.isFetching, playlist.activeTrack, siblings],
playlist.isFetching, );
playlist.activeTrack,
siblings,
]);
var mediaQuery = MediaQuery.of(context); var mediaQuery = MediaQuery.of(context);
return SafeArea( return SafeArea(
child: BackdropFilter( child: ClipRRect(
filter: ImageFilter.blur( borderRadius: borderRadius,
sigmaX: 12.0, clipBehavior: Clip.hardEdge,
sigmaY: 12.0, child: BackdropFilter(
), filter: ImageFilter.blur(
child: AnimatedSize( sigmaX: 12.0,
duration: const Duration(milliseconds: 300), sigmaY: 12.0,
child: Container( ),
height: isSearching.value && mediaQuery.smAndDown child: AnimatedSize(
? mediaQuery.size.height duration: const Duration(milliseconds: 300),
: mediaQuery.size.height * .6, child: Container(
margin: const EdgeInsets.all(8.0), height: isSearching.value && mediaQuery.smAndDown
decoration: BoxDecoration( ? mediaQuery.size.height - mediaQuery.padding.top
borderRadius: borderRadius, : mediaQuery.size.height * .6,
color: theme.scaffoldBackgroundColor.withOpacity(.3), decoration: BoxDecoration(
), borderRadius: borderRadius,
child: Scaffold( color: theme.colorScheme.surfaceVariant.withOpacity(.5),
backgroundColor: Colors.transparent, ),
appBar: AppBar( child: Scaffold(
centerTitle: true,
title: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: !isSearching.value
? Text(
context.l10n.alternative_track_sources,
style: theme.textTheme.headlineSmall,
)
: TextField(
autofocus: true,
controller: searchController,
decoration: InputDecoration(
hintText: context.l10n.search,
hintStyle: theme.textTheme.headlineSmall,
border: InputBorder.none,
),
style: theme.textTheme.headlineSmall,
),
),
automaticallyImplyLeading: false,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
actions: [ appBar: AppBar(
if (!isSearching.value) centerTitle: true,
IconButton( title: AnimatedSwitcher(
icon: const Icon(SpotubeIcons.search, size: 18), duration: const Duration(milliseconds: 300),
onPressed: () { child: !isSearching.value
isSearching.value = true; ? Text(
}, context.l10n.alternative_track_sources,
) style: theme.textTheme.headlineSmall,
else ...[ )
if (preferences.youtubeApiType == YoutubeApiType.piped) : TextField(
PopupMenuButton( autofocus: true,
icon: const Icon(SpotubeIcons.filter, size: 18), controller: searchController,
onSelected: (SearchMode mode) { decoration: InputDecoration(
searchMode.value = mode; hintText: context.l10n.search,
hintStyle: theme.textTheme.headlineSmall,
border: InputBorder.none,
),
style: theme.textTheme.headlineSmall,
),
),
automaticallyImplyLeading: false,
backgroundColor: Colors.transparent,
actions: [
if (!isSearching.value)
IconButton(
icon: const Icon(SpotubeIcons.search, size: 18),
onPressed: () {
isSearching.value = true;
},
)
else ...[
if (preferences.youtubeApiType == YoutubeApiType.piped)
PopupMenuButton(
icon: const Icon(SpotubeIcons.filter, size: 18),
onSelected: (SearchMode mode) {
searchMode.value = mode;
},
initialValue: searchMode.value,
itemBuilder: (context) => SearchMode.values
.map(
(e) => PopupMenuItem(
value: e,
child: Text(e.label),
),
)
.toList(),
),
IconButton(
icon: const Icon(SpotubeIcons.close, size: 18),
onPressed: () {
isSearching.value = false;
}, },
initialValue: searchMode.value,
itemBuilder: (context) => SearchMode.values
.map(
(e) => PopupMenuItem(
value: e,
child: Text(e.label),
),
)
.toList(),
), ),
IconButton( ]
icon: const Icon(SpotubeIcons.close, size: 18), ],
onPressed: () { ),
isSearching.value = false; body: Padding(
padding: const EdgeInsets.all(8.0),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: InterScrollbar(
child: switch (isSearching.value) {
false => ListView.builder(
itemCount: siblings.length,
itemBuilder: (context, index) =>
itemBuilder(siblings[index]),
),
true => FutureBuilder(
future: searchRequest,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(snapshot.error.toString()),
);
} else if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator());
}
return InterScrollbar(
child: ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) =>
itemBuilder(snapshot.data![index]),
),
);
},
),
}, },
), ),
]
],
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: InterScrollbar(
child: switch (isSearching.value) {
false => ListView.builder(
itemCount: siblings.length,
itemBuilder: (context, index) =>
itemBuilder(siblings[index]),
),
true => FutureBuilder(
future: searchRequest,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(snapshot.error.toString()),
);
} else if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator());
}
return InterScrollbar(
child: ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) =>
itemBuilder(snapshot.data![index]),
),
);
},
),
},
), ),
), ),
), ),

View File

@ -3,11 +3,13 @@ import 'dart:async';
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_desktop_tools/flutter_desktop_tools.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:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/player/player_queue.dart';
import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart'; import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart';
import 'package:spotube/components/root/bottom_player.dart'; import 'package:spotube/components/root/bottom_player.dart';
import 'package:spotube/components/root/sidebar.dart'; import 'package:spotube/components/root/sidebar.dart';
@ -164,6 +166,22 @@ class RootApp extends HookConsumerWidget {
child: child, child: child,
), ),
extendBody: true, extendBody: true,
drawerScrimColor: Colors.transparent,
endDrawer: DesktopTools.platform.isDesktop
? 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: const PlayerQueue(floating: true),
)
: null,
bottomNavigationBar: Column( bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [