package link.roadio.riff.ui import link.roadio.riff.indexer.core.media.MediaId import java.util.Locale data class NowPlayingUiState( val mediaId: MediaId? = null, val title: String = "Nothing played yet", val artist: String = "", val album: String = "", val chapter: String? = null, val positionFraction: Float = 0f, val canSeek: Boolean = false, val elapsed: String = "0:00", val duration: String = "0:00", val playing: Boolean = false, val shuffle: Boolean = false, val repeat: Boolean = false, val aiReactionEligible: Boolean = false, val bookTitleContext: Boolean = false, val effectiveBook: Boolean = false, val artwork: ArtworkUiState = ArtworkUiState(), ) data class PlaybackGenreDiagnostic( val confidence: Float, val label: String, ) data class PlaybackDiagnosticsUiState( val arousal: Float? = null, val valence: Float? = null, val pte: Float? = null, val replayGainDb: Float? = null, val genre: PlaybackGenreDiagnostic? = null, ) internal fun playbackStatusLine(diagnostics: PlaybackDiagnosticsUiState): String = listOf( diagnosticValue("A", diagnostics.arousal), diagnosticValue("V", diagnostics.valence), diagnosticValue("PTE", diagnostics.pte), diagnostics.replayGainDb?.let { String.format(Locale.ROOT, "RG %.1f", it) } ?: "RG --", diagnostics.genre?.let { String.format(Locale.ROOT, "%.1f/%s", it.confidence, it.label) } ?: "--/--", ).joinToString(" ") private fun diagnosticValue( label: String, value: Float?, ): String = value?.let { String.format(Locale.ROOT, "%s %.1f", label, it) } ?: "$label --" internal fun completedPlayback(state: NowPlayingUiState): NowPlayingUiState = if (state.repeat) state else state.copy(playing = false, positionFraction = 1f) internal fun trackReactionKey(mediaId: MediaId): String = "track:${mediaId.hex()}" internal fun isAudiobookOverrideKey(objectKey: String): Boolean = (objectKey.startsWith("track:") || objectKey.startsWith("folder:") || objectKey.startsWith("album:")) && objectKey.substringAfter(':').isNotBlank() internal fun currentChapterLabel( chapters: List, positionMs: Long, durationMs: Long?, ): String? { if (chapters.any { it.startMs < 0L } || chapters.zipWithNext().any { (left, right) -> left.startMs >= right.startMs }) { return null } val available = chapters.filter { chapter -> durationMs == null || chapter.startMs < durationMs } if (available.size <= 1) return null val index = available .indexOfLast { chapter -> chapter.startMs <= positionMs.coerceAtLeast(0L) } .coerceAtLeast(0) return available[index].title?.takeIf(String::isNotBlank) ?: "Chapter ${index + 1}" } internal fun nextReactionState( current: ReactionState, requested: Preference?, toggleBan: Boolean, ): ReactionState = current.copy( preference = requested?.let { if (current.preference == it) Preference.NEUTRAL else it } ?: current.preference, ownBanned = if (toggleBan) !current.ownBanned else current.ownBanned, ) internal fun nextQueueIndex( queueSize: Int, currentIndex: Int, repeat: Boolean, ): Int? { if (queueSize <= 0) return null val normalized = currentIndex.coerceIn(0, queueSize - 1) return if (normalized + 1 < queueSize) normalized + 1 else 0.takeIf { repeat } }