spotube/lib/pages/connect/control/control.dart
2025-01-31 21:07:51 +06:00

367 lines
16 KiB
Dart

import 'package:auto_route/auto_route.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/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/player/player_queue.dart';
import 'package:spotube/modules/player/volume_slider.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/links/anchor_button.dart';
import 'package:spotube/components/links/artist_link.dart';
import 'package:spotube/components/titlebar/titlebar.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';
import 'package:spotube/provider/connect/clients.dart';
import 'package:spotube/provider/connect/connect.dart';
import 'package:media_kit/media_kit.dart' hide Track;
class RemotePlayerQueue extends ConsumerWidget {
const RemotePlayerQueue({super.key});
@override
Widget build(BuildContext context, ref) {
final connectNotifier = ref.watch(connectProvider.notifier);
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),
);
},
);
}
}
@RoutePage()
class ConnectControlPage extends HookConsumerWidget {
static const name = "connect_control";
const ConnectControlPage({super.key});
@override
Widget build(BuildContext context, ref) {
final ThemeData(:typography, :colorScheme) = Theme.of(context);
final resolvedService =
ref.watch(connectClientsProvider).asData?.value.resolvedService;
final connectNotifier = ref.read(connectProvider.notifier);
final playlist = ref.watch(queueProvider);
final playing = ref.watch(playingProvider);
final shuffled = ref.watch(shuffleProvider);
final loopMode = ref.watch(loopModeProvider);
ref.listen(connectClientsProvider, (prev, next) {
if (next.asData?.value.resolvedService == null) {
context.back();
}
});
return SafeArea(
bottom: false,
child: Scaffold(
headers: [
TitleBar(
title: Text(resolvedService!.name),
automaticallyImplyLeading: true,
)
],
child: 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: 350, maxWidth: 350),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: UniversalImage(
path: (playlist.activeTrack?.album?.images)
.asUrlString(
placeholder: ImagePlaceholder.albumArt,
),
fit: BoxFit.cover,
),
),
),
),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 20),
sliver: SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(
child: AnchorButton(
playlist.activeTrack?.name ?? "",
style: typography.h4,
onTap: () {
if (playlist.activeTrack == null) return;
context.pushRoute(
TrackRoute(
trackId: playlist.activeTrack!.id!),
);
},
),
),
SliverToBoxAdapter(
child: ArtistLink(
artists: playlist.activeTrack?.artists ?? [],
textStyle: typography.normal,
mainAxisAlignment: WrapAlignment.start,
onOverflowArtistClick: () => context.pushRoute(
TrackRoute(trackId: playlist.activeTrack!.id!),
),
),
),
],
),
),
const SliverGap(30),
SliverToBoxAdapter(
child: Consumer(
builder: (context, ref, _) {
final position = ref.watch(positionProvider);
final duration = ref.watch(durationProvider);
final progress = duration.inSeconds == 0
? 0
: position.inSeconds / duration.inSeconds;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
children: [
Slider(
value:
SliderValue.single(progress.toDouble()),
onChanged: (value) {
connectNotifier.seek(
Duration(
seconds:
(value.value * duration.inSeconds)
.toInt(),
),
);
},
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(position.toHumanReadableString()),
Text(duration.toHumanReadableString()),
],
),
],
),
);
},
),
),
SliverToBoxAdapter(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 20,
children: [
Tooltip(
tooltip: TooltipContainer(
child: Text(
shuffled
? context.l10n.unshuffle_playlist
: context.l10n.shuffle_playlist,
),
),
child: IconButton(
icon: const Icon(SpotubeIcons.shuffle),
variance: shuffled
? ButtonVariance.secondary
: ButtonVariance.ghost,
onPressed: playlist.activeTrack == null
? null
: () {
connectNotifier.setShuffle(!shuffled);
},
),
),
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.previous_track),
),
child: IconButton.ghost(
icon: const Icon(SpotubeIcons.skipBack),
onPressed: playlist.activeTrack == null
? null
: connectNotifier.previous,
),
),
Tooltip(
tooltip: TooltipContainer(
child: Text(
playing
? context.l10n.pause_playback
: context.l10n.resume_playback,
),
),
child: IconButton.primary(
shape: ButtonShape.circle,
icon: playlist.activeTrack == null
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
onSurface: false),
)
: Icon(
playing
? SpotubeIcons.pause
: SpotubeIcons.play,
),
onPressed: playlist.activeTrack == null
? null
: () {
if (playing) {
connectNotifier.pause();
} else {
connectNotifier.resume();
}
},
),
),
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.next_track)),
child: IconButton.ghost(
icon: const Icon(SpotubeIcons.skipForward),
onPressed: playlist.activeTrack == null
? null
: connectNotifier.next,
),
),
Tooltip(
tooltip: TooltipContainer(
child: Text(
loopMode == PlaylistMode.single
? context.l10n.loop_track
: loopMode == PlaylistMode.loop
? context.l10n.repeat_playlist
: context.l10n.no_loop,
),
),
child: IconButton(
icon: Icon(
loopMode == PlaylistMode.single
? SpotubeIcons.repeatOne
: SpotubeIcons.repeat,
),
variance: loopMode == PlaylistMode.single ||
loopMode == PlaylistMode.loop
? ButtonVariance.secondary
: ButtonVariance.ghost,
onPressed: playlist.activeTrack == null
? null
: () async {
connectNotifier.setLoopMode(
switch (loopMode) {
PlaylistMode.loop =>
PlaylistMode.single,
PlaylistMode.single =>
PlaylistMode.none,
PlaylistMode.none =>
PlaylistMode.loop,
},
);
},
),
)
],
),
),
const SliverGap(30),
if (constrains.mdAndDown)
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 20),
sliver: SliverToBoxAdapter(
child: Button.outline(
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) {
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight:
MediaQuery.sizeOf(context).height *
0.8,
),
child: const RemotePlayerQueue(),
);
},
);
},
),
),
),
const SliverGap(30),
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 20),
sliver: SliverToBoxAdapter(
child: Consumer(builder: (context, ref, _) {
final volume = ref.watch(volumeProvider);
return VolumeSlider(
fullWidth: true,
value: volume,
onChanged: (value) {
ref.read(volumeProvider.notifier).state = value;
connectNotifier.setVolume(value);
},
);
}),
),
),
const SliverSafeArea(sliver: SliverGap(10)),
],
),
),
if (constrains.lgAndUp) ...[
const VerticalDivider(thickness: 1),
const Expanded(
child: RemotePlayerQueue(),
),
]
],
);
}),
),
);
}
}