From f5735129409b2acd2deeb03cb0823d5c0c9c7b5b Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 25 May 2026 00:16:38 -0400 Subject: [PATCH] =?UTF-8?q?fix(android):=20split=20DiscoverScreen=20?= =?UTF-8?q?=E2=80=94=20VM=20into=20its=20own=20file=20(detekt=20TooManyFun?= =?UTF-8?q?ctions)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../minstrel/discover/ui/DiscoverScreen.kt | 134 ----------------- .../minstrel/discover/ui/DiscoverViewModel.kt | 141 ++++++++++++++++++ 2 files changed, 141 insertions(+), 134 deletions(-) create mode 100644 android/app/src/main/java/com/fabledsword/minstrel/discover/ui/DiscoverViewModel.kt diff --git a/android/app/src/main/java/com/fabledsword/minstrel/discover/ui/DiscoverScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/discover/ui/DiscoverScreen.kt index a2a1c384..c536a0cf 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/discover/ui/DiscoverScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/discover/ui/DiscoverScreen.kt @@ -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 = emptySet(), -) - -sealed interface SuggestionState { - data object Loading : SuggestionState - data class Loaded(val items: List) : 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) : 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 = 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 ────────────────────────────────────────────────────────── diff --git a/android/app/src/main/java/com/fabledsword/minstrel/discover/ui/DiscoverViewModel.kt b/android/app/src/main/java/com/fabledsword/minstrel/discover/ui/DiscoverViewModel.kt new file mode 100644 index 00000000..912d0941 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/discover/ui/DiscoverViewModel.kt @@ -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 = emptySet(), +) + +sealed interface SuggestionState { + data object Loading : SuggestionState + data class Loaded(val items: List) : 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) : 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 = 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) } + } +}