diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 0c915b08..16ee6c72 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -98,223 +98,229 @@ class PlayerView extends HookConsumerWidget { onPopInvoked: (didPop) async { await panelController.close(); }, - child: Scaffold( - headers: [ - SafeArea( - child: TitleBar( - surfaceOpacity: 0, - surfaceBlur: 0, - leading: [ - IconButton.ghost( - icon: const Icon(SpotubeIcons.angleDown, size: 18), - onPressed: panelController.close, - ) - ], - trailing: [ - if (currentTrack is YoutubeSourcedTrack) - TextButton( - leading: Assets.logos.songlinkTransparent.image( - width: 20, - height: 20, - color: theme.colorScheme.foreground, - ), - onPressed: () { - final url = "https://song.link/s/${currentTrack.id}"; + child: SurfaceCard( + borderWidth: 0, + surfaceOpacity: 0.9, + padding: EdgeInsets.zero, + child: Scaffold( + backgroundColor: Colors.transparent, + headers: [ + SafeArea( + child: TitleBar( + surfaceOpacity: 0, + surfaceBlur: 0, + leading: [ + IconButton.ghost( + icon: const Icon(SpotubeIcons.angleDown, size: 18), + onPressed: panelController.close, + ) + ], + trailing: [ + if (currentTrack is YoutubeSourcedTrack) + TextButton( + leading: Assets.logos.songlinkTransparent.image( + width: 20, + height: 20, + color: theme.colorScheme.foreground, + ), + onPressed: () { + final url = "https://song.link/s/${currentTrack.id}"; - launchUrlString(url); - }, - child: Text(context.l10n.song_link), - ), - Tooltip( - tooltip: TooltipContainer( - child: Text(context.l10n.details), - ), - child: IconButton.ghost( - icon: const Icon(SpotubeIcons.info, size: 18), - onPressed: currentTrack == null - ? null - : () { - showDialog( - context: context, - builder: (context) { - return TrackDetailsDialog( - track: currentTrack, - ); - }); - }, - ), - ) - ], + launchUrlString(url); + }, + child: Text(context.l10n.song_link), + ), + Tooltip( + tooltip: TooltipContainer( + child: Text(context.l10n.details), + ), + child: IconButton.ghost( + icon: const Icon(SpotubeIcons.info, size: 18), + onPressed: currentTrack == null + ? null + : () { + showDialog( + context: context, + builder: (context) { + return TrackDetailsDialog( + track: currentTrack, + ); + }); + }, + ), + ) + ], + ), ), - ), - ], - child: SingleChildScrollView( - controller: scrollController, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - Container( - margin: const EdgeInsets.all(8), - constraints: - const BoxConstraints(maxHeight: 300, maxWidth: 300), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(100), - spreadRadius: 2, - blurRadius: 10, - offset: Offset.zero, + ], + child: SingleChildScrollView( + controller: scrollController, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + Container( + margin: const EdgeInsets.all(8), + constraints: + const BoxConstraints(maxHeight: 300, maxWidth: 300), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(100), + spreadRadius: 2, + blurRadius: 10, + offset: Offset.zero, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: UniversalImage( + path: albumArt, + placeholder: Assets.albumPlaceholder.path, + fit: BoxFit.cover, ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: UniversalImage( - path: albumArt, - placeholder: Assets.albumPlaceholder.path, - fit: BoxFit.cover, ), ), - ), - const SizedBox(height: 60), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16), - alignment: Alignment.centerLeft, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AutoSizeText( - currentTrack?.name ?? context.l10n.not_playing, - style: const TextStyle(fontSize: 22), - maxFontSize: 22, - maxLines: 1, - textAlign: TextAlign.start, - ), - if (isLocalTrack) - Text( - currentTrack.artists?.asString() ?? "", - style: theme.typography.normal - .copyWith(fontWeight: FontWeight.bold), - ) - else - ArtistLink( - artists: currentTrack?.artists ?? [], - textStyle: theme.typography.normal - .copyWith(fontWeight: FontWeight.bold), - onRouteChange: (route) { - panelController.close(); - GoRouter.of(context).push(route); - }, - onOverflowArtistClick: () => ServiceUtils.pushNamed( - context, - TrackPage.name, - pathParameters: { - "id": currentTrack!.id!, - }, - ), + const SizedBox(height: 60), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + alignment: Alignment.centerLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AutoSizeText( + currentTrack?.name ?? context.l10n.not_playing, + style: const TextStyle(fontSize: 22), + maxFontSize: 22, + maxLines: 1, + textAlign: TextAlign.start, ), - ], - ), - ), - const SizedBox(height: 10), - const PlayerControls(), - const SizedBox(height: 25), - const PlayerActions( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - showQueue: false, - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - const SizedBox(width: 10), - Expanded( - child: OutlineButton( - leading: const Icon(SpotubeIcons.queue), - child: Text(context.l10n.queue), - onPressed: () { - openDrawer( - context: context, - barrierDismissible: true, - draggable: true, - barrierColor: Colors.black.withAlpha(100), - borderRadius: BorderRadius.circular(10), - transformBackdrop: false, - position: OverlayPosition.bottom, - surfaceBlur: context.theme.surfaceBlur, - surfaceOpacity: 0.7, - expands: true, - builder: (context) => Consumer( - builder: (context, ref, _) { - final playlist = ref.watch( - audioPlayerProvider, - ); - final playlistNotifier = - ref.read(audioPlayerProvider.notifier); - return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: - MediaQuery.of(context).size.height * - 0.8, - ), - child: PlayerQueue.fromAudioPlayerNotifier( - floating: false, - playlist: playlist, - notifier: playlistNotifier, - ), - ); + if (isLocalTrack) + Text( + currentTrack.artists?.asString() ?? "", + style: theme.typography.normal + .copyWith(fontWeight: FontWeight.bold), + ) + else + ArtistLink( + artists: currentTrack?.artists ?? [], + textStyle: theme.typography.normal + .copyWith(fontWeight: FontWeight.bold), + onRouteChange: (route) { + panelController.close(); + GoRouter.of(context).push(route); + }, + onOverflowArtistClick: () => ServiceUtils.pushNamed( + context, + TrackPage.name, + pathParameters: { + "id": currentTrack!.id!, }, ), - ); - }, - ), + ), + ], ), - if (auth.asData?.value != null) const SizedBox(width: 10), - if (auth.asData?.value != null) + ), + const SizedBox(height: 10), + const PlayerControls(), + const SizedBox(height: 25), + const PlayerActions( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + showQueue: false, + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + const SizedBox(width: 10), Expanded( child: OutlineButton( - leading: const Icon(SpotubeIcons.music), - child: Text(context.l10n.lyrics), + leading: const Icon(SpotubeIcons.queue), + child: Text(context.l10n.queue), onPressed: () { - showModalBottomSheet( + openDrawer( context: context, - isDismissible: true, - enableDrag: true, - isScrollControlled: true, - backgroundColor: Colors.black.withAlpha(100), + barrierDismissible: true, + draggable: true, barrierColor: Colors.black.withAlpha(100), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), + borderRadius: BorderRadius.circular(10), + transformBackdrop: false, + position: OverlayPosition.bottom, + surfaceBlur: context.theme.surfaceBlur, + surfaceOpacity: 0.7, + expands: true, + builder: (context) => Consumer( + builder: (context, ref, _) { + final playlist = ref.watch( + audioPlayerProvider, + ); + final playlistNotifier = + ref.read(audioPlayerProvider.notifier); + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: + MediaQuery.of(context).size.height * + 0.8, + ), + child: PlayerQueue.fromAudioPlayerNotifier( + floating: false, + playlist: playlist, + notifier: playlistNotifier, + ), + ); + }, ), - builder: (context) => - const LyricsPage(isModal: true), ); }, ), ), - const SizedBox(width: 10), - ], - ), - const SizedBox(height: 25), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Consumer(builder: (context, ref, _) { - final volume = ref.watch(volumeProvider); - return VolumeSlider( - fullWidth: true, - value: volume, - onChanged: (value) { - ref.read(volumeProvider.notifier).setVolume(value); - }, - ); - }), - ), - ], + if (auth.asData?.value != null) const SizedBox(width: 10), + if (auth.asData?.value != null) + Expanded( + child: OutlineButton( + leading: const Icon(SpotubeIcons.music), + child: Text(context.l10n.lyrics), + onPressed: () { + showModalBottomSheet( + context: context, + isDismissible: true, + enableDrag: true, + isScrollControlled: true, + backgroundColor: Colors.black.withAlpha(100), + barrierColor: Colors.black.withAlpha(100), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + builder: (context) => + const LyricsPage(isModal: true), + ); + }, + ), + ), + const SizedBox(width: 10), + ], + ), + const SizedBox(height: 25), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Consumer(builder: (context, ref, _) { + final volume = ref.watch(volumeProvider); + return VolumeSlider( + fullWidth: true, + value: volume, + onChanged: (value) { + ref.read(volumeProvider.notifier).setVolume(value); + }, + ); + }), + ), + ], + ), ), ), ), diff --git a/lib/modules/player/player_actions.dart b/lib/modules/player/player_actions.dart index 12965da4..584af70d 100644 --- a/lib/modules/player/player_actions.dart +++ b/lib/modules/player/player_actions.dart @@ -5,6 +5,7 @@ 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/extensions/constrains.dart'; import 'package:spotube/modules/player/player_queue.dart'; import 'package:spotube/modules/player/sibling_tracks_sheet.dart'; import 'package:spotube/components/adaptive/adaptive_pop_sheet_list.dart'; @@ -118,23 +119,54 @@ class PlayerActions extends HookConsumerWidget { tooltip: TooltipContainer( child: Text(context.l10n.alternative_track_sources)), child: IconButton.ghost( + enabled: playlist.activeTrack != null, icon: const Icon(SpotubeIcons.alternativeRoute), - onPressed: playlist.activeTrack != null - ? () { - openDrawer( - context: context, - position: OverlayPosition.bottom, - barrierDismissible: true, - draggable: true, - barrierColor: Colors.black.withValues(alpha: .2), - borderRadius: BorderRadius.circular(10), - transformBackdrop: false, - builder: (context) { - return SiblingTracksSheet(floating: floatingQueue); - }, + onPressed: () { + final screenSize = MediaQuery.sizeOf(context); + if (screenSize.mdAndUp) { + showPopover( + alignment: Alignment.bottomCenter, + context: context, + builder: (context) { + return SurfaceCard( + padding: EdgeInsets.zero, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 600, + maxWidth: 500, + ), + child: SiblingTracksSheet(floating: floatingQueue), + ), ); - } - : null, + }, + ); + } else { + openDrawer( + context: context, + position: OverlayPosition.bottom, + barrierDismissible: true, + draggable: true, + barrierColor: Colors.black.withValues(alpha: .2), + borderRadius: BorderRadius.circular(10), + transformBackdrop: false, + surfaceBlur: context.theme.surfaceBlur, + surfaceOpacity: context.theme.surfaceOpacity, + builder: (context) { + return Card( + borderWidth: 0, + borderColor: Colors.transparent, + padding: EdgeInsets.zero, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: screenSize.height * .8, + ), + child: SiblingTracksSheet(floating: floatingQueue), + ), + ); + }, + ); + } + }, ), ), if (!kIsWeb && !isLocalTrack) diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index 3a31d88e..9bd1ea22 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -1,16 +1,15 @@ -import 'dart:ui'; - import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' show ListTile, Material, MaterialType; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/extensions/artist_simple.dart'; -import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/duration.dart'; import 'package:spotube/hooks/utils/use_debounce.dart'; @@ -152,13 +151,6 @@ class SiblingTracksSheet extends HookConsumerWidget { [activeTrack, isFetchingActiveTrack], ); - final borderRadius = floating - ? BorderRadius.circular(10) - : const BorderRadius.only( - topLeft: Radius.circular(10), - topRight: Radius.circular(10), - ); - useEffect(() { if (activeTrack is SourcedTrack && activeTrack.siblings.isEmpty) { activeTrackNotifier.populateSibling(); @@ -170,9 +162,17 @@ class SiblingTracksSheet extends HookConsumerWidget { (SourceInfo sourceInfo) { final icon = sourceInfoToIconMap[sourceInfo.runtimeType]; return ListTile( + hoverColor: theme.colorScheme.primary.withOpacity(.1), + dense: true, + subtitleTextStyle: theme.typography.small.copyWith( + color: theme.colorScheme.mutedForeground, + ), + titleTextStyle: theme.typography.normal, + leadingAndTrailingTextStyle: theme.typography.normal, title: Text(sourceInfo.title), + horizontalTitleGap: 0, leading: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.only(top: 8.0, right: 8.0), child: UniversalImage( path: sourceInfo.thumbnail, height: 60, @@ -192,12 +192,13 @@ class SiblingTracksSheet extends HookConsumerWidget { enabled: !isFetchingActiveTrack, selected: !isFetchingActiveTrack && sourceInfo.id == (activeTrack as SourcedTrack).sourceInfo.id, - selectedTileColor: theme.popupMenuTheme.color, + selectedTileColor: theme.colorScheme.primary.withOpacity(.1), + selectedColor: theme.colorScheme.primary, onTap: () { if (!isFetchingActiveTrack && sourceInfo.id != (activeTrack as SourcedTrack).sourceInfo.id) { activeTrackNotifier.swapSibling(sourceInfo); - Navigator.of(context).pop(); + closeDrawer(context); } }, ); @@ -205,131 +206,127 @@ class SiblingTracksSheet extends HookConsumerWidget { [activeTrack, siblings], ); - final mediaQuery = MediaQuery.of(context); - return SafeArea( - 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 - 50 - : mediaQuery.size.height * .6, - decoration: BoxDecoration( - borderRadius: borderRadius, - color: - theme.colorScheme.surfaceContainerHighest.withOpacity(.5), - ), - 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, - backgroundColor: Colors.transparent, - actions: [ - if (!isSearching.value) - IconButton( - icon: const Icon(SpotubeIcons.search, size: 18), - onPressed: () { - isSearching.value = true; - }, - ) - else ...[ - if (preferences.audioSource == AudioSource.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; - }, - ), - ] - ], - ), - 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( - controller: controller, - child: switch (isSearching.value) { - false => ListView.builder( - controller: controller, - 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()); - } + final scale = context.theme.scaling; - return InterScrollbar( - controller: controller, - child: ListView.builder( - controller: controller, - itemCount: snapshot.data!.length, - itemBuilder: (context, index) => - itemBuilder(snapshot.data![index]), - ), - ); - }, + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16), + child: Row( + spacing: 5, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: !isSearching.value + ? Text( + context.l10n.alternative_track_sources, + style: theme.typography.bold, + ) + : Flexible( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 320 * scale, + maxHeight: 38 * scale, + ), + child: TextField( + autofocus: true, + controller: searchController, + placeholder: Text(context.l10n.search), + style: theme.typography.bold, + ), ), + ), + ), + const Spacer(), + if (!isSearching.value) + IconButton.outline( + icon: const Icon(SpotubeIcons.search, size: 18), + onPressed: () { + isSearching.value = true; + }, + ) + else ...[ + if (preferences.audioSource == AudioSource.piped) + IconButton.outline( + icon: const Icon(SpotubeIcons.filter, size: 18), + onPressed: () { + showPopover( + context: context, + alignment: Alignment.bottomRight, + builder: (context) { + return DropdownMenu( + children: SearchMode.values + .map( + (e) => MenuButton( + onPressed: (context) { + searchMode.value = e; + }, + enabled: searchMode.value != e, + child: Text(e.label), + ), + ) + .toList(), + ); + }, + ); }, ), + IconButton.outline( + icon: const Icon(SpotubeIcons.close, size: 18), + onPressed: () { + isSearching.value = false; + }, ), + ] + ], + ), + ), + Expanded( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: InterScrollbar( + controller: controller, + child: Material( + type: MaterialType.transparency, + child: switch (isSearching.value) { + false => ListView.builder( + padding: const EdgeInsets.all(8.0), + controller: controller, + 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 ListView.builder( + padding: const EdgeInsets.all(8.0), + controller: controller, + itemCount: snapshot.data!.length, + itemBuilder: (context, index) => + itemBuilder(snapshot.data![index]), + ); + }, + ), + }, ), ), ), ), - ), + ], ), ); }