nkbmusic/lib/audio/player_providers.dart
metatroncubeswdev ad8c4f1c6b App: speaker/Bluetooth picker, Settings & help, Privacy Policy, Terms, FAQ, song reports
- Now Playing shows where music plays and opens the phone's output switcher
- Settings hub: Wi-Fi-only downloads (default on), storage, sound, help, legal
- Privacy Policy and Terms bundled as Markdown (assets/legal), shown in-app
- 'Is this your song?' report form feeding takedown_requests (+ kind, user_id)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:00:16 -04:00

117 lines
4.6 KiB
Dart

import 'package:audio_service/audio_service.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/models.dart';
import '../data/catalog_repository.dart';
import '../features/settings/settings_screen.dart';
import '../offline/download_store.dart';
import 'nk_audio_handler.dart';
/// Created in main() by AudioService.init and injected with an override.
final audioHandlerProvider = Provider<NkAudioHandler>(
(ref) => throw UnimplementedError('audioHandlerProvider must be overridden in main()'),
);
final mediaItemProvider = StreamProvider<MediaItem?>((ref) => ref.watch(audioHandlerProvider).mediaItem);
final playbackStateProvider =
StreamProvider<PlaybackState>((ref) => ref.watch(audioHandlerProvider).playbackState);
final queueProvider = StreamProvider<List<MediaItem>>((ref) => ref.watch(audioHandlerProvider).queue);
final positionProvider = StreamProvider<Duration>((ref) => ref.watch(audioHandlerProvider).positionStream);
/// The catalog track behind the current media item, if any.
final currentTrackProvider = Provider<Track?>((ref) {
final id = ref.watch(mediaItemProvider).value?.id;
final catalog = ref.watch(catalogProvider).value;
if (id == null || catalog == null) return null;
return catalog.trackById(id);
});
final playerControllerProvider = Provider<PlayerController>((ref) => PlayerController(ref));
class PlayerController {
PlayerController(this._ref);
final Ref _ref;
NkAudioHandler get _handler => _ref.read(audioHandlerProvider);
MediaItem toMediaItem(Track t) {
final catalog = _ref.read(catalogProvider).requireValue;
return MediaItem(
id: t.id,
title: t.titleLatin,
artist: catalog.creditLine(t),
album: t.occasion.label,
duration: t.duration == Duration.zero ? null : t.duration,
artUri: t.coverUrl == null ? null : Uri.parse(t.coverUrl!),
extras: {'url': t.audioUrl, 'coverAsset': t.coverAsset, 'occasion': t.occasion.id},
);
}
Future<void> playTracks(List<Track> tracks, {int index = 0, bool shuffle = false}) async {
await _handler.setShuffleMode(AudioServiceShuffleMode.none);
await _handler.playQueue(tracks.map(toMediaItem).toList(), index: index);
if (shuffle) await _handler.setShuffleMode(AudioServiceShuffleMode.all);
}
/// Plays downloaded songs from their saved details, so it works with no internet.
Future<void> playDownloads(List<DownloadEntry> entries, {int index = 0}) async {
await _handler.setShuffleMode(AudioServiceShuffleMode.none);
await _handler.playQueue([
for (final e in entries)
MediaItem(
id: e.trackId,
title: e.title,
artist: e.credit,
album: e.album,
duration: e.durationMs == 0 ? null : Duration(milliseconds: e.durationMs),
extras: {'url': '', 'occasion': e.occasion},
),
], index: index);
}
/// Queues songs for download. Returns why it could not start, or null.
Future<String?> download(Iterable<Track> tracks) async {
if (_ref.read(wifiOnlyDownloadsProvider)) {
final links = await Connectivity().checkConnectivity();
if (!links.contains(ConnectivityResult.wifi) && !links.contains(ConnectivityResult.ethernet)) {
return 'Connect to Wi-Fi to download, or turn off "Download on Wi-Fi only" in Settings.';
}
}
final catalog = _ref.read(catalogProvider).requireValue;
final store = _ref.read(downloadStoreProvider);
for (final t in tracks) {
store.enqueue(t, credit: catalog.creditLine(t), album: catalog.albumOf(t)?.title);
}
return null;
}
Future<void> playNext(Track t) => _handler.playNext(toMediaItem(t));
Future<void> addToQueue(Track t) => _handler.addQueueItem(toMediaItem(t));
Future<void> togglePlay() {
final playing = _ref.read(playbackStateProvider).value?.playing ?? false;
return playing ? _handler.pause() : _handler.play();
}
Future<void> toggleShuffle() {
final on = _ref.read(playbackStateProvider).value?.shuffleMode != AudioServiceShuffleMode.none;
return _handler.setShuffleMode(on ? AudioServiceShuffleMode.none : AudioServiceShuffleMode.all);
}
/// Off -> all -> one -> off.
Future<void> cycleRepeat() {
final mode = _ref.read(playbackStateProvider).value?.repeatMode ?? AudioServiceRepeatMode.none;
return _handler.setRepeatMode(switch (mode) {
AudioServiceRepeatMode.none => AudioServiceRepeatMode.all,
AudioServiceRepeatMode.all || AudioServiceRepeatMode.group => AudioServiceRepeatMode.one,
AudioServiceRepeatMode.one => AudioServiceRepeatMode.none,
});
}
}