package dev.stave.ui.albuminfo
import androidx.lifecycle.ViewModel
import dev.stave.domain.model.DidlItem
import dev.stave.ui.safeLaunch
import dev.stave.upnp.AlbumInfoLoader
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/** Album-notes state. [Idle] before the user asks, [Loading] while we look,
* [Loaded] with the prose, [Empty] when no source had anything (no comment
* tag, no sibling .txt). Same shape as Android's DetailViewModel.AlbumInfoState. */
sealed interface AlbumInfoState {
data object Idle : AlbumInfoState
data object Loading : AlbumInfoState
data class Loaded(val text: String) : AlbumInfoState
data object Empty : AlbumInfoState
}
/**
* Drives the "Album notes" dialog (port of Android's DetailViewModel
* loadAlbumInfo / resetAlbumInfo). Every path that sets [AlbumInfoState.Loading]
* ends in Loaded or Empty, including on an unexpected throw, so the dialog can't
* spin forever.
*/
class AlbumInfoViewModel(
private val loader: AlbumInfoLoader,
) : ViewModel() {
private val _state = MutableStateFlow<AlbumInfoState>(AlbumInfoState.Idle)
val state: StateFlow<AlbumInfoState> = _state.asStateFlow()
private var job: Job? = null
/** Look up notes for the album [track] belongs to. Replaces any lookup in
* flight (e.g. the user reopened the dialog after the track changed). */
fun load(track: DidlItem.Track) {
job?.cancel()
_state.value = AlbumInfoState.Loading
job = safeLaunch {
val text = try {
loader.load(track.resourceUrl)
} catch (c: CancellationException) {
throw c
} catch (e: Exception) {
System.err.println("[Earhorn] album notes lookup failed: ${e.javaClass.simpleName}: ${e.message}")
null
}
_state.value = if (text.isNullOrBlank()) AlbumInfoState.Empty else AlbumInfoState.Loaded(text)
}
}
/** Back to Idle when the dialog closes; cancels a lookup in flight. */
fun reset() {
job?.cancel()
job = null
_state.value = AlbumInfoState.Idle
}
}