EARHORNMUSIC UPnP · Android TV · Phones · Android Auto · GNU/Linux
Release 0.5.0

AlbumInfoViewModel.kt

Each line, with the release that last changed it.

0.5.01package dev.stave.ui.albuminfo
2
3import androidx.lifecycle.ViewModel
4import dev.stave.domain.model.DidlItem
5import dev.stave.ui.safeLaunch
6import dev.stave.upnp.AlbumInfoLoader
7import kotlinx.coroutines.CancellationException
8import kotlinx.coroutines.Job
9import kotlinx.coroutines.flow.MutableStateFlow
10import kotlinx.coroutines.flow.StateFlow
11import kotlinx.coroutines.flow.asStateFlow
12
13/** Album-notes state. [Idle] before the user asks, [Loading] while we look,
14 * [Loaded] with the prose, [Empty] when no source had anything (no comment
15 * tag, no sibling .txt). Same shape as Android's DetailViewModel.AlbumInfoState. */
16sealed interface AlbumInfoState {
17 data object Idle : AlbumInfoState
18 data object Loading : AlbumInfoState
19 data class Loaded(val text: String) : AlbumInfoState
20 data object Empty : AlbumInfoState
21}
22
23/**
24 * Drives the "Album notes" dialog (port of Android's DetailViewModel
25 * loadAlbumInfo / resetAlbumInfo). Every path that sets [AlbumInfoState.Loading]
26 * ends in Loaded or Empty, including on an unexpected throw, so the dialog can't
27 * spin forever.
28 */
29class AlbumInfoViewModel(
30 private val loader: AlbumInfoLoader,
31) : ViewModel() {
32
33 private val _state = MutableStateFlow<AlbumInfoState>(AlbumInfoState.Idle)
34 val state: StateFlow<AlbumInfoState> = _state.asStateFlow()
35
36 private var job: Job? = null
37
38 /** Look up notes for the album [track] belongs to. Replaces any lookup in
39 * flight (e.g. the user reopened the dialog after the track changed). */
40 fun load(track: DidlItem.Track) {
41 job?.cancel()
42 _state.value = AlbumInfoState.Loading
43 job = safeLaunch {
44 val text = try {
45 loader.load(track.resourceUrl)
46 } catch (c: CancellationException) {
47 throw c
48 } catch (e: Exception) {
49 System.err.println("[Earhorn] album notes lookup failed: ${e.javaClass.simpleName}: ${e.message}")
50 null
51 }
52 _state.value = if (text.isNullOrBlank()) AlbumInfoState.Empty else AlbumInfoState.Loaded(text)
53 }
54 }
55
56 /** Back to Idle when the dialog closes; cancels a lookup in flight. */
57 fun reset() {
58 job?.cancel()
59 job = null
60 _state.value = AlbumInfoState.Idle
61 }
62}