package dev.stave.cast import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import dev.stave.domain.model.DidlItem import dev.stave.domain.renderer.RepeatMode import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject import javax.inject.Singleton /** * Libre flavor: Google Cast is compiled out. This class exists so common code * can depend on [CastIntegration] unconditionally. All methods are no-ops; * [isCasting] is always false. */ @Singleton class CastIntegration @Inject constructor( @Suppress("UnusedPrivateProperty") @ApplicationContext private val context: Context, ) { val available: Boolean = false private val _devices = MutableStateFlow>(emptyList()) val devices: StateFlow> = _devices.asStateFlow() private val _session = MutableStateFlow(null) val session: StateFlow = _session.asStateFlow() val isCasting: StateFlow = MutableStateFlow(false).asStateFlow() // Match play-flavor surface so common code (LocalRenderer) can reference // these flows unconditionally without a flavor check. All three are // permanently zero/false in libre since no session can ever start. val castIsPlaying: StateFlow = MutableStateFlow(false).asStateFlow() val castPositionMs: StateFlow = MutableStateFlow(0L).asStateFlow() val castDurationMs: StateFlow = MutableStateFlow(0L).asStateFlow() val castCurrentItemIndex: StateFlow = MutableStateFlow(-1).asStateFlow() val userInitiatedDisconnect: StateFlow = MutableStateFlow(false).asStateFlow() fun consumeDisconnectIntent(): Boolean = false // Diagnostic mirror of the play-flavor surface so shared code (CastPickerSheet) // can render the same diagnostic UI in both flavors. In libre, the diagnostic // always reports "compiled out" — which at least makes it clear to the user // why the picker is empty. val diagnostic: StateFlow = MutableStateFlow( DiagnosticState( castContextOk = false, mediaRouterOk = false, playServices = "libre build — Cast compiled out", rawRouteCount = 0, filteredDeviceCount = 0, tick = 0, rawRouteNames = emptyList(), ) ).asStateFlow() fun loadTrack(track: DidlItem.Track, autoplay: Boolean = true) = Unit fun loadQueue( tracks: List, startIndex: Int = 0, autoplay: Boolean = true, repeat: RepeatMode = RepeatMode.OFF, shuffle: Boolean = false, ) = Unit fun setRepeatAndShuffle(repeat: RepeatMode, shuffle: Boolean) = Unit fun queueAppend(tracks: List, fullQueue: List = tracks) = Unit fun queueInsertAfterCurrent(tracks: List) = Unit fun queueRemoveAt(index: Int) = Unit fun queueMove(from: Int, to: Int) = Unit fun consumeAttachWasResume(): Boolean = false fun currentContentId(): String? = null fun detachWithoutStopping() = Unit fun play() = Unit fun pause() = Unit fun stop() = Unit fun seekTo(positionMs: Long) = Unit fun queueNext() = Unit fun queuePrev() = Unit fun queueJumpTo(index: Int) = Unit // Maintainer, 2026-10-29: mirror of the play flavour's wait-then-jump (D4); no-op here. suspend fun queueJumpToWhenReady(index: Int) = Unit fun setVolume(volume: Double) = Unit fun getDeviceVolume(): Double = 1.0 fun setDeviceVolume(volume: Double) = Unit fun startDiscovery() = Unit fun stopDiscovery() = Unit fun connect(deviceId: String) = Unit fun disconnect() = Unit fun requestStatusRefresh() = Unit } data class CastDevice( val id: String, val friendlyName: String, val isGroup: Boolean, ) data class CastSession( val deviceId: String, val deviceName: String, ) data class DiagnosticState( val castContextOk: Boolean = false, val castInitError: String? = null, val mediaRouterOk: Boolean = false, val playServices: String = "unknown", val rawRouteCount: Int = 0, val filteredDeviceCount: Int = 0, val tick: Int = 0, val rawRouteNames: List = emptyList(), )