mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-02 16:20:57 +02:00
fix(library): sync track playback controls
This commit is contained in:
@@ -38,6 +38,26 @@ final playbackLoadingProvider = Provider<bool>((ref) {
|
||||
);
|
||||
});
|
||||
|
||||
/// Transport state for one media item. Non-current Library cells only watch
|
||||
/// the current media ID; the active cell additionally follows play/loading so
|
||||
/// a transport toggle does not rebuild the entire Library grid.
|
||||
final mediaItemPlaybackUiProvider =
|
||||
Provider.family<({bool isCurrent, bool isPlaying, bool isLoading}), String>(
|
||||
(ref, mediaId) {
|
||||
final currentId = ref.watch(
|
||||
currentMediaItemProvider.select((state) => state.value?.id),
|
||||
);
|
||||
if (mediaId.isEmpty || currentId != mediaId) {
|
||||
return (isCurrent: false, isPlaying: false, isLoading: false);
|
||||
}
|
||||
return (
|
||||
isCurrent: true,
|
||||
isPlaying: ref.watch(playbackPlayingProvider),
|
||||
isLoading: ref.watch(playbackLoadingProvider),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final playQueueProvider = StreamProvider<List<MediaItem>>((ref) {
|
||||
return musicPlayerQueueEvents();
|
||||
});
|
||||
|
||||
@@ -192,18 +192,20 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
|
||||
final resolvedPath = result.path;
|
||||
if (result.status == CompletionBridgePlayableStatus.playable &&
|
||||
resolvedPath != null) {
|
||||
return TrackGridPlayButton(
|
||||
tooltip: context.l10n.a11yPlayTrackByArtist(
|
||||
return _LibraryPlaybackButton(
|
||||
mediaId: _cleanFilePath(resolvedPath),
|
||||
playTooltip: context.l10n.a11yPlayTrackByArtist(
|
||||
trackName,
|
||||
artistName,
|
||||
),
|
||||
onPressed: () => _openFile(
|
||||
onPlay: () => _openFile(
|
||||
resolvedPath,
|
||||
title: trackName,
|
||||
artist: artistName,
|
||||
album: albumName,
|
||||
coverUrl: coverUrl,
|
||||
),
|
||||
grid: true,
|
||||
);
|
||||
}
|
||||
if (result.status == CompletionBridgePlayableStatus.checking) {
|
||||
@@ -343,22 +345,19 @@ extension _QueueTabCollectionItemWidgets on _QueueTabState {
|
||||
final resolvedPath = result.path;
|
||||
if (result.status == CompletionBridgePlayableStatus.playable &&
|
||||
resolvedPath != null) {
|
||||
return IconButton(
|
||||
onPressed: () => _openFile(
|
||||
return _LibraryPlaybackButton(
|
||||
mediaId: _cleanFilePath(resolvedPath),
|
||||
playTooltip: context.l10n.a11yPlayTrackByArtist(
|
||||
trackName,
|
||||
artistName,
|
||||
),
|
||||
onPlay: () => _openFile(
|
||||
resolvedPath,
|
||||
title: trackName,
|
||||
artist: artistName,
|
||||
album: albumName,
|
||||
coverUrl: coverUrl,
|
||||
),
|
||||
icon: Icon(Icons.play_arrow, color: colorScheme.primary),
|
||||
tooltip: context.l10n.tooltipPlay,
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: Size.square(context.tokens.minTouchTarget),
|
||||
backgroundColor: colorScheme.primaryContainer.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (result.status == CompletionBridgePlayableStatus.checking) {
|
||||
|
||||
@@ -567,21 +567,19 @@ extension _QueueTabItemWidgets on _QueueTabState {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (fileExists)
|
||||
IconButton(
|
||||
onPressed: () => _openFile(
|
||||
_LibraryPlaybackButton(
|
||||
mediaId: _cleanFilePath(item.filePath!),
|
||||
playTooltip: context.l10n.a11yPlayTrackByArtist(
|
||||
item.track.name,
|
||||
item.track.artistName,
|
||||
),
|
||||
onPlay: () => _openFile(
|
||||
item.filePath!,
|
||||
title: item.track.name,
|
||||
artist: item.track.artistName,
|
||||
album: item.track.albumName,
|
||||
coverUrl: item.track.coverUrl ?? '',
|
||||
),
|
||||
icon: Icon(Icons.play_arrow, color: colorScheme.primary),
|
||||
tooltip: context.l10n.tooltipPlay,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: colorScheme.primaryContainer.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Semantics(
|
||||
@@ -954,16 +952,13 @@ extension _QueueTabItemWidgets on _QueueTabState {
|
||||
valueListenable: fileExistsListenable,
|
||||
builder: (context, fileExists, child) {
|
||||
if (fileExists) {
|
||||
return IconButton(
|
||||
onPressed: () => _playLibraryItem(item, libraryItems),
|
||||
icon: Icon(Icons.play_arrow, color: colorScheme.primary),
|
||||
tooltip: context.l10n.tooltipPlay,
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: Size.square(context.tokens.minTouchTarget),
|
||||
backgroundColor: colorScheme.primaryContainer.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
return _LibraryPlaybackButton(
|
||||
mediaId: _libraryPlaybackMediaId(item),
|
||||
playTooltip: context.l10n.a11yPlayTrackByArtist(
|
||||
item.trackName,
|
||||
item.artistName,
|
||||
),
|
||||
onPlay: () => _playLibraryItem(item, libraryItems),
|
||||
);
|
||||
}
|
||||
return Tooltip(
|
||||
@@ -1069,12 +1064,14 @@ extension _QueueTabItemWidgets on _QueueTabState {
|
||||
valueListenable: fileExistsListenable,
|
||||
builder: (context, fileExists, child) {
|
||||
if (fileExists) {
|
||||
return TrackGridPlayButton(
|
||||
tooltip: context.l10n.a11yPlayTrackByArtist(
|
||||
return _LibraryPlaybackButton(
|
||||
mediaId: _libraryPlaybackMediaId(item),
|
||||
playTooltip: context.l10n.a11yPlayTrackByArtist(
|
||||
item.trackName,
|
||||
item.artistName,
|
||||
),
|
||||
onPressed: () => _playLibraryItem(item, libraryItems),
|
||||
onPlay: () => _playLibraryItem(item, libraryItems),
|
||||
grid: true,
|
||||
);
|
||||
}
|
||||
return Tooltip(
|
||||
@@ -1133,3 +1130,54 @@ extension _QueueTabItemWidgets on _QueueTabState {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LibraryPlaybackButton extends ConsumerWidget {
|
||||
const _LibraryPlaybackButton({
|
||||
required this.mediaId,
|
||||
required this.playTooltip,
|
||||
required this.onPlay,
|
||||
this.grid = false,
|
||||
});
|
||||
|
||||
final String mediaId;
|
||||
final String playTooltip;
|
||||
final VoidCallback onPlay;
|
||||
final bool grid;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final playback = ref.watch(mediaItemPlaybackUiProvider(mediaId));
|
||||
final icon = playback.isPlaying ? Icons.pause : Icons.play_arrow;
|
||||
final tooltip = playback.isPlaying ? context.l10n.actionPause : playTooltip;
|
||||
final onPressed = playback.isLoading
|
||||
? null
|
||||
: () {
|
||||
if (playback.isCurrent) {
|
||||
ref
|
||||
.read(musicPlayerControllerProvider)
|
||||
.togglePlayPause(playback.isPlaying);
|
||||
} else {
|
||||
onPlay();
|
||||
}
|
||||
};
|
||||
|
||||
if (grid) {
|
||||
return TrackGridPlayButton(
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
icon: icon,
|
||||
);
|
||||
}
|
||||
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, color: colorScheme.primary),
|
||||
tooltip: tooltip,
|
||||
style: IconButton.styleFrom(
|
||||
minimumSize: Size.square(context.tokens.minTouchTarget),
|
||||
backgroundColor: colorScheme.primaryContainer.withValues(alpha: 0.3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
part of 'queue_tab.dart';
|
||||
|
||||
extension _QueueTabNavigation on _QueueTabState {
|
||||
String _libraryPlaybackMediaId(UnifiedLibraryItem item) {
|
||||
return item.historyItem?.id ?? item.localItem?.id ?? item.id;
|
||||
}
|
||||
|
||||
Future<void> _openFile(
|
||||
String filePath, {
|
||||
String title = '',
|
||||
|
||||
@@ -278,12 +278,14 @@ class TrackGridPlayButton extends StatelessWidget {
|
||||
super.key,
|
||||
required this.tooltip,
|
||||
required this.onPressed,
|
||||
this.icon = Icons.play_arrow,
|
||||
});
|
||||
|
||||
static const double visualDiameter = 36;
|
||||
|
||||
final String tooltip;
|
||||
final VoidCallback onPressed;
|
||||
final VoidCallback? onPressed;
|
||||
final IconData icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -307,7 +309,7 @@ class TrackGridPlayButton extends StatelessWidget {
|
||||
color: colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(Icons.play_arrow, size: 18, color: colorScheme.onPrimary),
|
||||
child: Icon(icon, size: 18, color: colorScheme.onPrimary),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/providers/music_player_provider.dart';
|
||||
|
||||
void main() {
|
||||
test('Library playback state follows only the current media item', () async {
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
currentMediaItemProvider.overrideWith(
|
||||
(ref) =>
|
||||
Stream.value(const MediaItem(id: 'track-1', title: 'Track 1')),
|
||||
),
|
||||
playbackPlayingProvider.overrideWith((ref) => true),
|
||||
playbackLoadingProvider.overrideWith((ref) => false),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
final currentMediaSubscription = container.listen(
|
||||
currentMediaItemProvider,
|
||||
(_, _) {},
|
||||
);
|
||||
addTearDown(currentMediaSubscription.close);
|
||||
|
||||
await container.read(currentMediaItemProvider.future);
|
||||
|
||||
expect(container.read(mediaItemPlaybackUiProvider('track-1')), (
|
||||
isCurrent: true,
|
||||
isPlaying: true,
|
||||
isLoading: false,
|
||||
));
|
||||
expect(container.read(mediaItemPlaybackUiProvider('track-2')), (
|
||||
isCurrent: false,
|
||||
isPlaying: false,
|
||||
isLoading: false,
|
||||
));
|
||||
});
|
||||
}
|
||||
@@ -407,12 +407,13 @@ void main() {
|
||||
expect(gridSource, contains('CompletionBridgePlayableStatus.checking'));
|
||||
expect(gridSource, contains('semanticsLabel:'));
|
||||
expect(gridSource, contains('queueCheckingDownloadedFile'));
|
||||
expect(gridSource, contains('TrackGridPlayButton('));
|
||||
expect(gridSource, contains('_LibraryPlaybackButton('));
|
||||
expect(gridSource, contains('grid: true'));
|
||||
expect(listSource, contains('resolveCompletionBridgePlayablePath('));
|
||||
expect(listSource, contains('_completionBridgePlayableProbe.listenable('));
|
||||
expect(listSource, contains('CompletionBridgePlayableStatus.checking'));
|
||||
expect(listSource, contains('semanticsLabel:'));
|
||||
expect(listSource, contains('queueCheckingDownloadedFile'));
|
||||
expect(listSource, contains('Icons.play_arrow'));
|
||||
expect(listSource, contains('_LibraryPlaybackButton('));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user