spotube/lib/modules/player/player.dart

283 lines
11 KiB
Dart

import 'package:auto_route/auto_route.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
import 'package:sliding_up_panel/sliding_up_panel.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/framework/app_pop_scope.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/modules/player/player_actions.dart';
import 'package:spotube/modules/player/player_controls.dart';
import 'package:spotube/modules/player/volume_slider.dart';
import 'package:spotube/components/dialogs/track_details_dialog.dart';
import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
import 'package:spotube/provider/server/active_track_sources.dart';
import 'package:spotube/provider/volume_provider.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:spotube/utils/platform.dart';
import 'package:url_launcher/url_launcher_string.dart';
class PlayerView extends HookConsumerWidget {
final PanelController panelController;
final ScrollController scrollController;
const PlayerView({
super.key,
required this.panelController,
required this.scrollController,
});
@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final authenticated = ref.watch(metadataPluginAuthenticatedProvider);
final sourcedCurrentTrack = ref.watch(activeTrackSourcesProvider);
final currentActiveTrack =
ref.watch(audioPlayerProvider.select((s) => s.activeTrack));
final currentActiveTrackSource = sourcedCurrentTrack.asData?.value?.source;
final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject;
final mediaQuery = MediaQuery.sizeOf(context);
final shouldHide = useState(true);
ref.listen(navigationPanelHeight, (_, height) {
shouldHide.value = height.ceil() == 50;
});
if (shouldHide.value) {
return const SizedBox();
}
useEffect(() {
if (mediaQuery.lgAndUp) {
WidgetsBinding.instance.addPostFrameCallback((_) {
panelController.close();
});
}
return null;
}, [mediaQuery.lgAndUp]);
String albumArt = useMemoized(
() => (currentActiveTrack?.album.images).asUrlString(
placeholder: ImagePlaceholder.albumArt,
),
[currentActiveTrack?.album.images],
);
useEffect(() {
for (final renderView in WidgetsBinding.instance.renderViews) {
renderView.automaticSystemUiAdjustment = false;
}
return () {
for (final renderView in WidgetsBinding.instance.renderViews) {
renderView.automaticSystemUiAdjustment = true;
}
};
}, [panelController.isAttached && panelController.isPanelOpen]);
return AppPopScope(
canPop: false,
onPopInvoked: (didPop) async {
await panelController.close();
},
child: SurfaceCard(
borderWidth: 0,
surfaceOpacity: 0.9,
padding: EdgeInsets.zero,
child: Scaffold(
backgroundColor: Colors.transparent,
headers: [
SafeArea(
minimum:
kIsMobile ? const EdgeInsets.only(top: 80) : EdgeInsets.zero,
bottom: false,
child: TitleBar(
surfaceOpacity: 0,
surfaceBlur: 0,
leading: [
IconButton.ghost(
icon: const Icon(SpotubeIcons.angleDown, size: 18),
onPressed: panelController.close,
)
],
trailing: [
if (currentActiveTrackSource is YoutubeSourcedTrack)
TextButton(
leading: Assets.logos.songlinkTransparent.image(
width: 20,
height: 20,
color: theme.colorScheme.foreground,
),
onPressed: () {
final url =
"https://song.link/s/${currentActiveTrack?.id}";
launchUrlString(url);
},
child: Text(context.l10n.song_link),
),
if (!isLocalTrack)
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.details),
).call,
child: IconButton.ghost(
icon: const Icon(SpotubeIcons.info, size: 18),
onPressed: currentActiveTrackSource == null
? null
: () {
showDialog(
context: context,
builder: (context) {
return TrackDetailsDialog(
track: currentActiveTrack
as SpotubeFullTrackObject,
);
});
},
),
)
],
),
),
],
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,
),
),
),
const SizedBox(height: 60),
Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AutoSizeText(
currentActiveTrack?.name ?? context.l10n.not_playing,
style: const TextStyle(fontSize: 22),
maxFontSize: 22,
maxLines: 1,
textAlign: TextAlign.start,
),
if (isLocalTrack)
Text(
currentActiveTrack.artists.asString(),
style: theme.typography.normal
.copyWith(fontWeight: FontWeight.bold),
)
else
ArtistLink(
artists: currentActiveTrack?.artists ?? [],
textStyle: theme.typography.normal
.copyWith(fontWeight: FontWeight.bold),
onRouteChange: (route) {
panelController.close();
context.router.navigateNamed(route);
},
onOverflowArtistClick: () => context.navigateTo(
TrackRoute(
trackId: currentActiveTrack!.id,
),
),
),
],
),
),
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: () {
context.pushRoute(const PlayerQueueRoute());
},
),
),
if (authenticated.asData?.value == true)
const SizedBox(width: 10),
if (authenticated.asData?.value == true)
Expanded(
child: OutlineButton(
leading: const Icon(SpotubeIcons.music),
child: Text(context.l10n.lyrics),
onPressed: () {
context.pushRoute(const PlayerLyricsRoute());
},
),
),
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);
},
);
}),
),
],
),
),
),
),
),
);
}
}