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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.composables.icons.lucide.ArrowLeft
|
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.Lucide
|
||||||
import com.composables.icons.lucide.Search as LucideSearch
|
import com.composables.icons.lucide.Search as LucideSearch
|
||||||
import com.composables.icons.lucide.User
|
import com.composables.icons.lucide.User
|
||||||
import com.fabledsword.minstrel.discover.data.DiscoverRepository
|
|
||||||
import com.fabledsword.minstrel.discover.data.RequestOutcome
|
import com.fabledsword.minstrel.discover.data.RequestOutcome
|
||||||
import com.fabledsword.minstrel.models.ArtistSuggestionRef
|
import com.fabledsword.minstrel.models.ArtistSuggestionRef
|
||||||
import com.fabledsword.minstrel.models.LidarrRequestKind
|
import com.fabledsword.minstrel.models.LidarrRequestKind
|
||||||
import com.fabledsword.minstrel.models.LidarrSearchResultRef
|
import com.fabledsword.minstrel.models.LidarrSearchResultRef
|
||||||
import com.fabledsword.minstrel.nav.Discover
|
import com.fabledsword.minstrel.nav.Discover
|
||||||
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
|
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 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 ──────────────────────────────────────────────────────────
|
// ─── 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