feat(android): ErrorCopy — friendly error messages across the app (audit v3 §4.2)
Ports Flutter's error-copy.json (45 server codes → sentence-case
copy) as a Kotlin ErrorCopy object. fromThrowable(t):
- Retrofit HttpException → parse {"error":{"code":...}} body,
map code (e.g. "wrong_password" → "Current password is
incorrect.", "playlist_not_found" → "That playlist no longer
exists.")
- IOException → connection_refused → "Couldn't reach the server.
Check the URL and try again."
- anything else → "Something went wrong."
Wired into 22 ViewModels / screens, replacing the raw
`e.message ?: "<generic>"` fallbacks that leaked exception text
(e.g. "HTTP 404", "Unable to resolve host") into the UI. Load-state
errors now read as actionable copy; settings form messages dropped
their "Couldn't save:" prefixes since the friendly strings stand
alone. ArtistDetail's playback path keeps its "Couldn't start
playback: " prefix (local-action context). PlayerController's
controller-connect failure and the About update-check are left on
their own copy (not server-code errors).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+3
-2
@@ -3,6 +3,7 @@ package com.fabledsword.minstrel.admin.ui
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.admin.data.AdminInvitesRepository
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.models.Invite
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@@ -49,7 +50,7 @@ class AdminInvitesViewModel @Inject constructor(
|
||||
internal.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
message = "Couldn't load invites: ${e.message ?: "unknown error"}",
|
||||
message = ErrorCopy.fromThrowable(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -67,7 +68,7 @@ class AdminInvitesViewModel @Inject constructor(
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
internal.update {
|
||||
it.copy(message = "Couldn't generate invite: ${e.message ?: "unknown error"}")
|
||||
it.copy(message = ErrorCopy.fromThrowable(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.composables.icons.lucide.Users
|
||||
import com.fabledsword.minstrel.admin.data.AdminQuarantineRepository
|
||||
import com.fabledsword.minstrel.admin.data.AdminRequestsRepository
|
||||
import com.fabledsword.minstrel.admin.data.AdminUsersRepository
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.nav.Admin
|
||||
import com.fabledsword.minstrel.nav.AdminQuarantine
|
||||
import com.fabledsword.minstrel.nav.AdminRequests
|
||||
@@ -95,7 +96,7 @@ class AdminLandingViewModel @Inject constructor(
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
internal.value = AdminLandingUiState.Error(
|
||||
e.message ?: "Admin counts load failed",
|
||||
ErrorCopy.fromThrowable(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@ package com.fabledsword.minstrel.admin.ui
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.admin.data.AdminQuarantineRepository
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.events.EventsStream
|
||||
import com.fabledsword.minstrel.models.AdminQuarantineItemRef
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -52,7 +53,7 @@ class AdminQuarantineViewModel @Inject constructor(
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
internal.value = AdminQuarantineUiState.Error(
|
||||
e.message ?: "Admin quarantine load failed",
|
||||
ErrorCopy.fromThrowable(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.admin.data.AdminRequestsRepository
|
||||
import com.fabledsword.minstrel.admin.data.AdminUsersRepository
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.events.EventsStream
|
||||
import com.fabledsword.minstrel.models.RequestRef
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -69,7 +70,7 @@ class AdminRequestsViewModel @Inject constructor(
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
internal.value = AdminRequestsUiState.Error(
|
||||
e.message ?: "Admin requests load failed",
|
||||
ErrorCopy.fromThrowable(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.fabledsword.minstrel.admin.ui
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.admin.data.AdminUsersRepository
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.models.AdminUserRef
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -43,7 +44,7 @@ class AdminUsersViewModel @Inject constructor(
|
||||
} catch (
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
internal.value = AdminUsersUiState.Error(e.message ?: "Users load failed")
|
||||
internal.value = AdminUsersUiState.Error(ErrorCopy.fromThrowable(e))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.fabledsword.minstrel.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Maps server error codes (and common transport failures) to
|
||||
* friendly, sentence-case copy. Mirrors
|
||||
* `flutter_client/assets/error-copy.json` + `error_copy.dart`.
|
||||
*
|
||||
* Server errors are `{"error":{"code":"...","message":"..."}}`.
|
||||
* [fromThrowable] pulls the code out of a Retrofit [HttpException]'s
|
||||
* error body and maps it; network failures (no connection / DNS /
|
||||
* refused) map to `connection_refused`; anything unrecognised falls
|
||||
* back to `unknown`.
|
||||
*
|
||||
* Replaces the `e.message ?: "unknown error"` pattern scattered
|
||||
* across ViewModels with copy a user can actually act on.
|
||||
*/
|
||||
object ErrorCopy {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
@Serializable
|
||||
private data class Envelope(val error: Body? = null)
|
||||
|
||||
@Serializable
|
||||
private data class Body(val code: String = "", val message: String = "")
|
||||
|
||||
/** Friendly copy for a known error [code], or the `unknown` fallback. */
|
||||
fun messageFor(code: String): String = TABLE[code] ?: TABLE.getValue("unknown")
|
||||
|
||||
/**
|
||||
* Friendly copy for any caught throwable. Parses Retrofit
|
||||
* HttpException bodies for the server code; treats IOExceptions
|
||||
* as connection failures.
|
||||
*/
|
||||
fun fromThrowable(t: Throwable): String = when (t) {
|
||||
is HttpException -> messageFor(codeFromHttp(t))
|
||||
is IOException -> messageFor("connection_refused")
|
||||
else -> TABLE.getValue("unknown")
|
||||
}
|
||||
|
||||
private fun codeFromHttp(e: HttpException): String {
|
||||
val raw = runCatching { e.response()?.errorBody()?.string() }.getOrNull()
|
||||
?: return "unknown"
|
||||
val code = runCatching { json.decodeFromString<Envelope>(raw).error?.code }
|
||||
.getOrNull()
|
||||
.orEmpty()
|
||||
return code.ifEmpty { "unknown" }
|
||||
}
|
||||
|
||||
private val TABLE: Map<String, String> = mapOf(
|
||||
"unknown" to "Something went wrong.",
|
||||
"unauthenticated" to "Your session has ended. Please sign in again.",
|
||||
"auth_required" to "You need to sign in to do that.",
|
||||
"forbidden" to "You don't have permission to do that.",
|
||||
"not_authorized" to "You don't have permission to do that.",
|
||||
"invalid_credentials" to "Wrong username or password.",
|
||||
"wrong_password" to "Current password is incorrect.",
|
||||
"password_too_short" to "Password must be at least 8 characters.",
|
||||
"username_invalid" to "That username isn't valid.",
|
||||
"username_taken" to "That username is already taken.",
|
||||
"email_invalid" to "Enter a valid email address.",
|
||||
"email_taken" to "That email is already in use.",
|
||||
"invalid_token" to "That link has expired or already been used. Request a new one.",
|
||||
"invite_invalid" to "That invite has expired or already been used.",
|
||||
"invite_required" to "An invite is required to register on this server.",
|
||||
"last_admin" to "Can't demote or delete the last admin.",
|
||||
"no_email_on_file" to "No email is associated with that account.",
|
||||
"not_configured" to "This integration isn't set up yet.",
|
||||
"validation" to "Some fields aren't valid. Check and try again.",
|
||||
"missing_fields" to "Required fields are missing.",
|
||||
"missing_query" to "Search needs at least one keyword.",
|
||||
"invalid_id" to "Invalid identifier.",
|
||||
"invalid_body" to "Couldn't read the request.",
|
||||
"bad_request" to "Invalid request.",
|
||||
"bad_body" to "Couldn't read the request.",
|
||||
"bad_kind" to "Invalid request type.",
|
||||
"bad_paging" to "Invalid page size or offset.",
|
||||
"bad_reason" to "Invalid quarantine reason.",
|
||||
"mbid_required" to "An MBID is required for this lookup.",
|
||||
"system_playlist_readonly" to "System playlists can't be edited directly.",
|
||||
"connection_refused" to "Couldn't reach the server. Check the URL and try again.",
|
||||
"lidarr_unreachable" to
|
||||
"Lidarr is unreachable right now. Try again, or check Admin → Integrations.",
|
||||
"lidarr_disabled" to "Lidarr integration is not enabled.",
|
||||
"lidarr_auth_failed" to "Lidarr authentication failed.",
|
||||
"lidarr_defaults_incomplete" to
|
||||
"Lidarr is missing a default quality profile or root folder. " +
|
||||
"Set them in Admin → Integrations.",
|
||||
"lidarr_server_error" to "Lidarr returned an error. Check Lidarr's logs for the cause.",
|
||||
"lidarr_rejected" to
|
||||
"Lidarr rejected the request. Check the server logs for the field-level reason.",
|
||||
"lidarr_album_lookup_failed" to
|
||||
"Lidarr doesn't recognize this album. Try Resolve or Delete file instead.",
|
||||
"album_mbid_missing" to "This track has no Lidarr album to remove.",
|
||||
"request_not_pending" to "This request is no longer pending.",
|
||||
"request_not_found" to "That request no longer exists.",
|
||||
"track_not_found" to "That track no longer exists.",
|
||||
"album_not_found" to "That album no longer exists.",
|
||||
"artist_not_found" to "That artist no longer exists.",
|
||||
"playlist_not_found" to "That playlist no longer exists.",
|
||||
"user_not_found" to "That user no longer exists.",
|
||||
"not_found" to "That no longer exists.",
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.fabledsword.minstrel.auth.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.auth.AuthController
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -49,7 +50,7 @@ class LoginViewModel @Inject constructor(
|
||||
internal.update {
|
||||
it.copy(
|
||||
isSubmitting = false,
|
||||
errorMessage = e.message ?: "Sign-in failed",
|
||||
errorMessage = ErrorCopy.fromThrowable(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.fabledsword.minstrel.discover.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.discover.data.DiscoverRepository
|
||||
import com.fabledsword.minstrel.discover.data.RequestOutcome
|
||||
import com.fabledsword.minstrel.models.ArtistSuggestionRef
|
||||
@@ -78,7 +79,7 @@ class DiscoverViewModel @Inject constructor(
|
||||
internal.update {
|
||||
it.copy(
|
||||
suggestions = SuggestionState.Error(
|
||||
e.message ?: "Suggestions failed",
|
||||
ErrorCopy.fromThrowable(e),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -100,7 +101,7 @@ class DiscoverViewModel @Inject constructor(
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
internal.update {
|
||||
it.copy(results = ResultsState.Error(e.message ?: "Search failed"))
|
||||
it.copy(results = ResultsState.Error(ErrorCopy.fromThrowable(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.history.data.HistoryEntry
|
||||
import com.fabledsword.minstrel.history.data.HistoryRepository
|
||||
import com.fabledsword.minstrel.models.TrackRef
|
||||
@@ -81,7 +82,7 @@ class HistoryTabViewModel @Inject constructor(
|
||||
} catch (
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
internal.value = HistoryUiState.Error(e.message ?: "History load failed")
|
||||
internal.value = HistoryUiState.Error(ErrorCopy.fromThrowable(e))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import androidx.navigation.NavHostController
|
||||
import coil3.compose.AsyncImage
|
||||
import com.composables.icons.lucide.Lucide
|
||||
import com.composables.icons.lucide.Music
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.home.data.HomeRepository
|
||||
import com.fabledsword.minstrel.library.widgets.AlbumCard
|
||||
import com.fabledsword.minstrel.library.widgets.ArtistCard
|
||||
@@ -174,7 +175,7 @@ class HomeViewModel @Inject constructor(
|
||||
observeHomeSections().combine(playlistsRepository.observeAll()) { sections, playlists ->
|
||||
val merged = sections.copy(playlists = playlists)
|
||||
if (merged.isAllEmpty) HomeUiState.Empty else HomeUiState.Success(merged)
|
||||
}.catch { e -> emit(HomeUiState.Error(e.message ?: "Home load failed")) }
|
||||
}.catch { e -> emit(HomeUiState.Error(ErrorCopy.fromThrowable(e))) }
|
||||
}
|
||||
|
||||
// ─── Screen ──────────────────────────────────────────────────────────
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.toRoute
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.library.data.LibraryRepository
|
||||
import com.fabledsword.minstrel.likes.data.LikesRepository
|
||||
import com.fabledsword.minstrel.models.AlbumDetailRef
|
||||
@@ -80,7 +81,7 @@ class AlbumDetailViewModel @Inject constructor(
|
||||
} catch (
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
internal.value = AlbumDetailUiState.Error(e.message ?: "Album load failed")
|
||||
internal.value = AlbumDetailUiState.Error(ErrorCopy.fromThrowable(e))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.toRoute
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.library.data.LibraryRepository
|
||||
import com.fabledsword.minstrel.likes.data.LikesRepository
|
||||
import com.fabledsword.minstrel.models.ArtistDetailRef
|
||||
@@ -84,7 +85,7 @@ class ArtistDetailViewModel @Inject constructor(
|
||||
} catch (
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
internal.value = ArtistDetailUiState.Error(e.message ?: "Artist load failed")
|
||||
internal.value = ArtistDetailUiState.Error(ErrorCopy.fromThrowable(e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +113,7 @@ class ArtistDetailViewModel @Inject constructor(
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
transientMessagesChannel.trySend(
|
||||
"Couldn't start playback: ${e.message ?: "unknown error"}",
|
||||
"Couldn't start playback: ${ErrorCopy.fromThrowable(e)}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.fabledsword.minstrel.library.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.cache.sync.SyncController
|
||||
import com.fabledsword.minstrel.library.data.LibraryRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -35,7 +36,7 @@ class LibraryViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
.catch { e ->
|
||||
emit(LibraryUiState.Error(e.message ?: "Library load failed"))
|
||||
emit(LibraryUiState.Error(ErrorCopy.fromThrowable(e)))
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavHostController
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.likes.data.LikesRepository
|
||||
import com.fabledsword.minstrel.library.widgets.AlbumCard
|
||||
import com.fabledsword.minstrel.library.widgets.ArtistCard
|
||||
@@ -107,7 +108,7 @@ class LikedTabViewModel @Inject constructor(
|
||||
val sections = LikedSections(artists, albums, tracks)
|
||||
if (sections.isAllEmpty) LikedTabUiState.Empty else LikedTabUiState.Success(sections)
|
||||
}
|
||||
.catch { e -> emit(LikedTabUiState.Error(e.message ?: "Liked load failed")) }
|
||||
.catch { e -> emit(LikedTabUiState.Error(ErrorCopy.fromThrowable(e))) }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
|
||||
|
||||
+2
-1
@@ -58,6 +58,7 @@ import com.composables.icons.lucide.Lucide
|
||||
import com.composables.icons.lucide.Play
|
||||
import com.composables.icons.lucide.RefreshCw
|
||||
import com.composables.icons.lucide.Shuffle
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.events.EventsStream
|
||||
import com.fabledsword.minstrel.events.LiveEvent
|
||||
import com.fabledsword.minstrel.models.PlaylistRef
|
||||
@@ -196,7 +197,7 @@ class PlaylistDetailViewModel @Inject constructor(
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
internal.value = PlaylistDetailUiState.Error(
|
||||
e.message ?: "Couldn't load playlist",
|
||||
ErrorCopy.fromThrowable(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavHostController
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.events.EventsStream
|
||||
import com.fabledsword.minstrel.models.PlaylistRef
|
||||
import com.fabledsword.minstrel.nav.PlaylistDetail
|
||||
@@ -89,7 +90,7 @@ class PlaylistsListViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
.catch { e ->
|
||||
emit(PlaylistsListUiState.Error(e.message ?: "Playlists load failed"))
|
||||
emit(PlaylistsListUiState.Error(ErrorCopy.fromThrowable(e)))
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.fabledsword.minstrel.quarantine.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.models.QuarantineRef
|
||||
import com.fabledsword.minstrel.quarantine.data.QuarantineRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -43,7 +44,7 @@ class HiddenTabViewModel @Inject constructor(
|
||||
} catch (
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
internal.value = HiddenTabUiState.Error(e.message ?: "Hidden load failed")
|
||||
internal.value = HiddenTabUiState.Error(ErrorCopy.fromThrowable(e))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.fabledsword.minstrel.requests.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.events.EventsStream
|
||||
import com.fabledsword.minstrel.models.RequestRef
|
||||
import com.fabledsword.minstrel.requests.data.RequestsRepository
|
||||
@@ -54,7 +55,7 @@ class RequestsViewModel @Inject constructor(
|
||||
} catch (
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
internal.value = RequestsUiState.Error(e.message ?: "Requests load failed")
|
||||
internal.value = RequestsUiState.Error(ErrorCopy.fromThrowable(e))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.fabledsword.minstrel.search.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.models.SearchResponseRef
|
||||
import com.fabledsword.minstrel.models.TrackRef
|
||||
import com.fabledsword.minstrel.player.PlayerController
|
||||
@@ -103,7 +104,7 @@ class SearchViewModel @Inject constructor(
|
||||
@Suppress("TooGenericExceptionCaught") e: Throwable,
|
||||
) {
|
||||
internal.update {
|
||||
it.copy(results = SearchResultsState.Error(e.message ?: "Search failed"))
|
||||
it.copy(results = SearchResultsState.Error(ErrorCopy.fromThrowable(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -2,6 +2,7 @@ package com.fabledsword.minstrel.settings.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.models.ListenBrainzStatus
|
||||
import com.fabledsword.minstrel.settings.data.MeRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -56,7 +57,7 @@ class ListenBrainzViewModel @Inject constructor(
|
||||
internal.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
message = "Couldn't load: ${e.message ?: "unknown error"}",
|
||||
message = ErrorCopy.fromThrowable(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -84,7 +85,7 @@ class ListenBrainzViewModel @Inject constructor(
|
||||
internal.update {
|
||||
it.copy(
|
||||
isSaving = false,
|
||||
message = "Couldn't save: ${e.message ?: "unknown error"}",
|
||||
message = ErrorCopy.fromThrowable(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -104,7 +105,7 @@ class ListenBrainzViewModel @Inject constructor(
|
||||
internal.update {
|
||||
it.copy(
|
||||
isSaving = false,
|
||||
message = "Couldn't update: ${e.message ?: "unknown error"}",
|
||||
message = ErrorCopy.fromThrowable(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.fabledsword.minstrel.settings.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.settings.data.MeRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -64,7 +65,7 @@ class PasswordViewModel @Inject constructor(
|
||||
internal.update {
|
||||
it.copy(
|
||||
isChanging = false,
|
||||
message = "Couldn't change password: ${e.message ?: "unknown error"}",
|
||||
message = ErrorCopy.fromThrowable(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.fabledsword.minstrel.settings.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.models.MyProfile
|
||||
import com.fabledsword.minstrel.settings.data.MeRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -71,7 +72,7 @@ class ProfileViewModel @Inject constructor(
|
||||
internal.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
message = "Couldn't load profile: ${e.message ?: "unknown error"}",
|
||||
message = ErrorCopy.fromThrowable(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -102,7 +103,7 @@ class ProfileViewModel @Inject constructor(
|
||||
internal.update {
|
||||
it.copy(
|
||||
isSaving = false,
|
||||
message = "Couldn't save: ${e.message ?: "unknown error"}",
|
||||
message = ErrorCopy.fromThrowable(e),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package com.fabledsword.minstrel.shared.widgets.trackactions
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.fabledsword.minstrel.api.ErrorCopy
|
||||
import com.fabledsword.minstrel.models.PlaylistRef
|
||||
import com.fabledsword.minstrel.playlists.data.PlaylistsRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -44,7 +45,7 @@ class AddToPlaylistViewModel @Inject constructor(
|
||||
}
|
||||
emit(AddToPlaylistUiState.Loading)
|
||||
}
|
||||
.catch { e -> emit(AddToPlaylistUiState.Error(e.message ?: "Failed to load playlists")) }
|
||||
.catch { e -> emit(AddToPlaylistUiState.Error(ErrorCopy.fromThrowable(e))) }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS),
|
||||
|
||||
Reference in New Issue
Block a user