- 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>
117 lines
4.6 KiB
Dart
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,
|
|
});
|
|
}
|
|
}
|