fix(android): split DiscoverScreen — VM into its own file (detekt TooManyFunctions)

DiscoverScreen.kt hit detekt's TooManyFunctions limit (11) at 13.
Moved DiscoverState / SuggestionState / ResultsState / DiscoverViewModel
into DiscoverViewModel.kt — same package, same imports, no behavior
change. Screen file now hosts only composables + the snackbarFor helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 00:16:38 -04:00
parent 734fc16ee6
commit f573512940
2 changed files with 141 additions and 134 deletions
@@ -43,8 +43,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import coil3.compose.AsyncImage
import com.composables.icons.lucide.ArrowLeft
@@ -52,145 +50,13 @@ import com.composables.icons.lucide.Disc3
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Search as LucideSearch
import com.composables.icons.lucide.User
import com.fabledsword.minstrel.discover.data.DiscoverRepository
import com.fabledsword.minstrel.discover.data.RequestOutcome
import com.fabledsword.minstrel.models.ArtistSuggestionRef
import com.fabledsword.minstrel.models.LidarrRequestKind
import com.fabledsword.minstrel.models.LidarrSearchResultRef
import com.fabledsword.minstrel.nav.Discover
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
// ─── State ───────────────────────────────────────────────────────────
data class DiscoverState(
val query: String = "",
val kind: LidarrRequestKind = LidarrRequestKind.ARTIST,
val suggestions: SuggestionState = SuggestionState.Loading,
val results: ResultsState = ResultsState.Idle,
val locallyRequestedMbids: Set<String> = emptySet(),
)
sealed interface SuggestionState {
data object Loading : SuggestionState
data class Loaded(val items: List<ArtistSuggestionRef>) : SuggestionState
data class Error(val message: String) : SuggestionState
}
sealed interface ResultsState {
/** No active search — the screen shows the suggestion feed. */
data object Idle : ResultsState
data object Loading : ResultsState
data class Loaded(val items: List<LidarrSearchResultRef>) : ResultsState
data class Error(val message: String) : ResultsState
}
// ─── ViewModel ───────────────────────────────────────────────────────
@HiltViewModel
class DiscoverViewModel @Inject constructor(
private val repository: DiscoverRepository,
) : ViewModel() {
private val internal = MutableStateFlow(DiscoverState())
val state: StateFlow<DiscoverState> = internal.asStateFlow()
init {
loadSuggestions()
}
fun setQuery(value: String) {
internal.update {
// Clearing the box returns to the suggestion feed.
val newResults = if (value.isBlank()) ResultsState.Idle else it.results
it.copy(query = value, results = newResults)
}
}
fun setKind(kind: LidarrRequestKind) {
internal.update { it.copy(kind = kind) }
if (internal.value.query.isNotBlank()) runSearch()
}
fun loadSuggestions() {
viewModelScope.launch {
internal.update { it.copy(suggestions = SuggestionState.Loading) }
try {
val items = repository.listSuggestions()
internal.update { it.copy(suggestions = SuggestionState.Loaded(items)) }
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.update {
it.copy(
suggestions = SuggestionState.Error(
e.message ?: "Suggestions failed",
),
)
}
}
}
}
fun runSearch() {
val q = internal.value.query.trim()
if (q.isEmpty()) {
internal.update { it.copy(results = ResultsState.Idle) }
return
}
viewModelScope.launch {
internal.update { it.copy(results = ResultsState.Loading) }
try {
val rows = repository.search(q, internal.value.kind)
internal.update { it.copy(results = ResultsState.Loaded(rows)) }
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.update {
it.copy(results = ResultsState.Error(e.message ?: "Search failed"))
}
}
}
}
/**
* Request a search-result row. Returns the outcome (ACCEPTED vs
* QUEUED) so the screen can pick the snackbar wording and update
* the local "requested" overlay.
*/
suspend fun requestRow(row: LidarrSearchResultRef): RequestOutcome {
val kind = internal.value.kind
val outcome = repository.createRequest(
kind = kind,
artistMbid = if (kind == LidarrRequestKind.ARTIST) row.mbid else row.artistMbid,
artistName = if (kind == LidarrRequestKind.ARTIST) row.name else row.secondaryText,
albumMbid = if (kind == LidarrRequestKind.ALBUM) row.mbid else null,
albumTitle = if (kind == LidarrRequestKind.ALBUM) row.name else null,
)
markRequested(row.mbid)
return outcome
}
suspend fun requestSuggestion(s: ArtistSuggestionRef): RequestOutcome {
val outcome = repository.createRequest(
kind = LidarrRequestKind.ARTIST,
artistMbid = s.mbid,
artistName = s.name,
)
markRequested(s.mbid)
return outcome
}
private fun markRequested(mbid: String) {
internal.update { it.copy(locallyRequestedMbids = it.locallyRequestedMbids + mbid) }
}
}
// ─── Screen ──────────────────────────────────────────────────────────
@@ -0,0 +1,141 @@
package com.fabledsword.minstrel.discover.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fabledsword.minstrel.discover.data.DiscoverRepository
import com.fabledsword.minstrel.discover.data.RequestOutcome
import com.fabledsword.minstrel.models.ArtistSuggestionRef
import com.fabledsword.minstrel.models.LidarrRequestKind
import com.fabledsword.minstrel.models.LidarrSearchResultRef
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
// ─── State ───────────────────────────────────────────────────────────
data class DiscoverState(
val query: String = "",
val kind: LidarrRequestKind = LidarrRequestKind.ARTIST,
val suggestions: SuggestionState = SuggestionState.Loading,
val results: ResultsState = ResultsState.Idle,
val locallyRequestedMbids: Set<String> = emptySet(),
)
sealed interface SuggestionState {
data object Loading : SuggestionState
data class Loaded(val items: List<ArtistSuggestionRef>) : SuggestionState
data class Error(val message: String) : SuggestionState
}
sealed interface ResultsState {
/** No active search — the screen shows the suggestion feed. */
data object Idle : ResultsState
data object Loading : ResultsState
data class Loaded(val items: List<LidarrSearchResultRef>) : ResultsState
data class Error(val message: String) : ResultsState
}
// ─── ViewModel ───────────────────────────────────────────────────────
@HiltViewModel
class DiscoverViewModel @Inject constructor(
private val repository: DiscoverRepository,
) : ViewModel() {
private val internal = MutableStateFlow(DiscoverState())
val state: StateFlow<DiscoverState> = internal.asStateFlow()
init {
loadSuggestions()
}
fun setQuery(value: String) {
internal.update {
// Clearing the box returns to the suggestion feed.
val newResults = if (value.isBlank()) ResultsState.Idle else it.results
it.copy(query = value, results = newResults)
}
}
fun setKind(kind: LidarrRequestKind) {
internal.update { it.copy(kind = kind) }
if (internal.value.query.isNotBlank()) runSearch()
}
fun loadSuggestions() {
viewModelScope.launch {
internal.update { it.copy(suggestions = SuggestionState.Loading) }
try {
val items = repository.listSuggestions()
internal.update { it.copy(suggestions = SuggestionState.Loaded(items)) }
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.update {
it.copy(
suggestions = SuggestionState.Error(
e.message ?: "Suggestions failed",
),
)
}
}
}
}
fun runSearch() {
val q = internal.value.query.trim()
if (q.isEmpty()) {
internal.update { it.copy(results = ResultsState.Idle) }
return
}
viewModelScope.launch {
internal.update { it.copy(results = ResultsState.Loading) }
try {
val rows = repository.search(q, internal.value.kind)
internal.update { it.copy(results = ResultsState.Loaded(rows)) }
} catch (
@Suppress("TooGenericExceptionCaught") e: Throwable,
) {
internal.update {
it.copy(results = ResultsState.Error(e.message ?: "Search failed"))
}
}
}
}
/**
* Request a search-result row. Returns the outcome (ACCEPTED vs
* QUEUED) so the screen can pick the snackbar wording and update
* the local "requested" overlay.
*/
suspend fun requestRow(row: LidarrSearchResultRef): RequestOutcome {
val kind = internal.value.kind
val outcome = repository.createRequest(
kind = kind,
artistMbid = if (kind == LidarrRequestKind.ARTIST) row.mbid else row.artistMbid,
artistName = if (kind == LidarrRequestKind.ARTIST) row.name else row.secondaryText,
albumMbid = if (kind == LidarrRequestKind.ALBUM) row.mbid else null,
albumTitle = if (kind == LidarrRequestKind.ALBUM) row.name else null,
)
markRequested(row.mbid)
return outcome
}
suspend fun requestSuggestion(s: ArtistSuggestionRef): RequestOutcome {
val outcome = repository.createRequest(
kind = LidarrRequestKind.ARTIST,
artistMbid = s.mbid,
artistName = s.name,
)
markRequested(s.mbid)
return outcome
}
private fun markRequested(mbid: String) {
internal.update { it.copy(locallyRequestedMbids = it.locallyRequestedMbids + mbid) }
}
}