diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index e2098570..778558f6 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -283,12 +283,17 @@ class UserLocalTracks extends HookConsumerWidget { trackSnapshot.isLoading ? 5 : filteredTracks.length, itemBuilder: (context, index) { if (trackSnapshot.isLoading) { - return TrackTile(track: FakeData.track, index: index); + return TrackTile( + playlist: playlist, + track: FakeData.track, + index: index, + ); } final track = filteredTracks[index]; return TrackTile( index: index, + playlist: playlist, track: track, userPlaylist: false, onTap: () async { @@ -311,8 +316,11 @@ class UserLocalTracks extends HookConsumerWidget { enabled: true, child: ListView.builder( itemCount: 5, - itemBuilder: (context, index) => - TrackTile(track: FakeData.track, index: index), + itemBuilder: (context, index) => TrackTile( + track: FakeData.track, + index: index, + playlist: playlist, + ), ), ), ), diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart index 4cb4acac..fff10b9f 100644 --- a/lib/components/player/player_queue.dart +++ b/lib/components/player/player_queue.dart @@ -66,7 +66,6 @@ class PlayerQueue extends HookConsumerWidget { topRight: Radius.circular(10), ); final theme = Theme.of(context); - final mediaQuery = MediaQuery.of(context); final headlineColor = theme.textTheme.headlineSmall?.color; final filteredTracks = useMemoized( @@ -105,201 +104,206 @@ class PlayerQueue extends HookConsumerWidget { return const NotFound(vertical: true); } - return ClipRRect( - borderRadius: borderRadius, - clipBehavior: Clip.hardEdge, - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 15, - sigmaY: 15, - ), - child: Container( - padding: const EdgeInsets.only( - top: 5.0, + return LayoutBuilder(builder: (context, constrains) { + return ClipRRect( + borderRadius: borderRadius, + clipBehavior: Clip.hardEdge, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 15, + sigmaY: 15, ), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceVariant.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: 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), - ), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (mediaQuery.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: 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 (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, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (constrains.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 (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), - ], - ), - onPressed: () { - onStop(); - Navigator.of(context).pop(); - }, - ), - const SizedBox(width: 10), - ], - ], - ), - const SizedBox(height: 10), - if (!isSearching.value && searchText.value.isEmpty) - Flexible( - child: ReorderableListView.builder( - onReorder: (oldIndex, newIndex) { - onReorder(oldIndex, newIndex); - }, - scrollController: controller, - itemCount: tracks.length, - shrinkWrap: true, - buildDefaultDragHandles: false, - onReorderStart: (index) { - HapticFeedback.selectionClick(); - }, - onReorderEnd: (index) { - HapticFeedback.selectionClick(); - }, - 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; - } - onJump(track); - }, - leadingActions: [ - ReorderableDragStartListener( - index: i, - child: const Icon(SpotubeIcons.dragHandle), - ), - ], + const Spacer(), + ], + if (constrains.mdAndUp || isSearching.value) + TextField( + onChanged: (value) { + searchText.value = value; + }, + decoration: InputDecoration( + hintText: context.l10n.search, + isDense: true, + prefixIcon: constrains.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: constrains.smAndDown + ? constrains.maxWidth - 40 + : 300, ), ), - ); - }, - ), - ) - else - Flexible( - child: InterScrollbar( - controller: controller, - child: ListView.builder( - controller: controller, - itemCount: filteredTracks.length, + ) + else + IconButton.filledTonal( + icon: const Icon(SpotubeIcons.filter), + onPressed: () { + isSearching.value = !isSearching.value; + }, + ), + if (constrains.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: () { + onStop(); + Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 10), + ], + ], + ), + const SizedBox(height: 10), + if (!isSearching.value && searchText.value.isEmpty) + Flexible( + child: ReorderableListView.builder( + onReorder: (oldIndex, newIndex) { + onReorder(oldIndex, newIndex); + }, + scrollController: controller, + itemCount: tracks.length, + shrinkWrap: true, + buildDefaultDragHandles: false, + onReorderStart: (index) { + HapticFeedback.selectionClick(); + }, + onReorderEnd: (index) { + HapticFeedback.selectionClick(); + }, 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; - } - onJump(track); - }, + 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, + playlist: playlist, + onTap: () async { + if (playlist.activeTrack?.id == track.id) { + return; + } + onJump(track); + }, + leadingActions: [ + ReorderableDragStartListener( + index: i, + child: const Icon(SpotubeIcons.dragHandle), + ), + ], + ), ), ); }, ), + ) + else + Flexible( + child: InterScrollbar( + controller: controller, + child: ListView.builder( + controller: controller, + itemCount: filteredTracks.length, + itemBuilder: (context, i) { + final track = filteredTracks.elementAt(i); + return Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: TrackTile( + index: i, + playlist: playlist, + track: track, + onTap: () async { + if (playlist.activeTrack?.id == track.id) { + return; + } + onJump(track); + }, + ), + ); + }, + ), + ), ), - ), - ], + ], + ), ), ), ), - ), - ); + ); + }); } } diff --git a/lib/components/shared/track_tile/track_tile.dart b/lib/components/shared/track_tile/track_tile.dart index 897abdae..61061d24 100644 --- a/lib/components/shared/track_tile/track_tile.dart +++ b/lib/components/shared/track_tile/track_tile.dart @@ -18,7 +18,7 @@ import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/provider/blacklist_provider.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; class TrackTile extends HookConsumerWidget { /// [index] will not be shown if null @@ -30,6 +30,7 @@ class TrackTile extends HookConsumerWidget { final VoidCallback? onLongPress; final bool userPlaylist; final String? playlistId; + final ProxyPlaylist playlist; final List? leadingActions; @@ -38,6 +39,7 @@ class TrackTile extends HookConsumerWidget { this.index, required this.track, this.selected = false, + required this.playlist, this.onTap, this.onLongPress, this.onChanged, @@ -48,7 +50,6 @@ class TrackTile extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - final playlist = ref.watch(ProxyPlaylistNotifier.provider); final theme = Theme.of(context); final blacklist = ref.watch(BlackListNotifier.provider); @@ -65,10 +66,10 @@ class TrackTile extends HookConsumerWidget { final showOptionCbRef = useRef?>(null); - final isPlaying = track.id == playlist.activeTrack?.id; - final isLoading = useState(false); + final isPlaying = playlist.activeTrack?.id == track.id; + final isSelected = isPlaying || isLoading.value; return LayoutBuilder(builder: (context, constrains) { diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body.dart b/lib/components/shared/tracks_view/sections/body/track_view_body.dart index 661e5af4..ee5b8da7 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/shared/tracks_view/sections/body/track_view_body.dart @@ -89,6 +89,7 @@ class TrackViewBodySection extends HookConsumerWidget { loadingBuilder: (context) => Skeletonizer( enabled: true, child: TrackTile( + playlist: playlist, track: FakeData.track, index: 0, ), @@ -98,13 +99,18 @@ class TrackViewBodySection extends HookConsumerWidget { child: Column( children: List.generate( 10, - (index) => TrackTile(track: FakeData.track, index: index), + (index) => TrackTile( + track: FakeData.track, + index: index, + playlist: playlist, + ), ), ), ), itemBuilder: (context, index) { final track = tracks[index]; return TrackTile( + playlist: playlist, track: track, index: index, selected: trackViewState.selectedTrackIds.contains(track.id!), diff --git a/lib/pages/artist/section/top_tracks.dart b/lib/pages/artist/section/top_tracks.dart index 173ace54..f5b0cddf 100644 --- a/lib/pages/artist/section/top_tracks.dart +++ b/lib/pages/artist/section/top_tracks.dart @@ -107,6 +107,7 @@ class ArtistPageTopTracks extends HookConsumerWidget { final track = topTracks.elementAt(index); return TrackTile( index: index, + playlist: playlist, track: track, onTap: () async { playPlaylist( diff --git a/lib/pages/connect/control/control.dart b/lib/pages/connect/control/control.dart index 53f51969..f1798dbc 100644 --- a/lib/pages/connect/control/control.dart +++ b/lib/pages/connect/control/control.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -9,6 +8,7 @@ import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/links/anchor_button.dart'; import 'package:spotube/components/shared/links/artist_link.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart'; +import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/image.dart'; @@ -49,220 +49,250 @@ class ConnectControlPage extends HookConsumerWidget { minimumSize: const Size(28, 28), ); + final playerQueue = Consumer(builder: (context, ref, _) { + final playlist = ref.watch(queueProvider); + return PlayerQueue( + playlist: playlist, + floating: true, + onJump: (track) async { + final index = playlist.tracks.toList().indexOf(track); + connectNotifier.jumpTo(index); + }, + onRemove: (track) async { + await connectNotifier.removeTrack(track); + }, + onStop: () async => connectNotifier.stop(), + onReorder: (oldIndex, newIndex) async { + await connectNotifier.reorder( + (oldIndex: oldIndex, newIndex: newIndex), + ); + }, + ); + }); + ref.listen(connectClientsProvider, (prev, next) { if (next.asData?.value.resolvedService == null) { context.pop(); } }); - return Scaffold( - appBar: PageWindowTitleBar( - title: Text(resolvedService!.name), - automaticallyImplyLeading: true, - ), - body: CustomScrollView( - slivers: [ - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: UniversalImage( - path: (playlist.activeTrack?.album?.images).asUrlString( - placeholder: ImagePlaceholder.albumArt, - ), - fit: BoxFit.cover, - ), - ), - ), - ), - const SliverGap(10), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 20), - sliver: SliverMainAxisGroup( - slivers: [ - SliverToBoxAdapter( - child: AnchorButton( - playlist.activeTrack?.name ?? "", - style: textTheme.titleLarge!, - onTap: () { - ServiceUtils.push( - context, - "/track/${playlist.activeTrack?.id}", - ); - }, - ), - ), - SliverToBoxAdapter( - child: ArtistLink( - artists: playlist.activeTrack?.artists ?? [], - textStyle: textTheme.bodyMedium!, - mainAxisAlignment: WrapAlignment.start, - ), - ), - ], - ), - ), - const SliverGap(30), - SliverToBoxAdapter( - child: Consumer( - builder: (context, ref, _) { - final position = ref.watch(positionProvider); - final duration = ref.watch(durationProvider); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Column( - children: [ - Slider( - value: position > duration - ? 0 - : position.inSeconds.toDouble(), - min: 0, - max: duration.inSeconds.toDouble(), - onChanged: (value) { - connectNotifier - .seek(Duration(seconds: value.toInt())); - }, + return SafeArea( + child: Scaffold( + appBar: PageWindowTitleBar( + title: Text(resolvedService!.name), + automaticallyImplyLeading: true, + ), + body: LayoutBuilder(builder: (context, constrains) { + return Row( + children: [ + Expanded( + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ).copyWith(top: 0), + constraints: + const BoxConstraints(maxHeight: 400, maxWidth: 400), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: UniversalImage( + path: (playlist.activeTrack?.album?.images) + .asUrlString( + placeholder: ImagePlaceholder.albumArt, + ), + fit: BoxFit.cover, + ), + ), ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(position.toHumanReadableString()), - Text(duration.toHumanReadableString()), + ), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 20), + sliver: SliverMainAxisGroup( + slivers: [ + SliverToBoxAdapter( + child: AnchorButton( + playlist.activeTrack?.name ?? "", + style: textTheme.titleLarge!, + onTap: () { + ServiceUtils.push( + context, + "/track/${playlist.activeTrack?.id}", + ); + }, + ), + ), + SliverToBoxAdapter( + child: ArtistLink( + artists: playlist.activeTrack?.artists ?? [], + textStyle: textTheme.bodyMedium!, + mainAxisAlignment: WrapAlignment.start, + ), + ), ], ), - ], - ), - ); - }, - ), - ), - SliverToBoxAdapter( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - tooltip: shuffled - ? context.l10n.unshuffle_playlist - : context.l10n.shuffle_playlist, - icon: const Icon(SpotubeIcons.shuffle), - style: shuffled ? activeButtonStyle : buttonStyle, - onPressed: playlist.activeTrack == null - ? null - : () { - connectNotifier.setShuffle(!shuffled); - }, - ), - IconButton( - tooltip: context.l10n.previous_track, - icon: const Icon(SpotubeIcons.skipBack), - onPressed: playlist.activeTrack == null - ? null - : connectNotifier.previous, - ), - IconButton( - tooltip: playing - ? context.l10n.pause_playback - : context.l10n.resume_playback, - icon: playlist.activeTrack == null - ? SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - color: colorScheme.onPrimary, - ), - ) - : Icon( - playing ? SpotubeIcons.pause : SpotubeIcons.play, - ), - style: resumePauseStyle, - onPressed: playlist.activeTrack == null - ? null - : () { - if (playing) { - connectNotifier.pause(); - } else { - connectNotifier.resume(); - } - }, - ), - IconButton( - tooltip: context.l10n.next_track, - icon: const Icon(SpotubeIcons.skipForward), - onPressed: playlist.activeTrack == null - ? null - : connectNotifier.next, - ), - IconButton( - tooltip: loopMode == PlaybackLoopMode.one - ? context.l10n.loop_track - : loopMode == PlaybackLoopMode.all - ? context.l10n.repeat_playlist - : null, - icon: Icon( - loopMode == PlaybackLoopMode.one - ? SpotubeIcons.repeatOne - : SpotubeIcons.repeat, - ), - style: loopMode == PlaybackLoopMode.one || - loopMode == PlaybackLoopMode.all - ? activeButtonStyle - : buttonStyle, - onPressed: playlist.activeTrack == null - ? null - : () async { - connectNotifier.setLoopMode( - switch (loopMode) { - PlaybackLoopMode.all => PlaybackLoopMode.one, - PlaybackLoopMode.one => PlaybackLoopMode.none, - PlaybackLoopMode.none => PlaybackLoopMode.all, - }, + ), + const SliverGap(30), + SliverToBoxAdapter( + child: Consumer( + builder: (context, ref, _) { + final position = ref.watch(positionProvider); + final duration = ref.watch(durationProvider); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + children: [ + Slider( + value: position > duration + ? 0 + : position.inSeconds.toDouble(), + min: 0, + max: duration.inSeconds.toDouble(), + onChanged: (value) { + connectNotifier + .seek(Duration(seconds: value.toInt())); + }, + ), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text(position.toHumanReadableString()), + Text(duration.toHumanReadableString()), + ], + ), + ], + ), ); }, - ) - ], - ), - ), - const SliverGap(30), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 20), - sliver: SliverToBoxAdapter( - child: OutlinedButton.icon( - icon: const Icon(SpotubeIcons.queue), - label: Text(context.l10n.queue), - onPressed: () { - showModalBottomSheet( - context: context, - builder: (context) { - return Consumer(builder: (context, ref, _) { - final playlist = ref.watch(queueProvider); - return PlayerQueue( - playlist: playlist, - floating: true, - onJump: (track) async { - final index = - playlist.tracks.toList().indexOf(track); - connectNotifier.jumpTo(index); - }, - onRemove: (track) async { - await connectNotifier.removeTrack(track); - }, - onStop: () async => connectNotifier.stop(), - onReorder: (oldIndex, newIndex) async { - await connectNotifier.reorder( - (oldIndex: oldIndex, newIndex: newIndex), - ); - }, - ); - }); - }, - ); - }, + ), + ), + SliverToBoxAdapter( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + tooltip: shuffled + ? context.l10n.unshuffle_playlist + : context.l10n.shuffle_playlist, + icon: const Icon(SpotubeIcons.shuffle), + style: shuffled ? activeButtonStyle : buttonStyle, + onPressed: playlist.activeTrack == null + ? null + : () { + connectNotifier.setShuffle(!shuffled); + }, + ), + IconButton( + tooltip: context.l10n.previous_track, + icon: const Icon(SpotubeIcons.skipBack), + onPressed: playlist.activeTrack == null + ? null + : connectNotifier.previous, + ), + IconButton( + tooltip: playing + ? context.l10n.pause_playback + : context.l10n.resume_playback, + icon: playlist.activeTrack == null + ? SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: colorScheme.onPrimary, + ), + ) + : Icon( + playing + ? SpotubeIcons.pause + : SpotubeIcons.play, + ), + style: resumePauseStyle, + onPressed: playlist.activeTrack == null + ? null + : () { + if (playing) { + connectNotifier.pause(); + } else { + connectNotifier.resume(); + } + }, + ), + IconButton( + tooltip: context.l10n.next_track, + icon: const Icon(SpotubeIcons.skipForward), + onPressed: playlist.activeTrack == null + ? null + : connectNotifier.next, + ), + IconButton( + tooltip: loopMode == PlaybackLoopMode.one + ? context.l10n.loop_track + : loopMode == PlaybackLoopMode.all + ? context.l10n.repeat_playlist + : null, + icon: Icon( + loopMode == PlaybackLoopMode.one + ? SpotubeIcons.repeatOne + : SpotubeIcons.repeat, + ), + style: loopMode == PlaybackLoopMode.one || + loopMode == PlaybackLoopMode.all + ? activeButtonStyle + : buttonStyle, + onPressed: playlist.activeTrack == null + ? null + : () async { + connectNotifier.setLoopMode( + switch (loopMode) { + PlaybackLoopMode.all => + PlaybackLoopMode.one, + PlaybackLoopMode.one => + PlaybackLoopMode.none, + PlaybackLoopMode.none => + PlaybackLoopMode.all, + }, + ); + }, + ) + ], + ), + ), + const SliverGap(30), + if (constrains.mdAndDown) + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 20), + sliver: SliverToBoxAdapter( + child: OutlinedButton.icon( + icon: const Icon(SpotubeIcons.queue), + label: Text(context.l10n.queue), + onPressed: () { + showModalBottomSheet( + context: context, + builder: (context) { + return playerQueue; + }, + ); + }, + ), + ), + ) + ], + ), ), - ), - ) - ], + if (constrains.lgAndUp) ...[ + const VerticalDivider(thickness: 1), + Expanded( + child: playerQueue, + ), + ] + ], + ); + }), ), ); } diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index 1217f7af..a3590378 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -53,7 +53,8 @@ class HomePage extends HookConsumerWidget { }, ); }, - ) + ), + const Gap(10), ], ), const HomeGenresSection(), diff --git a/lib/pages/search/sections/tracks.dart b/lib/pages/search/sections/tracks.dart index 0fdb50af..a790c7fd 100644 --- a/lib/pages/search/sections/tracks.dart +++ b/lib/pages/search/sections/tracks.dart @@ -46,6 +46,7 @@ class SearchTracksSection extends HookConsumerWidget { return TrackTile( index: i, track: track, + playlist: playlist, onTap: () async { final isTrackPlaying = playlist.activeTrack?.id == track.id; if (!isTrackPlaying && context.mounted) {