mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
366 lines
16 KiB
Dart
366 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),
|
|
)
|
|
],
|
|
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.navigateTo(
|
|
TrackRoute(
|
|
trackId: playlist.activeTrack!.id!),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: ArtistLink(
|
|
artists: playlist.activeTrack?.artists ?? [],
|
|
textStyle: typography.normal,
|
|
mainAxisAlignment: WrapAlignment.start,
|
|
onOverflowArtistClick: () => context.navigateTo(
|
|
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(),
|
|
),
|
|
]
|
|
],
|
|
);
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
}
|