From a1cc44759b63e731a7f73e24fd9ff29636e9bb77 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 8 Nov 2023 18:51:19 +0600 Subject: [PATCH] refactor: show queue from side in desktop --- lib/components/player/player_actions.dart | 22 +- lib/components/player/player_queue.dart | 338 +++++++++--------- .../player/sibling_tracks_sheet.dart | 274 +++++++------- lib/pages/root/root_app.dart | 18 + 4 files changed, 333 insertions(+), 319 deletions(-) diff --git a/lib/components/player/player_actions.dart b/lib/components/player/player_actions.dart index b3a1e340..7a248aa5 100644 --- a/lib/components/player/player_actions.dart +++ b/lib/components/player/player_actions.dart @@ -5,10 +5,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart' hide Offset; 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/shared/adaptive/adaptive_pop_sheet_list.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/duration.dart'; import 'package:spotube/models/local_track.dart'; @@ -35,6 +35,7 @@ class PlayerActions extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { + final mediaQuery = MediaQuery.of(context); final playlist = ref.watch(ProxyPlaylistNotifier.provider); final isLocalTrack = playlist.activeTrack is LocalTrack; ref.watch(downloadManagerProvider); @@ -86,23 +87,7 @@ class PlayerActions extends HookConsumerWidget { tooltip: context.l10n.queue, onPressed: playlist.activeTrack != null ? () { - showModalBottomSheet( - 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); - }, - ); + Scaffold.of(context).openEndDrawer(); } : null, ), @@ -119,6 +104,7 @@ class PlayerActions extends HookConsumerWidget { isScrollControlled: true, backgroundColor: Colors.black12, barrierColor: Colors.black12, + elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart index 725af22b..2d8ba329 100644 --- a/lib/components/player/player_queue.dart +++ b/lib/components/player/player_queue.dart @@ -36,7 +36,9 @@ class PlayerQueue extends HookConsumerWidget { final tracks = playlist.tracks; final borderRadius = floating - ? BorderRadius.circular(10) + ? const BorderRadius.only( + topLeft: Radius.circular(10), + ) : const BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10), @@ -80,140 +82,177 @@ class PlayerQueue extends HookConsumerWidget { return const NotFound(vertical: true); } - return BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 12.0, - sigmaY: 12.0, - ), - child: Container( - margin: EdgeInsets.all(floating ? 8.0 : 0), - padding: const EdgeInsets.only( - top: 5.0, + return ClipRRect( + borderRadius: borderRadius, + clipBehavior: Clip.hardEdge, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 15, + sigmaY: 15, ), - decoration: BoxDecoration( - color: theme.scaffoldBackgroundColor.withOpacity(0.5), - borderRadius: borderRadius, - ), - child: CallbackShortcuts( - bindings: { - LogicalKeySet(LogicalKeyboardKey.escape): () { - if (!isSearching.value) { - Navigator.of(context).pop(); + child: Container( + padding: const EdgeInsets.only( + top: 5.0, + ), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceVariant.withOpacity(0.5), + borderRadius: borderRadius, + ), + 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: [ - Container( - height: 5, - width: 100, - margin: const EdgeInsets.only(bottom: 5, top: 2), - decoration: BoxDecoration( - 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, - ), + }, + child: LayoutBuilder(builder: (context, constraints) { + return Column( + children: [ + if (!floating) + Container( + height: 5, + width: 100, + margin: const EdgeInsets.only(bottom: 5, top: 2), + decoration: BoxDecoration( + color: headlineColor, + borderRadius: BorderRadius.circular(20), ), - const Spacer(), - ], - if (constraints.mdAndUp || isSearching.value) - TextField( - onChanged: (value) { - searchText.value = value; - }, - decoration: InputDecoration( - hintText: context.l10n.search, - isDense: true, - prefixIcon: constraints.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: constraints.smAndDown - ? constraints.maxWidth - 20 - : 300, + ), + 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, ), ), - ) - else - IconButton.filledTonal( - icon: const Icon(SpotubeIcons.filter), - onPressed: () { - isSearching.value = !isSearching.value; - }, - ), - if (constraints.mdAndUp || !isSearching.value) ...[ - const SizedBox(width: 10), - FilledButton( - style: FilledButton.styleFrom( - backgroundColor: - theme.scaffoldBackgroundColor.withOpacity(0.5), - foregroundColor: theme.textTheme.headlineSmall?.color, + const Spacer(), + ], + if (constraints.mdAndUp || isSearching.value) + TextField( + onChanged: (value) { + searchText.value = value; + }, + decoration: InputDecoration( + hintText: context.l10n.search, + isDense: true, + prefixIcon: constraints.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: constraints.smAndDown + ? constraints.maxWidth - 20 + : 300, + ), + ), + ) + else + IconButton.filledTonal( + icon: const Icon(SpotubeIcons.filter), + onPressed: () { + isSearching.value = !isSearching.value; + }, ), - child: Row( - children: [ - const Icon(SpotubeIcons.playlistRemove), - const SizedBox(width: 5), - Text(context.l10n.clear_all), - ], + if (constraints.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), + ], + ), + onPressed: () { + playlistNotifier.stop(); + Navigator.of(context).pop(); + }, ), - onPressed: () { - playlistNotifier.stop(); - Navigator.of(context).pop(); - }, - ), - const SizedBox(width: 10), + const SizedBox(width: 10), + ], ], - ], - ), - const SizedBox(height: 10), - if (!isSearching.value && searchText.value.isEmpty) - Flexible( - child: InterScrollbar( - controller: controller, - child: ReorderableListView.builder( - onReorder: (oldIndex, newIndex) { - playlistNotifier.moveTrack(oldIndex, newIndex); - }, - scrollController: controller, - itemCount: tracks.length, - shrinkWrap: true, - buildDefaultDragHandles: false, - itemBuilder: (context, i) { - final track = tracks.elementAt(i); - return AutoScrollTag( - key: ValueKey(i), - controller: controller, - index: i, - child: Padding( + ), + const SizedBox(height: 10), + if (!isSearching.value && searchText.value.isEmpty) + Flexible( + child: InterScrollbar( + controller: controller, + child: ReorderableListView.builder( + onReorder: (oldIndex, newIndex) { + playlistNotifier.moveTrack(oldIndex, newIndex); + }, + scrollController: controller, + itemCount: tracks.length, + shrinkWrap: true, + buildDefaultDragHandles: false, + itemBuilder: (context, i) { + final track = tracks.elementAt(i); + return AutoScrollTag( + key: ValueKey(i), + controller: controller, + index: i, + 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: const EdgeInsets.symmetric(horizontal: 8.0), child: TrackTile( @@ -225,47 +264,16 @@ class PlayerQueue extends HookConsumerWidget { } 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); - }, - ), - ); - }, - ), - ), - ), - ], - ); - }), + ], + ); + }), + ), ), ), ); diff --git a/lib/components/player/sibling_tracks_sheet.dart b/lib/components/player/sibling_tracks_sheet.dart index 6587b8b3..14c042b8 100644 --- a/lib/components/player/sibling_tracks_sheet.dart +++ b/lib/components/player/sibling_tracks_sheet.dart @@ -86,151 +86,153 @@ class SiblingTracksSheet extends HookConsumerWidget { return null; }, [playlist.activeTrack]); - final itemBuilder = useCallback((YoutubeVideoInfo video) { - return ListTile( - title: Text(video.title), - leading: Padding( - padding: const EdgeInsets.all(8.0), - child: UniversalImage( - path: video.thumbnailUrl, - height: 60, - width: 60, + final itemBuilder = useCallback( + (YoutubeVideoInfo video) { + return ListTile( + title: Text(video.title), + leading: Padding( + padding: const EdgeInsets.all(8.0), + child: UniversalImage( + path: video.thumbnailUrl, + height: 60, + width: 60, + ), ), - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - trailing: Text(video.duration.toHumanReadableString()), - subtitle: Text(video.channelName), - enabled: playlist.isFetching != true, - selected: playlist.isFetching != true && - video.id == (playlist.activeTrack as SpotubeTrack).ytTrack.id, - selectedTileColor: theme.popupMenuTheme.color, - onTap: () { - if (playlist.isFetching == false && - video.id != (playlist.activeTrack as SpotubeTrack).ytTrack.id) { - playlistNotifier.swapSibling(video); - Navigator.of(context).pop(); - } - }, - ); - }, [ - playlist.isFetching, - playlist.activeTrack, - siblings, - ]); + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + trailing: Text(video.duration.toHumanReadableString()), + subtitle: Text(video.channelName), + enabled: playlist.isFetching != true, + selected: playlist.isFetching != true && + video.id == (playlist.activeTrack as SpotubeTrack).ytTrack.id, + selectedTileColor: theme.popupMenuTheme.color, + onTap: () { + if (playlist.isFetching == false && + video.id != (playlist.activeTrack as SpotubeTrack).ytTrack.id) { + playlistNotifier.swapSibling(video); + Navigator.of(context).pop(); + } + }, + ); + }, + [playlist.isFetching, playlist.activeTrack, siblings], + ); var mediaQuery = MediaQuery.of(context); return SafeArea( - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 12.0, - sigmaY: 12.0, - ), - child: AnimatedSize( - duration: const Duration(milliseconds: 300), - child: Container( - height: isSearching.value && mediaQuery.smAndDown - ? mediaQuery.size.height - : mediaQuery.size.height * .6, - margin: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - borderRadius: borderRadius, - color: theme.scaffoldBackgroundColor.withOpacity(.3), - ), - child: Scaffold( - backgroundColor: Colors.transparent, - appBar: AppBar( - 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, + child: ClipRRect( + borderRadius: borderRadius, + clipBehavior: Clip.hardEdge, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 12.0, + sigmaY: 12.0, + ), + child: AnimatedSize( + duration: const Duration(milliseconds: 300), + child: Container( + height: isSearching.value && mediaQuery.smAndDown + ? mediaQuery.size.height - mediaQuery.padding.top + : mediaQuery.size.height * .6, + decoration: BoxDecoration( + borderRadius: borderRadius, + color: theme.colorScheme.surfaceVariant.withOpacity(.5), + ), + child: Scaffold( 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; + appBar: AppBar( + 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, + 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]), - ), - ); - }, - ), - }, ), ), ), diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index bdbc1c75..5797b63f 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -3,11 +3,13 @@ import 'dart:async'; import 'package:fl_query/fl_query.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_desktop_tools/flutter_desktop_tools.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.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/root/bottom_player.dart'; import 'package:spotube/components/root/sidebar.dart'; @@ -164,6 +166,22 @@ class RootApp extends HookConsumerWidget { child: child, ), 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( mainAxisSize: MainAxisSize.min, children: [