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:
@@ -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) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user