network addition
This commit is contained in:
parent
3bd713d667
commit
2099d3d64d
27 changed files with 2237 additions and 40 deletions
|
|
@ -339,7 +339,7 @@ class _TracksPanelState extends ConsumerState<_TracksPanel> {
|
|||
Widget build(BuildContext context) {
|
||||
final index = ref.watch(libraryIndexProvider);
|
||||
final visible = ref.watch(visibleTracksProvider);
|
||||
final playback = ref.read(playbackProvider.notifier);
|
||||
final playback = ref.read(playbackCommandsProvider);
|
||||
final client = ref.watch(subsonicClientProvider);
|
||||
|
||||
final Widget body;
|
||||
|
|
@ -564,7 +564,7 @@ class AlbumScreen extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final album = ref.watch(albumProvider(id));
|
||||
final downloads = ref.watch(downloadManagerProvider);
|
||||
final playback = ref.read(playbackProvider.notifier);
|
||||
final playback = ref.read(playbackCommandsProvider);
|
||||
final songs = album.valueOrNull?.songs ?? const <Song>[];
|
||||
|
||||
return _DetailScaffold(
|
||||
|
|
|
|||
228
lib/screens/devices_sheet.dart
Normal file
228
lib/screens/devices_sheet.dart
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../remote/discovery.dart';
|
||||
import '../remote/remote_session.dart';
|
||||
import '../state/remote_providers.dart';
|
||||
import '../theme/tokens.dart';
|
||||
|
||||
/// Open the cross-device control sheet: pick a same-Wi-Fi Timbre device to
|
||||
/// control, or detach back to local playback (updates-features.md #3).
|
||||
Future<void> showDevicesSheet(BuildContext context) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: TimbreColors.background,
|
||||
isScrollControlled: true,
|
||||
builder: (_) => const _DevicesSheet(),
|
||||
);
|
||||
}
|
||||
|
||||
class _DevicesSheet extends ConsumerStatefulWidget {
|
||||
const _DevicesSheet();
|
||||
|
||||
@override
|
||||
ConsumerState<_DevicesSheet> createState() => _DevicesSheetState();
|
||||
}
|
||||
|
||||
class _DevicesSheetState extends ConsumerState<_DevicesSheet> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Browse only while the sheet is open — discovery is comparatively costly.
|
||||
Future.microtask(
|
||||
() => ref.read(remoteControlProvider.notifier).startBrowsing());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ref.read(remoteControlProvider.notifier).stopBrowsing();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rc = ref.watch(remoteControlProvider);
|
||||
final accent = Theme.of(context).colorScheme.primary;
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: TimbreSpacing.lg),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: TimbreSpacing.xl),
|
||||
child: Text('Devices',
|
||||
style:
|
||||
TextStyle(color: accent, fontWeight: FontWeight.w700)),
|
||||
),
|
||||
const SizedBox(height: TimbreSpacing.md),
|
||||
if (!rc.supported)
|
||||
const _Note(
|
||||
"Cross-device control isn't available on this platform.")
|
||||
else ...[
|
||||
// "Play here" row — active when currently controlling a remote.
|
||||
_LocalRow(
|
||||
active: !rc.isAttached,
|
||||
onTap: rc.isAttached
|
||||
? () => ref.read(remoteControlProvider.notifier).detach()
|
||||
: null,
|
||||
),
|
||||
const _Divider(),
|
||||
for (final d in rc.devices)
|
||||
_DeviceRow(
|
||||
device: d,
|
||||
active: rc.attachedDevice?.id == d.id,
|
||||
connecting: rc.attachedDevice?.id == d.id &&
|
||||
rc.status == RemoteConnStatus.connecting,
|
||||
onTap: () =>
|
||||
ref.read(remoteControlProvider.notifier).attach(d),
|
||||
),
|
||||
if (rc.devices.isEmpty)
|
||||
const _Note('Searching for devices on your Wi-Fi…',
|
||||
spinner: true),
|
||||
if (rc.status == RemoteConnStatus.denied)
|
||||
const _Note(
|
||||
'That device refused the connection (different account?).'),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The "play on this device" (local) row, shown selected when not attached.
|
||||
class _LocalRow extends StatelessWidget {
|
||||
const _LocalRow({required this.active, this.onTap});
|
||||
|
||||
final bool active;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accent = Theme.of(context).colorScheme.primary;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: TimbreSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.symmetric(horizontal: TimbreSpacing.xl),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.smartphone,
|
||||
size: 16,
|
||||
color: active ? accent : TimbreColors.foreground),
|
||||
const SizedBox(width: TimbreSpacing.md),
|
||||
Expanded(
|
||||
child: Text('This device',
|
||||
style: TextStyle(
|
||||
color: TimbreColors.foreground,
|
||||
fontWeight: active ? FontWeight.w700 : FontWeight.w400,
|
||||
)),
|
||||
),
|
||||
if (active)
|
||||
Text('●', style: TextStyle(color: accent)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeviceRow extends StatelessWidget {
|
||||
const _DeviceRow({
|
||||
required this.device,
|
||||
required this.active,
|
||||
required this.connecting,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final DiscoveredDevice device;
|
||||
final bool active;
|
||||
final bool connecting;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accent = Theme.of(context).colorScheme.primary;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
constraints:
|
||||
const BoxConstraints(minHeight: TimbreSpacing.minTouchTarget),
|
||||
padding: const EdgeInsets.symmetric(horizontal: TimbreSpacing.xl),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.cast,
|
||||
size: 16,
|
||||
color: active ? accent : TimbreColors.foreground),
|
||||
const SizedBox(width: TimbreSpacing.md),
|
||||
Expanded(
|
||||
child: Text(
|
||||
device.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: TimbreColors.foreground,
|
||||
fontWeight: active ? FontWeight.w700 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (connecting)
|
||||
const SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
else if (active)
|
||||
Text('●', style: TextStyle(color: accent)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Divider extends StatelessWidget {
|
||||
const _Divider();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: TimbreSpacing.xl, vertical: TimbreSpacing.sm),
|
||||
child: Divider(color: TimbreColors.border, height: 1),
|
||||
);
|
||||
}
|
||||
|
||||
class _Note extends StatelessWidget {
|
||||
const _Note(this.text, {this.spinner = false});
|
||||
|
||||
final String text;
|
||||
final bool spinner;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: TimbreSpacing.xl, vertical: TimbreSpacing.md),
|
||||
child: Row(
|
||||
children: [
|
||||
if (spinner) ...[
|
||||
const SizedBox(
|
||||
height: 14,
|
||||
width: 14,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
const SizedBox(width: TimbreSpacing.md),
|
||||
],
|
||||
Flexible(
|
||||
child: Text(text,
|
||||
style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ class DownloadsScreen extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final downloads = ref.watch(downloadManagerProvider);
|
||||
final controller = ref.read(downloadManagerProvider.notifier);
|
||||
final playback = ref.read(playbackProvider.notifier);
|
||||
final playback = ref.read(playbackCommandsProvider);
|
||||
|
||||
final active = downloads.byId.values.where((d) => d.isActive).toList();
|
||||
final completed = downloads.completed;
|
||||
|
|
|
|||
|
|
@ -46,13 +46,13 @@ class FavoritesScreen extends ConsumerWidget {
|
|||
title: s.songs[i].title ?? 'Untitled',
|
||||
trailing: s.songs[i].artist,
|
||||
onTap: () => ref
|
||||
.read(playbackProvider.notifier)
|
||||
.read(playbackCommandsProvider)
|
||||
.playSongs(s.songs, startIndex: i),
|
||||
onPlayNext: () => ref
|
||||
.read(playbackProvider.notifier)
|
||||
.read(playbackCommandsProvider)
|
||||
.playNext(s.songs[i]),
|
||||
onAddToQueue: () => ref
|
||||
.read(playbackProvider.notifier)
|
||||
.read(playbackCommandsProvider)
|
||||
.addToQueue(s.songs[i]),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ class _HeroCard extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final accent = Theme.of(context).colorScheme.primary;
|
||||
final client = ref.watch(subsonicClientProvider);
|
||||
final current = ref.watch(playbackProvider.select((s) => s.current));
|
||||
final current = ref.watch(activePlaybackProvider.select((s) => s.current));
|
||||
|
||||
// Fall back to the most recent track so the hero is useful before playback.
|
||||
final recent = ref.watch(recentSongsProvider);
|
||||
|
|
@ -136,7 +136,7 @@ class _HeroCard extends ConsumerWidget {
|
|||
if (hasCurrent) {
|
||||
ref.read(selectedTabProvider.notifier).state = nowPlayingTabIndex;
|
||||
} else if (fallback != null) {
|
||||
ref.read(playbackProvider.notifier).playSongs([fallback.toSong()]);
|
||||
ref.read(playbackCommandsProvider).playSongs([fallback.toSong()]);
|
||||
ref.read(selectedTabProvider.notifier).state = nowPlayingTabIndex;
|
||||
}
|
||||
},
|
||||
|
|
@ -210,7 +210,7 @@ class _HeroProgress extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final progress = ref.watch(playbackProvider.select((s) => s.progress));
|
||||
final progress = ref.watch(activePlaybackProvider.select((s) => s.progress));
|
||||
return BlockProgressBar(progress: progress, cells: 32, height: 6);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@ import '../layout/breakpoints.dart';
|
|||
import '../playback/playback_engine.dart';
|
||||
import '../settings/settings_store.dart';
|
||||
import '../state/providers.dart';
|
||||
import '../state/remote_providers.dart';
|
||||
import '../subsonic/models.dart';
|
||||
import '../theme/tokens.dart';
|
||||
import '../widgets/block_progress_bar.dart';
|
||||
import '../widgets/cassette_view.dart';
|
||||
import '../widgets/hairline_panel.dart';
|
||||
import 'add_to_playlist_sheet.dart';
|
||||
import 'devices_sheet.dart';
|
||||
|
||||
/// Now Playing tab — album art + info strip + transport, bound to the live
|
||||
/// playback engine. On compact screens the top region shows the full-size album
|
||||
|
|
@ -44,18 +46,18 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
|||
/// [FavoritesController.seedSong].
|
||||
void _seedFavorites() {
|
||||
if (!mounted) return;
|
||||
final song = ref.read(playbackProvider).current;
|
||||
final song = ref.read(activePlaybackProvider).current;
|
||||
if (song != null) ref.read(favoritesProvider.notifier).seedSong(song);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(playbackProvider);
|
||||
final state = ref.watch(activePlaybackProvider);
|
||||
final accent = Theme.of(context).colorScheme.primary;
|
||||
final current = state.current;
|
||||
|
||||
// Re-seed the favorites store whenever the track changes.
|
||||
ref.listen(playbackProvider.select((s) => s.current?.id),
|
||||
ref.listen(activePlaybackProvider.select((s) => s.current?.id),
|
||||
(_, _) => _seedFavorites());
|
||||
|
||||
if (current == null) {
|
||||
|
|
@ -65,6 +67,11 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
// Cross-device control is offered next to the queue toggle (compact) or
|
||||
// beneath the transport (wide); hidden where the platform can't host/browse.
|
||||
final remoteSupported =
|
||||
ref.watch(remoteControlProvider.select((s) => s.supported));
|
||||
|
||||
// Everything below the art region — shared by both layouts. The queue
|
||||
// toggle is deliberately excluded: it belongs only to the compact layout
|
||||
// (where art and queue share one region), so it's appended separately.
|
||||
|
|
@ -98,7 +105,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
|||
),
|
||||
const SizedBox(width: TimbreSpacing.sm),
|
||||
TextButton(
|
||||
onPressed: () => ref.read(playbackProvider.notifier).retry(),
|
||||
onPressed: () => ref.read(playbackCommandsProvider).retry(),
|
||||
child: Text('Retry', style: TextStyle(color: accent)),
|
||||
),
|
||||
],
|
||||
|
|
@ -132,6 +139,10 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
|||
const Expanded(child: _FittedArt()),
|
||||
const SizedBox(height: TimbreSpacing.lg),
|
||||
...controls,
|
||||
if (remoteSupported) ...[
|
||||
const SizedBox(height: TimbreSpacing.sm),
|
||||
const Center(child: _RemoteButton()),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -142,13 +153,25 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
// Compact: art and queue share the top region, swapped by the toggle.
|
||||
// Compact: art and queue share the top region, swapped by the toggle. The
|
||||
// devices button sits beside the toggle so both bottom affordances share one
|
||||
// centered row.
|
||||
final queueToggle = _QueueToggle(
|
||||
showQueue: _showQueue,
|
||||
queueLength: state.queue.length,
|
||||
accent: accent,
|
||||
onTap: () => setState(() => _showQueue = !_showQueue),
|
||||
);
|
||||
final bottomBar = remoteSupported
|
||||
? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
queueToggle,
|
||||
const SizedBox(width: TimbreSpacing.sm),
|
||||
const _RemoteButton(),
|
||||
],
|
||||
)
|
||||
: queueToggle;
|
||||
|
||||
return Padding(
|
||||
padding: padding,
|
||||
|
|
@ -161,7 +184,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
|||
const SizedBox(height: TimbreSpacing.lg),
|
||||
...controls,
|
||||
const SizedBox(height: TimbreSpacing.sm),
|
||||
queueToggle,
|
||||
bottomBar,
|
||||
],
|
||||
)
|
||||
// Art absorbs the leftover height, capped to a square, so the
|
||||
|
|
@ -174,7 +197,7 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
|||
const SizedBox(height: TimbreSpacing.lg),
|
||||
...controls,
|
||||
const SizedBox(height: TimbreSpacing.sm),
|
||||
queueToggle,
|
||||
bottomBar,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -275,12 +298,12 @@ class _QueuePanelState extends ConsumerState<_QueuePanel> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(playbackProvider);
|
||||
final state = ref.watch(activePlaybackProvider);
|
||||
final accent = Theme.of(context).colorScheme.primary;
|
||||
|
||||
// Follow the playing track as it advances (or as shuffle reorders things).
|
||||
ref.listen<int?>(
|
||||
playbackProvider.select((s) => s.currentIndex),
|
||||
activePlaybackProvider.select((s) => s.currentIndex),
|
||||
(_, next) => _scrollToIndex(next),
|
||||
);
|
||||
// Jump to the current track the first time the queue is populated.
|
||||
|
|
@ -302,7 +325,7 @@ class _QueuePanelState extends ConsumerState<_QueuePanel> {
|
|||
// index — exactly the convention reorderQueue (and just_audio's
|
||||
// moveAudioSource) expects — so no off-by-one adjustment is needed.
|
||||
onReorderItem: (oldIndex, newIndex) =>
|
||||
ref.read(playbackProvider.notifier).reorderQueue(oldIndex, newIndex),
|
||||
ref.read(playbackCommandsProvider).reorderQueue(oldIndex, newIndex),
|
||||
itemBuilder: (context, i) {
|
||||
final song = state.queue[i];
|
||||
final current = state.currentIndex;
|
||||
|
|
@ -317,7 +340,7 @@ class _QueuePanelState extends ConsumerState<_QueuePanel> {
|
|||
// collide and ReorderableListView requires unique keys. These rows
|
||||
// are stateless, so keying by index leaks no state.
|
||||
key: ValueKey(i),
|
||||
onTap: () => ref.read(playbackProvider.notifier).jumpTo(i),
|
||||
onTap: () => ref.read(playbackCommandsProvider).jumpTo(i),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: TimbreSpacing.lg,
|
||||
|
|
@ -346,7 +369,7 @@ class _QueuePanelState extends ConsumerState<_QueuePanel> {
|
|||
style: const TextStyle(color: TimbreColors.dimmed)),
|
||||
InkWell(
|
||||
onTap: () =>
|
||||
ref.read(playbackProvider.notifier).removeAt(i),
|
||||
ref.read(playbackCommandsProvider).removeAt(i),
|
||||
customBorder: const CircleBorder(),
|
||||
child: const SizedBox(
|
||||
width: TimbreSpacing.minTouchTarget,
|
||||
|
|
@ -403,7 +426,7 @@ class _AlbumArtPanel extends ConsumerWidget {
|
|||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final coverArt =
|
||||
ref.watch(playbackProvider.select((s) => s.current?.coverArt));
|
||||
ref.watch(activePlaybackProvider.select((s) => s.current?.coverArt));
|
||||
final client = ref.watch(subsonicClientProvider);
|
||||
final cassette =
|
||||
ref.watch(settingsProvider.select((s) => s.nowPlayingCassette));
|
||||
|
|
@ -525,6 +548,53 @@ class _FavRating extends ConsumerWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// Cross-device control affordance sitting beside the queue toggle
|
||||
/// (updates-features.md #3). Styled to match [_QueueToggle]: when controlling
|
||||
/// another device it shows the device name in the accent colour; otherwise a
|
||||
/// dim "Devices" entry point. Tapping opens the devices sheet (which also holds
|
||||
/// the "This device" / detach action).
|
||||
class _RemoteButton extends ConsumerWidget {
|
||||
const _RemoteButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final rc = ref.watch(remoteControlProvider);
|
||||
final accent = Theme.of(context).colorScheme.primary;
|
||||
final attached = rc.isAttached;
|
||||
final label = attached ? (rc.attachedDevice?.name ?? 'Remote') : 'Devices';
|
||||
return InkWell(
|
||||
onTap: () => showDevicesSheet(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: TimbreSpacing.lg,
|
||||
vertical: TimbreSpacing.sm,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
attached ? Icons.cast_connected : Icons.cast,
|
||||
size: 16,
|
||||
color: attached ? accent : TimbreColors.dimmed,
|
||||
),
|
||||
const SizedBox(width: TimbreSpacing.sm),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: attached ? accent : TimbreColors.foreground,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoStrip extends StatelessWidget {
|
||||
const _InfoStrip({required this.song, required this.accent});
|
||||
|
||||
|
|
@ -562,10 +632,10 @@ class _NowPlayingProgress extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final position = ref.watch(playbackProvider.select((s) => s.position));
|
||||
final position = ref.watch(activePlaybackProvider.select((s) => s.position));
|
||||
final duration =
|
||||
ref.watch(playbackProvider.select((s) => s.effectiveDuration));
|
||||
final progress = ref.watch(playbackProvider.select((s) => s.progress));
|
||||
ref.watch(activePlaybackProvider.select((s) => s.effectiveDuration));
|
||||
final progress = ref.watch(activePlaybackProvider.select((s) => s.progress));
|
||||
return Row(
|
||||
children: [
|
||||
Text(_fmtDur(position),
|
||||
|
|
@ -592,7 +662,7 @@ class _Transport extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = ref.read(playbackProvider.notifier);
|
||||
final controller = ref.read(playbackCommandsProvider);
|
||||
final loopIcon = switch (state.loop) {
|
||||
LoopMode.one => Icons.repeat_one,
|
||||
_ => Icons.repeat,
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ class _PlaylistDetailScreenState extends ConsumerState<PlaylistDetailScreen> {
|
|||
s.playlists.where((p) => p.id == widget.id).firstOrNull));
|
||||
final connected = ref.watch(subsonicClientProvider) != null;
|
||||
final me = ref.watch(currentUsernameProvider);
|
||||
final playback = ref.read(playbackProvider.notifier);
|
||||
final playback = ref.read(playbackCommandsProvider);
|
||||
final songs = detail?.songs ?? const <Song>[];
|
||||
|
||||
// Ownership drives which sharing affordance shows: owner → share toggle;
|
||||
|
|
|
|||
|
|
@ -102,13 +102,13 @@ class _Results extends ConsumerWidget {
|
|||
title: r.songs[i].title ?? 'Untitled',
|
||||
trailing: r.songs[i].artist,
|
||||
onTap: () => ref
|
||||
.read(playbackProvider.notifier)
|
||||
.read(playbackCommandsProvider)
|
||||
.playSongs(r.songs, startIndex: i),
|
||||
onPlayNext: () => ref
|
||||
.read(playbackProvider.notifier)
|
||||
.read(playbackCommandsProvider)
|
||||
.playNext(r.songs[i]),
|
||||
onAddToQueue: () => ref
|
||||
.read(playbackProvider.notifier)
|
||||
.read(playbackCommandsProvider)
|
||||
.addToQueue(r.songs[i]),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue